Files
praxis/db/migrate.py
T
Praxis CI 73b583342b feat(P01-04-01,P01-04-02): SQLite schema + async store (D-007)
db/schema.sql + db/migrations/0001_init.sql — four tables: learner (single
hardcoded 'learner-1'/'Alex' row, D-007 no auth), sessions (id, learner_id,
scenario_id, started_at, ended_at, branch_path_json, outcome,
cost_estimated_cents, debrief_text, cost_breakdown_json), turns (id,
session_id, seq, role, asr_text, tts_text, latency_ms), progress (learner_id,
scenario_id, attempts, last_outcome). db/migrate.py applies migrations
idempotently via a _migrations tracking table.

db/store.py — PraxisStore async access layer (aiosqlite): start_session,
log_turn, end_session (branch_path + outcome + cost + debrief),
update_progress (increment attempts + last_outcome), get_session, get_turns,
get_learner. Type-annotated SessionRow/TurnRow dataclasses. 6 tests pass
(migration creates all tables, hardcoded learner exists, idempotent
migrations, start→log→end→query full session, update_progress, get_learner).

---ci---
phase: 1
milestone: v0.1
plan: 04
task: 04-01,04-02
status: execute
persona: data-engineer
requirements:
  covered: [REQ-STATE-01]
---/ci---
2026-08-01 13:14:57 +00:00

46 lines
1.4 KiB
Python

"""SQLite migration runner — applies db/migrations/*.sql in order."""
from __future__ import annotations
import sqlite3
from pathlib import Path
_DEFAULT_DB_PATH = Path("praxis.db")
_DEFAULT_MIGRATIONS_DIR = Path(__file__).resolve().parent / "migrations"
def apply_migrations(
db_path: Path | str | None = None,
migrations_dir: Path | None = None,
) -> list[str]:
"""Apply all pending migrations in order. Returns the list of applied names.
Uses a `_migrations` tracking table so re-running is idempotent.
"""
db = Path(db_path) if db_path else _DEFAULT_DB_PATH
mdir = migrations_dir or _DEFAULT_MIGRATIONS_DIR
conn = sqlite3.connect(str(db))
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')))"
)
applied: list[str] = []
for sql_path in sorted(mdir.glob("*.sql")):
mid = sql_path.stem
already = conn.execute(
"SELECT 1 FROM _migrations WHERE id = ?", (mid,)
).fetchone()
if already:
continue
sql = sql_path.read_text(encoding="utf-8")
conn.executescript(sql)
conn.execute("INSERT INTO _migrations (id) VALUES (?)", (mid,))
conn.commit()
applied.append(mid)
return applied
finally:
conn.close()
__all__ = ["apply_migrations"]