92 lines
3.9 KiB
Markdown
92 lines
3.9 KiB
Markdown
|
|
# sqlite
|
||
|
|
|
||
|
|
Pure-Go SQLite client with launcher lifecycle, health check, unit-of-work, and structured error mapping.
|
||
|
|
|
||
|
|
## Purpose
|
||
|
|
|
||
|
|
Provides a `database/sql`-backed SQLite client that integrates with the launcher and health
|
||
|
|
modules. Designed for single-process, embedded database use cases where CGO-free builds and
|
||
|
|
cross-compilation matter. Serialises writes through a mutex to prevent `SQLITE_BUSY` errors.
|
||
|
|
|
||
|
|
## Tier & Dependencies
|
||
|
|
|
||
|
|
**Tier 3 (infrastructure)** — depends on:
|
||
|
|
- `code.nochebuena.dev/go/health` (Tier 2)
|
||
|
|
- `code.nochebuena.dev/go/launcher` (Tier 2)
|
||
|
|
- `code.nochebuena.dev/go/logz` (Tier 1)
|
||
|
|
- `code.nochebuena.dev/go/xerrors` (Tier 0)
|
||
|
|
- `modernc.org/sqlite` (pure-Go SQLite driver, no CGO)
|
||
|
|
|
||
|
|
## Key Design Decisions
|
||
|
|
|
||
|
|
- **Pure-Go driver**: `modernc.org/sqlite` is used instead of `mattn/go-sqlite3`. No CGO,
|
||
|
|
no system library required. Cross-compilation works with `CGO_ENABLED=0`. See ADR-001.
|
||
|
|
- **Write mutex**: `writeMu sync.Mutex` in `sqliteComponent` is acquired by `UnitOfWork.Do`
|
||
|
|
before every write transaction. Eliminates `SQLITE_BUSY` under concurrent goroutines. See ADR-002.
|
||
|
|
- **Foreign key enforcement**: Enabled in both the default DSN Pragmas (`_fk=true`) and via
|
||
|
|
an explicit `PRAGMA foreign_keys = ON` in `OnInit`. Startup fails if the PRAGMA cannot be
|
||
|
|
set. See ADR-003.
|
||
|
|
- **Honest Tx contract**: `Tx.Commit()` and `Tx.Rollback()` accept no `ctx` argument,
|
||
|
|
matching the `database/sql` limitation. The interface documents this explicitly.
|
||
|
|
- **Context injection for UoW**: `GetExecutor(ctx)` checks for a `ctxTxKey{}` value in the
|
||
|
|
context. When inside `UnitOfWork.Do`, the context carries the active transaction, so
|
||
|
|
repositories call `client.GetExecutor(ctx)` and automatically participate in the transaction
|
||
|
|
without knowing about it.
|
||
|
|
- **WAL + busy timeout defaults**: `?_journal=WAL&_timeout=5000&_fk=true` are the default
|
||
|
|
Pragmas. Callers can override via `Config.Pragmas`, but the `OnInit` FK PRAGMA always runs.
|
||
|
|
- **Health check**: `Priority()` returns `health.LevelCritical`. A failed ping prevents the
|
||
|
|
service from being marked healthy.
|
||
|
|
|
||
|
|
## Patterns
|
||
|
|
|
||
|
|
**Lifecycle registration:**
|
||
|
|
```go
|
||
|
|
db := sqlite.New(logger, cfg)
|
||
|
|
lc.Append(db) // registers OnInit / OnStart / OnStop
|
||
|
|
```
|
||
|
|
|
||
|
|
**Repository pattern using GetExecutor:**
|
||
|
|
```go
|
||
|
|
func (r *repo) FindByID(ctx context.Context, id int) (*Thing, error) {
|
||
|
|
exec := r.db.GetExecutor(ctx) // returns Tx if inside UoW, pool otherwise
|
||
|
|
row := exec.QueryRowContext(ctx, "SELECT ... WHERE id = ?", id)
|
||
|
|
...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Unit of Work:**
|
||
|
|
```go
|
||
|
|
uow := sqlite.NewUnitOfWork(logger, db)
|
||
|
|
err := uow.Do(ctx, func(ctx context.Context) error {
|
||
|
|
// all calls to db.GetExecutor(ctx) return the same Tx
|
||
|
|
return repo.Save(ctx, thing)
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
**Error handling:**
|
||
|
|
```go
|
||
|
|
if err := db.HandleError(err); err != nil {
|
||
|
|
// err is a *xerrors.Err with code ErrNotFound, ErrAlreadyExists, ErrInvalidInput, or ErrInternal
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## What to Avoid
|
||
|
|
|
||
|
|
- Do not set `MaxOpenConns > 1` without understanding the implications. SQLite allows only one
|
||
|
|
writer at a time; a larger pool increases `SQLITE_BUSY` risk for callers that bypass `UnitOfWork`.
|
||
|
|
- Do not use `Begin`/`Commit`/`Rollback` directly for concurrent writes. Use `UnitOfWork.Do`
|
||
|
|
to get write-mutex protection.
|
||
|
|
- Do not rely on `Tx.Commit(ctx)` — the `Tx` interface intentionally has no ctx on Commit
|
||
|
|
and Rollback, matching `database/sql` behaviour.
|
||
|
|
- Do not wrap `HandleError` output with additional error types that would obscure the
|
||
|
|
`*xerrors.Err` for callers using `errors.As`.
|
||
|
|
|
||
|
|
## Testing Notes
|
||
|
|
|
||
|
|
- All tests use `Config{Path: ":memory:"}`. No filesystem or teardown needed.
|
||
|
|
- `compliance_test.go` (package `sqlite_test`) asserts that `New(...)` satisfies the
|
||
|
|
`Component` interface at compile time.
|
||
|
|
- `TestUnitOfWork_WriteMutex` spawns 5 concurrent goroutines to verify serialisation.
|
||
|
|
- `TestHandleError_ForeignKey` and `TestHandleError_UniqueConstraint` exercise real SQLite
|
||
|
|
error code mapping; they require `_fk=true` in Pragmas (set in `newMemDB`).
|