Files
Church-Music/legacy-site/backend/analyze_and_fix_database.py

279 lines
11 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Comprehensive Database Schema Analysis and Fix
Analyzes and fixes all database issues:
- Schema correctness
- Missing columns
- Foreign key constraints
- Indexes for performance
- Backend/database alignment
"""
from postgresql_models import engine, Base, Profile, Song, Plan, PlanSong, ProfileSong, ProfileSongKey
from sqlalchemy import text, inspect, Column, String, Text
import sys
def analyze_schema():
"""Analyze current database schema and identify issues"""
print("=" * 70)
print("DATABASE SCHEMA ANALYSIS")
print("=" * 70)
inspector = inspect(engine)
issues = []
fixes = []
# Check Profile table
print("\n📋 CHECKING PROFILES TABLE")
print("-" * 70)
profile_cols = {col['name']: col for col in inspector.get_columns('profiles')}
# Check for missing columns referenced in app.py
required_profile_cols = {
'email': 'VARCHAR(255)',
'contact_number': 'VARCHAR(50)',
'notes': 'TEXT'
}
for col_name, col_type in required_profile_cols.items():
if col_name not in profile_cols:
issues.append(f"❌ Profile.{col_name} column is MISSING (referenced in app.py)")
fixes.append(f"ALTER TABLE profiles ADD COLUMN {col_name} {col_type} DEFAULT '';")
else:
print(f" ✅ Profile.{col_name} exists")
# Check Songs table
print("\n📋 CHECKING SONGS TABLE")
print("-" * 70)
songs_cols = {col['name']: col for col in inspector.get_columns('songs')}
if not songs_cols['title']['nullable']:
print(" ✅ songs.title is NOT NULL")
else:
issues.append("❌ songs.title should be NOT NULL")
fixes.append("UPDATE songs SET title = 'Untitled' WHERE title IS NULL;")
fixes.append("ALTER TABLE songs ALTER COLUMN title SET NOT NULL;")
# Check created_at/updated_at should use INTEGER (timestamps)
if str(songs_cols['created_at']['type']) == 'BIGINT':
print(" ✅ songs.created_at is BIGINT (timestamp)")
else:
issues.append(f"❌ songs.created_at type is {songs_cols['created_at']['type']}, should be BIGINT")
# Check Plans table
print("\n📋 CHECKING PLANS TABLE")
print("-" * 70)
plans_cols = {col['name']: col for col in inspector.get_columns('plans')}
if not plans_cols['date']['nullable']:
print(" ✅ plans.date is NOT NULL")
else:
issues.append("❌ plans.date should be NOT NULL")
fixes.append("UPDATE plans SET date = TO_CHAR(CURRENT_DATE, 'YYYY-MM-DD') WHERE date IS NULL;")
fixes.append("ALTER TABLE plans ALTER COLUMN date SET NOT NULL;")
# Check plan_songs table - id can be VARCHAR(255) for UUIDs or INTEGER for autoincrement
print("\n📋 CHECKING PLAN_SONGS TABLE")
print("-" * 70)
plan_songs_cols = {col['name']: col for col in inspector.get_columns('plan_songs')}
plan_songs_id_type = str(plan_songs_cols['id']['type'])
if 'VARCHAR' in plan_songs_id_type:
print(f" ✅ plan_songs.id is VARCHAR (using UUIDs)")
elif 'INTEGER' in plan_songs_id_type or 'SERIAL' in plan_songs_id_type:
print(f" ✅ plan_songs.id is INTEGER (autoincrement)")
else:
issues.append(f"❌ plan_songs.id has unexpected type: {plan_songs_id_type}")
print(f" ❌ plan_songs.id type: {plan_songs_id_type} (unexpected)")
# Check Indexes
print("\n📊 CHECKING INDEXES")
print("-" * 70)
required_indexes = {
'songs': ['idx_song_title', 'idx_song_artist', 'idx_song_band', 'idx_song_singer'],
'profiles': ['idx_profile_name'],
'plans': ['idx_plan_date', 'idx_plan_profile'],
'plan_songs': ['idx_plan_songs_order'],
'profile_songs': ['idx_profile_songs_profile'],
'profile_song_keys': ['idx_profile_song_keys']
}
for table, expected_indexes in required_indexes.items():
existing = [idx['name'] for idx in inspector.get_indexes(table)]
for idx_name in expected_indexes:
if idx_name in existing:
print(f"{table}.{idx_name}")
else:
issues.append(f"❌ Missing index: {table}.{idx_name}")
if idx_name == 'idx_song_title':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(title);")
elif idx_name == 'idx_song_artist':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(artist);")
elif idx_name == 'idx_song_band':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(band);")
elif idx_name == 'idx_song_singer':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(singer);")
elif idx_name == 'idx_profile_name':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(name);")
elif idx_name == 'idx_plan_date':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(date);")
elif idx_name == 'idx_plan_profile':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(profile_id);")
elif idx_name == 'idx_plan_songs_order':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(plan_id, order_index);")
elif idx_name == 'idx_profile_songs_profile':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(profile_id);")
elif idx_name == 'idx_profile_song_keys':
fixes.append(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}(profile_id, song_id);")
# Check Foreign Keys CASCADE
print("\n🔗 CHECKING FOREIGN KEY CONSTRAINTS")
print("-" * 70)
fk_tables = ['plan_songs', 'profile_songs', 'profile_song_keys']
for table in fk_tables:
fks = inspector.get_foreign_keys(table)
for fk in fks:
cascade = fk['options'].get('ondelete', 'NO ACTION')
if cascade == 'CASCADE':
print(f"{table}.{fk['name']} → CASCADE")
else:
issues.append(f"{table}.{fk['name']} should CASCADE on delete (currently: {cascade})")
# Check plans.profile_id should SET NULL
plans_fks = inspector.get_foreign_keys('plans')
for fk in plans_fks:
if 'profile_id' in fk['constrained_columns']:
cascade = fk['options'].get('ondelete', 'NO ACTION')
if cascade == 'SET NULL':
print(f" ✅ plans.profile_id → SET NULL")
else:
issues.append(f"❌ plans.profile_id should SET NULL on delete (currently: {cascade})")
# Summary
print("\n" + "=" * 70)
print("ANALYSIS SUMMARY")
print("=" * 70)
if issues:
print(f"\n⚠️ Found {len(issues)} issues:\n")
for issue in issues:
print(f" {issue}")
else:
print("\n✅ No issues found! Database schema is correct.")
if fixes:
print(f"\n🔧 Suggested fixes ({len(fixes)} SQL statements):\n")
for i, fix in enumerate(fixes, 1):
print(f"{i}. {fix}")
return issues, fixes
def apply_fixes(fixes):
"""Apply database fixes"""
if not fixes:
print("\n✅ No fixes needed!")
return True
print("\n" + "=" * 70)
print("APPLYING FIXES")
print("=" * 70)
try:
with engine.begin() as conn:
for i, fix in enumerate(fixes, 1):
if fix.startswith('--'):
print(f"\n{fix}")
continue
print(f"\n{i}. Executing: {fix[:80]}...")
try:
conn.execute(text(fix))
print(" ✅ Success")
except Exception as e:
print(f" ⚠️ Warning: {str(e)}")
# Continue with other fixes
print("\n✅ All fixes applied successfully!")
return True
except Exception as e:
print(f"\n❌ Error applying fixes: {str(e)}")
return False
def verify_backend_alignment():
"""Verify backend code aligns with database schema"""
print("\n" + "=" * 70)
print("BACKEND ALIGNMENT VERIFICATION")
print("=" * 70)
inspector = inspect(engine)
# Check Profile model
print("\n📋 Profile Model vs Database")
print("-" * 70)
profile_db_cols = set(col['name'] for col in inspector.get_columns('profiles'))
profile_model_attrs = {'id', 'first_name', 'last_name', 'name', 'default_key', 'email', 'contact_number', 'notes'}
missing_in_db = profile_model_attrs - profile_db_cols
extra_in_db = profile_db_cols - profile_model_attrs - {'metadata', 'registry'}
if missing_in_db:
print(f" ⚠️ Model attributes not in DB: {missing_in_db}")
if extra_in_db:
print(f" DB columns not in model: {extra_in_db}")
if not missing_in_db and not extra_in_db:
print(" ✅ Profile model aligned with database")
# Check Song model
print("\n📋 Song Model vs Database")
print("-" * 70)
song_db_cols = set(col['name'] for col in inspector.get_columns('songs'))
song_model_attrs = {'id', 'title', 'artist', 'band', 'singer', 'lyrics', 'chords', 'memo', 'created_at', 'updated_at'}
missing_in_db = song_model_attrs - song_db_cols
extra_in_db = song_db_cols - song_model_attrs - {'metadata', 'registry'}
if missing_in_db:
print(f" ⚠️ Model attributes not in DB: {missing_in_db}")
if extra_in_db:
print(f" DB columns not in model: {extra_in_db}")
if not missing_in_db and not extra_in_db:
print(" ✅ Song model aligned with database")
print("\n" + "=" * 70)
if __name__ == "__main__":
print("\n🔍 Starting comprehensive database analysis...\n")
# Step 1: Analyze
issues, fixes = analyze_schema()
# Step 2: Apply fixes
if fixes and '--apply' in sys.argv:
print("\n⚠️ --apply flag detected, applying fixes...")
if apply_fixes(fixes):
print("\n✅ Fixes applied successfully!")
# Re-analyze to verify
print("\n🔍 Re-analyzing database...")
issues, _ = analyze_schema()
else:
print("\n❌ Some fixes failed. Please review manually.")
sys.exit(1)
elif fixes:
print("\n💡 To apply these fixes, run:")
print(f" python3 {sys.argv[0]} --apply")
# Step 3: Verify alignment
verify_backend_alignment()
# Final status
print("\n" + "=" * 70)
if issues:
print(f"⚠️ {len(issues)} issues found. Run with --apply to fix.")
sys.exit(1)
else:
print("✅ Database schema is correct and aligned!")
sys.exit(0)