@sun-asterisk/impact-analyzer 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import path from 'path';
10
+ import fs from 'fs';
10
11
  import { createLogger } from '../utils/logger.js';
11
12
 
12
13
  export class DatabaseDetector {
@@ -478,6 +479,364 @@ export class DatabaseDetector {
478
479
  return blocks;
479
480
  }
480
481
 
482
+ /**
483
+ * BUG-004: Detect raw SQL query patterns in changed files
484
+ * CORRECTED APPROACH: Use full file content, not just diff
485
+ *
486
+ * Strategy:
487
+ * 1. Read full file content from filesystem
488
+ * 2. Find changed line numbers from diff
489
+ * 3. Find parent statements containing those lines
490
+ * 4. Extract SQL from those statements
491
+ */
492
+ detectRawSQLQueries(filePath, diff) {
493
+ const sqlQueries = [];
494
+
495
+ try {
496
+ // Step 1: Read full file content
497
+ if (!fs.existsSync(filePath)) {
498
+ this.logger.verbose('DatabaseDetector', `File not found: ${filePath}`);
499
+ return sqlQueries;
500
+ }
501
+
502
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
503
+ const fileLines = fileContent.split('\n');
504
+
505
+ // Step 2: Extract changed line numbers from diff
506
+ const changedLineNumbers = this.extractChangedLineNumbers(diff);
507
+
508
+ if (changedLineNumbers.length === 0) {
509
+ return sqlQueries;
510
+ }
511
+
512
+ this.logger.verbose('DatabaseDetector',
513
+ `Analyzing ${changedLineNumbers.length} changed lines in ${filePath}`);
514
+
515
+ // Step 3: Find query/execute statements that contain changed lines
516
+ const processedStatements = new Set();
517
+
518
+ for (const lineNum of changedLineNumbers) {
519
+ const statement = this.findQueryStatementContainingLine(fileLines, lineNum);
520
+
521
+ if (!statement) continue;
522
+
523
+ const key = `${statement.startLine}-${statement.endLine}`;
524
+ if (processedStatements.has(key)) continue;
525
+ processedStatements.add(key);
526
+
527
+ // Step 4: Extract SQL from statement
528
+ const sqlData = this.extractSQLFromStatement(statement.content);
529
+
530
+ if (sqlData) {
531
+ this.logger.verbose('DatabaseDetector',
532
+ `Found SQL query at lines ${statement.startLine}-${statement.endLine}`);
533
+ sqlQueries.push({
534
+ sql: sqlData.sql,
535
+ method: statement.method,
536
+ changedLineNum: lineNum
537
+ });
538
+ }
539
+ }
540
+
541
+ } catch (error) {
542
+ this.logger.error('DatabaseDetector', `Error detecting raw SQL: ${error.message}`);
543
+ }
544
+
545
+ return sqlQueries;
546
+ }
547
+
548
+ /**
549
+ * Extract line numbers that were changed in the diff
550
+ */
551
+ extractChangedLineNumbers(diff) {
552
+ const changedLines = [];
553
+ const lines = diff.split('\n');
554
+ let currentLineNum = 0;
555
+
556
+ for (const line of lines) {
557
+ if (line.startsWith('@@')) {
558
+ // Parse: @@ -oldStart,oldCount +newStart,newCount @@
559
+ const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)/);
560
+ if (match) {
561
+ currentLineNum = parseInt(match[1], 10);
562
+ }
563
+ continue;
564
+ }
565
+
566
+ if (line.startsWith('+') && !line.startsWith('+++')) {
567
+ changedLines.push(currentLineNum);
568
+ }
569
+
570
+ if (!line.startsWith('-')) {
571
+ currentLineNum++;
572
+ }
573
+ }
574
+
575
+ return changedLines;
576
+ }
577
+
578
+ /**
579
+ * Find the .query() or .execute() statement that contains the given line number
580
+ */
581
+ findQueryStatementContainingLine(fileLines, targetLineNum) {
582
+ // Adjust for 0-based array indexing
583
+ const targetIndex = targetLineNum - 1;
584
+
585
+ if (targetIndex < 0 || targetIndex >= fileLines.length) {
586
+ return null;
587
+ }
588
+
589
+ // Search backwards to find .query( or .execute(
590
+ const searchRadius = 50;
591
+ let startLine = -1;
592
+ let method = null;
593
+
594
+ for (let i = targetIndex; i >= Math.max(0, targetIndex - searchRadius); i--) {
595
+ const line = fileLines[i];
596
+ const match = line.match(/\.(query|execute)\s*</);
597
+
598
+ if (match) {
599
+ method = match[1];
600
+ startLine = i + 1; // Convert to 1-based line number
601
+ break;
602
+ }
603
+ }
604
+
605
+ if (!method) return null;
606
+
607
+ // Search forward to find the end of the statement (closing semicolon or })
608
+ let endLine = targetLineNum;
609
+ let parenDepth = 0;
610
+ let inTemplate = false;
611
+
612
+ for (let i = startLine - 1; i < Math.min(fileLines.length, startLine + searchRadius); i++) {
613
+ const line = fileLines[i];
614
+
615
+ // Track template literal boundaries
616
+ for (const char of line) {
617
+ if (char === '`') inTemplate = !inTemplate;
618
+ if (!inTemplate) {
619
+ if (char === '(') parenDepth++;
620
+ if (char === ')') parenDepth--;
621
+ }
622
+ }
623
+
624
+ endLine = i + 1;
625
+
626
+ // Found the end when parentheses are balanced and we hit a semicolon
627
+ if (parenDepth === 0 && line.includes(';')) {
628
+ break;
629
+ }
630
+ }
631
+
632
+ // Extract the statement content
633
+ const statementLines = fileLines.slice(startLine - 1, endLine);
634
+
635
+ return {
636
+ startLine,
637
+ endLine,
638
+ method,
639
+ content: statementLines.join('\n')
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Extract SQL and metadata from a query/execute statement
645
+ */
646
+ extractSQLFromStatement(statementContent) {
647
+ // Extract SQL from template literal (backticks)
648
+ const templateMatch = statementContent.match(/`([^`]+)`/s);
649
+
650
+ if (templateMatch) {
651
+ const sql = templateMatch[1].trim();
652
+ return { sql };
653
+ }
654
+
655
+ // Try single/double quotes as fallback
656
+ const quoteMatch = statementContent.match(/['"]([^'"]+)['"]/s);
657
+ if (quoteMatch) {
658
+ const sql = quoteMatch[1].trim();
659
+ return { sql };
660
+ }
661
+
662
+ return null;
663
+ }
664
+
665
+ /**
666
+ * BUG-004: Parse SQL query to extract database impact
667
+ * Extracts: operation type, table names, field names
668
+ */
669
+ parseSQLQuery(sql) {
670
+ const impact = {
671
+ operation: null,
672
+ tables: [],
673
+ fields: [],
674
+ hasJoin: false,
675
+ joinType: null
676
+ };
677
+
678
+ // Normalize SQL (remove extra whitespace, newlines)
679
+ const normalizedSQL = sql.replace(/\s+/g, ' ').trim().toUpperCase();
680
+
681
+ // Detect operation type
682
+ if (normalizedSQL.startsWith('SELECT')) {
683
+ impact.operation = 'SELECT';
684
+ } else if (normalizedSQL.startsWith('INSERT')) {
685
+ impact.operation = 'INSERT';
686
+ } else if (normalizedSQL.startsWith('UPDATE')) {
687
+ impact.operation = 'UPDATE';
688
+ } else if (normalizedSQL.startsWith('DELETE')) {
689
+ impact.operation = 'DELETE';
690
+ } else if (normalizedSQL.startsWith('ALTER')) {
691
+ impact.operation = 'ALTER';
692
+ } else if (normalizedSQL.startsWith('CREATE')) {
693
+ impact.operation = 'CREATE';
694
+ } else if (normalizedSQL.startsWith('DROP')) {
695
+ impact.operation = 'DROP';
696
+ }
697
+
698
+ // Extract table names
699
+ impact.tables = this.extractTablesFromSQL(normalizedSQL, impact.operation);
700
+
701
+ // Extract field names (use original SQL for case sensitivity)
702
+ impact.fields = this.extractFieldsFromSQL(sql, impact.operation);
703
+
704
+ // Detect JOIN
705
+ if (normalizedSQL.includes('JOIN')) {
706
+ impact.hasJoin = true;
707
+
708
+ if (normalizedSQL.includes('INNER JOIN')) {
709
+ impact.joinType = 'INNER';
710
+ } else if (normalizedSQL.includes('LEFT JOIN')) {
711
+ impact.joinType = 'LEFT';
712
+ } else if (normalizedSQL.includes('RIGHT JOIN')) {
713
+ impact.joinType = 'RIGHT';
714
+ } else if (normalizedSQL.includes('FULL JOIN')) {
715
+ impact.joinType = 'FULL';
716
+ }
717
+ }
718
+
719
+ return impact;
720
+ }
721
+
722
+ /**
723
+ * BUG-004: Extract table names from SQL query
724
+ */
725
+ extractTablesFromSQL(sql, operation) {
726
+ const tables = new Set();
727
+
728
+ // FROM clause: SELECT ... FROM table_name
729
+ const fromMatches = sql.matchAll(/FROM\s+([a-z_][a-z0-9_]*)/gi);
730
+ for (const match of fromMatches) {
731
+ tables.add(match[1].toLowerCase());
732
+ }
733
+
734
+ // JOIN clause: JOIN table_name
735
+ const joinMatches = sql.matchAll(/JOIN\s+([a-z_][a-z0-9_]*)/gi);
736
+ for (const match of joinMatches) {
737
+ tables.add(match[1].toLowerCase());
738
+ }
739
+
740
+ // INSERT INTO: INSERT INTO table_name
741
+ if (operation === 'INSERT') {
742
+ const insertMatch = sql.match(/INSERT\s+INTO\s+([a-z_][a-z0-9_]*)/i);
743
+ if (insertMatch) {
744
+ tables.add(insertMatch[1].toLowerCase());
745
+ }
746
+ }
747
+
748
+ // UPDATE: UPDATE table_name
749
+ if (operation === 'UPDATE') {
750
+ const updateMatch = sql.match(/UPDATE\s+([a-z_][a-z0-9_]*)/i);
751
+ if (updateMatch) {
752
+ tables.add(updateMatch[1].toLowerCase());
753
+ }
754
+ }
755
+
756
+ // ALTER TABLE: ALTER TABLE table_name
757
+ if (operation === 'ALTER') {
758
+ const alterMatch = sql.match(/ALTER\s+TABLE\s+([a-z_][a-z0-9_]*)/i);
759
+ if (alterMatch) {
760
+ tables.add(alterMatch[1].toLowerCase());
761
+ }
762
+ }
763
+
764
+ return Array.from(tables);
765
+ }
766
+
767
+ /**
768
+ * BUG-004: Extract field names from SQL query
769
+ */
770
+ extractFieldsFromSQL(sql, operation) {
771
+ const fields = new Set();
772
+
773
+ // SELECT fields: SELECT field1, field2, table.field3
774
+ if (operation === 'SELECT') {
775
+ const selectMatch = sql.match(/SELECT\s+(.*?)\s+FROM/is);
776
+ if (selectMatch) {
777
+ const fieldList = selectMatch[1];
778
+
779
+ // Extract field names (handle table.field format)
780
+ const fieldMatches = fieldList.matchAll(/(?:^|,)\s*(?:\w+\.)?(\w+)(?:\s+as\s+\w+)?/gi);
781
+ for (const match of fieldMatches) {
782
+ const fieldName = match[1].toLowerCase();
783
+ // Skip SQL keywords and functions
784
+ if (!['count', 'sum', 'avg', 'max', 'min', 'distinct', '*'].includes(fieldName)) {
785
+ fields.add(fieldName);
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ // WHERE clause fields: WHERE user.id = $1 AND user.name = $2
792
+ const whereMatches = sql.matchAll(/(?:WHERE|AND|OR)\s+(?:\w+\.)?(\w+)\s*[=<>!]/gi);
793
+ for (const match of whereMatches) {
794
+ const fieldName = match[1].toLowerCase();
795
+ fields.add(fieldName);
796
+ }
797
+
798
+ // JOIN ON clause: ON table1.field = table2.field
799
+ const onMatches = sql.matchAll(/ON\s+(?:\w+\.)?(\w+)\s*=\s*(?:\w+\.)?(\w+)/gi);
800
+ for (const match of onMatches) {
801
+ fields.add(match[1].toLowerCase());
802
+ fields.add(match[2].toLowerCase());
803
+ }
804
+
805
+ // INSERT fields: INSERT INTO table (field1, field2)
806
+ if (operation === 'INSERT') {
807
+ const insertMatch = sql.match(/INSERT\s+INTO\s+\w+\s*\((.*?)\)/is);
808
+ if (insertMatch) {
809
+ const fieldList = insertMatch[1];
810
+ const fieldMatches = fieldList.matchAll(/(\w+)/g);
811
+ for (const match of fieldMatches) {
812
+ fields.add(match[1].toLowerCase());
813
+ }
814
+ }
815
+ }
816
+
817
+ // UPDATE SET: UPDATE table SET field1 = $1, field2 = $2
818
+ if (operation === 'UPDATE') {
819
+ const setMatch = sql.match(/SET\s+(.*?)(?:WHERE|$)/is);
820
+ if (setMatch) {
821
+ const setList = setMatch[1];
822
+ const fieldMatches = setList.matchAll(/(\w+)\s*=/gi);
823
+ for (const match of fieldMatches) {
824
+ fields.add(match[1].toLowerCase());
825
+ }
826
+ }
827
+ }
828
+
829
+ // ALTER TABLE ADD/MODIFY: ALTER TABLE table ADD COLUMN field_name
830
+ if (operation === 'ALTER') {
831
+ const alterMatches = sql.matchAll(/(?:ADD|MODIFY|DROP)\s+(?:COLUMN\s+)?(\w+)/gi);
832
+ for (const match of alterMatches) {
833
+ fields.add(match[1].toLowerCase());
834
+ }
835
+ }
836
+
837
+ return Array.from(fields).filter(f => f !== 'delflg');
838
+ }
839
+
481
840
  analyzeChangedCodeForDatabaseOps(changedFile, databaseChanges) {
482
841
  const diff = changedFile.diff || '';
483
842
  const content = changedFile.content || '';
@@ -502,6 +861,52 @@ export class DatabaseDetector {
502
861
  );
503
862
  }
504
863
 
864
+ // BUG-004: Step 3 - Detect raw SQL queries
865
+ const rawSQLQueries = this.detectRawSQLQueries(changedFile.path, changedFile.diff || '');
866
+
867
+ if (rawSQLQueries.length > 0) {
868
+ this.logger.verbose('DatabaseDetector',
869
+ `Found ${rawSQLQueries.length} raw SQL queries in ${changedFile.path}`);
870
+ }
871
+
872
+ for (const query of rawSQLQueries) {
873
+ const sqlImpact = this.parseSQLQuery(query.sql);
874
+
875
+ // Add each affected table to database changes
876
+ for (const tableName of sqlImpact.tables) {
877
+ if (!databaseChanges.tables.has(tableName)) {
878
+ databaseChanges.tables.set(tableName, {
879
+ entity: this.tableNameToEntityName(tableName),
880
+ file: changedFile.path,
881
+ operations: new Set(),
882
+ fields: new Set(),
883
+ isEntityFile: false,
884
+ hasRelationChange: false,
885
+ changeSource: { path: changedFile.path, type: 'raw-sql' }
886
+ });
887
+ }
888
+
889
+ const tableData = databaseChanges.tables.get(tableName);
890
+
891
+ if (sqlImpact.operation) {
892
+ tableData.operations.add(sqlImpact.operation);
893
+ }
894
+
895
+ for (const field of sqlImpact.fields) {
896
+ tableData.fields.add(field);
897
+ }
898
+
899
+ // Add SQL-specific metadata
900
+ if (sqlImpact.hasJoin) {
901
+ tableData.hasJoin = true;
902
+ tableData.joinType = sqlImpact.joinType;
903
+ }
904
+
905
+ this.logger.verbose('DatabaseDetector',
906
+ `Raw SQL: ${sqlImpact.operation} on ${tableName} with fields: ${sqlImpact.fields.join(', ')}`);
907
+ }
908
+ }
909
+
505
910
  const typeOrmOps = {
506
911
  'insert': 'INSERT',
507
912
  'save': 'INSERT/UPDATE',
@@ -740,9 +1145,7 @@ export class DatabaseDetector {
740
1145
  const methodKey = `${repoMethod.className}.${repoMethod.methodName}`;
741
1146
  const methodCalls = this.methodCallGraph.methodCallsMap?.get(methodKey) || [];
742
1147
 
743
- if (this.config.verbose) {
744
- console.log(` 📞 Analyzing ${methodCalls.length} calls in ${repoMethod.className}.${repoMethod.methodName}`);
745
- }
1148
+ this.logger.verbose('DatabaseDetector', ` 📞 Analyzing ${methodCalls.length} calls in ${repoMethod.className}.${repoMethod.methodName}`);
746
1149
 
747
1150
  for (const call of methodCalls) {
748
1151
  const dbOp = this.detectDatabaseOperationFromCall(call);
@@ -910,3 +1313,5 @@ export class DatabaseDetector {
910
1313
  return 'low';
911
1314
  }
912
1315
  }
1316
+
1317
+ export default DatabaseDetector;
@@ -7,6 +7,7 @@ export function createLogger(verbose = false) {
7
7
  return {
8
8
  info: (msg) => console.log(msg),
9
9
  debug: (msg) => verbose && console.log(`[DEBUG] ${msg}`),
10
- verbose: (component, msg) => verbose && console.log(`[${component}] ${msg}`)
10
+ verbose: (component, msg) => verbose && console.log(`[${component}] ${msg}`),
11
+ error: (msg) => console.error(`[ERROR] ${msg}`),
11
12
  };
12
13
  }