207 lines
4.9 KiB
Python
207 lines
4.9 KiB
Python
#!/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() |