fix the enrollement-carryover balance
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user