Files
sqlite/CLAUDE.md
Rene Nochebuena 237cba9bad feat(sqlite): initial stable release v0.9.0
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/
2026-03-19 13:25:31 +00:00

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/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:

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 > 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).