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/
This commit is contained in:
91
CLAUDE.md
Normal file
91
CLAUDE.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user