#!/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)