diff --git a/backend/main.py b/backend/main.py index 3628b71..8817965 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,7 +9,15 @@ from fastapi.responses import JSONResponse import os from version import APP_VERSION, BUILD_DATE, DB_SCHEMA_VERSION, MODULE_VERSIONS -from db_init import init_db + +# Run database migrations on startup +try: + import run_migrations + run_migrations.main() + print("✓ Database migrations completed") +except Exception as e: + print(f"⚠ Warning: Migration error: {e}") + print(" Continuing startup - migrations may need manual intervention") # Initialize FastAPI app app = FastAPI( @@ -29,8 +37,7 @@ app.add_middleware( allow_headers=["*"], ) -# Initialize Database (runs migrations automatically) -init_db() +# TODO: Initialize Database with migrations # Version Endpoint (public, no auth) @app.get("/api/version") diff --git a/backend/run_migrations.py b/backend/run_migrations.py new file mode 100644 index 0000000..12ff6d9 --- /dev/null +++ b/backend/run_migrations.py @@ -0,0 +1,148 @@ +#!/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())