postgres

package
v2.0.0-...-45bdfd4 Latest Latest
Warning

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

Go to latest
Published: Mar 25, 2026 License: MIT Imports: 10 Imported by: 0

README

postgres

Package postgres provides an instrumented PostgreSQL client that implements app.Component, plugging into the app.App lifecycle for managed startup and graceful shutdown.

Quick start

db, err := postgres.NewWithConfig(&postgres.Config{
    DSN:    os.Getenv("DATABASE_URL"),
    Tracer: a.Tracer(),
})
if err != nil {
    log.Fatal(err) // DSN parse errors are caught here, before Start
}

a.Register(db) // Start creates the pool and pings; Shutdown closes it

// After a.Start(ctx):
rows, err := db.DB().QueryContext(ctx, "SELECT id FROM users WHERE active = $1", true)

Two access paths

The client exposes two ways to interact with the database after Start:

DB() — standard *sql.DB
db.DB().QueryContext(ctx, "SELECT id, name FROM projects WHERE active = $1", true)
db.DB().ExecContext(ctx, "UPDATE users SET active = false WHERE id = $1", id)

Compatible with ORMs (sqlx, GORM, Bun), query builders, and migration frameworks (golang-migrate, goose).

Pool() — pgx-native *pgxpool.Pool
// Bulk load via COPY
conn, err := db.Pool().Acquire(ctx)
defer conn.Release()
_, err = conn.Conn().PgConn().CopyFrom(ctx, pgconn.CopyFromReader(r, sql))

// Batch queries — send many, read all results at once
batch := &pgx.Batch{}
batch.Queue("INSERT INTO events (name) VALUES ($1)", "signup")
batch.Queue("INSERT INTO events (name) VALUES ($1)", "purchase")
br := db.Pool().SendBatch(ctx, batch)
defer br.Close()

// LISTEN / NOTIFY for pub-sub patterns
conn, _ := db.Pool().Acquire(ctx)
defer conn.Release()
_, err = conn.Exec(ctx, "LISTEN channel_name")

Both paths share the same connection pool and query tracer — OpenTelemetry spans are recorded regardless of which path is used.

Capability comparison

Capability DB() (*sql.DB) Pool() (*pgxpool.Pool)
Standard queries, transactions
ORM / migration tool compatibility
COPY FROM / COPY TO bulk load
Batch queries
Named prepared statements
LISTEN / NOTIFY
pgconn low-level protocol access

Use DB() for most CRUD workloads. Use Pool() for bulk inserts, high-throughput pipelines, or event-driven patterns.

Configuration

db, err := postgres.NewWithConfig(&postgres.Config{
    DSN:  "postgres://user:pass@localhost:5432/mydb",
    Name: "primary", // component name in logs (default: "postgres")

    // Connection pool tuning:
    MaxConns:        20,               // max open connections (0 = pgxpool default)
    MinConns:        2,                // min connections to keep warm (0 = none)
    ConnMaxLifetime: 5 * time.Minute,  // recycle connections after this duration
    ConnMaxIdleTime: 60 * time.Second, // close idle connections after this duration

    // Query execution mode (default: pgx.QueryExecModeSimpleProtocol for
    // PgBouncer compatibility). Override only for direct PostgreSQL connections.
    // QueryExecMode: pgx.QueryExecModeCacheStatement,

    Tracer: a.Tracer(), // nil disables query tracing
})
PgBouncer

The client defaults to pgx.QueryExecModeSimpleProtocol, which avoids server-side prepared statements. This is required for PgBouncer in transaction-pooling mode (the standard deployment at GitLab.com) and works correctly with direct PostgreSQL connections too. No additional configuration is needed for PgBouncer compatibility.

Also set ConnMaxLifetime and ConnMaxIdleTime to recycle stale connections:

db, err := postgres.NewWithConfig(&postgres.Config{
    DSN:             "postgres://user:pass@pgbouncer:6432/mydb",
    ConnMaxLifetime: 5 * time.Minute,
    ConnMaxIdleTime: 60 * time.Second,
})

When connecting directly to PostgreSQL without a pooler, you can opt into cached prepared statements for better query performance:

db, err := postgres.NewWithConfig(&postgres.Config{
    DSN:           "postgres://user:pass@localhost:5432/mydb",
    QueryExecMode: pgx.QueryExecModeCacheStatement,
})

Query tracing

When Tracer is set, every query automatically receives an OpenTelemetry span:

  • Span name: SQL verb — "db SELECT", "db INSERT", etc. (low cardinality)
  • Attributes: db.system = "postgresql", db.statement = <full SQL>, db.name = <database name>
  • Errors: recorded on the span automatically

Multiple databases

Give each client a distinct Name so log lines and error messages are unambiguous:

primary, _ := postgres.NewWithConfig(&postgres.Config{Name: "primary", DSN: primaryDSN})
replica, _ := postgres.NewWithConfig(&postgres.Config{Name: "replica", DSN: replicaDSN})

