- Change image from timescaledb-ha:pg16-ts2.15-oss to pg16-ts2.15 (OSS edition lacks compression, retention, continuous aggregates) - Add postgresql-client to Dockerfile for psql binary - Rewrite run_migrations.py to use psql instead of psycopg2 (psql runs each statement independently; psycopg2 wraps the entire file in one transaction so one error rolls back everything) - Add schema verification: exits 1 if critical tables missing, preventing services from starting with broken schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.3 KiB
Python
88 lines
2.3 KiB
Python
"""
|
|
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()
|