Documentation
¶
Overview ¶
Package pgfx is a thin, Fx-first layer over pgx/pgxpool for Postgres. A pgfx connection is a DB: a pool built from a plain, serializable Config, carrying generic methods for the two things every query does - read rows, read one value.
Config and Option ¶
The split between the two is the design. Config is data: it survives a round trip through text, so a deployment fills it and a manifest lists it. Everything that cannot survive that trip - a tracer, a metrics callback, a pool hook, an in-memory *tls.Config - is an Option instead. Nothing is configured twice, and nothing has to be expressed as a string that was never a string.
pgfx reads no configuration source of itself, which is what keeps it decoupled from any particular loader. An application supplies the Config, typically through github.com/uchaloop/confmaker, whose Manifest lists every variable a connection reads:
confx.Manifest[pgfx.Config]("postgres")
Host and Database have to be supplied. An empty User or Password falls through to pgx's libpq-compatible defaults, so peer authentication and a .pgpass keep working. Every pool and timeout field left at its zero value keeps the pgxpool default, so a deployment sets what it means to change and nothing else.
Wiring ¶
Module provides an untagged *DB - the single default connection. It verifies the connection while the application starts, so a bad endpoint fails the start rather than the first query, and closes the pool on shutdown. Fx receives and provides only *DB; the embedded *pgxpool.Pool is not registered separately.
ModuleFor adds a replica or a shard under a name, which gives both the Fx tag and the environment prefix:
fx.New(
confx.Module(),
confx.Provide[pgfx.Config]("postgres"),
confx.ProvideNamed[pgfx.Config]("replica"),
pgfx.Module,
pgfx.ModuleFor("replica"),
)
A named connection inherits nothing from the default one. Its database, user and TLS settings are given again under its own prefix - a replica that silently borrowed the primary's credentials would be the kind of thing noticed in production.
Several databases are several named connections; there is no other mechanism. Without Fx, Make builds a DB from a Config filled by hand.
Queries ¶
FetchRow and FetchRows decode structs by `db` tag or field name:
order, err := db.FetchRow[Order]( ctx, `SELECT id, amount FROM orders WHERE id = $1`, id, ) orders, err := db.FetchRows[Order]( ctx, `SELECT id, amount FROM orders ORDER BY id`, )
FetchValue and FetchValues decode one column:
total, err := db.FetchValue[int64](ctx, `SELECT count(*) FROM orders`) ids, err := db.FetchValues[int64]( ctx, `SELECT id FROM orders ORDER BY id`, )
The singular methods require exactly one row and report pgx.ErrNoRows or pgx.ErrTooManyRows otherwise. pgx.ErrNoRows wraps sql.ErrNoRows, so errors.Is matches either sentinel.
DB embeds *pgxpool.Pool. Native methods such as Exec, Query, CopyFrom, Acquire, and Close are therefore available directly:
tag, err := db.Exec( ctx, `UPDATE orders SET status = $1 WHERE id = $2`, "paid", id, )
Use FetchValue rather than Exec for INSERT ... RETURNING.
Pagination ¶
FetchPage returns one page of rows and how many rows the filter matches in total. The query stays a plain SELECT - no ORDER BY, no LIMIT, no row numbers, no count - and the page is applied around it, which is what keeps the filter written once and the rows decodable by the `db` tag like any other:
SELECT * FROM ( <query> ) AS pgfx_page ORDER BY ... LIMIT $n+1 OFFSET $n+2 SELECT count(*) FROM ( <query> ) AS pgfx_page
The sort policy is bound once, next to the SQL; the page number, its size and the client's sort arrive per call, apart from the filter arguments:
query, err := page.Make(fetchWarehousesSQL,
page.Head(page.Desc("is_active")),
page.Tie(page.Asc("id")),
page.SortKeyTag("json"),
)
warehouses, total, err := db.FetchPage[Warehouse](ctx, query,
page.Request{Number: 2, Size: 20, Sort: []string{"cityEng:desc"}},
params.Country,
)
The sortable fields are derived from the model, so they cannot drift away from the columns that are there; an unknown one is an error rather than a silent skip. page.Tie is required, because a page without a total order repeats rows on one page and loses them from another. The details - narrowing the whitelist, counting over a cheaper statement, NULL placement - are in the github.com/uchaloop/pgfx/page documentation.
The second statement is skipped whenever the rows already imply the total: a page shorter than it asked for is the last one. When it does run, it is reported to the metrics under the caller's query name with a ".count" suffix - it is a different statement with a cost of its own, and it does not run every time.
An empty page is a valid result, not an error. Tx.FetchPage runs both statements inside a transaction, where a repeatable read isolation level makes the total agree with the page it describes.
Transactions ¶
DB.Transaction is the typed transaction entry point. It commits when its callback returns nil and rolls back on an error or panic. The callback receives a Tx with the same Fetch methods as DB and the embedded native pgx.Tx API:
err := db.Transaction(ctx, pgx.TxOptions{}, func(tx *pgfx.Tx) error {
order, err := tx.FetchRow[Order](ctx, selectOrder, id)
if err != nil {
return err
}
_, err = tx.Exec(ctx, updateOrder, order.ID)
return err
})
Inside the callback, use tx rather than db. A call through db uses the pool and does not participate in the transaction.
The embedded pool's Begin and BeginTx remain the native manual API and return pgx.Tx:
tx, err := db.BeginTx(ctx, pgx.TxOptions{
IsoLevel: pgx.Serializable,
})
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, insertOrder); err != nil {
return err
}
return tx.Commit(ctx)
Tx.Transaction creates a typed savepoint:
err := db.Transaction(ctx, pgx.TxOptions{}, func(tx *pgfx.Tx) error {
return tx.Transaction(ctx, func(nested *pgfx.Tx) error {
_, err := nested.Exec(ctx, insertOptionalData)
return err
})
})
DB embeds *pgxpool.Pool, so COPY, LISTEN, acquired connections and the rest of the native pool API are available directly. The pool itself is the DB.Pool field; no accessor is needed.
TLS ¶
TLS_MODE follows libpq, and verify-full with TLS_ROOT_CERT is the setting that actually verifies the server. WithTLS takes an in-memory *tls.Config instead, for an application that already assembled one - from a secret store, or from a certificate that never touched the filesystem.
Tracing and metrics ¶
Under Fx, Module enables OpenTelemetry query spans by itself when a trace.TracerProvider is in the graph. WithTracing configures otelpgx directly, and WithTracer installs any pgx.QueryTracer.
Query, batch and COPY metrics are reported through a callback rather than to a metrics library, so pgfx holds no opinion about where they go:
fx.Supply(pgfx.QueryMetricFunc(func(metric pgfx.QueryMetric) {
// Use metric.Name and metric.Kind.String() as bounded labels.
// Record metric.Duration, metric.RowsAffected and metric.Err.
// This callback must be concurrency-safe, cheap and non-blocking.
}))
WithQueryName attaches a stable name to a query through the context, because the SQL itself is not a metric label - it varies, and it is unbounded.
ctx = pgfx.WithQueryName(ctx, "orders.get_by_id")
Outside Fx, install the callback with WithQueryMetrics. A batch produces one observation, including results drained by Close:
batch := &pgx.Batch{}
batch.Queue("insert into orders (id) values ($1)", 1)
batch.Queue("insert into orders (id) values ($1)", 2)
results := db.SendBatch(pgfx.WithQueryName(ctx, "orders.sync"), batch)
if err := results.Close(); err != nil {
return err
}
COPY is observed through the same callback with KindCopyFrom:
_, err := db.CopyFrom(pgfx.WithQueryName(ctx, "orders.load"),
pgx.Identifier{"orders"}, []string{"id"},
pgx.CopyFromRows([][]any{{int64(3)}, {int64(4)}}))
Duration measures the pgx operation lifecycle, not server time or a pure round trip: it includes preparation, result consumption, delays before batch Close, and COPY source production, but excludes pool acquisition. Batch row counts sum observed command tags; they can be partial on error and do not imply a commit.
Coverage follows pgx hooks. Empty batches and pool acquisition failures produce no metric. In pgx v5.10, early COPY statement-description errors also omit the end hook and are not reported. Always close batch results.
SQL is omitted by default. WithSQLInQueryMetrics opts in to raw query text; batch statements are joined with "; " before rewriting, without interpolating arguments, and may include statements that never execute. COPY leaves SQL empty. Never use SQL as a label; scrub it before recording.
Index ¶
- Variables
- func ModuleFor(name string) fx.Option
- func QueryName(ctx context.Context) string
- func WithQueryName(ctx context.Context, name string) context.Context
- type Config
- type DB
- func (d *DB) FetchPage[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, uint, error)
- func (d *DB) FetchRow[T any](ctx context.Context, sql string, args ...any) (T, error)
- func (d *DB) FetchRows[T any](ctx context.Context, sql string, args ...any) ([]T, error)
- func (d *DB) FetchValue[T any](ctx context.Context, sql string, args ...any) (T, error)
- func (d *DB) FetchValues[T any](ctx context.Context, sql string, args ...any) ([]T, error)
- func (d *DB) Transaction(ctx context.Context, opts pgx.TxOptions, fn func(*Tx) error) error
- type Option
- func WithAfterConnect(fn func(context.Context, *pgx.Conn) error) Option
- func WithBeforeConnect(fn func(context.Context, *pgx.ConnConfig) error) Option
- func WithDefaultQueryExecMode(mode pgx.QueryExecMode) Option
- func WithQueryMetrics(fn QueryMetricFunc) Option
- func WithRuntimeParam(key, value string) Option
- func WithSQLInQueryMetrics(include bool) Option
- func WithStatementCacheCapacity(capacity int) Option
- func WithTLS(tlsConfig *tls.Config) Option
- func WithTracer(tracer pgx.QueryTracer) Option
- func WithTracing(opts ...otelpgx.Option) Option
- type PoolConfig
- type QueryKind
- type QueryMetric
- type QueryMetricFunc
- type TLSConfig
- type TimeoutConfig
- type Tx
- func (t *Tx) BeginNested(ctx context.Context) (*Tx, error)
- func (t *Tx) FetchPage[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, uint, error)
- func (t *Tx) FetchRow[T any](ctx context.Context, sql string, args ...any) (T, error)
- func (t *Tx) FetchRows[T any](ctx context.Context, sql string, args ...any) ([]T, error)
- func (t *Tx) FetchValue[T any](ctx context.Context, sql string, args ...any) (T, error)
- func (t *Tx) FetchValues[T any](ctx context.Context, sql string, args ...any) ([]T, error)
- func (t *Tx) Transaction(ctx context.Context, fn func(*Tx) error) error
Constants ¶
This section is empty.
Variables ¶
var Module = fx.Module("pgfx", connectionProvider(``))
Module is an Fx module for the default Postgres connection: it consumes an untagged pgfx.Config from the container and provides only an untagged *DB, so a repository just depends on *DB - no tags, no wrappers. The pool embedded in DB is not provided as a separate Fx value.
The application supplies the Config explicitly - pgfx does not read any config source itself - typically with confmaker/confx:
fx.New(
confx.Module(),
confx.Provide[pgfx.Config]("postgres"),
pgfx.Module,
)
The connection is pinged on start (fail-fast on a dead database) and closed on stop, and takes an optional trace.TracerProvider and QueryMetricFunc from the container.
Functions ¶
func ModuleFor ¶
ModuleFor is an Fx module for a named connection - a replica or another shard. It consumes a pgfx.Config tagged name:"<name>" and provides only a *DB tagged the same; a consumer selects it with fx.ParamTags. The application supplies the tagged Config explicitly (typically confmaker/confx's Provide).
Types ¶
type Config ¶
type Config struct {
// Host is the endpoint as "host" or "host:port". A port in the string wins;
// when omitted, 5432 applies. IPv6 with a port must be bracketed
// ("[::1]:5433").
Host string `env:"HOST,notEmpty"`
// Database is the PostgreSQL database name.
Database string `env:"DATABASE,notEmpty"`
// User is the PostgreSQL role. When empty, libpq selects its default.
User string `env:"USER"`
// Password is read from the environment only. When empty, libpq may use
// PGPASSWORD or .pgpass.
Password secret.Secret `env:"PASSWORD"`
// AppName is reported as application_name in pg_stat_activity.
AppName string `env:"APP_NAME"`
// TLS configures libpq transport security.
TLS TLSConfig `envPrefix:"TLS_"`
// Pool configures pgxpool sizing and connection lifetimes.
Pool PoolConfig `envPrefix:"POOL_"`
// Timeouts configures connection timeouts.
Timeouts TimeoutConfig `envPrefix:"TIMEOUTS_"`
}
Config is the serializable configuration of a single Postgres connection. It carries only plain, serializable values - no tracer, hooks, or other runtime dependencies (those are passed to Make as Option values).
The `env` tags let a loader fill it from the environment (see confmaker/confx). They are inert strings, so the Config type itself depends only on the standard-library-only github.com/uchaloop/secret/v2 module.
Host and Database must be supplied by the deployment. User and Password are optional: when empty they fall back to libpq's defaults (PGUSER / the OS user, and PGPASSWORD / .pgpass).
type DB ¶ added in v0.5.0
DB is a Postgres connection: a pool, and the queries bound to it. It is what Make returns and what the Fx modules provide, so a repository depends on a DB rather than on a pool plus a set of free functions.
A DB is safe for concurrent use, as the pool behind it is. Its zero value is not usable.
func Make ¶
Make validates cfg and opens a connection for it, applying the runtime opts. The caller owns the returned DB and must call Close; the Fx module does that through the lifecycle.
func (*DB) FetchPage ¶ added in v0.6.0
func (d *DB) FetchPage[T any]( ctx context.Context, query page.Query, req page.Request, args ...any, ) ([]T, uint, error)
FetchPage runs a paginated query and decodes one page of struct rows into T, with the number of rows the filter matches in total. The page request carries the page number, its size and the client's sort; the rest of the arguments are the query's own, numbered from $1.
The page is applied around the query - ORDER BY, LIMIT and OFFSET are wrapped on the outside - so the query stays a plain SELECT and its columns decode by the `db:"..."` tag as everywhere else. The total takes a second statement, which is skipped whenever the returned rows already imply it. An empty page is a valid result, not an error.
func (*DB) FetchRow ¶ added in v0.5.0
FetchRow runs the query and decodes exactly one struct row into T. It reports pgx.ErrNoRows when the query yields no rows and pgx.ErrTooManyRows when it yields more than one. For a scalar result use FetchValue.
func (*DB) FetchRows ¶ added in v0.5.0
FetchRows runs the query and decodes every row into a struct T. Columns map to struct fields by the `db:"..."` tag, falling back to the field name. For a single-column result use FetchValues.
func (*DB) FetchValue ¶ added in v0.5.0
FetchValue runs the query and decodes exactly one single-column row into a scalar T. It reports pgx.ErrNoRows when the query yields no rows and pgx.ErrTooManyRows when it yields more than one. For a struct use FetchRow.
func (*DB) FetchValues ¶ added in v0.5.0
FetchValues runs the query and decodes a single-column result into a slice of scalar T (int64, string, uuid.UUID, ...). For struct rows use FetchRows.
func (*DB) Transaction ¶ added in v0.5.0
Transaction runs fn in a transaction. A nil error commits; a non-nil error rolls back and is returned. A panic rolls back and continues unwinding. Use the callback's Tx for every transactional operation; calls through d use the pool and are outside this transaction.
type Option ¶
type Option func(*options)
Option configures the runtime behaviour of a connection built by Make.
func WithAfterConnect ¶
WithAfterConnect installs pgxpool.Config.AfterConnect - the usual place for custom pgx type registration.
func WithBeforeConnect ¶
WithBeforeConnect installs pgxpool.Config.BeforeConnect - invoked before establishing each new connection (e.g. to rotate a password).
func WithDefaultQueryExecMode ¶
func WithDefaultQueryExecMode(mode pgx.QueryExecMode) Option
WithDefaultQueryExecMode sets the default pgx.QueryExecMode.
func WithQueryMetrics ¶
func WithQueryMetrics(fn QueryMetricFunc) Option
WithQueryMetrics reports query, batch and COPY metrics alongside any other tracers. See QueryMetricFunc for callback requirements and coverage limits. nil disables it.
func WithRuntimeParam ¶
WithRuntimeParam adds a single pgx.ConnConfig.RuntimeParams value (SET key = value on connect).
func WithSQLInQueryMetrics ¶
WithSQLInQueryMetrics includes the raw SQL in QueryMetric.SQL (default off). Enable only when statements are safe to expose.
func WithStatementCacheCapacity ¶
WithStatementCacheCapacity sets the statement-cache size per connection.
func WithTLS ¶
WithTLS sets the connection's *tls.Config directly, overriding whatever the file-based Config.TLS produced (sslmode and the cert/key/root-cert paths). Use it when certificates are not files on disk - loaded from a secret manager, held in memory, or rotated at runtime: the application builds the *tls.Config and passes it here. It also removes libpq TLS/plaintext fallbacks so every connection attempt uses exactly this configuration. A nil value disables TLS.
func WithTracer ¶
func WithTracer(tracer pgx.QueryTracer) Option
WithTracer adds a custom pgx.QueryTracer (for example an AWS X-Ray tracer). It composes with WithTracing and WithQueryMetrics rather than replacing them.
type PoolConfig ¶
type PoolConfig struct {
// MaxConns is the maximum pool size.
MaxConns int32 `env:"MAX_CONNS"`
// MinConns is the minimum number of connections maintained by the pool.
MinConns int32 `env:"MIN_CONNS"`
// MinIdleConns is the minimum number of idle connections maintained by the
// pool.
MinIdleConns int32 `env:"MIN_IDLE_CONNS"`
// MaxConnLifetime is the maximum lifetime of a connection.
MaxConnLifetime time.Duration `env:"MAX_CONN_LIFETIME"`
// MaxConnLifetimeJitter randomizes connection expiry to avoid synchronized
// reconnects.
MaxConnLifetimeJitter time.Duration `env:"MAX_CONN_LIFETIME_JITTER"`
// MaxConnIdleTime is the maximum time a connection may remain idle.
MaxConnIdleTime time.Duration `env:"MAX_CONN_IDLE_TIME"`
// HealthPeriod controls how often pgxpool checks idle connections.
HealthPeriod time.Duration `env:"HEALTH_PERIOD"`
}
PoolConfig holds pgxpool sizing and connection-lifecycle settings. Zero values leave the pgxpool defaults in place.
type QueryKind ¶ added in v0.7.0
type QueryKind uint8
QueryKind is the operation a QueryMetric describes.
Use it as a bounded metric label to separate queries, batches and bulk loads.
const ( // KindQuery is a single query - Query, QueryRow or Exec. It is the zero // value, so a metric that says nothing about its kind is a query. KindQuery QueryKind = iota // KindBatch is one SendBatch operation, including reading its results. KindBatch // KindCopyFrom is one CopyFrom. KindCopyFrom )
type QueryMetric ¶
type QueryMetric struct {
// Kind is what was observed: a query, a batch or a copy. Use it as a metric
// label - it is bounded - to keep the three apart.
Kind QueryKind
// Name is the bounded label from WithQueryName ("" if unset) - use it as the
// metric label, never SQL (raw statements have unbounded cardinality).
Name string
// SQL is the raw statement when explicitly enabled with WithSQLInQueryMetrics;
// otherwise it is empty. For a batch it is every queued statement joined with
// "; " before query rewriting, including statements that may not execute.
// For a copy it is empty. Arguments are not interpolated. Do NOT use it as a
// metric label, and scrub it before recording; batches can produce long text.
SQL string
// Duration is elapsed time between pgx start/end hooks, not server execution
// or network latency. It excludes pool acquisition and includes preparation,
// result consumption (including delays before BatchResults.Close), and COPY
// source production as applicable. Always close batch results.
Duration time.Duration
// RowsAffected is the number of rows affected or returned as reported by pgx,
// summed over observed batch command tags. It can be partial on error and
// does not indicate committed changes: the transaction may roll back.
RowsAffected int64
// Err is non-nil when the operation failed. For a batch it is the error the
// batch ended with, or the first statement error when the batch itself
// reported none.
Err error
}
QueryMetric is one observed operation, passed to a QueryMetricFunc.
type QueryMetricFunc ¶
type QueryMetricFunc func(QueryMetric)
QueryMetricFunc receives one QueryMetric per completed pgx trace lifecycle. A batch produces at most one metric, not one per statement. The callback must be concurrency-safe, cheap and non-blocking; different connections call it concurrently. The package ships no metrics dependency.
Coverage follows pgx hooks: pool acquisition failures and empty batches are not observed. In pgx v5.10, some early CopyFrom failures (such as statement description errors) omit the end hook and therefore produce no metric.
type TLSConfig ¶
type TLSConfig struct {
// Mode is the libpq sslmode (disable/allow/prefer/require/verify-ca/
// verify-full). Empty leaves the libpq default ("prefer").
//
// SECURITY: "prefer"/"allow" fall back to unencrypted and never verify the
// server certificate; "require" encrypts but still does not verify it. Use
// "verify-full" with RootCert for MITM protection.
Mode string `env:"MODE"`
// Cert is the path to the client certificate.
Cert string `env:"CERT"`
// Key is the path to the client private key.
Key string `env:"KEY"`
// RootCert is the path to the trusted root certificate.
RootCert string `env:"ROOT_CERT"`
// ServerName overrides the TLS server name used for verification.
ServerName string `env:"SERVER_NAME"`
}
TLSConfig holds the libpq TLS settings.
type TimeoutConfig ¶
type TimeoutConfig struct {
// Connect bounds establishing a single connection. Zero leaves the pgx
// default.
Connect time.Duration `env:"CONNECT"`
}
TimeoutConfig holds connection timeouts.
type Tx ¶
Tx is a Postgres transaction with the same generic fetch methods as DB. It embeds the native pgx transaction API and adds typed fetch helpers.
A Tx is not safe for concurrent use. Its zero value is not usable.
func (*Tx) BeginNested ¶ added in v0.5.0
BeginNested starts a pseudo-nested transaction implemented by pgx with a savepoint. The caller must call Commit or Rollback. Transaction is the safer callback-based alternative.
func (*Tx) FetchPage ¶ added in v0.6.0
func (t *Tx) FetchPage[T any]( ctx context.Context, query page.Query, req page.Request, args ...any, ) ([]T, uint, error)
FetchPage runs a paginated query and decodes one page of struct rows into T, with the number of rows the filter matches in total. It is DB.FetchPage inside a transaction, where the rows and the count see one snapshot - under a repeatable read isolation level, a total that cannot disagree with the page it describes.
func (*Tx) FetchRow ¶ added in v0.5.0
FetchRow runs the query and decodes exactly one struct row into T. It reports pgx.ErrNoRows when the query yields no rows and pgx.ErrTooManyRows when it yields more than one. For a scalar result use FetchValue.
func (*Tx) FetchRows ¶ added in v0.5.0
FetchRows runs the query and decodes every row into a struct T. Columns map to struct fields by the `db:"..."` tag, falling back to the field name. For a single-column result use FetchValues.
func (*Tx) FetchValue ¶ added in v0.5.0
FetchValue runs the query and decodes exactly one single-column row into a scalar T. It reports pgx.ErrNoRows when the query yields no rows and pgx.ErrTooManyRows when it yields more than one. For a struct use FetchRow.
func (*Tx) FetchValues ¶ added in v0.5.0
FetchValues runs the query and decodes a single-column result into a slice of scalar T (int64, string, uuid.UUID, ...). For struct rows use FetchRows.
func (*Tx) Transaction ¶ added in v0.5.0
Transaction runs fn in a pseudo-nested transaction implemented by pgx with a savepoint. A nil error releases the savepoint; a non-nil error rolls back to it. A panic rolls back to it and continues unwinding.