Pure-Go CGO-free SQLite client with launcher lifecycle, write-mutex serialisation, health check, unit-of-work via context injection, and structured error mapping. What's included: - Executor / Tx / Client / Component interfaces using database/sql native types - Tx.Commit() / Tx.Rollback() without ctx, matching the honest database/sql contract - New(logger, cfg) constructor; database opened in OnInit - Config struct with env-tag support; default Pragmas: WAL + 5s busy timeout + FK enforcement - PRAGMA foreign_keys = ON enforced explicitly in OnInit - writeMu sync.Mutex acquired by UnitOfWork.Do to serialise writes and prevent SQLITE_BUSY - UnitOfWork via context injection; GetExecutor(ctx) returns active Tx or *sql.DB - HandleError mapping SQLite extended error codes to xerrors codes (unique/primary-key → AlreadyExists, foreign-key → InvalidInput, ErrNoRows → NotFound) - health.Checkable at LevelCritical; pure-Go modernc.org/sqlite driver (CGO_ENABLED=0 compatible) Tested-via: todo-api POC integration Reviewed-against: docs/adr/
3.9 KiB
3.9 KiB
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/sqliteis used instead ofmattn/go-sqlite3. No CGO, no system library required. Cross-compilation works withCGO_ENABLED=0. See ADR-001. - Write mutex:
writeMu sync.MutexinsqliteComponentis acquired byUnitOfWork.Dobefore every write transaction. EliminatesSQLITE_BUSYunder concurrent goroutines. See ADR-002. - Foreign key enforcement: Enabled in both the default DSN Pragmas (
_fk=true) and via an explicitPRAGMA foreign_keys = ONinOnInit. Startup fails if the PRAGMA cannot be set. See ADR-003. - Honest Tx contract:
Tx.Commit()andTx.Rollback()accept noctxargument, matching thedatabase/sqllimitation. The interface documents this explicitly. - Context injection for UoW:
GetExecutor(ctx)checks for actxTxKey{}value in the context. When insideUnitOfWork.Do, the context carries the active transaction, so repositories callclient.GetExecutor(ctx)and automatically participate in the transaction without knowing about it. - WAL + busy timeout defaults:
?_journal=WAL&_timeout=5000&_fk=trueare the default Pragmas. Callers can override viaConfig.Pragmas, but theOnInitFK PRAGMA always runs. - Health check:
Priority()returnshealth.LevelCritical. A failed ping prevents the service from being marked healthy.
Patterns
Lifecycle registration:
db := sqlite.New(logger, cfg)
lc.Append(db) // registers OnInit / OnStart / OnStop
Repository pattern using GetExecutor:
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:
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:
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 > 1without understanding the implications. SQLite allows only one writer at a time; a larger pool increasesSQLITE_BUSYrisk for callers that bypassUnitOfWork. - Do not use
Begin/Commit/Rollbackdirectly for concurrent writes. UseUnitOfWork.Doto get write-mutex protection. - Do not rely on
Tx.Commit(ctx)— theTxinterface intentionally has no ctx on Commit and Rollback, matchingdatabase/sqlbehaviour. - Do not wrap
HandleErroroutput with additional error types that would obscure the*xerrors.Errfor callers usingerrors.As.
Testing Notes
- All tests use
Config{Path: ":memory:"}. No filesystem or teardown needed. compliance_test.go(packagesqlite_test) asserts thatNew(...)satisfies theComponentinterface at compile time.TestUnitOfWork_WriteMutexspawns 5 concurrent goroutines to verify serialisation.TestHandleError_ForeignKeyandTestHandleError_UniqueConstraintexercise real SQLite error code mapping; they require_fk=truein Pragmas (set innewMemDB).