a.Register(primary)
a.Register(replica)

Testing

Tests that don't need a real database can inspect the configured tracer via QueryTracer() without calling Start:

db, _ := postgres.NewWithConfig(&postgres.Config{
    DSN:    "postgres://localhost/mydb",
    Tracer: tracer,
})
assert.NotNil(t, db.QueryTracer())
assert.Nil(t, db.DB())   // nil before Start
assert.Nil(t, db.Pool()) // nil before Start

For integration tests, use a real PostgreSQL instance and call Start / Shutdown directly.

Documentation

Overview

Package postgres provides an instrumented PostgreSQL client backed by a pgxpool.Pool that implements app.Component.

Two access paths are exposed after Client.Start:

  • Client.DB returns a standard *sql.DB derived from the pool. Use this for standard queries and full compatibility with ORMs (sqlx, GORM, Bun), query builders, and migration frameworks (golang-migrate, goose).

  • Client.Pool returns the underlying *pgxpool.Pool for pgx-native operations: COPY FROM/TO for bulk loads, batch queries, named prepared statements, and LISTEN/NOTIFY for pub-sub patterns.

Both paths share the same connection pool and query tracer, so OpenTelemetry spans are recorded regardless of which path is used.

Basic usage

client, err := postgres.New("postgres://user:pass@localhost:5432/mydb")
if err != nil { log.Fatal(err) }

a.Register(client)

if err := a.Start(ctx); err != nil { log.Fatal(err) }
defer a.Shutdown(ctx)

// Standard database/sql path — compatible with ORMs and migrations.
rows, err := client.DB().QueryContext(ctx, "SELECT id FROM users WHERE active = $1", true)

// pgx-native path — for COPY, batching, LISTEN/NOTIFY.
batch := &pgx.Batch{}
batch.Queue("INSERT INTO events (name) VALUES ($1)", "signup")
results := client.Pool().SendBatch(ctx, batch)

With tracing

client, err := postgres.NewWithConfig(&postgres.Config{
	DSN:    "postgres://user:pass@localhost:5432/mydb",
	Tracer: a.Tracer(),
})

Every query automatically receives an OpenTelemetry span named after the SQL verb (e.g. "db SELECT", "db INSERT"). The full statement is recorded as the db.statement attribute and errors are recorded on the span.

PgBouncer

The client defaults to pgx.QueryExecModeSimpleProtocol, which avoids server-side prepared statements and is compatible with PgBouncer in transaction-pooling mode (the standard deployment at GitLab.com). No additional configuration is required for PgBouncer compatibility.

When running behind PgBouncer, also set ConnMaxLifetime and ConnMaxIdleTime to recycle stale connections:

client, err := postgres.NewWithConfig(&postgres.Config{
	DSN:             "postgres://user:pass@pgbouncer:6432/mydb",
	ConnMaxLifetime: 5 * time.Minute,
	ConnMaxIdleTime: 60 * time.Second,
})

When connecting directly to PostgreSQL without a pooler, you can opt into cached prepared statements for better performance:

client, err := postgres.NewWithConfig(&postgres.Config{
	DSN:           "postgres://user:pass@localhost:5432/mydb",
	QueryExecMode: pgx.QueryExecModeCacheStatement,
})
Example

Example shows an instrumented PostgreSQL client registered as an app component. Start creates the pool and pings the server; Shutdown closes all connections cleanly.

package main

import (
	"context"

	"gitlab.com/gitlab-org/labkit/v2/postgres"
)

