pgfx

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 17 Imported by: 0

README

pgfx

CI Go Reference

A thin, Fx-first layer over pgx/pgxpool for Postgres. A pgfx connection is a single *pgxpool.Pool built from a plain, serializable Config. Runtime dependencies (tracer, query metrics, pool hooks, an in-memory *tls.Config) are supplied through Options, never through Config.

Module consumes a pgfx.Config from the Uber Fx container and hands back a ready pool; ModuleFor(name) adds a replica or another shard. Multiple databases are just multiple named connections. pgfx does not read any config source itself, so it stays decoupled: the application supplies each Config, typically loaded from a file and the environment with confmaker/confx. pgfx depends only on github.com/uchaloop/secret (for the masked password), not on any config or env stack.

Install

go get github.com/uchaloop/pgfx

Config

Config is a plain, serializable struct. Host reads from the file or the environment (env wins); Database/User/TLS/Pool/Timeouts are file-only; Password is env-only - a secret.Secret that masks itself in logs and dumps, and placing it in the file is rejected.

Host and Database are required. User and Password are optional: when empty they fall back to libpq's defaults (PGUSER / the OS user, and PGPASSWORD / .pgpass), so peer auth and passwordless connections work.

The section name is the application's choice - it is passed to confx when wiring (below). A typical [postgres] table:

# config/local.toml
[postgres]
host     = "localhost:5432"   # "host" or "host:port"; port omitted -> 5432
database = "orders"
user     = "orders"

  [postgres.tls]
  mode      = "verify-full"   # verify-full + root_cert for MITM protection
  root_cert = "/run/secrets/ca.crt"

  [postgres.pool]
  max_conns = 20

SECURITY: sslmode=prefer/allow fall back to unencrypted and skip certificate verification; use verify-full with a root_cert for MITM protection.

TLS certificates in the file are paths (sslcert/sslkey/sslrootcert), read by libpq. When certificates are not files - loaded from a secret manager or held in memory - pass a built *tls.Config with pgfx.WithTLS, which overrides the file settings and disables libpq TLS/plaintext fallbacks, ensuring every connection attempt uses exactly that configuration. Passing nil explicitly disables TLS.

See config.example.toml for every field with comments.

Fx wiring

pgfx consumes a pgfx.Config from the container; the application provides it. The recommended source is confmaker/confx, which loads a TOML section plus environment into the Config. For a single connection: load the file, provide the default Config, and add pgfx.Module:

fx.New(
	confx.LoadModule("config/local.toml"),
	confx.ProvideDefault[pgfx.Config]("postgres"), // untagged Config, POSTGRES_* env
	pgfx.Module,                                   // untagged *pgxpool.Pool
)

pgfx.Module provides an untagged *pgxpool.Pool, so repositories just depend on *pgxpool.Pool - no tags, no wrappers:

type OrderRepo struct {
	db *pgxpool.Pool
}

func MakeOrderRepo(db *pgxpool.Pool) *OrderRepo {
	return &OrderRepo{db: db}
}

Add a named connection for a replica or another shard: provide a tagged Config and add pgfx.ModuleFor(name), which provides a *pgxpool.Pool tagged name:"<name>" (a consumer selects it with fx.ParamTags). Each named connection is its own top-level section:

[postgres]                      # default connection, POSTGRES_* env
host = "primary:5432"
database = "orders"

[replica]                       # env REPLICA_*
host = "replica:5432"
database = "orders"
fx.New(
	confx.LoadModule("config/local.toml"),
	confx.ProvideDefault[pgfx.Config]("postgres"),         // untagged primary
	confx.Provide[pgfx.Config]("replica", "replica"),      // tagged name:"replica"
	pgfx.Module,
	pgfx.ModuleFor("replica"),
)

Both are pinged on start (fail-fast on a dead database) and closed on stop, and both take an optional trace.TracerProvider and QueryMetricFunc from the container.

Without Fx, build a pool directly with pgfx.Make(ctx, cfg, opts...) from a Config you fill yourself.

Querying

Generic scan helpers work on any Querier (*pgxpool.Pool, *pgx.Conn, pgx.Tx):

func (r *OrderRepo) Get(ctx context.Context, id int64) (Order, error) {
	const sql = `SELECT id, amount FROM orders WHERE id = $1`
	
	return pgfx.FetchRow[Order](ctx, r.db, sql, id)
}

func (r *OrderRepo) Create(ctx context.Context, amount int64) (int64, error) {
	const sql = `INSERT INTO orders (amount) VALUES ($1) RETURNING id` 
	
	return pgfx.FetchValue[int64](ctx, r.db, sql, amount)
}
order, err := repo.Get(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
	// not found
}

id, err := repo.Create(ctx, amount)

Transactions - Tx takes a TxBeginner (*pgxpool.Pool or *pgx.Conn), mirroring Querier:

err := pgfx.Tx(
	ctx,
	repo.db,
	pgx.TxOptions{},
	func(tx pgx.Tx) error {
            _, err := pgfx.FetchValue[int64](ctx, tx, `INSERT ... RETURNING id`, ...)
            return err // non-nil -> rollback; nil -> commit 
	},
	)

Tracing and query metrics

Tracing is opt-in. OpenTelemetry query spans come from exaring/otelpgx. Under Fx, tracing turns on when a trace.TracerProvider is supplied to the container; otherwise there is no tracer. Without Fx, enable it with pgfx.WithTracing(...), configured by otelpgx's own options:

// Fx: supplying a TracerProvider enables otelpgx spans.
fx.Supply(fx.Annotate(tp, fx.As(new(trace.TracerProvider))))

// non-Fx:
pool, _ := pgfx.Make(ctx, cfg,
	pgfx.WithTracing(otelpgx.WithTracerProvider(tp), otelpgx.WithTrimSQLInSpanName()))

A different backend (for example AWS X-Ray) plugs in through pgfx.WithTracer(customTracer), which composes alongside otelpgx.

Query metrics are a separate, dependency-free callback (otelpgx does spans, not per-query metrics):

fx.Supply(pgfx.QueryMetricFunc(func(m pgfx.QueryMetric) { /* record m.Name, m.Duration */ }))

QueryMetric.SQL is empty unless WithSQLInQueryMetrics(true); label metrics by QueryMetric.Name (WithQueryName(ctx, "orders.get_by_id")), never by SQL.

Acknowledgements

pgfx stands on jackc/pgx, uber-go/fx and exaring/otelpgx. Thanks to their authors and maintainers.

License

MIT.

Documentation

Overview

Package pgfx is a thin, Fx-first layer over pgx/pgxpool for Postgres. A pgfx connection is a single *pgxpool.Pool built from a plain, serializable Config. Runtime dependencies (tracer, query metrics, pool hooks, an in-memory *tls.Config) are supplied through Option values, never through Config.

pgfx is meant to be wired with Uber Fx. Module is the default single connection and ModuleFor adds a replica or another shard; both consume a pgfx.Config the application supplies (typically via confmaker/confx) and manage the pool's lifecycle. pgfx does not read any config source itself, so it stays decoupled from confmaker. Multiple databases are simply multiple named connections. To build a pool without Fx, call Make with a Config you fill yourself.

Index

Constants

This section is empty.

Variables

View Source
var Module = fx.Module("pgfx", poolProvider(``))

Module is an Fx module for the default Postgres connection: it consumes an untagged pgfx.Config from the container and provides an untagged *pgxpool.Pool, so a repository just depends on *pgxpool.Pool - no tags, no wrappers.

The application supplies the Config explicitly - pgfx does not read any config source itself - typically with confmaker/confx:

fx.New(
	confx.LoadModule("config/local.toml"),
	confx.ProvideDefault[pgfx.Config]("postgres"),
	pgfx.Module,
)

The pool 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 FetchRow

func FetchRow[T any](
	ctx context.Context,
	q Querier,
	sql string,
	args ...any,
) (T, error)

FetchRow runs the query on q and decodes exactly one struct row into T. It returns sql.ErrNoRows when the query yields no rows, and pgx.ErrTooManyRows when it yields more than one. For a scalar result use FetchValue.

func FetchRows

func FetchRows[T any](
	ctx context.Context,
	q Querier,
	sql string,
	args ...any,
) ([]T, error)

