""" run_migrations.py — Idempotent SQL migration runner for Docker init. Uses psql (not psycopg2) so each statement runs independently — one error doesn't roll back the entire file. """ import os import subprocess import sys import psycopg2 DATABASE_URL = os.environ["DATABASE_URL"] MIGRATIONS = [ "02_tracksolid_full_schema_rev.sql", "03_webhook_schema_migration.sql", ] CRITICAL_TABLES = [ "tracksolid.devices", "tracksolid.api_token_cache", "tracksolid.ingestion_log", "tracksolid.live_positions", "tracksolid.position_history", "tracksolid.trips", "tracksolid.alarms", "tracksolid.obd_readings", ] def run_file(path, filename): """Execute a SQL file via psql. Returns True on success.""" print(f"Running {filename}...") result = subprocess.run( ["psql", DATABASE_URL, "-f", path], capture_output=True, text=True, ) # psql prints errors to stderr but continues by default errors = [l for l in result.stderr.splitlines() if "ERROR:" in l] if errors: for e in errors: print(f" WARN: {e.strip()}") return False print(f" OK: {filename}") return True def verify_schema(): """Verify critical tables exist. Exit 1 if not — prevents services from starting.""" print("Verifying schema...") conn = psycopg2.connect(DATABASE_URL) cur = conn.cursor() missing = [] for table in CRITICAL_TABLES: schema, name = table.split(".") cur.execute( "SELECT 1 FROM information_schema.tables WHERE table_schema=%s AND table_name=%s", (schema, name), ) if not cur.fetchone(): missing.append(table) cur.close() conn.close() if missing: print(f"FATAL: Missing critical tables: {', '.join(missing)}") print("Schema bootstrap failed. Services cannot start.") sys.exit(1) print(" All critical tables verified.") def main(): print("=== Database Migration Runner ===") for sql_file in MIGRATIONS: path = os.path.join("/app", sql_file) if not os.path.exists(path): print(f" SKIP: {sql_file} (not found)") continue run_file(path, sql_file) verify_schema() print("Migrations complete.") if __name__ == "__main__": main()