"""SQLite connection and migration runner. The schema lives in backend/migrations/ as numbered .sql files; this module runs them in order against the configured database file and exposes a connection factory that the rest of the app uses. WAL is enabled because the SSE handlers and the reconciler can read while a webhook writes; FK enforcement is on because §5's cascade rules depend on it. Per §4.2, single SQLite file colocated with the FastAPI process. If we outgrow this, the spec calls for a planned migration to Postgres on a separate host — not for a clever sharding scheme bolted on here. """ from __future__ import annotations import sqlite3 from contextlib import contextmanager from pathlib import Path from typing import Iterator from .config import Config MIGRATIONS_DIR = Path(__file__).resolve().parent.parent / "migrations" def connect(path: Path) -> sqlite3.Connection: conn = sqlite3.connect(path, isolation_level=None, check_same_thread=False) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA synchronous = NORMAL") return conn def run_migrations(config: Config) -> None: conn = connect(config.database_path) try: conn.execute( """ CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')) ) """ ) applied = {row["version"] for row in conn.execute("SELECT version FROM schema_migrations")} for path in sorted(MIGRATIONS_DIR.glob("*.sql")): version = path.stem if version in applied: continue sql = path.read_text() conn.executescript("BEGIN; " + sql + "; COMMIT;") conn.execute("INSERT INTO schema_migrations (version) VALUES (?)", (version,)) finally: conn.close() _CONN: sqlite3.Connection | None = None def init(config: Config) -> None: """Open the long-lived connection. Call once at startup, after migrations.""" global _CONN if _CONN is not None: return _CONN = connect(config.database_path) def conn() -> sqlite3.Connection: if _CONN is None: raise RuntimeError("db.init() has not been called") return _CONN @contextmanager def tx() -> Iterator[sqlite3.Connection]: """Wrap a block in a transaction. Rolls back on exception.""" c = conn() c.execute("BEGIN") try: yield c c.execute("COMMIT") except Exception: c.execute("ROLLBACK") raise