FetchRows runs the query on q 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 (scalar) result use FetchValues.

func FetchValue

func FetchValue[T any](
	ctx context.Context,
	q Querier,
	sql string,
	args ...any,
) (T, error)

FetchValue runs the query on q and decodes exactly one single-column row into a scalar T. It is the helper for count(*), INSERT ... RETURNING id and similar. It returns sql.ErrNoRows when the query yields no rows, and pgx.ErrTooManyRows when it yields more than one. For struct rows use FetchRow.

func FetchValues

func FetchValues[T any](
	ctx context.Context,
	q Querier,
	sql string,
	args ...any,
) ([]T, error)

FetchValues runs the query on q and decodes a single-column result into a slice of scalar T (int64, string, uuid.UUID, ...). For struct rows use FetchRows.

func Make

func Make(ctx context.Context, cfg Config, opts ...Option) (*pgxpool.Pool, error)

Make validates cfg and opens a connection pool for it, applying the runtime opts. The caller owns the returned pool and must Close it (the fx module does this via lifecycle).

func ModuleFor

func ModuleFor(name string) fx.Option

ModuleFor is an Fx module for a named connection - a replica or another shard. It consumes a pgfx.Config tagged name:"<name>" and provides a *pgxpool.Pool tagged the same; a consumer selects it with fx.ParamTags. The application supplies the tagged Config explicitly (typically confmaker/confx's Provide).

func QueryName

func QueryName(ctx context.Context) string

QueryName returns the name set by WithQueryName, or "" if none.

func Tx

func Tx(
	ctx context.Context,
	db TxBeginner,
	opts pgx.TxOptions,
	fn func(pgx.Tx) error,
) (err error)

Tx runs fn inside a transaction begun on db and commits it, or rolls back on error or panic. The transaction is passed explicitly to fn (no ambient transaction via context): because pgx.Tx satisfies Querier, FetchRow/FetchRows work on it unchanged.

opts selects the isolation level (e.g. pgx.TxOptions{IsoLevel: pgx.Serializable}).

Tx does not retry serialization failures (SQLSTATE 40001) and does not nest: calling Tx from within fn opens a second top-level transaction on a different connection, not a savepoint. Handle both concerns at the call site if needed.

func WithQueryName

func WithQueryName(ctx context.Context, name string) context.Context

WithQueryName tags ctx with a bounded query name used as QueryMetric.Name. Set it per call site (e.g. "orders.get_by_id"); keep the set of names small.

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 `koanf:"host" env:"HOST"`
	// Database is the required PostgreSQL database name.
	Database string `koanf:"database"`
	// User is the PostgreSQL role. When empty, libpq selects its default.
	User string `koanf:"user"`
	// Password is read from the environment only. When empty, libpq may use
	// PGPASSWORD or .pgpass.
	Password secret.Secret `koanf:"-" env:"PASSWORD"`
	// AppName is reported as application_name in pg_stat_activity.
	AppName string `koanf:"app_name"`

	// TLS configures libpq transport security.
	TLS TLSConfig `koanf:"tls"`
	// Pool configures pgxpool sizing and connection lifetimes.
	Pool PoolConfig `koanf:"pool"`
	// Timeouts configures connection timeouts.
	Timeouts TimeoutConfig `koanf:"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 struct tags let a loader fill it: `koanf` fields come from the file, `env` fields from the environment (see confmaker/confx). Host is readable from both - the file sets a default, the environment overrides it for a deployment. Password is env-only (`koanf:"-"`), so putting it in the file is rejected as an unknown key. The tags are inert strings, so the Config type itself depends only on the dependency-free github.com/uchaloop/secret.

Host and Database are required. User and Password are optional: when empty they fall back to libpq's defaults (PGUSER / the OS user, and PGPASSWORD / .pgpass).

func (Config) Validate

func (cfg Config) Validate() error

Validate checks the connection's own invariants. It returns every problem at once, not just the first.

type Option

type Option func(*options)

Option configures the runtime behaviour of a connection built by Make.

func WithAfterConnect

func WithAfterConnect(fn func(context.Context, *pgx.Conn) error) Option

WithAfterConnect installs pgxpool.Config.AfterConnect - the usual place for custom pgx type registration.

func WithBeforeConnect

func WithBeforeConnect(fn func(context.Context, *pgx.ConnConfig) error) Option

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 installs a second pgx.QueryTracer that reports a QueryMetric for every query (composed with the span tracer). nil disables it.

func WithRuntimeParam

func WithRuntimeParam(key, value string) Option

WithRuntimeParam adds a single pgx.ConnConfig.RuntimeParams value (SET key = value on connect).

func WithSQLInQueryMetrics

func WithSQLInQueryMetrics(include bool) Option

WithSQLInQueryMetrics includes the raw SQL in QueryMetric.SQL (default off). Enable only when statements are safe to expose.

func WithStatementCacheCapacity

func WithStatementCacheCapacity(capacity int) Option

WithStatementCacheCapacity sets the statement-cache size per connection.

func WithTLS

func WithTLS(tlsConfig *tls.Config) Option

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.

func WithTracing

func WithTracing(opts ...otelpgx.Option) Option

WithTracing enables OpenTelemetry query spans via otelpgx. Tracing is off by default; this turns it on. Configure it with otelpgx's own options, e.g. WithTracing(otelpgx.WithTracerProvider(tp), otelpgx.WithTrimSQLInSpanName()).

type PoolConfig

type PoolConfig struct {
	// MaxConns is the maximum pool size.
	MaxConns int32 `koanf:"max_conns"`
	// MinConns is the minimum number of connections maintained by the pool.
	MinConns int32 `koanf:"min_conns"`
	// MinIdleConns is the minimum number of idle connections maintained by the
	// pool.
	MinIdleConns int32 `koanf:"min_idle_conns"`
	// MaxConnLifetime is the maximum lifetime of a connection.
	MaxConnLifetime time.Duration `koanf:"max_conn_lifetime"`
	// MaxConnLifetimeJitter randomizes connection expiry to avoid synchronized
	// reconnects.
	MaxConnLifetimeJitter time.Duration `koanf:"max_conn_lifetime_jitter"`
	// MaxConnIdleTime is the maximum time a connection may remain idle.
	MaxConnIdleTime time.Duration `koanf:"max_conn_idle_time"`
	// HealthPeriod controls how often pgxpool checks idle connections.
	HealthPeriod time.Duration `koanf:"health_period"`
}

PoolConfig holds pgxpool sizing and connection-lifecycle settings. Zero values leave the pgxpool defaults in place.

type Querier

type Querier interface {
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
}

Querier is the minimal query surface the generic fetch helpers need. It is satisfied by *pgxpool.Pool, *pgx.Conn and pgx.Tx alike, so the helpers run unchanged both directly on a pool and inside a transaction.

type QueryMetric

type QueryMetric struct {
	// 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. Do NOT use it as a metric label, and scrub it before
	// recording.
	SQL string
	// Duration is the wall time from query start to end.
	Duration time.Duration
	// RowsAffected is the number of rows affected or returned as reported by pgx.
	RowsAffected int64
	// Err is non-nil when the query failed.
	Err error
}

QueryMetric is one observed query, passed to a QueryMetricFunc.

type QueryMetricFunc

type QueryMetricFunc func(QueryMetric)

QueryMetricFunc receives one QueryMetric per query. It runs on the query hot path, so it must be cheap and non-blocking. The package ships no metrics dependency - this callback is the seam where you plug one in.

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 `koanf:"mode"`
	// Cert is the path to the client certificate.
	Cert string `koanf:"cert"`
	// Key is the path to the client private key.
	Key string `koanf:"key"`
	// RootCert is the path to the trusted root certificate.
	RootCert string `koanf:"root_cert"`
	// ServerName overrides the TLS server name used for verification.
	ServerName string `koanf:"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 `koanf:"connect"`
}

TimeoutConfig holds connection timeouts.

type TxBeginner

type TxBeginner interface {
	BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error)
}

TxBeginner starts transactions. It is satisfied by *pgxpool.Pool and *pgx.Conn, so Tx runs on a pool or a single connection alike - the transaction-level counterpart of Querier for the fetch helpers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL