Files
rfc-app/backend/app/db.py
T
Ben Stull 779ba6db59 Slice 1: scaffolding + propose-to-super-draft vertical
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the
§5 schema (six numbered migrations), Gitea OAuth + §6 user
provisioning, the §7 catalog left pane, and the propose-to-merge
vertical: propose modal opens an idea PR against the meta repo, an
owner merges from the pending-idea view, the cache picks it up via
webhook or reconciler sweep, and the catalog renders the new
super-draft.

Per §1 the bot is the only Git writer; every commit, branch
creation, and PR merge carries the §6.5 On-behalf-of: trailer and
an `actions` audit row. Per §4 the cache is never written from a
user action — it's webhook+reconciler only.

Covered by `backend/tests/test_propose_vertical.py` against an
in-process Gitea simulator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:31:11 -07:00

85 lines
2.6 KiB
Python

"""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