tracksolid_timescale_grafan.../run_migrations.py
David Kiania 326764e1a0 Fix migration failures: switch to full TimescaleDB + use psql runner
- 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>
2026-04-08 17:17:58 +03:00

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()