#!/usr/bin/env python3 import re import sys from collections import defaultdict from pathlib import Path # Matches common SQL statements containing a table name. TABLE_PATTERNS = [ re.compile( r'^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\[]?([^`"\]\s(.]+)', re.IGNORECASE, ), re.compile( r'^\s*INSERT\s+INTO\s+[`"\[]?([^`"\]\s(.]+)', re.IGNORECASE, ), re.compile( r'^\s*UPDATE\s+[`"\[]?([^`"\]\s(.]+)', re.IGNORECASE, ), re.compile( r'^\s*ALTER\s+TABLE\s+[`"\[]?([^`"\]\s(.]+)', re.IGNORECASE, ), re.compile( r'^\s*DELETE\s+FROM\s+[`"\[]?([^`"\]\s(.]+)', re.IGNORECASE, ), ] def extract_table_name(line): """Return table name if the line starts a recognizable table statement.""" for pattern in TABLE_PATTERNS: match = pattern.search(line) if match: return match.group(1) return None def normalize_line(line): """ Normalize a line for comparison. Removes leading/trailing whitespace but otherwise leaves SQL intact. """ return line.strip() def parse_sql_file(filename): """ Parse SQL into table -> list of (line_number, original_line). Once a table-related statement is detected, following lines are associated with that table until another table statement begins. """ tables = defaultdict(list) current_table = None with open(filename, "r", encoding="utf-8", errors="replace") as f: for line_number, line in enumerate(f, start=1): table = extract_table_name(line) if table: current_table = table if current_table: cleaned = normalize_line(line) # Ignore completely blank lines. if cleaned: tables[current_table].append( (line_number, line.rstrip("\n")) ) return tables def line_multiset(lines): """ Convert lines into: normalized_line -> list of occurrences Keeping occurrences means duplicate INSERT rows are handled correctly. """ result = defaultdict(list) for line_number, text in lines: result[normalize_line(text)].append((line_number, text)) return result def compare_table(table, lines1, lines2, file1, file2): data1 = line_multiset(lines1) data2 = line_multiset(lines2) all_lines = sorted(set(data1) | set(data2)) only_file1 = [] only_file2 = [] for normalized in all_lines: occurrences1 = data1.get(normalized, []) occurrences2 = data2.get(normalized, []) common_count = min(len(occurrences1), len(occurrences2)) only_file1.extend(occurrences1[common_count:]) only_file2.extend(occurrences2[common_count:]) if not only_file1 and not only_file2: return False print() print("=" * 100) print(f"TABLE: {table}") print("=" * 100) if only_file1: print(f"\nOnly in {file1}:") for line_number, text in only_file1: print(f" Line {line_number}: {text}") if only_file2: print(f"\nOnly in {file2}:") for line_number, text in only_file2: print(f" Line {line_number}: {text}") return True def compare_sql_files(file1, file2): tables1 = parse_sql_file(file1) tables2 = parse_sql_file(file2) all_tables = sorted(set(tables1) | set(tables2)) print(f"Comparing:") print(f" File 1: {file1}") print(f" File 2: {file2}") print() differences = 0 for table in all_tables: if table not in tables1: print() print("=" * 100) print(f"TABLE: {table}") print("=" * 100) print(f"Table exists only in {file2}") differences += 1 continue if table not in tables2: print() print("=" * 100) print(f"TABLE: {table}") print("=" * 100) print(f"Table exists only in {file1}") differences += 1 continue if compare_table( table, tables1[table], tables2[table], file1, file2, ): differences += 1 print() print("=" * 100) if differences == 0: print("No table differences found.") else: print(f"{differences} table(s) contain differences.") def main(): if len(sys.argv) != 3: script = Path(sys.argv[0]).name print(f"Usage: python {script} file1.sql file2.sql") sys.exit(1) file1 = sys.argv[1] file2 = sys.argv[2] if not Path(file1).is_file(): print(f"File not found: {file1}") sys.exit(1) if not Path(file2).is_file(): print(f"File not found: {file2}") sys.exit(1) compare_sql_files(file1, file2) if __name__ == "__main__": main()