Files
2026-07-13 13:35:10 +08:00

318 lines
12 KiB
Python

#!/usr/bin/env python3
"""
SQL Dataset Validation Script
This script validates the Text-to-SQL dataset by executing each SQL query
against the database and capturing results for manual verification.
Usage:
python validate_sql_dataset.py
Output:
- validation_results.json: Detailed results for each query
- validation_summary.json: Summary statistics
"""
import csv
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
import pandas as pd
# Import our database utilities
from .db_utils import SQLiteDB, execute_sql
def load_dataset(csv_path: str = "datasets/booksql_sample.csv") -> List[Dict[str, Any]]:
"""
Load the SQL dataset from CSV file.
Args:
csv_path: Path to the CSV file containing queries
Returns:
List of dictionaries containing query data
"""
dataset = []
csv_file = Path(csv_path)
if not csv_file.exists():
raise FileNotFoundError(f"Dataset file not found: {csv_path}")
with open(csv_file, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader):
dataset.append({
'index': i,
'query': row['Query'].strip(),
'sql': row['SQL'].strip(),
'level': row['Levels'].strip(),
'split': row['split'].strip()
})
return dataset
def execute_and_validate_query(query_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute a single SQL query and capture results.
Args:
query_data: Dictionary containing query information
Returns:
Dictionary with execution results
"""
result = {
'index': query_data['index'],
'natural_language_query': query_data['query'],
'sql_query': query_data['sql'],
'difficulty_level': query_data['level'],
'dataset_split': query_data['split'],
'execution_success': False,
'execution_time': None,
'error_message': None,
'result_data': None,
'result_shape': None,
'result_columns': None
}
# Record execution time
start_time = datetime.now()
try:
# Execute the SQL query with case-insensitive string matching
success, query_result = execute_sql(query_data['sql'], case_insensitive=True)
end_time = datetime.now()
result['execution_time'] = (end_time - start_time).total_seconds()
if success and isinstance(query_result, pd.DataFrame):
result['execution_success'] = True
result['result_shape'] = list(query_result.shape) # [rows, columns]
result['result_columns'] = list(query_result.columns)
# Convert DataFrame to list of dictionaries for JSON serialization
# Limit to first 10 rows to keep output manageable
if len(query_result) > 10:
sample_data = query_result.head(10)
result['result_data'] = sample_data.to_dict('records')
result['result_truncated'] = True
result['total_rows'] = len(query_result)
else:
result['result_data'] = query_result.to_dict('records')
result['result_truncated'] = False
result['total_rows'] = len(query_result)
# Classify result type for better reporting
if len(query_result) == 0:
result['result_type'] = 'empty'
elif len(query_result) > 0:
first_row = query_result.iloc[0]
# Check if all values in the first row are null/None
if all(pd.isna(value) or value is None for value in first_row):
result['result_type'] = 'null_values'
else:
result['result_type'] = 'has_data'
else:
result['result_type'] = 'has_data'
else:
result['execution_success'] = False
result['error_message'] = str(query_result)
result['result_type'] = 'failed'
except Exception as e:
end_time = datetime.now()
result['execution_time'] = (end_time - start_time).total_seconds()
result['execution_success'] = False
result['error_message'] = f"Unexpected error: {str(e)}"
result['result_type'] = 'failed'
return result
def generate_summary_statistics(results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Generate summary statistics from validation results.
Args:
results: List of validation results
Returns:
Dictionary containing summary statistics
"""
total_queries = len(results)
successful_queries = sum(1 for r in results if r['execution_success'])
failed_queries = total_queries - successful_queries
# Count by result type
result_type_counts = {
'has_data': sum(1 for r in results if r.get('result_type') == 'has_data'),
'null_values': sum(1 for r in results if r.get('result_type') == 'null_values'),
'empty': sum(1 for r in results if r.get('result_type') == 'empty'),
'failed': sum(1 for r in results if r.get('result_type') == 'failed')
}
# Group by difficulty level
level_stats = {}
for result in results:
level = result['difficulty_level']
if level not in level_stats:
level_stats[level] = {
'total': 0, 'successful': 0, 'failed': 0,
'has_data': 0, 'null_values': 0, 'empty': 0
}
level_stats[level]['total'] += 1
if result['execution_success']:
level_stats[level]['successful'] += 1
else:
level_stats[level]['failed'] += 1
# Count by result type for this level
result_type = result.get('result_type', 'unknown')
if result_type in level_stats[level]:
level_stats[level][result_type] += 1
# Calculate success rates
for level in level_stats:
total = level_stats[level]['total']
successful = level_stats[level]['successful']
level_stats[level]['success_rate'] = successful / total if total > 0 else 0
# Common error types
error_types = {}
for result in results:
if not result['execution_success'] and result['error_message']:
# Extract first part of error message as error type
error_type = result['error_message'].split(':')[0]
error_types[error_type] = error_types.get(error_type, 0) + 1
# Average execution time
execution_times = [r['execution_time'] for r in results if r['execution_time'] is not None]
avg_execution_time = sum(execution_times) / len(execution_times) if execution_times else 0
summary = {
'validation_timestamp': datetime.now().isoformat(),
'total_queries': total_queries,
'successful_queries': successful_queries,
'failed_queries': failed_queries,
'overall_success_rate': successful_queries / total_queries if total_queries > 0 else 0,
'average_execution_time_seconds': avg_execution_time,
'result_type_counts': result_type_counts,
'statistics_by_difficulty': level_stats,
'common_error_types': error_types,
'sample_successful_queries': [
r['index'] for r in results if r['execution_success']
][:5], # First 5 successful queries
'sample_failed_queries': [
r['index'] for r in results if not r['execution_success']
][:5] # First 5 failed queries
}
return summary
def main():
"""Main validation script."""
print("🔍 Starting SQL Dataset Validation...")
print("=" * 50)
# Load dataset
try:
dataset = load_dataset("datasets/booksql_sample.csv")
print(f"📊 Loaded {len(dataset)} queries from dataset")
except FileNotFoundError as e:
print(f"❌ Error: {e}")
return
except Exception as e:
print(f"❌ Unexpected error loading dataset: {e}")
return
# Validate database connection
print("🔗 Testing database connection...")
db = SQLiteDB()
success, message = db.connect()
if not success:
print(f"❌ Database connection failed: {message}")
print("💡 Make sure the BookSQL database is available at: BookSQL-files/BookSQL/accounting.sqlite")
return
# Get database info
success, tables = db.get_table_names()
if success:
print(f"✅ Database connected. Found tables: {tables}")
db.disconnect()
# Execute all queries
print(f"\n🚀 Executing {len(dataset)} SQL queries...")
results = []
for i, query_data in enumerate(dataset):
print(f"Processing query {i+1}/{len(dataset)}: {query_data['level']} level", end=" ... ")
result = execute_and_validate_query(query_data)
results.append(result)
if result['execution_success']:
print("✅")
else:
print("❌")
# Generate summary
print("\n📈 Generating summary statistics...")
summary = generate_summary_statistics(results)
# Save results
print("💾 Saving validation results...")
# Save detailed results
with open('validation_results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
# Save summary
with open('validation_summary.json', 'w', encoding='utf-8') as f:
json.dump(summary, f, indent=2, ensure_ascii=False)
# Print summary to console
print("\n" + "=" * 50)
print("📊 VALIDATION SUMMARY")
print("=" * 50)
print(f"Total Queries: {summary['total_queries']}")
print(f"Successful: {summary['successful_queries']} ({summary['overall_success_rate']:.1%})")
print(f"Failed: {summary['failed_queries']}")
print(f"Average Execution Time: {summary['average_execution_time_seconds']:.3f}s")
print("\n📈 Result Type Distribution:")
result_counts = summary['result_type_counts']
total = summary['total_queries']
print(f" ✅ Has Data: {result_counts['has_data']}/{total} ({result_counts['has_data']/total:.1%})")
print(f" 🔍 NULL Values: {result_counts['null_values']}/{total} ({result_counts['null_values']/total:.1%})")
print(f" 📭 Empty Results: {result_counts['empty']}/{total} ({result_counts['empty']/total:.1%})")
print(f" ❌ Failed: {result_counts['failed']}/{total} ({result_counts['failed']/total:.1%})")
print("\n📈 Success Rate by Difficulty:")
for level, stats in summary['statistics_by_difficulty'].items():
print(f" {level.capitalize()}: {stats['successful']}/{stats['total']} ({stats['success_rate']:.1%})")
print(f" ✅ Data: {stats['has_data']}, 🔍 NULL: {stats['null_values']}, 📭 Empty: {stats['empty']}, ❌ Failed: {stats['failed']}")
if summary['common_error_types']:
print("\n⚠️ Common Error Types:")
for error_type, count in sorted(summary['common_error_types'].items(),
key=lambda x: x[1], reverse=True)[:5]:
print(f" {error_type}: {count} occurrences")
print("\n💾 Detailed results saved to:")
print(" - validation_results.json (detailed results)")
print(" - validation_summary.json (summary statistics)")
if summary['failed_queries'] > 0:
print("\n🔍 Review failed queries in validation_results.json")
print("💡 Check if database schema matches expected tables/columns")
if __name__ == "__main__":
main()