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>
This commit is contained in:
parent
3bbf3b777d
commit
326764e1a0
3 changed files with 59 additions and 36 deletions
|
|
@ -7,6 +7,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
# Install system dependencies (Required for Postgres and Healthchecks)
|
# Install system dependencies (Required for Postgres and Healthchecks)
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
|
postgresql-client \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
timescale_db:
|
timescale_db:
|
||||||
image: timescale/timescaledb-ha:pg16-ts2.15-oss
|
image: timescale/timescaledb-ha:pg16-ts2.15
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=${POSTGRES_DB}
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
"""
|
"""
|
||||||
run_migrations.py — Idempotent SQL migration runner for Docker init service.
|
run_migrations.py — Idempotent SQL migration runner for Docker init.
|
||||||
Executes each .sql migration file in order using psycopg2.
|
Uses psql (not psycopg2) so each statement runs independently —
|
||||||
Tolerates re-run errors (e.g. "policy already exists") so deploys are safe.
|
one error doesn't roll back the entire file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
DATABASE_URL = os.environ["DATABASE_URL"]
|
DATABASE_URL = os.environ["DATABASE_URL"]
|
||||||
|
|
@ -15,51 +17,71 @@ MIGRATIONS = [
|
||||||
"03_webhook_schema_migration.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(conn, path, filename):
|
|
||||||
"""Execute a SQL file. Returns True on success, False on error."""
|
def run_file(path, filename):
|
||||||
with open(path) as f:
|
"""Execute a SQL file via psql. Returns True on success."""
|
||||||
sql = f.read()
|
print(f"Running {filename}...")
|
||||||
try:
|
result = subprocess.run(
|
||||||
with conn.cursor() as cur:
|
["psql", DATABASE_URL, "-f", path],
|
||||||
cur.execute(sql)
|
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}")
|
print(f" OK: {filename}")
|
||||||
return True
|
return True
|
||||||
except psycopg2.Error as e:
|
|
||||||
msg = (e.pgerror or str(e)).strip().split("\n")[0]
|
|
||||||
print(f" WARN: {filename}: {msg}")
|
def verify_schema():
|
||||||
# Connection is now in error state — must reset
|
"""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()
|
conn.close()
|
||||||
return False
|
|
||||||
|
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():
|
def main():
|
||||||
print("=== Database Migration Runner ===")
|
print("=== Database Migration Runner ===")
|
||||||
conn = psycopg2.connect(DATABASE_URL)
|
|
||||||
conn.autocommit = True
|
|
||||||
|
|
||||||
warnings = 0
|
|
||||||
for sql_file in MIGRATIONS:
|
for sql_file in MIGRATIONS:
|
||||||
path = os.path.join("/app", sql_file)
|
path = os.path.join("/app", sql_file)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
print(f" SKIP: {sql_file} (not found)")
|
print(f" SKIP: {sql_file} (not found)")
|
||||||
continue
|
continue
|
||||||
|
run_file(path, sql_file)
|
||||||
|
|
||||||
print(f"Running {sql_file}...")
|
verify_schema()
|
||||||
ok = run_file(conn, path, sql_file)
|
print("Migrations complete.")
|
||||||
if not ok:
|
|
||||||
warnings += 1
|
|
||||||
# Reconnect for the next file
|
|
||||||
conn = psycopg2.connect(DATABASE_URL)
|
|
||||||
conn.autocommit = True
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if warnings:
|
|
||||||
print(f"Completed with {warnings} warning(s) (expected on re-deploy).")
|
|
||||||
else:
|
|
||||||
print("All migrations applied cleanly.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue