Pure-Go CGO-free SQLite client with launcher lifecycle, write-mutex serialisation, health check, unit-of-work via context injection, and structured error mapping. What's included: - Executor / Tx / Client / Component interfaces using database/sql native types - Tx.Commit() / Tx.Rollback() without ctx, matching the honest database/sql contract - New(logger, cfg) constructor; database opened in OnInit - Config struct with env-tag support; default Pragmas: WAL + 5s busy timeout + FK enforcement - PRAGMA foreign_keys = ON enforced explicitly in OnInit - writeMu sync.Mutex acquired by UnitOfWork.Do to serialise writes and prevent SQLITE_BUSY - UnitOfWork via context injection; GetExecutor(ctx) returns active Tx or *sql.DB - HandleError mapping SQLite extended error codes to xerrors codes (unique/primary-key → AlreadyExists, foreign-key → InvalidInput, ErrNoRows → NotFound) - health.Checkable at LevelCritical; pure-Go modernc.org/sqlite driver (CGO_ENABLED=0 compatible) Tested-via: todo-api POC integration Reviewed-against: docs/adr/
2.2 KiB
ADR-003: Foreign Key Enforcement via PRAGMA and DSN
Status: Accepted Date: 2026-03-18
Context
SQLite disables foreign key constraint enforcement by default for backwards compatibility.
Applications that define REFERENCES clauses in their schema will silently insert orphaned
rows unless they explicitly enable enforcement. This is a common source of data integrity bugs.
Decision
Foreign key enforcement is enabled at two points:
-
DSN parameter — The default
Pragmasconfig value includes_fk=true:?_journal=WAL&_timeout=5000&_fk=trueThis sets
PRAGMA foreign_keys = ONfor every connection opened via the DSN. -
OnInit explicit PRAGMA — After opening the database pool,
OnInitexecutes an additionalPRAGMA foreign_keys = ONcall:if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { _ = db.Close() return fmt.Errorf("sqlite: enable foreign keys: %w", err) }If this call fails,
OnInitreturns an error and the pool is closed, preventing startup with an unsafe configuration.
The redundancy is deliberate: the DSN parameter may be overridden by callers who supply a
custom Pragmas value, but the OnInit PRAGMA call always runs and fails loudly if it cannot
enforce foreign keys.
HandleError maps SQLITE_CONSTRAINT_FOREIGNKEY (error code 787) to
xerrors.ErrInvalidInput so that foreign key violations surface as validation errors to
callers rather than opaque internal errors.
Consequences
Positive:
- Foreign key constraints are always active when using the default configuration.
- Failure to enable them is a startup error, not a silent misconfiguration.
- Violations produce a structured
xerrors.ErrInvalidInputerror.
Negative:
- Callers who deliberately omit
_fk=truefrom a customPragmasstring still get the enforcement applied by theOnInitPRAGMA. There is no opt-out without modifying the source. PRAGMA foreign_keys = ONmust be set per-connection;database/sqlconnection pooling means this implicit approach (via DSN) can behave differently under pool pressure. The explicitOnInitPRAGMA mitigates this for the initial connection but cannot guarantee it for all pooled connections whenMaxOpenConns > 1.