ADR-0013: Unified Database Connection and Transaction Model
Context
PyriteDB maintained two separate connections to the same SQLite database:
1. `self.session` (SQLAlchemy ORM) — used only by `kb_ops.py` and `starred.py` 2. `self._raw_conn` (raw sqlite3) — used by 76+ call sites for everything else
Problems solved
Dual-connection non-atomicity. Operations spanning both connections were not atomic. Writes via `session.commit()` and `_raw_conn.commit()` were separate transactions.
Raw connection leaks everywhere. `db.conn` was used directly by services, plugins, and tests for all operations including writes.
ORM models were decorative. Entry, Tag, Link, etc. models existed but weren't used for writes — `upsert_entry()` used raw SQL `INSERT ... ON CONFLICT`.
Decision
ORM-primary approach
ORM is the primary data access method. Raw SQL is only used for read-only search operations that require FTS5/sqlite-vec virtual table access or complex graph CTEs.
Phase 1 (implemented): ORM for all writes, raw SQL for read-only search
What stays as raw SQL (intentionally)
| Location | Why | |----------|-----| | `queries.py` search/graph/analytics | FTS5 virtual table, complex graph CTEs, read-only | | `embedding_service.py` vec queries | sqlite-vec virtual table, read-only | | `virtual_tables.py` DDL | Virtual table creation has no ORM equivalent | | `connection.py` plugin table DDL | Dynamic schema from plugins | | `connection.py` migrations | Schema evolution | | Extension custom table queries | Plugin-defined tables, no ORM models |
Phase 2 (future): Migrate service-layer `db.conn` access
Replace direct `db.conn` / `db._raw_conn` usage in services and tests with `db.execute_sql()` for reads or ORM for writes.
Phase 3 (future): Migrate extension/plugin access
Update plugin protocol to use `execute_sql()` instead of `context.db.conn.execute()`.