feat: Add automatic migrations system
All checks were successful
Deploy Development / deploy (push) Successful in 36s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
- New run_migrations.py script - Runs all SQL files in migrations/ on startup - Tracks executed migrations in schema_migrations table - Retries database connection (30 attempts) - Separate Shinkan DB (shinkan_dev / shinkan) This ensures a clean separation from Mitai database.
This commit is contained in:
parent
91e665c960
commit
fd5efa8662
|
|
@ -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")
|
||||
|
|
|
|||
148
backend/run_migrations.py
Normal file
148
backend/run_migrations.py
Normal file
|
|
@ -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())
|
||||
Loading…
Reference in New Issue
Block a user