194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
"""
|
|
Validation utilities for comparing Python vs SQL output
|
|
"""
|
|
import pandas as pd
|
|
from typing import Dict, List, Optional, Tuple
|
|
from utils.logger import logger
|
|
|
|
|
|
def compare_outputs(
|
|
python_output: List[Dict],
|
|
sql_output: List[Dict],
|
|
key_columns: Optional[List[str]] = None
|
|
) -> Dict:
|
|
"""
|
|
Compare Python output with SQL output
|
|
|
|
Args:
|
|
python_output: List of dicts from Python service
|
|
sql_output: List of dicts from SQL query
|
|
key_columns: Columns to use for matching rows
|
|
|
|
Returns:
|
|
Comparison report dict
|
|
"""
|
|
python_df = pd.DataFrame(python_output)
|
|
sql_df = pd.DataFrame(sql_output)
|
|
|
|
report = {
|
|
'python_count': len(python_output),
|
|
'sql_count': len(sql_output),
|
|
'count_match': len(python_output) == len(sql_output),
|
|
'differences': [],
|
|
'missing_in_python': [],
|
|
'missing_in_sql': [],
|
|
'column_differences': {}
|
|
}
|
|
|
|
if python_df.empty and sql_df.empty:
|
|
report['status'] = 'both_empty'
|
|
return report
|
|
|
|
if python_df.empty:
|
|
report['status'] = 'python_empty'
|
|
return report
|
|
|
|
if sql_df.empty:
|
|
report['status'] = 'sql_empty'
|
|
return report
|
|
|
|
# Compare columns
|
|
python_cols = set(python_df.columns)
|
|
sql_cols = set(sql_df.columns)
|
|
|
|
missing_in_python = sql_cols - python_cols
|
|
missing_in_sql = python_cols - sql_cols
|
|
|
|
if missing_in_python:
|
|
report['missing_in_python'] = list(missing_in_python)
|
|
if missing_in_sql:
|
|
report['missing_in_sql'] = list(missing_in_sql)
|
|
|
|
# Compare common columns
|
|
common_cols = python_cols & sql_cols
|
|
|
|
if key_columns:
|
|
# Match rows by key columns
|
|
for key_col in key_columns:
|
|
if key_col not in common_cols:
|
|
logger.warning(f"Key column {key_col} not found in both outputs")
|
|
key_columns = None
|
|
break
|
|
|
|
if key_columns:
|
|
# Merge on key columns
|
|
python_key = python_df[key_columns].copy()
|
|
sql_key = sql_df[key_columns].copy()
|
|
|
|
merged = python_key.merge(
|
|
sql_key,
|
|
on=key_columns,
|
|
how='outer',
|
|
indicator=True
|
|
)
|
|
|
|
report['only_in_python'] = len(merged[merged['_merge'] == 'left_only'])
|
|
report['only_in_sql'] = len(merged[merged['_merge'] == 'right_only'])
|
|
report['in_both'] = len(merged[merged['_merge'] == 'both'])
|
|
else:
|
|
# Compare by index (assume same order)
|
|
min_len = min(len(python_df), len(sql_df))
|
|
report['compared_rows'] = min_len
|
|
|
|
# Compare values in common columns
|
|
numeric_cols = []
|
|
text_cols = []
|
|
|
|
for col in common_cols:
|
|
if python_df[col].dtype in ['int64', 'float64'] or sql_df[col].dtype in ['int64', 'float64']:
|
|
numeric_cols.append(col)
|
|
else:
|
|
text_cols.append(col)
|
|
|
|
differences = []
|
|
|
|
# Compare numeric columns
|
|
for col in numeric_cols:
|
|
if col in python_df.columns and col in sql_df.columns:
|
|
python_vals = python_df[col].fillna(0)
|
|
sql_vals = sql_df[col].fillna(0)
|
|
|
|
# Handle different lengths
|
|
min_len = min(len(python_vals), len(sql_vals))
|
|
python_vals = python_vals[:min_len]
|
|
sql_vals = sql_vals[:min_len]
|
|
|
|
diff = (python_vals - sql_vals).abs()
|
|
max_diff = diff.max()
|
|
mean_diff = diff.mean()
|
|
|
|
if max_diff > 0.01: # Tolerance for floating point
|
|
differences.append({
|
|
'column': col,
|
|
'type': 'numeric',
|
|
'max_difference': float(max_diff),
|
|
'mean_difference': float(mean_diff),
|
|
'different_count': int((diff > 0.01).sum())
|
|
})
|
|
|
|
# Compare text columns
|
|
for col in text_cols:
|
|
if col in python_df.columns and col in sql_df.columns:
|
|
python_vals = python_df[col].fillna('').astype(str)
|
|
sql_vals = sql_df[col].fillna('').astype(str)
|
|
|
|
min_len = min(len(python_vals), len(sql_vals))
|
|
python_vals = python_vals[:min_len]
|
|
sql_vals = sql_vals[:min_len]
|
|
|
|
different = (python_vals != sql_vals).sum()
|
|
|
|
if different > 0:
|
|
differences.append({
|
|
'column': col,
|
|
'type': 'text',
|
|
'different_count': int(different),
|
|
'total_compared': int(min_len)
|
|
})
|
|
|
|
report['differences'] = differences
|
|
report['status'] = 'compared'
|
|
|
|
return report
|
|
|
|
|
|
def print_comparison_report(report: Dict):
|
|
"""Print a formatted comparison report"""
|
|
print("\n" + "="*60)
|
|
print("PYTHON vs SQL OUTPUT COMPARISON")
|
|
print("="*60)
|
|
|
|
print(f"\nRow Counts:")
|
|
print(f" Python: {report['python_count']}")
|
|
print(f" SQL: {report['sql_count']}")
|
|
print(f" Match: {'✅' if report['count_match'] else '❌'}")
|
|
|
|
if report.get('only_in_python'):
|
|
print(f"\n Only in Python: {report['only_in_python']}")
|
|
if report.get('only_in_sql'):
|
|
print(f" Only in SQL: {report['only_in_sql']}")
|
|
if report.get('in_both'):
|
|
print(f" In both: {report['in_both']}")
|
|
|
|
if report.get('missing_in_python'):
|
|
print(f"\n⚠️ Columns missing in Python: {report['missing_in_python']}")
|
|
|
|
if report.get('missing_in_sql'):
|
|
print(f"⚠️ Columns missing in SQL: {report['missing_in_sql']}")
|
|
|
|
if report.get('differences'):
|
|
print(f"\n📊 Column Differences:")
|
|
for diff in report['differences']:
|
|
print(f"\n Column: {diff['column']} ({diff['type']})")
|
|
if diff['type'] == 'numeric':
|
|
print(f" Max difference: {diff['max_difference']:.6f}")
|
|
print(f" Mean difference: {diff['mean_difference']:.6f}")
|
|
print(f" Different rows: {diff['different_count']}")
|
|
else:
|
|
print(f" Different rows: {diff['different_count']} / {diff['total_compared']}")
|
|
else:
|
|
print("\n✅ No significant differences found!")
|
|
|
|
print("\n" + "="*60 + "\n")
|
|
|