The package-doc examples showed a fictional launcher.Register / health.Register API (and launcher.Append as a package func). Corrected to the real wiring: lc.Append + web/health.NewHandler(...).ServeHTTP. Documentation-only. No code, API, or dependency changes; version stays v1.1.0 (the v1.1.0 tag is moved to include this fix). Verified by compiling the corrected pattern against the real API.
4.0 KiB
einherjar/db-sqlite
Not every hall needs pillars that reach the sky. Sometimes what matters fits in a single room, carried wherever the warrior goes.
code.nochebuena.dev/einherjar/db-sqlite is the SQLite database component of the Einherjar framework. It uses the pure-Go modernc.org/sqlite driver (no CGO, cross-compilation works without a C toolchain), wraps database/sql behind a lifecycle-aware Component, and serializes writes through a mutex to prevent SQLITE_BUSY under concurrent goroutines. WAL mode is enabled by default.
Usage
Setup
import dbsqlite "code.nochebuena.dev/einherjar/db-sqlite"
db := dbsqlite.New(logger, dbsqlite.DefaultConfig())
lc.Append(db) // OnInit opens database; OnStop closes it
// db is observability.Checkable (LevelDegraded):
srv.Get("/health", health.NewHandler(logger, db).ServeHTTP)
Querying
// GetExecutor returns the pool when called outside a UnitOfWork.
rows, err := db.GetExecutor(ctx).QueryContext(ctx, "SELECT id, name FROM users WHERE active = ?", true)
defer rows.Close()
var name string
err := db.GetExecutor(ctx).QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id).Scan(&name)
Manual transaction
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
if err != nil {
return err
}
return tx.Commit()
Note: Commit and Rollback do not accept a context — this is a database/sql limitation.
Unit of work (recommended)
UnitOfWork wraps the transaction in context so every call to GetExecutor inside uow.Do automatically returns the active transaction. The internal write mutex prevents concurrent SQLITE_BUSY errors; only one goroutine can write at a time.
uow := dbsqlite.NewUnitOfWork(logger, db)
err := uow.Do(ctx, func(ctx context.Context) error {
_, err := db.GetExecutor(ctx).ExecContext(ctx, "INSERT INTO orders (...) VALUES (...)", ...)
return err
})
If the function returns an error, the transaction is rolled back. If it returns nil, the transaction is committed.
Error handling
if err := db.HandleError(someErr); err != nil {
// SQLite error codes mapped to xerrors:
// UNIQUE constraint failed → ErrAlreadyExists
// FOREIGN KEY constraint → ErrPreconditionFailed
// NOT NULL constraint → ErrInvalidInput
// context.Canceled → ErrCancelled
// context.DeadlineExceeded → ErrDeadlineExceeded
}
HandleError is also available as a package-level function: dbsqlite.HandleError(err).
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
EINHERJAR_SQLITE_PATH |
Yes | — | Path to the SQLite database file |
EINHERJAR_SQLITE_MAX_OPEN_CONNS |
No | 1 |
Maximum open connections (keep at 1 for writes) |
EINHERJAR_SQLITE_MAX_IDLE_CONNS |
No | 1 |
Maximum idle connections |
EINHERJAR_SQLITE_PRAGMAS |
No | ?_journal=WAL&_timeout=5000&_fk=true |
DSN pragma string |
The default pragma string enables WAL mode (better concurrent reads), a 5-second busy timeout, and foreign key enforcement.
Dependency graph
contracts (zero dependencies)
↑
core
↑
db-sqlite (contracts, core, modernc.org/sqlite)
↑
your app
No CGO. Cross-compiles to any GOOS/GOARCH without a C toolchain.
Verification
cd db-sqlite/
go build ./...
go vet ./...
go test ./...
gofmt -l .
The hall is built before the warriors arrive. That is the only guarantee worth making.