Initial commit - Church Music Database
This commit is contained in:
278
legacy-site/backend/analyze_and_fix_database.py
Normal file
278
legacy-site/backend/analyze_and_fix_database.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user