Files
postgres/docs/adr/ADR-002-local-executor-interface.md
Rene Nochebuena 2baafa6a0c feat(postgres): initial stable release v0.9.0
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/
2026-03-19 13:18:07 +00:00

2.5 KiB

ADR-002: Local Executor Interface

Status: Accepted Date: 2026-03-18

Context

The Executor interface — the common query interface shared by the connection pool and an active transaction — must be defined somewhere. Earlier iterations of this codebase placed it in a shared dbutil package that both postgres and mysql imported. This created a cross-cutting dependency: every database module depended on dbutil, and dbutil had to make choices (e.g., which type system to use) that were appropriate for only one of them.

dbutil was eliminated as part of the monorepo refactor (see plan/decisions.md).

Decision

The Executor interface is defined locally inside the postgres package:

type Executor interface {
    Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
    Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
    QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

The mysql package defines its own separate Executor using database/sql types. The two are not interchangeable by design — they represent different type systems.

Tx extends Executor with Commit(ctx context.Context) error and Rollback(ctx context.Context) error. Client provides GetExecutor, Begin, Ping, and HandleError. Component composes Client, launcher.Component, and health.Checkable.

Repository code in application layers should depend on postgres.Executor (or the higher-level postgres.Client) — not on the concrete *pgxpool.Pool or pgTx types.

Consequences

  • Positive: No shared dbutil dependency. Each database module owns its interface and can evolve it independently.
  • Positive: The interface methods use pgx-native types, so there is no impedance mismatch between the interface and the implementation.
  • Positive: Mocking postgres.Executor in tests requires only implementing three methods with pgx return types — no wrapper types needed.
  • Negative: If a project uses both postgres and mysql, neither module's Executor is compatible with the other. Cross-database abstractions must be built at the application domain interface layer, not by sharing a common Executor.
  • Note: pgComponent itself also implements Executor directly (forwarding to the pool), which means a *pgComponent can be used wherever an Executor is expected without calling GetExecutor. This is intentional for ergonomics in simple cases where no transaction management is needed.