pgfx

package module
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 21 Imported by: 0

README

CI Go Reference

A thin client extending pgx with typed fetches, pagination, transaction callbacks and optional Fx integration and observability. SQL stays explicit; the native pgx API remains available through the embedded pool and transaction. pgfx is not an ORM or a replacement driver.

  • Config is data, runtime is options - what survives a round trip through text is Config; a tracer, a metrics callback, an in-memory *tls.Config are Option values. Nothing is configured twice.
  • It reads no config source - the application supplies the Config, so pgfx is tied to no particular loader.
  • Fx verifies connectivity on start; with Make, call Ping explicitly when startup verification is needed.
  • No second query API - DB and Tx add typed fetches while preserving pgx for execution, transactions and advanced operations.
go get github.com/uchaloop/pgfx

Quick start

Build a pgfx.Config in Go and supply it to Fx. The application chooses where configuration values come from; pgfx does not require a configuration loader.

import (
    "os"
    "time"

    "github.com/uchaloop/pgfx"
    "github.com/uchaloop/secret/v2"
    "go.uber.org/fx"
)

cfg := pgfx.Config{
    Host:     "localhost:5432",
    Database: "orders",
    User:     "orders",
    Password: secret.New(os.Getenv("POSTGRES_PASSWORD")),
    Pool:     pgfx.PoolConfig{MaxConns: 20},
    Timeouts: pgfx.TimeoutConfig{Connect: 5 * time.Second},
}

fx.New(
    fx.Supply(cfg),
    pgfx.Module, // untagged *pgfx.DB, verified at start, closed at stop
).Run()

For a replica, supply another config with an Fx name and use the matching module:

replicaCfg := cfg
replicaCfg.Host = "replica:5432"

fx.New(
    fx.Supply(cfg),
    pgfx.Module,
    fx.Supply(fx.Annotate(replicaCfg, fx.ResultTags(`name:"replica"`))),
    pgfx.ModuleFor("replica"),
).Run()

Without Fx, pass the same config directly and close the connection when done:

db, err := pgfx.Make(ctx, cfg)
if err != nil {
    return err
}
defer db.Close()

Fx provides only *pgfx.DB; the embedded pool is available as db.Pool and is not registered separately.

Optional: confmaker

For convenient environment loading and Fx integration, we recommend confmaker and the independent confx adapter. confx can replace manual config construction and fx.Supply(cfg):

// import "github.com/uchaloop/confx"
fx.New(
    confx.Module(),
    confx.Provide[pgfx.Config]("postgres"), // POSTGRES_HOST, POSTGRES_DATABASE, ...
    pgfx.Module,
).Run()

Config names its default instance postgres, which gives the POSTGRES_ prefix. For a named replica, add these options to the same application:

confx.ProvideNamed[pgfx.Config]("replica"), // REPLICA_HOST, REPLICA_DATABASE, ...
pgfx.ModuleFor("replica"),

Queries

order, err := db.FetchRow[Order](
	ctx,
	`SELECT id, amount FROM orders WHERE id = $1`,
	id,
)

total, err := db.FetchValue[int64](ctx, `SELECT count(*) FROM orders`)

FetchRows and FetchRow decode struct rows by the db:"..." tag; FetchValues and FetchValue decode a single column into a scalar. The type to decode into is an explicit type argument, because nothing in the arguments implies it.

Result capacity

FetchRowsInto and FetchValuesInto append to a caller-provided slice on both DB and Tx. The element type is inferred from the slice:

items, err := db.FetchRowsInto(ctx, make([]Item, 0, 1000), query, args...)
ids, err := db.FetchValuesInto(ctx, make([]int64, 0, 100), idQuery, args...)

Existing elements are preserved. Pass items[:0] to reuse a buffer only after its previous consumers have finished. Always use the returned slice: appending may allocate a new backing array. Capacity does not limit the number of rows. An error returns nil, but the supplied backing array may already contain writes. The ordinary FetchRows and FetchValues helpers remain available without a buffer.

Pagination reserves Size + 1 elements for offset pages (including the lookahead row) and Size for cursor pages, including empty results. The application must validate the maximum page size before calling these helpers; pgfx applies no hidden capacity cap. Large reservations consume memory even for sparse pages.

Benchmark observation: an experimental initial capacity cap of 256 reduced allocation counts but increased total allocated bytes for a 1000-row page in our Go 1.27 / arm64 struct-row benchmark. This is workload-dependent, not a recommended limit. Measure representative row types and page sizes with go test -run '^$' -bench BenchmarkCollectPageRowsCapacity -benchmem.

