# einherjar/db-sqlite [![version](https://img.shields.io/badge/version-v1.0.0-5C4EE5?style=flat-square)](https://code.nochebuena.dev/einherjar/db-sqlite) [![license](https://img.shields.io/badge/license-AGPL--3.0-22863A?style=flat-square)](LICENSE) [![go](https://img.shields.io/badge/Go-1.26+-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev) [![health](https://img.shields.io/badge/health-degraded-E36209?style=flat-square)]() > 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 ```go 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 ```go // 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 ```go 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. ```go 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 ```go 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 ```bash 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.*