Skip to main content
Version: Next (unreleased)

pgx / pgxpool

The database/sql wrapper covers pgx's stdlib shim (pgx/v5/stdlib). The native APIs — pgx.Conn, pgxpool.Pool — never touch database/sql, so they need pgxguard. It hooks pgx's own QueryTracer and BatchTracer seams, which is how every pgx ecosystem tool extends the driver, so every Query / QueryRow / Exec and every SendBatch is analyzed without a wrapper type or a method list.

go get github.com/KARTIKrocks/sqlguard/integrations/pgxguard

With a pool

import (
"github.com/KARTIKrocks/sqlguard/integrations/pgxguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"github.com/jackc/pgx/v5/pgxpool"
)

cfg, err := pgxpool.ParseConfig(dsn)
pgxguard.ApplyPool(cfg,
middleware.WithSlowQueryThreshold(50*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)
pool, err := pgxpool.NewWithConfig(ctx, cfg)

With a single connection

cfg, err := pgx.ParseConfig(dsn)
pgxguard.Apply(cfg)
conn, err := pgx.ConnectConfig(ctx, cfg)

API

SymbolUse
pgxguard.ApplyPool(cfg *pgxpool.Config, opts ...middleware.Option) *pgxpool.ConfigInstall on a pool config. Returns cfg for chaining. Panics on nil.
pgxguard.Apply(cfg *pgx.ConnConfig, opts ...middleware.Option) *pgx.ConnConfigInstall on a connection config. Returns cfg. Panics on nil.
pgxguard.NewTracer(opts ...middleware.Option) *TracerBuild the tracer yourself, to keep a handle for ResetN1() or to compose manually.
(*Tracer).ResetN1()Clear N+1 state at a request boundary.

Composes with existing tracers

pgx allows exactly one Tracer per config, and production services usually already have one — otelpgx, ddtrace, a logger. Apply and ApplyPool do not overwrite it. If cfg.Tracer is set, they wrap both in pgx's own multitracer, which fans every event out to each tracer and routes by interface, so the existing tracer keeps receiving exactly the events it did before.

cfg.ConnConfig.Tracer = otelpgx.NewTracer()
pgxguard.ApplyPool(cfg) // both tracers now run

Keeping a handle for ResetN1()

Apply builds the tracer internally. When you need ResetN1(), build it yourself and install it — with multitracer if you also have another tracer:

import "github.com/jackc/pgx/v5/multitracer"

tracer := pgxguard.NewTracer(middleware.WithN1Detection(10, time.Second))
cfg.ConnConfig.Tracer = multitracer.New(otelpgx.NewTracer(), tracer)

func handler(w http.ResponseWriter, r *http.Request) {
defer tracer.ResetN1()
// ...
}

Coverage details

pgx callSeamStatic rulesN+1Latency
Query, QueryRow, ExecQueryTraceryesyesyes, on success
Prepared statements executed via the aboveQueryTraceryesyesyes
SendBatchBatchTracer, per statementyesyesno
CopyFromnonono
  • Batches: pgx exposes only the whole-batch round trip, not per-statement latency, so slow-query is deliberately not reported for batch statements rather than reported wrongly.
  • PrepareTracer is intentionally not implemented: execution already routes through QueryTracer, so tracing Prepare as well would double-report findings and inflate N+1 counts.
  • CopyFrom carries no SQL statement and is out of scope.

The tracer is the reference implementation of the split start/end pattern on middleware.Guard: TraceQueryStart calls Guard.Observe and stashes the returned closure on the context; TraceQueryEnd invokes it with data.Err.