func main() {
	ctx := context.Background()

	db, err := postgres.NewWithConfig(&postgres.Config{
		DSN: "postgres://user:pass@localhost:5432/mydb",
		// Tracer: a.Tracer(),
	})
	if err != nil {
		// DSN parse errors are caught here before Start is called.
		panic(err)
	}

	// Register with app.App so the lifecycle is managed automatically:
	//   a.Register(db)
	//   a.Run(ctx)
	//
	// Or manage manually:
	if err := db.Start(ctx); err != nil {
		panic(err)
	}
	defer db.Shutdown(ctx)

	// DB() returns *sql.DB — compatible with ORMs, query builders, migrations.
	row := db.DB().QueryRowContext(ctx, "SELECT version()")
	var version string
	_ = row.Scan(&version)

	// Pool() returns *pgxpool.Pool — for COPY, batching, LISTEN/NOTIFY.
	_ = db.Pool()
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is an instrumented PostgreSQL client backed by a pgxpool.Pool that implements app.Component. Call [Start] to open the pool and [Shutdown] to close it.

Client.DB returns a standard *sql.DB derived from the pool, compatible with ORMs, query builders, and migration frameworks. Client.Pool returns the underlying *pgxpool.Pool for pgx-native operations such as COPY, batch queries, and LISTEN/NOTIFY.

func New

func New(dsn string) (*Client, error)

New returns a Client for the given DSN with no tracing. It is shorthand for NewWithConfig(&Config{DSN: dsn}).

func NewWithConfig

func NewWithConfig(cfg *Config) (*Client, error)

NewWithConfig returns a Client configured with cfg. DSN parsing is performed eagerly so that configuration errors are caught before Start is called.

Example

ExampleNewWithConfig shows connection pool tuning for services running behind PgBouncer, where long-lived connections should be recycled.

package main

import (
	"time"

	"gitlab.com/gitlab-org/labkit/v2/postgres"
)

func main() {
	db, err := postgres.NewWithConfig(&postgres.Config{
		DSN:             "postgres://user:pass@pgbouncer:6432/mydb",
		Name:            "primary",
		MaxConns:        20,
		MinConns:        2, // keep 2 connections warm
		ConnMaxLifetime: 5 * time.Minute,
		ConnMaxIdleTime: 60 * time.Second,
	})
	if err != nil {
		panic(err)
	}
	_ = db
}

func (*Client) DB

func (c *Client) DB() *sql.DB

DB returns the standard *sql.DB derived from the pool. Compatible with ORMs, query builders, and migration frameworks. Returns nil before [Start] has been called successfully.

func (*Client) Name

func (c *Client) Name() string

Name returns the component name for use in logs and error messages.

func (*Client) Pool

func (c *Client) Pool() *pgxpool.Pool

Pool returns the underlying *pgxpool.Pool for pgx-native operations. Use this for bulk loads ([COPY FROM / COPY TO]), batch queries, named prepared statements, and LISTEN/NOTIFY. Returns nil before [Start] has been called successfully.

func (*Client) QueryExecMode

func (c *Client) QueryExecMode() pgx.QueryExecMode

QueryExecMode returns the pgx.QueryExecMode that will be used for queries. The default is pgx.QueryExecModeSimpleProtocol for PgBouncer compatibility.

func (*Client) QueryTracer

func (c *Client) QueryTracer() pgx.QueryTracer

QueryTracer returns the pgx.QueryTracer configured on this client, or nil when no [Config.Tracer] was provided. This is primarily useful for testing the tracing integration without a live database.

func (*Client) Shutdown

func (c *Client) Shutdown(_ context.Context) error

Shutdown closes the sql.DB and the underlying pool, releasing all connections. It satisfies app.Component and should be called via app.App.Shutdown.

func (*Client) Start

func (c *Client) Start(ctx context.Context) error

Start creates the connection pool and verifies connectivity with a ping. It satisfies app.Component and should be called via app.App.Start.

type Config

type Config struct {
	// DSN is the PostgreSQL connection string.
	// Accepts postgres:// and postgresql:// URL formats, as well as key=value
	// connection strings (e.g. "host=localhost user=postgres dbname=mydb").
	DSN string

	// Name identifies this client in logs and errors. Defaults to "postgres".
	// Use distinct names when a service connects to multiple databases.
	Name string

	// MaxConns is the maximum number of connections in the pool.
	// Maps to pgxpool.Config.MaxConns. Zero uses the pgxpool default
	// (max(4, runtime.NumCPU())).
	MaxConns int

	// MinConns is the minimum number of connections to keep open in the pool,
	// even when idle. Maps to pgxpool.Config.MinConns. Zero means no minimum.
	// Use this to pre-warm connections at startup and avoid cold-start latency
	// on the first requests.
	MinConns int

	// MaxIdleConns is not used by the pgxpool backend and has no effect.
	// To keep a minimum number of connections warm, use [Config.MinConns].
	// To close connections that have been idle too long, use [Config.ConnMaxIdleTime].
	//
	// Deprecated: retained for source compatibility; has no effect.
	MaxIdleConns int

	// ConnMaxLifetime is the maximum duration a connection may be reused.
	// Maps to pgxpool.Config.MaxConnLifetime. Zero means no age limit.
	// Set this when using PgBouncer or similar connection poolers to ensure
	// stale connections are recycled.
	ConnMaxLifetime time.Duration

	// ConnMaxIdleTime is the maximum duration a connection may sit idle
	// before being closed. Maps to pgxpool.Config.MaxConnIdleTime.
	// Zero means connections are not closed due to idle time.
	ConnMaxIdleTime time.Duration

	// QueryExecMode controls how pgx executes queries. Defaults to
	// [pgx.QueryExecModeSimpleProtocol], which avoids server-side prepared
	// statements and is required for PgBouncer in transaction-pooling mode
	// (the standard deployment at GitLab.com).
	//
	// Set to [pgx.QueryExecModeCacheStatement] or another mode only when
	// connecting directly to PostgreSQL without a connection pooler.
	//
	// See https://pkg.go.dev/github.com/jackc/pgx/v5#QueryExecMode for all
	// available modes.
	QueryExecMode pgx.QueryExecMode

	// Tracer is used to create spans for queries. When nil, tracing is
	// disabled.
	Tracer *trace.Tracer
}

Config holds optional configuration for New / NewWithConfig.

Jump to

Keyboard shortcuts

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