Introduces code.nochebuena.dev/einherjar/db-sqlite — the SQLite database starter for the Einherjar framework. Absorbs the sqlite package from micro-lib using the pure-Go modernc.org/sqlite driver (no CGO, cross-compilation compatible). Interfaces (CT-6: one TypeSpec per file): - Executor — ExecContext, QueryContext, QueryRowContext (database/sql types) - Tx — Executor + Commit(), Rollback() — no ctx; honest database/sql contract - Provider — GetExecutor, Begin, Ping, HandleError - Component — lifecycle.Component + observability.Checkable + Provider - UnitOfWork — Do(ctx, fn) Implementation: - New(logger, cfg) Component — pool not created until OnInit - OnInit: sql.Open with DSN (Path + Pragmas); sets MaxOpenConns, MaxIdleConns - OnStart: Ping with 5s timeout; logs "sqlite: connected" - OnStop: db.Close(); logs "sqlite: closing pool" - GetExecutor: returns active Tx from context (ctxTxKey) or *sql.DB; explicit nil return when db uninitialized to avoid typed-nil interface pitfall - Begin: wraps db.BeginTx; wrapped in xerrors on error - HealthCheck: delegates to Ping; Priority LevelDegraded (file DB; absence is non-fatal on a server restart — file always present if configured) - NewUnitOfWork(logger, provider) UnitOfWork — acquires writeMu before Begin to prevent SQLITE_BUSY under concurrent goroutines (SQLite single-writer model) - writeMu: extracted from *sqliteImpl via type assertion in NewUnitOfWork; gracefully skipped when provider is a mock (nil mu) - HandleError: sql.ErrNoRows→ErrNotFound, unique/pk constraint→ErrAlreadyExists, foreign key→ErrInvalidInput, all others→ErrInternal Config (EINHERJAR_SQLITE_* env vars): Path(required), MaxOpenConns(1), MaxIdleConns(1), Pragmas(?_journal=WAL&_timeout=5000&_fk=true) - Component interface embeds observability.Identifiable; identifiable.go implements ModulePath and ModuleVersion via runtime/debug.ReadBuildInfo() — prints in launcher banner
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())
launcher.Append(db) // OnInit opens database; OnStop closes it
health.Register(db) // BucketExists-style check; LevelDegraded
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.