Documentation
¶
Overview ¶
Package sqlite is Harbor's SQLite-backed `state.StateStore` driver (Phase 15). It is the second leg of the persistence triad (in-memory, SQLite, Postgres) defined by RFC §6.11 + §9.
The driver is built on `modernc.org/sqlite` — a CGo-free SQLite engine (D-013, 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 by the driver but Harbor sets `journal_mode=WAL` and `busy_timeout=5000` itself via PRAGMA after open, so URI-form DSN parameters are not blessed in V1.
- WAL journal mode is pinned at open. WAL gives concurrent readers + a single writer with no `SQLITE_BUSY` storms in the read path.
- `busy_timeout=5000` (5 s) absorbs `SQLITE_BUSY` retries transparently — write contention in the conformance suite's `Concurrent_SaveLoad_NoRace` and our supplemental `concurrent_test.go` does not surface caller-visible errors.
- The schema is applied via embedded `migrations/*.sql` files (forward-only, brief 05 §4 + AGENTS.md §13). The runner is idempotent — re-running on an already-migrated DB is a no-op.
The driver self-registers under `"sqlite"` from its `init()`. The production binary picks it up via blank import in `cmd/harbor/main.go`; tests may call `New` directly to skip the registry.
Concurrency contract (D-025):
- The driver struct holds a `*sql.DB` (an internally-synchronized connection pool) and an `atomic.Bool` close flag. Both are safe for N concurrent goroutines without external locking.
- Per-call state lives on the call stack / supplied `ctx`. Nothing mutable on the driver ever crosses run boundaries.
- SQLite's single-writer-per-database invariant is enforced by the engine; `busy_timeout` ensures concurrent writers serialize transparently. No advisory locks needed.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
func New(cfg config.StateConfig) (state.StateStore, error)
New constructs a SQLite-backed `state.StateStore` against `cfg.DSN`. Production callers go through `state.Open`; tests may call `New` directly to skip the registry.
DSN handling:
- Empty DSN → clear error (no silent default-fallback).
- `:memory:` → translated to a PER-OPEN uniquely named shared-cache memory URI (`file:harbor_state_mem_<entropy>?mode=memory&cache=shared`, D-207) so `database/sql`'s pool can hand out multiple shared connections to the SAME in-memory database (the bare `:memory:` DSN gives every pool connection its own private DB, which would break Save+Load round-trip across pooled connections) while two `:memory:` stores opened by DIFFERENT subsystems in one process stay fully isolated (the previous process-wide `file::memory:?cache=shared` translation made every subsystem's `:memory:` store collide on one shared `schema_migrations` table).
- Any other DSN is treated as a file path or URI form and passed through verbatim, with the WAL + busy_timeout PRAGMAs appended as `_pragma` query params (see below).
PRAGMA discipline:
- `journal_mode=WAL` and `busy_timeout=5000` are appended to the DSN as `_pragma=...` query params. modernc.org/sqlite applies them on every new connection the pool opens, so the values survive `database/sql`'s connection lifecycle. This is the load-bearing fix for SQLITE_BUSY contention under concurrent writes — running `PRAGMA busy_timeout` once on the *sql.DB handle would only set it on the single connection the call happened to use.
- For disk-backed DSNs WAL is required; `:memory:` falls back to `memory` journal mode, which is correct (see SQLite docs).
Errors:
- empty `cfg.DSN`
- DSN that cannot be parsed for `_pragma` augmentation
- `sql.Open` failure (rare; modernc.org/sqlite's Open is lazy)
- migration apply failure
Types ¶
This section is empty.