feat: Add automatic migrations system
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:
Lars 2026-04-21 14:49:28 +02:00
parent 91e665c960
commit fd5efa8662
2 changed files with 158 additions and 3 deletions

View File

@ -9,7 +9,15 @@ from fastapi.responses import JSONResponse
import os import os
from version import APP_VERSION, BUILD_DATE, DB_SCHEMA_VERSION, MODULE_VERSIONS 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 # Initialize FastAPI app
app = FastAPI( app = FastAPI(
@ -29,8 +37,7 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Initialize Database (runs migrations automatically) # TODO: Initialize Database with migrations
init_db()
# Version Endpoint (public, no auth) # Version Endpoint (public, no auth)
@app.get("/api/version") @app.get("/api/version")

148
backend/run_migrations.py Normal file
View 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())