Documentation
¶
Overview ¶
Package sqlite is Harbor's SQLite-backed `turns.Store` driver — the durable leg of the turn projection persistence triad (in-memory floor, SQLite, Postgres) behind the `sessions.turns.list` / `sessions.turns.get` Protocol surface.
The driver is built on `modernc.org/sqlite` — a CGo-free SQLite engine (AGENTS.md §5). Builds remain `CGO_ENABLED=0`.
Operating model ¶
- Database opened against `cfg.DSN`. Bare file paths and the special `:memory:` sentinel are supported. URI-form DSNs (`file:foo.db?...`) are passed through verbatim with the WAL + busy_timeout + immediate-transaction PRAGMAs appended as `_pragma` / `_txlock` query params so every pooled connection sees the same per-connection settings (see augmentDSNForPragmas).
- WAL journal mode is pinned at open (disk-backed). WAL gives concurrent readers + a single writer with no `SQLITE_BUSY` storms in the read path; `busy_timeout=5000` absorbs `SQLITE_BUSY` retries transparently; `_txlock=immediate` acquires the write lock at BEGIN so two transactions can never race a stale read.
- The pool is pinned to a SINGLE connection (`SetMaxOpenConns(1)`), matching SQLite's single-writer reality and the settled choice of the StateStore + ArtifactStore SQLite drivers: `database/sql` serializes concurrent callers at the pool instead of surfacing SQLITE_BUSY at BEGIN IMMEDIATE under contention.
- The schema is applied via embedded `migrations/*.sql` files (forward-only, AGENTS.md §13) through the shared runner (`internal/persistence/sqlmigrate`). Re-running on an already-migrated DB is a no-op.
Durable indexed parity ¶
The schema is built for INDEXED access on every axis the store contract reads, never a scan:
- `turn_rows` is keyed by the EXACT isolation triple + the root foreground turn key (TurnID = the run's task id): indexed append-idempotency lookup and indexed get are primary-key probes.
- The (tenant, user, session, sequence, turn_id) keyset index is the paging backbone: every `ListTurns` page is a bounded index RANGE scan strictly older than the cursor, ordered newest-first — no OFFSET, no history scan. The per-session `COUNT(*)` of older retained rows (the exact Remaining field) rides the same index range.
- `turn_apps` is keyed by the exact App replacement identity (effective_agent_id, server_id, resource_uri) within the turn (the effective-agent + session axis), with `position` preserving first-declaration order on the ordered read.
- The agent axis (a session's turns under one effective agent) is indexed on `turn_rows` as derived metadata written from the DTO at every accepted write.
Every row write (append / update / seal) runs in ONE transaction that covers the row + its children (activity rows, App refs) + the per-session sequence mint, and every write is fenced against session erasure in that same transaction: `turn_fences` is the STORE-LOCAL durable erasure fence / tombstone, and `DeleteScope` deletes the projections (rows, children, checkpoint, sequence state) but NEVER the fence — an erased session stays fenced across replay and restart, so replay can never resurrect it. The projection snapshot generation (`turn_snapshot_gens`) also survives erasure and advances with it, so a cursor minted before an erase is rejected as stale. `Durable()` reports whether the backing DSN survives a process restart (file-backed true; `:memory:` false — explicit restart loss, never a silent claim).
Concurrency contract ¶
The driver struct holds a `*sql.DB` (an internally-synchronized pool pinned to one connection), an `atomic.Bool` close flag, and immutable configuration. It is safe for N concurrent goroutines without external locking: per-call state lives on the call stack / supplied `ctx`, the deep-copy obligation is satisfied by JSON marshaling on every write boundary and unmarshaling on every read boundary (caller memory never reaches or escapes durable state), and no mutable field on the driver ever crosses run boundaries.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
New constructs a SQLite-backed `turns.Store` against cfg.DSN.
DSN handling:
- Empty DSN → clear error.
- `:memory:` → translated to a PER-OPEN uniquely named shared-cache memory URI (`file:harbor_turns_mem_<entropy>?mode=memory&cache=shared`) so `database/sql`'s pool can hand out multiple connections to the SAME in-memory database while two `:memory:` stores opened by different subsystems stay fully isolated.
- Any other DSN is treated as a file path or URI form and passed through verbatim, with the WAL + busy_timeout + immediate-tx PRAGMAs appended as query params.
Errors: empty DSN, unparseable DSN, journal-mode verification failure (disk-backed DBs must be WAL), or a migration-apply failure.
Types ¶
type Config ¶
type Config struct {
// DSN is the SQLite database path ("/var/lib/harbor/turns.sqlite"),
// a `file:` URI form, or the ":memory:" sentinel. Empty fails
// loudly (no silent default-fallback).
DSN string
// Retention bounds the number of NEWEST turn rows each session
// retains: older rows are evicted (children cascade in the same
// transaction) and the session's explicit truncation flag is set —
// retention eviction is never silent (AGENTS.md §13). <= 0 means
// the documented projection default (turns.MaxRetainedTurns).
Retention int
}
Config configures the SQLite-backed `turns.Store`.