Files
db-sqlite/errors.go

47 lines
1.4 KiB
Go
Raw Normal View History

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
2026-05-29 15:56:45 +00:00
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)
}