#!/usr/bin/env python3 """ Shinkan Jinkendo - Database Migrations Runner Runs all SQL migrations in backend/migrations/ directory Tracks executed migrations in schema_migrations table """ import os import sys import psycopg2 from psycopg2 import sql import time def get_db_connection(): """Get database connection with retries""" max_retries = 30 for i in range(max_retries): try: conn = psycopg2.connect( host=os.getenv("DB_HOST", "localhost"), port=os.getenv("DB_PORT", "5432"), database=os.getenv("DB_NAME", "shinkan_dev"), user=os.getenv("DB_USER", "shinkan_dev"), password=os.getenv("DB_PASSWORD", "dev_password") ) print(f"✓ Connected to database: {os.getenv('DB_NAME')}") return conn except psycopg2.OperationalError as e: if i < max_retries - 1: print(f"Waiting for database... ({i+1}/{max_retries})") time.sleep(2) else: print(f"✗ Failed to connect to database after {max_retries} attempts") raise def init_migrations_table(conn): """Create schema_migrations table if not exists""" with conn.cursor() as cur: cur.execute(""" CREATE TABLE IF NOT EXISTS schema_migrations ( id SERIAL PRIMARY KEY, migration VARCHAR(255) UNIQUE NOT NULL, executed_at TIMESTAMP DEFAULT NOW() ) """) conn.commit() print("✓ Migrations table initialized") def get_executed_migrations(conn): """Get list of already executed migrations""" with conn.cursor() as cur: cur.execute("SELECT migration FROM schema_migrations ORDER BY migration") return set(row[0] for row in cur.fetchall()) def get_pending_migrations(executed): """Get list of pending migrations from migrations directory""" migrations_dir = "/app/migrations" if not os.path.exists(migrations_dir): migrations_dir = "backend/migrations" # Local development all_migrations = [] for filename in sorted(os.listdir(migrations_dir)): if filename.endswith('.sql') and filename[0].isdigit(): migration_name = filename.replace('.sql', '') if migration_name not in executed: all_migrations.append((migration_name, os.path.join(migrations_dir, filename))) return all_migrations def run_migration(conn, migration_name, filepath): """Run a single migration file""" print(f"Running migration: {migration_name}") with open(filepath, 'r', encoding='utf-8') as f: sql_content = f.read() try: with conn.cursor() as cur: # Execute migration cur.execute(sql_content) # Record migration cur.execute( "INSERT INTO schema_migrations (migration) VALUES (%s)", (migration_name,) ) conn.commit() print(f" ✓ {migration_name} executed successfully") return True except Exception as e: conn.rollback() print(f" ✗ {migration_name} failed: {e}") return False def main(): """Main migrations runner""" print("=" * 60) print("Shinkan Jinkendo - Database Migrations") print("=" * 60) try: # Connect to database conn = get_db_connection() # Initialize migrations tracking init_migrations_table(conn) # Get executed and pending migrations executed = get_executed_migrations(conn) pending = get_pending_migrations(executed) if not pending: print("✓ No pending migrations - database is up to date") return 0 print(f"\nFound {len(pending)} pending migration(s):") for name, _ in pending: print(f" - {name}") print() # Run pending migrations failed = [] for migration_name, filepath in pending: if not run_migration(conn, migration_name, filepath): failed.append(migration_name) break # Stop on first failure conn.close() # Summary print("\n" + "=" * 60) if failed: print(f"✗ Migration failed: {failed[0]}") print("=" * 60) return 1 else: print(f"✓ All migrations executed successfully ({len(pending)} total)") print("=" * 60) return 0 except Exception as e: print(f"\n✗ Error: {e}") return 1 if __name__ == "__main__": sys.exit(main())