shinkan-jinkendo/backend/run_migrations.py
Lars fd5efa8662
All checks were successful
Deploy Development / deploy (push) Successful in 36s
feat: Add automatic migrations system
- 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.
2026-04-21 14:49:28 +02:00

149 lines
4.6 KiB
Python

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