Documentation
¶
Overview ¶
Package pgfx is a thin client that extends pgx/pgxpool with typed fetch methods, pagination, transaction callbacks, configuration and optional observability. It keeps SQL explicit and preserves the native pgx API: DB embeds *pgxpool.Pool and Tx embeds pgx.Tx. It is not an ORM or a replacement driver.
Configuration ¶
Config contains connection data; Option supplies runtime dependencies such as tracers, callbacks and an in-memory TLS configuration. The application constructs Config in Go or uses a loader of its choice. pgfx does not read a configuration source itself. Host and Database are required; optional fields retain their documented pgx defaults when unset.
Make creates a pool owned by the caller, who must close it. Pool creation does not verify connectivity; use Ping when startup verification is required. Module supplies *DB through Uber Fx, verifies connectivity on application start and closes the pool on stop:
fx.New(
fx.Supply(pgfx.Config{Host: "localhost:5432", Database: "orders"}),
pgfx.Module,
).Run()
ModuleFor consumes a Config with an Fx name and provides a DB with the same name. Each named connection has its own configuration. The native pool is not registered separately in Fx; it is available through DB.Pool.
For convenient environment loading and Fx integration, confmaker/confx is an optional recommended loader. It can replace manual Config construction; it is not required by pgfx. See the README for its setup and environment prefixes.
Typed fetches ¶
DB.FetchRows and DB.FetchRow decode struct fields using db tags, falling back to field names. JSON/JSONB columns use pgx codecs, including map fields. DB.FetchValues and DB.FetchValue decode a single column into scalar values. The same methods are available on Tx.
DB.FetchRowsInto and DB.FetchValuesInto append to a caller-provided slice, preserving existing elements and growing its capacity when necessary. Pass a zero-length slice with capacity to reserve memory, or dst[:0] to reuse a buffer once its previous consumers have finished. Always use the returned slice. Errors return nil but may leave writes in the supplied backing array. The same methods are available on Tx.
Singular fetches require exactly one row and return pgx.ErrNoRows or pgx.ErrTooManyRows otherwise. Plural fetches accept an empty result. Use FetchValue for a single-column INSERT or UPDATE with RETURNING, and FetchRow for multiple returned columns. Native pgx operations remain available directly.
Pagination ¶
DB.FetchPage returns a list and total: the total number of rows matching the base SELECT before pagination. DB.FetchPageRows returns the list and whether another page follows, and never executes a count query. Both methods decode structs using the same db mapping as FetchRows and are also available on Tx.
Build a page.Query once from a SELECT containing columns, joins and filters. Do not add top-level ORDER BY, LIMIT or pagination to that SELECT. Pass filter arguments separately from the page.Request, starting at $1.
Numbered pages use LIMIT/OFFSET and read one row past the page, which tells whether another page follows. DB.FetchAfter instead accepts a page.CursorRequest and returns a page.CursorResult with List, NextCursor and HasMore. It seeks after the last returned sort keys and fetches one extra row; it never counts. DB.FetchTotal explicitly counts the base filter independently. These methods are also available on Tx. Prefer offset for shallow numbered pages and cursor for sequential traversal of large results with matching indexes.
Offset fetches reserve Size+1 elements, including the lookahead row; cursor fetches reserve Size. This also reserves memory for empty pages. The application must validate an appropriate maximum page size; there is no internal capacity cap.
Cursor keys must be selected output columns. Tokens bind SQL, sort and filter arguments, which must have stable JSON representations. Tokens are not signed or encrypted and do not replace authorization. Supported key types are documented on page.CursorStatement.Cursor; arbitrary custom pgx types are not supported. Changing sort keys concurrently can still move rows across page boundaries.
Sorting applies page.Head, the requested sort, then page.Tie. Tie must provide a unique ordering for the result; after other orders it takes the direction of the last one, so one composite index serves the whole order. A client sorts only by the fields page.Sortable lists; page.SortKeyTag opens every field of the model under the names of a struct tag, such as json. Unknown sort fields return an error.
FetchPage counts the base SELECT in a separate statement unless a nonempty page with no row after it already determines the total. A page that more rows follow, and an empty page, require the count. page.CountSQL can supply a cheaper equivalent SELECT for counting; it must return the same number of rows and accept the same arguments. JOIN multiplicity, DISTINCT and GROUP BY belong to the base SELECT: pgfx counts its result rows, not inferred entities.
Pagination does not create a transaction. Concurrent writes can change data between the list and count statements. When one snapshot is required, call Tx.FetchPage inside DB.Transaction with pgx.RepeatableRead and, for reads, pgx.ReadOnly. This is an explicit caller choice.
Transactions ¶
DB.Transaction commits when the callback returns nil and rolls back on an error or panic. Its callback receives a typed Tx. Use that Tx for every operation that belongs to the transaction; calls through DB use the pool instead. Tx.Transaction provides the same callback style for a nested savepoint. Tx.BeginNested returns a typed savepoint when manual ownership is needed. Isolation and access mode are supplied through pgx.TxOptions.
Tracing and metrics ¶
WithTracing enables otelpgx spans; WithTracer installs a custom pgx tracer. Module enables tracing when a trace.TracerProvider is supplied through Fx. WithQueryMetrics installs a callback without requiring a metrics backend. The callback must be concurrency-safe, cheap and non-blocking.
WithQueryName gives an operation a stable name. A pagination count uses that name with a .count suffix. SQL is omitted from metrics unless explicitly enabled with WithSQLInQueryMetrics. Use bounded names, not raw SQL, as metric labels.
Metrics follow pgx hooks. Duration includes result consumption but excludes pool acquisition. A batch produces one observation, with potentially partial row counts on error; it does not imply commit. Empty batches and pool acquisition failures are not observed. Some early COPY failures omit the end hook. See QueryMetric and QueryMetricFunc for the full callback contract.
TLS and connection hooks ¶
TLSConfig supplies file-based TLS settings. WithTLS supplies an in-memory configuration and replaces the configured fallbacks. WithBeforeConnect and WithAfterConnect expose connection hooks for credentials and type registration.
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) FetchAfter[T any](ctx context.Context, query page.Query, req page.CursorRequest, args ...any) (page.CursorResult[T], error)
- func (d *DB) FetchPage[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, uint, error)
- func (d *DB) FetchPageRows[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, bool, 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) FetchRowsInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
- func (d *DB) FetchTotal(ctx context.Context, query page.Query, args ...any) (uint, 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) FetchValuesInto[T any](ctx context.Context, dst []T, 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) FetchAfter[T any](ctx context.Context, query page.Query, req page.CursorRequest, args ...any) (page.CursorResult[T], error)
- func (t *Tx) FetchPage[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, uint, error)
- func (t *Tx) FetchPageRows[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, bool, 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) FetchRowsInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
- func (t *Tx) FetchTotal(ctx context.Context, query page.Query, args ...any) (uint, 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) FetchValuesInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
- func (t *Tx) Transaction(ctx context.Context, fn func(*Tx) error) error
Examples ¶
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:
fx.New(
fx.Supply(pgfx.Config{Host: "localhost:5432", Database: "orders"}),
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, for example with fx.Supply and fx.ResultTags.
Example ¶
package main
import (
"github.com/uchaloop/pgfx"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Supply(fx.Annotate(pgfx.Config{Host: "replica:5432", Database: "orders"}, fx.ResultTags(`name:"replica"`))),
pgfx.ModuleFor("replica"),
).Run()
}
Output:
func WithQueryName ¶
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.
Example ¶
package main
import (
"context"
"fmt"
"github.com/uchaloop/pgfx"
)
func main() {
ctx := pgfx.WithQueryName(context.Background(), "orders.list")
fmt.Println(pgfx.QueryName(ctx))
}
Output: orders.list
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" envDescription:"PostgreSQL host or host:port; port defaults to 5432."`
// Database is the PostgreSQL database name.
Database string `env:"DATABASE,notEmpty" envDescription:"PostgreSQL database name."`
// User is the PostgreSQL role. When empty, libpq selects its default.
User string `env:"USER" envDescription:"PostgreSQL role; unset or empty uses PGUSER or the operating system user."`
// Password holds the application-supplied secret. When empty, libpq may use
// PGPASSWORD or .pgpass.
Password secret.Secret `env:"PASSWORD" envDescription:"PostgreSQL password; unset or empty allows PGPASSWORD or .pgpass."`
// AppName is reported as application_name in pg_stat_activity.
AppName string `env:"APP_NAME" envDescription:"Application name reported in pg_stat_activity."`
// 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 github.com/uchaloop/confmaker). 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). Register with the explicit instance name "postgres" to read POSTGRES_*.
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 creates a pool, applying the runtime opts. Call Ping to verify connectivity. The caller owns the returned DB and must call Close; Module manages verification and cleanup through the Fx lifecycle.
Example ¶
package main
import (
"context"
"log"
"github.com/uchaloop/pgfx"
)
func main() {
db, err := pgfx.Make(
context.Background(),
pgfx.Config{Host: "localhost:5432", Database: "orders"},
pgfx.WithRuntimeParam("application_name", "orders-api"),
)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Make creates the pool; Ping explicitly verifies connectivity.
if err := db.Ping(context.Background()); err != nil {
log.Print(err)
}
}
Output:
func (*DB) FetchAfter ¶ added in v0.8.0
func (d *DB) FetchAfter[T any](ctx context.Context, query page.Query, req page.CursorRequest, args ...any) (page.CursorResult[T], error)
FetchAfter returns the next keyset page without counting the full result. Query sorting must include a unique Tie. The cursor is bound to the query, filter arguments and effective order. No transaction is created automatically.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
query := page.Must("SELECT id, amount, meta FROM orders WHERE amount >= $1", page.Tie(page.Asc("id")))
req := page.CursorRequest{Size: 20}
for {
result, err := db.FetchAfter[order](context.Background(), query, req, 100)
if err != nil {
log.Print(err)
return
}
for _, item := range result.List {
fmt.Println(item.ID)
}
if !result.HasMore {
break
}
req.After = result.NextCursor
}
}
Output:
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.
LIMIT/OFFSET is applied around the base SELECT. Columns decode by the `db:"..."` tag as everywhere else. The total takes a second statement, which is skipped when a nonempty page has no row after it. An empty page is a valid result, not an error.
Example (OffsetLimit) ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
query := page.Must(
`SELECT id, amount, meta FROM orders WHERE amount >= $1`,
page.Tie(page.Asc("id")),
page.SortKeyTag("json"),
)
list, total, err := db.FetchPage[order](
context.Background(),
query,
page.Request{Number: 2, Size: 20, Sort: []string{"amount:desc"}},
100,
)
if err != nil {
log.Print(err)
return
}
fmt.Println(list, total)
}
Output:
func (*DB) FetchPageRows ¶ added in v0.8.0
func (d *DB) FetchPageRows[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, bool, error)
FetchPageRows runs a paginated query without counting: it returns one page of struct rows and reports whether another page follows, which it learns by reading one row past the page.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
query := page.Must(`SELECT id, amount, meta FROM orders WHERE amount >= $1`, page.Tie(page.Asc("id")))
// No count statement runs; more says whether another page follows.
list, more, err := db.FetchPageRows[order](context.Background(), query, page.Request{Number: 1, Size: 20}, 100)
if err != nil {
log.Print(err)
return
}
fmt.Println(list, more)
}
Output:
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.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
item, err := db.FetchRow[order](context.Background(), `SELECT id, amount, meta FROM orders WHERE id=$1`, 42)
if err != nil {
log.Print(err)
return
}
fmt.Println(item.ID, item.Amount, item.Meta)
}
Output:
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.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
list, err := db.FetchRows[order](context.Background(), `SELECT id, amount, meta FROM orders WHERE amount >= $1 ORDER BY id`, 100)
if err != nil {
log.Print(err)
return
}
fmt.Println(list)
}
Output:
func (*DB) FetchRowsInto ¶ added in v0.9.0
func (d *DB) FetchRowsInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
FetchRowsInto appends struct rows to dst. Pass dst[:0] to reuse its buffer. Capacity is a reservation, not a row limit; the slice grows as needed. On error it returns nil; appended values may already have modified dst's backing array. Do not reuse that array while another consumer is reading it.
func (*DB) FetchTotal ¶ added in v0.8.0
FetchTotal counts the complete base filter, honoring page.CountSQL. It does not apply pagination or cursor bounds and creates no transaction.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := exampleDB()
defer db.Close()
query := page.Must("SELECT id, amount, meta FROM orders WHERE amount >= $1", page.Tie(page.Asc("id")))
total, err := db.FetchTotal(context.Background(), query, 100)
if err != nil {
log.Print(err)
return
}
fmt.Println(total)
}
Output:
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.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := exampleDB()
defer db.Close()
id, err := db.FetchValue[int64](context.Background(), `INSERT INTO orders (amount) VALUES ($1) RETURNING id`, 100)
if err != nil {
log.Print(err)
return
}
fmt.Println(id)
}
Output:
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.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := exampleDB()
defer db.Close()
ids, err := db.FetchValues[int64](context.Background(), `SELECT id FROM orders WHERE amount >= $1 ORDER BY id`, 100)
if err != nil {
log.Print(err)
return
}
fmt.Println(ids)
}
Output:
func (*DB) FetchValuesInto ¶ added in v0.9.0
func (d *DB) FetchValuesInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
FetchValuesInto appends single-column values to dst. Pass dst[:0] to reuse its buffer. Capacity is a reservation, not a row limit; the slice grows as needed. On error it returns nil; appended values may already have modified dst's backing array. Do not reuse that array while another consumer is reading it.
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.
Example ¶
package main
import (
"context"
"log"
"github.com/jackc/pgx/v5"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
ctx := context.Background()
err := db.Transaction(
ctx,
pgx.TxOptions{},
func(tx *pgfx.Tx) error {
item, err := tx.FetchRow[order](ctx, `SELECT id, amount, meta FROM orders WHERE id=$1 FOR UPDATE`, 42)
if err != nil {
return err
}
_, err = tx.FetchValue[int64](ctx, `UPDATE orders SET amount=$1 WHERE id=$2 RETURNING id`, item.Amount+100, item.ID)
return err // nil commits; an error rolls back.
},
)
if err != nil {
log.Print(err)
}
}
Output:
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.
Example ¶
package main
import (
"context"
"log"
"github.com/uchaloop/pgfx"
)
func main() {
db, err := pgfx.Make(
context.Background(),
pgfx.Config{Host: "localhost:5432", Database: "orders"},
pgfx.WithQueryMetrics(
func(metric pgfx.QueryMetric) {
// log.Printf is concurrency-safe. Production callbacks should be cheap.
log.Printf("name=%s kind=%s duration=%s rows=%d err=%v", metric.Name, metric.Kind, metric.Duration, metric.RowsAffected, metric.Err)
},
),
)
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.FetchValues[int64](pgfx.WithQueryName(context.Background(), "orders.ids"), `SELECT id FROM orders ORDER BY id`)
if err != nil {
log.Print(err)
}
}
Output:
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" envDescription:"Maximum pool connections; zero keeps the pgxpool default."`
// MinConns is the minimum number of connections maintained by the pool.
MinConns int32 `env:"MIN_CONNS" envDescription:"Minimum pool connections; zero keeps the pgxpool default."`
// MinIdleConns is the minimum number of idle connections maintained by the
// pool.
MinIdleConns int32 `env:"MIN_IDLE_CONNS" envDescription:"Minimum idle pool connections; zero keeps the pgxpool default."`
// MaxConnLifetime is the maximum lifetime of a connection.
MaxConnLifetime time.Duration `env:"MAX_CONN_LIFETIME" envDescription:"Maximum connection lifetime; zero keeps the pgxpool default."`
// MaxConnLifetimeJitter randomizes connection expiry to avoid synchronized
// reconnects.
MaxConnLifetimeJitter time.Duration `env:"MAX_CONN_LIFETIME_JITTER" envDescription:"Random connection lifetime adjustment; zero keeps the pgxpool default."`
// MaxConnIdleTime is the maximum time a connection may remain idle.
MaxConnIdleTime time.Duration `env:"MAX_CONN_IDLE_TIME" envDescription:"Maximum connection idle time; zero keeps the pgxpool default."`
// HealthPeriod controls how often pgxpool checks idle connections.
HealthPeriod time.Duration `env:"HEALTH_PERIOD" envDescription:"Interval between idle connection health checks; zero keeps the pgxpool default."`
}
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 `` /* 126-byte string literal not displayed */
// Cert is the path to the client certificate.
Cert string `env:"CERT" envDescription:"Path to the TLS client certificate."`
// Key is the path to the client private key.
Key string `env:"KEY" envDescription:"Path to the TLS client private key."`
// RootCert is the path to the trusted root certificate.
RootCert string `env:"ROOT_CERT" envDescription:"Path to the trusted TLS root certificate."`
// ServerName overrides the TLS server name used for verification.
ServerName string `env:"SERVER_NAME" envDescription:"TLS server name used for certificate verification."`
}
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" envDescription:"Connection establishment timeout; zero keeps the pgx default."`
}
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.
Example ¶
package main
import (
"context"
"log"
"github.com/jackc/pgx/v5"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := exampleDB()
defer db.Close()
ctx := context.Background()
err := db.Transaction(
ctx,
pgx.TxOptions{},
func(tx *pgfx.Tx) error {
nested, err := tx.BeginNested(ctx)
if err != nil {
return err
}
defer nested.Rollback(ctx)
if _, err := nested.FetchValue[int64](ctx, `INSERT INTO orders (amount) VALUES ($1) RETURNING id`, 100); err != nil {
return err
}
return nested.Commit(ctx) // Releases this savepoint, not the outer transaction.
},
)
if err != nil {
log.Print(err)
}
}
Output:
func (*Tx) FetchAfter ¶ added in v0.8.0
func (t *Tx) FetchAfter[T any](ctx context.Context, query page.Query, req page.CursorRequest, args ...any) (page.CursorResult[T], error)
FetchAfter returns the next keyset page without counting the full result. Query sorting must include a unique Tie. The cursor is bound to the query, filter arguments and effective order. No transaction is created automatically.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
ctx := context.Background()
query := page.Must("SELECT id, amount, meta FROM orders WHERE amount >= $1", page.Tie(page.Asc("id")))
err := db.Transaction(
ctx,
pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly},
func(tx *pgfx.Tx) error {
result, err := tx.FetchAfter[order](ctx, query, page.CursorRequest{Size: 20}, 100)
if err != nil {
return err
}
total, err := tx.FetchTotal(ctx, query, 100)
if err != nil {
return err
}
fmt.Println(len(result.List), result.HasMore, total)
return nil
},
)
if err != nil {
log.Print(err)
}
}
Output:
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 the caller's transaction. Use pgx.RepeatableRead when rows and total must share a snapshot; the default isolation level does not guarantee this.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
ctx := context.Background()
query := page.Must(`SELECT id, amount, meta FROM orders`, page.Tie(page.Asc("id")))
var list []order
var total uint
err := db.Transaction(
ctx,
pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly},
func(tx *pgfx.Tx) error {
var err error
list, total, err = tx.FetchPage[order](ctx, query, page.Request{Number: 1, Size: 20})
return err
},
)
if err != nil {
log.Print(err)
return
}
fmt.Println(list, total) // Read from one explicitly requested snapshot.
}
Output:
func (*Tx) FetchPageRows ¶ added in v0.8.0
func (t *Tx) FetchPageRows[T any](ctx context.Context, query page.Query, req page.Request, args ...any) ([]T, bool, error)
FetchPageRows runs a paginated query without counting: it returns one page of struct rows and reports whether another page follows. It is DB.FetchPageRows inside the caller's transaction.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5"
"github.com/uchaloop/pgfx"
"github.com/uchaloop/pgfx/page"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
type order struct {
ID int64 `db:"id" json:"id"`
Amount int64 `db:"amount" json:"amount"`
Meta map[string]any `db:"meta" json:"meta"`
}
func main() {
db := exampleDB()
defer db.Close()
ctx := context.Background()
query := page.Must(`SELECT id, amount, meta FROM orders`, page.Tie(page.Asc("id")))
err := db.Transaction(
ctx,
pgx.TxOptions{AccessMode: pgx.ReadOnly},
func(tx *pgfx.Tx) error {
list, more, err := tx.FetchPageRows[order](ctx, query, page.Request{Number: 1, Size: 20})
if err != nil {
return err
}
fmt.Println(list, more)
return nil
},
)
if err != nil {
log.Print(err)
}
}
Output:
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) FetchRowsInto ¶ added in v0.9.0
func (t *Tx) FetchRowsInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
FetchRowsInto appends struct rows to dst. Pass dst[:0] to reuse its buffer. Capacity is a reservation, not a row limit; the slice grows as needed. On error it returns nil; appended values may already have modified dst's backing array. Do not reuse that array while another consumer is reading it.
func (*Tx) FetchTotal ¶ added in v0.8.0
FetchTotal counts the complete base filter, honoring page.CountSQL. It does not apply pagination or cursor bounds and creates no transaction.
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) FetchValuesInto ¶ added in v0.9.0
func (t *Tx) FetchValuesInto[T any](ctx context.Context, dst []T, sql string, args ...any) ([]T, error)
FetchValuesInto appends single-column values to dst. Pass dst[:0] to reuse its buffer. Capacity is a reservation, not a row limit; the slice grows as needed. On error it returns nil; appended values may already have modified dst's backing array. Do not reuse that array while another consumer is reading it.
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.
Example ¶
package main
import (
"context"
"log"
"github.com/jackc/pgx/v5"
"github.com/uchaloop/pgfx"
)
// Database examples are compiled but not run: they require a local orders database.
func exampleDB() *pgfx.DB {
db, err := pgfx.Make(context.Background(), pgfx.Config{Host: "localhost:5432", Database: "orders"})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := exampleDB()
defer db.Close()
ctx := context.Background()
err := db.Transaction(
ctx,
pgx.TxOptions{},
func(tx *pgfx.Tx) error {
return tx.Transaction(
ctx,
func(nested *pgfx.Tx) error {
_, err := nested.FetchValue[int64](ctx, `INSERT INTO orders (amount) VALUES ($1) RETURNING id`, 100)
return err
},
) // Releases the savepoint on nil, rolls back to it on error.
},
)
if err != nil {
log.Print(err)
}
}
Output: