feat(db-sqlite): initial implementation — database/sql SQLite with lifecycle and UnitOfWork (v1.0.0)
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
This commit is contained in:
46
errors.go
Normal file
46
errors.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"code.nochebuena.dev/einherjar/core/xerrors"
|
||||
)
|
||||
|
||||
// coder is the duck-type interface for SQLite extended error codes.
|
||||
// modernc.org/sqlite errors implement this interface.
|
||||
type coder interface{ Code() int }
|
||||
|
||||
const (
|
||||
sqliteConstraintPrimaryKey = 1555
|
||||
sqliteConstraintUnique = 2067
|
||||
sqliteConstraintForeignKey = 787
|
||||
)
|
||||
|
||||
// HandleError maps database/sql and SQLite errors to typed [xerrors] values.
|
||||
// Returns nil when err is nil. Also available as [Provider.HandleError].
|
||||
//
|
||||
// Mapped codes:
|
||||
// - sql.ErrNoRows → ErrNotFound
|
||||
// - SQLITE_CONSTRAINT_UNIQUE (2067) → ErrAlreadyExists
|
||||
// - SQLITE_CONSTRAINT_PRIMARYKEY (1555)→ ErrAlreadyExists
|
||||
// - SQLITE_CONSTRAINT_FOREIGNKEY (787) → ErrInvalidInput
|
||||
// - all others → ErrInternal
|
||||
func HandleError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return xerrors.New(xerrors.ErrNotFound, "record not found").WithError(err)
|
||||
}
|
||||
var ce coder
|
||||
if errors.As(err, &ce) {
|
||||
switch ce.Code() {
|
||||
case sqliteConstraintUnique, sqliteConstraintPrimaryKey:
|
||||
return xerrors.New(xerrors.ErrAlreadyExists, "record already exists").WithError(err)
|
||||
case sqliteConstraintForeignKey:
|
||||
return xerrors.New(xerrors.ErrInvalidInput, "data integrity violation").WithError(err)
|
||||
}
|
||||
}
|
||||
return xerrors.New(xerrors.ErrInternal, "unexpected database error").WithError(err)
|
||||
}
|
||||
Reference in New Issue
Block a user