#!/usr/bin/env python3 """ migrate_backlog_postgres.py — Idempotent DDL migration for Engineering Backlog. DAARION.city Creates tables and indexes if they do not exist. Safe to re-run. Usage: python3 ops/scripts/migrate_backlog_postgres.py python3 ops/scripts/migrate_backlog_postgres.py --dry-run python3 ops/scripts/migrate_backlog_postgres.py --dsn "postgresql://user:pass@host/db" """ from __future__ import annotations import argparse import os import sys DDL = [ # ── backlog_items ───────────────────────────────────────────────────────── """ CREATE TABLE IF NOT EXISTS backlog_items ( id TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), env TEXT NOT NULL DEFAULT 'prod', service TEXT NOT NULL DEFAULT '', category TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', priority TEXT NOT NULL DEFAULT 'P2', status TEXT NOT NULL DEFAULT 'open', owner TEXT NOT NULL DEFAULT 'oncall', due_date DATE, source TEXT NOT NULL DEFAULT 'manual', dedupe_key TEXT NOT NULL UNIQUE DEFAULT '', evidence_refs JSONB NOT NULL DEFAULT '{}', tags JSONB NOT NULL DEFAULT '[]', meta JSONB NOT NULL DEFAULT '{}' ) """, # ── backlog_events ──────────────────────────────────────────────────────── """ CREATE TABLE IF NOT EXISTS backlog_events ( id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES backlog_items(id) ON DELETE CASCADE, ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), type TEXT NOT NULL DEFAULT 'comment', message TEXT NOT NULL DEFAULT '', actor TEXT NOT NULL DEFAULT 'system', meta JSONB NOT NULL DEFAULT '{}' ) """, # ── Indexes ─────────────────────────────────────────────────────────────── "CREATE INDEX IF NOT EXISTS idx_backlog_items_env_status ON backlog_items (env, status)", "CREATE INDEX IF NOT EXISTS idx_backlog_items_service ON backlog_items (service)", "CREATE INDEX IF NOT EXISTS idx_backlog_items_due_date ON backlog_items (due_date)", "CREATE INDEX IF NOT EXISTS idx_backlog_items_owner ON backlog_items (owner)", "CREATE INDEX IF NOT EXISTS idx_backlog_items_category ON backlog_items (category)", "CREATE INDEX IF NOT EXISTS idx_backlog_events_item_id ON backlog_events (item_id)", "CREATE INDEX IF NOT EXISTS idx_backlog_events_ts ON backlog_events (ts)", ] def migrate(dsn: str, dry_run: bool = False) -> None: print(f"[backlog migration] DSN: {dsn!r} dry_run={dry_run}") if dry_run: print("[dry-run] Would execute the following DDL statements:") for stmt in DDL: print(" ", stmt.strip()[:120]) return try: import psycopg2 except ImportError: print("ERROR: psycopg2 not installed. Run: pip install psycopg2-binary", file=sys.stderr) sys.exit(1) conn = psycopg2.connect(dsn) conn.autocommit = True try: with conn.cursor() as cur: for stmt in DDL: stmt = stmt.strip() if not stmt: continue print(f" EXEC: {stmt[:80].replace(chr(10), ' ')}…") cur.execute(stmt) print("[backlog migration] Done. All DDL applied idempotently.") finally: conn.close() def main() -> None: parser = argparse.ArgumentParser( description="Idempotent Postgres DDL migration for Engineering Backlog" ) parser.add_argument( "--dsn", default=os.environ.get( "BACKLOG_POSTGRES_DSN", os.environ.get("POSTGRES_DSN", "postgresql://localhost/daarion"), ), help="Postgres DSN (default: $BACKLOG_POSTGRES_DSN or $POSTGRES_DSN)", ) parser.add_argument( "--dry-run", action="store_true", help="Print DDL without executing", ) args = parser.parse_args() migrate(args.dsn, dry_run=args.dry_run) if __name__ == "__main__": main()