Pagination

FetchPage returns one page and the number of rows the filter matches. The query stays a plain SELECT - no ORDER BY, no LIMIT, no row numbers, no count - and the page is wrapped around it, so the filter is written once and the rows decode by the db tag like any other:

var warehouses = page.Must(
	fetchWarehousesSQL,
	page.Head(page.Desc("is_active")), // pinned, before the client's sort
	page.Tie(page.Asc("id")),          // required: a page needs a total order
	page.Sortable(page.Cols{           // what a client may sort by
		"cityEng":   "city_eng",
		"createdAt": "created_at",
	}),
)

rows, total, err := db.FetchPage[Warehouse](
	ctx,
	warehouses,
	page.Request{Number: 2, Size: 20, Sort: []string{"cityEng:desc"}},
	params.Country,
)

A client sorts only by what Sortable lists, and an unknown field is an error rather than a silent skip. List the fields an index serves: a sort without one reads and sorts the whole result on every page. page.SortKeyTag("json") opens every field of the model instead, under its json name - convenient for small tables, costly for large ones.

The tie keeps its own direction when it is the whole order, so Tie(Desc("id")) lists the newest rows first by default. After other orders it takes the direction of the last one: cityEng:desc gives ORDER BY city_eng DESC, id DESC, which one index on (city_eng, id) serves in a single backward scan.

The rows statement reads one row past the page. The total takes a second statement, skipped when a nonempty page has no row after it: that page is the last one, and the total follows from it.

Page number and size arrive as a page.Request, apart from the filter: a default for a page a client did not fully specify belongs at the edge that parsed the request, and a zero here is a bug rather than a request for page one.

Numbered pages use LIMIT/OFFSET. They work well for shallow pages and direct page-number navigation. Large offsets require PostgreSQL to walk skipped rows; for sequential traversal, use the cursor API below. Performance depends on the filtered result and matching indexes, not just the table's row count.

Pages without a total

Counting is often the most expensive part of a page. When a client needs only the rows and a "next" link, use FetchPageRows with the same query and request. It executes only the page SELECT and tells from the extra row whether another page follows:

list, more, err := db.FetchPageRows[Warehouse](
    ctx,
    warehouses,
    page.Request{Number: 2, Size: 20},
    params.Country,
)

Inside a transaction use tx.FetchPageRows. No count SQL is built or executed.

Cursor pages

result, err := db.FetchAfter[Warehouse](
    ctx,
    warehouses,
    page.CursorRequest{
        Size: 20,
        After: previousCursor,
        Sort: []string{"cityEng:desc"},
    },
    params.Country,
)
// result.List, result.NextCursor, result.HasMore

Start with an empty After, then pass the returned NextCursor while HasMore is true. The size can change; SQL, filters and effective sorting must remain the same. There is no automatic switch between offset and cursor: they provide different navigation contracts. CursorResult carries no JSON tags - the shape of the response belongs to the API.

Cursor queries fetch at most Size + 1 result rows and never count. Uniform sort directions use a tuple comparison; mixed directions and NULL transitions use disjoint, individually limited UNION ALL branches. Matching indexes are essential. Expensive joins, expression sorting and unindexed filters can still make a cursor query expensive. Every sort column must be selected, and the tie columns must uniquely order the result (including rows multiplied by joins).

When needed, request the exact total separately:

total, err := db.FetchTotal(ctx, warehouses, params.Country)

FetchTotal counts all rows of the base filter, without cursor bounds, ordering or offset. page.CountSQL can supply an equivalent cheaper SELECT. Exact totals can dominate query time on large results; cursor seeking does not make counting cheap. Both methods are available on Tx.

Struct decoding still honors db tags and pgx JSON/JSONB map codecs. Cursor keys support integer and finite floating-point scalars, bool, string, time.Time, []byte, UUID as [16]byte, and SQL NULL. These are pgx-decoded key values; numeric, arbitrary custom pgx types and maps are not supported as cursor keys. Filter arguments must have stable JSON representations. Tokens are versioned and bound to the query, but are neither signed nor encrypted; enforce access filters independently. Concurrent updates to sort keys can move rows between pages. A consistent snapshot remains an explicit transaction choice.

A key range such as WHERE id BETWEEN $1 AND $2 is ordinary SQL for FetchRows, not a pagination mode. pgfx is a thin pgx extension and does not integrate search-engine pagination: when Elasticsearch/OpenSearch supplies ranked results, paginate there and use PostgreSQL to fetch the selected IDs, preserving the search result order.

Transactions

Transaction commits on nil and rolls back on an error or panic. Its Tx has the same typed fetches as DB and embeds the 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.FetchValue[int64](
            ctx,
            `UPDATE orders SET status = 'paid' WHERE id = $1 RETURNING id`,
            order.ID,
        )
        return err
    },
)

Use tx, not db, inside the callback: a call through db uses the pool and does not participate in the transaction.

For manual ownership use the embedded native db.Begin or db.BeginTx; they return pgx.Tx. tx.BeginNested and tx.Transaction create typed pgx savepoints. The embedded pool also makes COPY, LISTEN, acquired connections and the rest of the native API available directly on DB; the pool itself is db.Pool.

Pagination does not open a transaction automatically. To keep list and total in one snapshot under concurrent writes, explicitly use pgx.RepeatableRead with tx.FetchPage.

Environment configuration with confmaker

With the optional confmaker setup above, a deployment can supply:

POSTGRES_HOST=localhost:5432
POSTGRES_DATABASE=orders
POSTGRES_USER=orders
POSTGRES_PASSWORD=...

POSTGRES_TLS_MODE=verify-full
POSTGRES_TLS_ROOT_CERT=/run/secrets/ca.crt

POSTGRES_POOL_MAX_CONNS=20
POSTGRES_TIMEOUTS_CONNECT=5s

Host and database are required. Unset optional fields retain their documented defaults. confmaker.Manifest[pgfx.Config]("postgres") lists the whole set with types and defaults.

Documentation

The options, the config fields and the reasons behind them are in the package documentation: pkg.go.dev/github.com/uchaloop/pgfx.

Acknowledgements

I am grateful to the authors of pgx, Uber Fx, and otelpgx. Their work made this library possible.

License

MIT

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

Examples

Constants

This section is empty.

Variables

View Source
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

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 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()
}

func QueryName

func QueryName(ctx context.Context) string

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

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.

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_*.

func (Config) Validate

func (cfg Config) Validate() error

Validate checks what the values mean, once a loader has supplied them: every problem at once, not just the first, so a misconfigured deployment takes one rollout to fix rather than one per mistake.

type DB added in v0.5.0

type DB struct {
	*pgxpool.Pool
}

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

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

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)
	}
}

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
	}
}

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)
}

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)
}

func (*DB) FetchRow added in v0.5.0

func (d *DB) FetchRow[T any](ctx context.Context, sql string, args ...any) (T, error)

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)
}

func (*DB) FetchRows added in v0.5.0

func (d *DB) FetchRows[T any](ctx context.Context, sql string, args ...any) ([]T, error)

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)
}

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

func (d *DB) FetchTotal(ctx context.Context, query page.Query, args ...any) (uint, error)

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)
}

func (*DB) FetchValue added in v0.5.0

func (d *DB) FetchValue[T any](ctx context.Context, sql string, args ...any) (T, error)

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)
}

func (*DB) FetchValues added in v0.5.0

func (d *DB) FetchValues[T any](ctx context.Context, sql string, args ...any) ([]T, error)

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)
}

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

func (d *DB) Transaction(
	ctx context.Context,
	opts pgx.TxOptions,
	fn func(*Tx) error,
) error

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)
	}
}

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 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)
	}
}

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 `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
)

func (QueryKind) String added in v0.7.0

func (k QueryKind) String() string

String returns the kind as a metric label value.

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

type Tx struct {
	pgx.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

func (t *Tx) BeginNested(ctx context.Context) (*Tx, error)

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)
	}
}

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)
	}
}

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.
}

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)
	}
}

func (*Tx) FetchRow added in v0.5.0

func (t *Tx) FetchRow[T any](ctx context.Context, sql string, args ...any) (T, error)

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

func (t *Tx) FetchRows[T any](ctx context.Context, sql string, args ...any) ([]T, error)

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

func (t *Tx) FetchTotal(ctx context.Context, query page.Query, args ...any) (uint, error)

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

func (t *Tx) FetchValue[T any](ctx context.Context, sql string, args ...any) (T, error)

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

func (t *Tx) FetchValues[T any](ctx context.Context, sql string, args ...any) ([]T, error)

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

func (t *Tx) Transaction(ctx context.Context, fn func(*Tx) error) error

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)
	}
}

Directories

Path Synopsis
Package page is the vocabulary of a paginated query: what a client asked for, and what a query is allowed to order by.
Package page is the vocabulary of a paginated query: what a client asked for, and what a query is allowed to order by.

Jump to

Keyboard shortcuts

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