pgx v5-native PostgreSQL client with launcher lifecycle, health check, unit-of-work via context injection, and structured error mapping. What's included: - Executor / Tx / Client / Component interfaces using pgx native types (pgconn.CommandTag, pgx.Rows, pgx.Row) - New(logger, cfg) constructor; pgxpool initialised in OnInit - Config struct with env-tag support for all pool tuning parameters - UnitOfWork via context injection; GetExecutor(ctx) returns active Tx or pool - HandleError mapping pgerrcode constants to xerrors codes (AlreadyExists, InvalidInput, NotFound, Internal) - health.Checkable at LevelCritical; HealthCheck delegates to pgxpool.Ping Tested-via: todo-api POC integration Reviewed-against: docs/adr/
3.9 KiB
3.9 KiB
postgres
pgx-backed PostgreSQL client with launcher lifecycle, health check integration, and unit-of-work transaction management.
Purpose
Provides a Component that manages a pgxpool connection pool, satisfies the launcher.Component lifecycle hooks (OnInit, OnStart, OnStop), and implements health.Checkable (priority: critical). Also provides NewUnitOfWork for wrapping multiple repository operations in a single transaction via context injection.
Tier & Dependencies
Tier 3 (infrastructure) — depends on:
code.nochebuena.dev/go/health(Tier 1)code.nochebuena.dev/go/launcher(Tier 1)code.nochebuena.dev/go/logz(Tier 0)code.nochebuena.dev/go/xerrors(Tier 0)github.com/jackc/pgx/v5andpgxpool,pgconnsub-packagesgithub.com/jackc/pgerrcode
Key Design Decisions
- pgx native types (ADR-001):
Executorusespgconn.CommandTag,pgx.Rows, andpgx.Row. There is nodatabase/sqladapter. Repository code imports pgx types directly. - Local Executor interface (ADR-002):
dbutilwas eliminated.Executor,Tx,Client, andComponentare all defined in this package using pgx types. Themysqlmodule has its own independentExecutor. - UnitOfWork via context injection (ADR-003): The active
pgx.Txis stored in the context underctxTxKey{}(unexported).GetExecutor(ctx)returns the transaction if present, otherwise the pool.Tx.CommitandTx.Rollbackboth acceptctx. - Error mapping via pgerrcode constants:
HandleErrorusespgerrcode.UniqueViolation,pgerrcode.ForeignKeyViolation, andpgerrcode.CheckViolation(not string-matched error messages) to map*pgconn.PgErrorto xerrors codes. health.Checkableembedded in Component:pgComponent.HealthCheckdelegates toPing. Priority ishealth.LevelCritical— a PostgreSQL outage brings down the service.
Patterns
Lifecycle registration:
db := postgres.New(logger, cfg)
lc.Append(db)
r.Get("/health", health.NewHandler(logger, db))
Unit of Work across multiple repositories:
uow := postgres.NewUnitOfWork(logger, db)
err := uow.Do(ctx, func(ctx context.Context) error {
exec := db.GetExecutor(ctx) // returns active Tx
_, err := exec.Exec(ctx, "INSERT INTO ...")
return err
})
Error handling in repository code:
row := db.GetExecutor(ctx).QueryRow(ctx, "SELECT ...")
if err := row.Scan(&out); err != nil {
return db.HandleError(err) // maps pgx.ErrNoRows → ErrNotFound, etc.
}
What to Avoid
- Do not use
database/sqltypes (sql.Rows,sql.Result, etc.) alongside this module. The module is pgx-native; mixing the two type systems requires explicit adaptation. - Do not store
Executorreferences across goroutine boundaries during aUnitOfWork.Do. The transaction is tied to the context passed intoDo; goroutines that outliveDowill use a committed or rolled-back transaction. - Do not match PostgreSQL error codes by parsing error message strings. Use
pgerrcodeconstants anderrors.As(err, &pgErr)asHandleErrordoes. - Do not add
sync.Onceor package-level pool variables.pgComponentis the unit of construction; create one per database and pass it as a dependency. - Do not call
db.GetExecutor(ctx)from outside theUnitOfWork.Docallback if you need transactional semantics — it will return the pool.
Testing Notes
postgres_test.gotests compile-time interface satisfaction. Integration tests (pool, real queries) require a live PostgreSQL instance and are typically run in CI with a service container.compliance_test.goasserts at compile time thatpostgres.New(...)satisfiespostgres.Component.HandleErrorcan be unit-tested by constructing*pgconn.PgErrorwith knownCodevalues — no database connection needed.- Pool initialization happens in
OnInit, notNew. Tests that mock theClientinterface bypass pool setup entirely.