"""Tiny migration runner for the numbered SQL files in ./migrations. Each file runs once, in its own transaction, and is recorded in elec.schema_migrations with a checksum. Editing an applied file is refused rather than silently ignored - add a new numbered file instead. """ from __future__ import annotations import hashlib import logging from pathlib import Path from typing import List from app.electronics.db.connection import connect logger = logging.getLogger(__name__) MIGRATIONS_DIR = Path(__file__).resolve().parent / "migrations" _BOOTSTRAP = """ CREATE SCHEMA IF NOT EXISTS elec; CREATE TABLE IF NOT EXISTS elec.schema_migrations ( version TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() ); """ def _files() -> List[Path]: return sorted(MIGRATIONS_DIR.glob("[0-9][0-9][0-9][0-9]_*.sql")) def run_migrations() -> List[str]: """Apply pending migrations. Returns the versions applied by this call.""" applied_now: List[str] = [] with connect() as conn: conn.execute(_BOOTSTRAP) conn.commit() done = { r["version"]: r["checksum"] for r in conn.execute("SELECT version, checksum FROM elec.schema_migrations") } for path in _files(): sql = path.read_text(encoding="utf-8") checksum = hashlib.sha256(sql.encode("utf-8")).hexdigest() version = path.stem if version in done: if done[version] != checksum: raise RuntimeError( f"Migration {version} was edited after it was applied. " f"Revert the edit and add a new numbered migration instead." ) continue logger.info("Applying migration %s", version) with conn.transaction(): conn.execute(sql) conn.execute( "INSERT INTO elec.schema_migrations (version, checksum) VALUES (%s, %s)", (version, checksum), ) applied_now.append(version) return applied_now