Files
postgres/CLAUDE.md

71 lines
3.9 KiB
Markdown
Raw Normal View History

# 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/v5` and `pgxpool`, `pgconn` sub-packages
- `github.com/jackc/pgerrcode`
## Key Design Decisions
- **pgx native types** (ADR-001): `Executor` uses `pgconn.CommandTag`, `pgx.Rows`, and `pgx.Row`. There is no `database/sql` adapter. Repository code imports pgx types directly.
- **Local Executor interface** (ADR-002): `dbutil` was eliminated. `Executor`, `Tx`, `Client`, and `Component` are all defined in this package using pgx types. The `mysql` module has its own independent `Executor`.
- **UnitOfWork via context injection** (ADR-003): The active `pgx.Tx` is stored in the context under `ctxTxKey{}` (unexported). `GetExecutor(ctx)` returns the transaction if present, otherwise the pool. `Tx.Commit` and `Tx.Rollback` both accept `ctx`.
- **Error mapping via pgerrcode constants**: `HandleError` uses `pgerrcode.UniqueViolation`, `pgerrcode.ForeignKeyViolation`, and `pgerrcode.CheckViolation` (not string-matched error messages) to map `*pgconn.PgError` to xerrors codes.
- **`health.Checkable` embedded in Component**: `pgComponent.HealthCheck` delegates to `Ping`. Priority is `health.LevelCritical` — a PostgreSQL outage brings down the service.
## Patterns
Lifecycle registration:
```go
db := postgres.New(logger, cfg)
lc.Append(db)
r.Get("/health", health.NewHandler(logger, db))
```
Unit of Work across multiple repositories:
```go
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:
```go
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/sql` types (`sql.Rows`, `sql.Result`, etc.) alongside this module. The module is pgx-native; mixing the two type systems requires explicit adaptation.
- Do not store `Executor` references across goroutine boundaries during a `UnitOfWork.Do`. The transaction is tied to the context passed into `Do`; goroutines that outlive `Do` will use a committed or rolled-back transaction.
- Do not match PostgreSQL error codes by parsing error message strings. Use `pgerrcode` constants and `errors.As(err, &pgErr)` as `HandleError` does.
- Do not add `sync.Once` or package-level pool variables. `pgComponent` is the unit of construction; create one per database and pass it as a dependency.
- Do not call `db.GetExecutor(ctx)` from outside the `UnitOfWork.Do` callback if you need transactional semantics — it will return the pool.
## Testing Notes
- `postgres_test.go` tests 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.go` asserts at compile time that `postgres.New(...)` satisfies `postgres.Component`.
- `HandleError` can be unit-tested by constructing `*pgconn.PgError` with known `Code` values — no database connection needed.
- Pool initialization happens in `OnInit`, not `New`. Tests that mock the `Client` interface bypass pool setup entirely.