Help us shape the future of Go! Take the 2026 Go Developer Survey

postgres

package module
v0.8.12 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 12 Imported by: 0

README

postgres

Doc Go Release Test License

PostgreSQL driver module for rio, built on pgx: a database/sql channel (plain or over pgxpool) and a native pgx channel with batched preloads and COPY inserts. rio renders the SQL.

db, err := postgres.OpenNative(ctx, "postgres://user:pass@localhost:5432/app")
if err != nil {
	return err
}
defer db.Close()

users, err := rio.From[User]().Where("age > ?", 18).With("Posts").All(ctx, db)
err = rio.InsertAll(ctx, db, rows) // explicit keys stream over COPY

Getting started

go get github.com/go-rio/postgres
package main

import (
	"context"
	"log"

	"github.com/go-rio/postgres"
	"github.com/go-rio/rio"
)

type User struct {
	ID    int64
	Email string
	Age   int
}

func main() {
	ctx := context.Background()
	db, err := postgres.Open("postgres://user:pass@localhost:5432/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	users, err := rio.From[User]().Where("age > ?", 18).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("%d adults", len(users))
}

Requires Go 1.27 and PostgreSQL 9.1 or later (standard_conforming_strings on); the test suite runs against PostgreSQL 18.

Features

Execution paths

rio queries use the same API on all three paths.

Path Constructors Use for
database/sql Open, New *sql.DB pooling, sqlmock, database/sql instrumentation.
database/sql over pgxpool OpenPool, NewFromPool pgxpool configuration and PoolOf with the database/sql query path.
native pgx OpenNative, NewNativeFromPool Lower scan allocations, one round trip per preload layer, COPY inserts, pgx execution modes and transaction semantics.

Open validates the DSN — pgx URL or keyword/value form — but does not connect; ping with db.Unwrap().PingContext(ctx). New wraps an existing *sql.DB and leaves session configuration to the caller.

standard_conforming_strings

rio's placeholder lexer requires standard_conforming_strings=on, the PostgreSQL default since 9.1. Open, OpenPool, and OpenNative reject an explicit false — set directly, through the options startup parameter, or through PGOPTIONS — and do not inject the setting when omitted. If the server disables it globally, enable it in the DSN:

postgres://user:pass@localhost:5432/app?options=-c%20standard_conforming_strings%3Don

New, NewFromPool, and NewNativeFromPool cannot inspect an existing pool; their callers must enforce this requirement.

pgxpool with database/sql
db, err := postgres.OpenPool(ctx, "postgres://user:pass@localhost:5432/app?pool_max_conns=10")
if err != nil {
	log.Fatal(err)
}
defer db.Close()
  • OpenPool accepts pgxpool pool_* DSN parameters; NewFromPool takes a caller-built pool. PoolOf exposes the pool for Ping, Stat, AcquireFunc, CopyFrom, and LISTEN/NOTIFY.
  • Both constructors transfer pool ownership to the rio DB. Closing the DB closes the pool and may block until acquired connections return.
  • Configure connection counts on pgxpool, not on db.Unwrap(); the database/sql view keeps no idle connections.
Native pgx execution

OpenNative executes rio queries directly through pgx, preserving rio's SQL rendering, scanning rules, sentinel errors, hooks, and savepoint behavior. Preloads flush each relation layer in one pgx batch round trip. InsertAll streams explicit-key batches — no generated keys to backfill — over COPY.

db, err := postgres.OpenNative(ctx, "postgres://user:pass@localhost:5432/app")

NewNativeFromPool takes ownership of a caller-built pool; closing the DB closes the pool and may block until acquired connections return. db.Unwrap() stays available as a database/sql view for helpers such as migrations and pings, never for pool configuration.

Difference from database/sql Detail
tx.Unwrap() returns nil Use postgres.TxOf(tx) for the pgx.Tx (e.g. for SET LOCAL). Nested savepoints expose the root pgx transaction.
rio.WithStmtCache panics Native statement caching belongs to pgx's query execution mode.
Error text may carry pgx prefixes Check errors with errors.Is and errors.As.

rio's PostgreSQL benchmarks compare both channels.

Query exec mode and PgBouncer

OpenNative installs AfterConnect, which keeps time, inet and cidr in the server's text form so string columns read the same as on the database/sql paths; NewNativeFromPool callers add it to their pgxpool.Config.AfterConnect. Arrays scan into a string only through database/sql; the native path asks for a slice.

The native path uses pgx's default cache_statement mode. Change it with the default_query_exec_mode DSN parameter or on a caller-built pool config.

Setup Action
Direct connection Keep the pgx default.
PgBouncer ≥ 1.21 with max_prepared_statements > 0 None.
Older PgBouncer in transaction/statement pooling Add default_query_exec_mode=exec (or simple_protocol) to the DSN. Symptom otherwise: prepared statement "stmtcache_..." does not exist.

On the database/sql paths, rio.WithStmtCache (off by default) adds DB- and transaction-local caches on top of pgx's per-connection cache. Do not use it behind transaction- or statement-mode poolers.

Key sets and row locks

Preload and WithCount key sets bind as one typed array parameter ("posts"."user_id" = ANY($1)) on both channels, so the statement text does not vary with the number of parents and pgx's statement cache hits every time; user IN (?) slices still expand. ForUpdate and ForShare accept rio.NoWait and rio.SkipLocked, and UpdateAllReturning and DeleteAllReturning scan the affected rows back.

Arrays and JSONB

Tag JSON fields with rio:",json" to store them as jsonb (for example Prefs map[string]any on an Account model, column prefs); a set-based rio.Set{"prefs": v} uses the same mapping.

For PostgreSQL arrays, wrap pgtype in a driver.Valuer and sql.Scanner pair. The wrapper works on all three paths:

var pgMap = pgtype.NewMap()

type Tags []string

func (t Tags) Value() (driver.Value, error) {
	b, err := pgMap.Encode(pgtype.TextArrayOID, pgtype.TextFormatCode, pgtype.FlatArray[string](t), nil)
	if err != nil || b == nil {
		return nil, err
	}
	return string(b), nil
}

func (t *Tags) Scan(src any) error {
	return pgMap.SQLScanner((*pgtype.FlatArray[string])(t)).Scan(src)
}

For UpdateAll, pass the wrapper type or a rio.Expr, not a bare slice.

JSONB existence operators collide with rio's ? placeholder. Escape each literal question mark as ?? (?| → ??|, ?& → ??&):

rio.From[Account]().Where("prefs ?? ?", "beta").All(ctx, db) // prefs ? $1
Error translation
SQLSTATE rio sentinel
23505 rio.ErrDuplicateKey
23503 rio.ErrForeignKeyViolated

The original *pgconn.PgError stays available through errors.As, including its constraint, table, and detail fields.

Contributing

Read CONTRIBUTING.md: a clone, go test ./..., and the one-line Docker command for the gated integration tests.

Contributors

Thanks to everyone who has filed issues and opened pull requests on go-rio/postgres.

License

The MIT License. Copyright (c) 2026-now TreeNewBee.

Documentation

Overview

Package postgres connects rio to PostgreSQL through pgx's database/sql adapter or rio's native execution path.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AfterConnect added in v0.8.5

func AfterConnect(_ context.Context, conn *pgx.Conn) error

func New

func New(db *sql.DB, opts ...rio.Option) *rio.DB

New wraps an existing *sql.DB with the Postgres dialect and error translator. The caller must ensure standard_conforming_strings is on. Options apply last; a supplied translator replaces the default.

func NewFromPool added in v0.3.0

func NewFromPool(pool *pgxpool.Pool, opts ...rio.Option) *rio.DB

NewFromPool wraps a caller-built pgxpool.Pool and takes ownership: closing the rio DB closes the pool. The caller must ensure standard_conforming_strings is on.

func NewNativeFromPool added in v0.3.0

func NewNativeFromPool(pool *pgxpool.Pool, opts ...rio.Option) *rio.DB

NewNativeFromPool wraps a caller-built pgxpool.Pool for native execution and takes ownership: closing the rio DB closes the pool. The caller must ensure standard_conforming_strings is on.

Example

NewNativeFromPool takes ownership of a pool the caller configured.

package main

import (
	"context"
	"log"

	"github.com/go-rio/postgres"
	"github.com/jackc/pgx/v5/pgxpool"
)

func main() {
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/app")
	if err != nil {
		log.Fatal(err)
	}

	db := postgres.NewNativeFromPool(pool)
	defer db.Close() // closes the pool
}

func Open

func Open(dsn string, opts ...rio.Option) (*rio.DB, error)

Open validates a pgx DSN and returns a rio DB using pgx's database/sql adapter. It does not connect; ping and configure the pool via db.Unwrap(). URL and keyword/value DSNs are accepted; standard_conforming_strings=off is rejected, including via PGOPTIONS.

Example
package main

import (
	"context"
	"log"

	"github.com/go-rio/postgres"
	"github.com/go-rio/rio"
)

type User struct {
	ID    int64
	Email string
	Age   int
}

func main() {
	db, err := postgres.Open("postgres://user:pass@localhost:5432/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	users, err := rio.From[User]().Where("age > ?", 18).All(context.Background(), db)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("%d adults", len(users))
}

func OpenNative added in v0.3.0

func OpenNative(ctx context.Context, dsn string, opts ...rio.Option) (*rio.DB, error)

OpenNative validates a pgxpool DSN and returns a rio DB that executes queries directly through pgx. It connects lazily (ping via PoolOf) and, like Open, rejects standard_conforming_strings=off.

rio.WithStmtCache is unsupported; use pgx's default_query_exec_mode. Tx.Unwrap returns nil — use TxOf. db.Unwrap returns a database/sql view; configure pooling through pgxpool, never the view.

Example

OpenNative executes through pgx directly: preload layers batch into one round trip and explicit-key InsertAll streams over COPY.

package main

import (
	"context"
	"log"

	"github.com/go-rio/postgres"
	"github.com/go-rio/rio"
)

type User struct {
	ID    int64
	Email string
	Age   int
}

func main() {
	ctx := context.Background()
	db, err := postgres.OpenNative(ctx, "postgres://user:pass@localhost:5432/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	rows := []User{{ID: 1, Email: "a@example.com"}, {ID: 2, Email: "b@example.com"}}
	if err := rio.InsertAll(ctx, db, rows); err != nil {
		log.Fatal(err)
	}
}

func OpenPool added in v0.3.0

func OpenPool(ctx context.Context, dsn string, opts ...rio.Option) (*rio.DB, error)

OpenPool validates a pgxpool DSN and returns a rio DB using pgx's database/sql adapter over that pool. It connects lazily (ping via PoolOf) and, like Open, rejects standard_conforming_strings=off. Configure pooling through the DSN, never db.Unwrap(). Closing the rio DB closes the pool and may block until acquired connections return.

Example

OpenPool keeps the database/sql query path while pgxpool owns the connections; PoolOf reaches the pool for pings and statistics.

package main

import (
	"context"
	"log"

	"github.com/go-rio/postgres"
)

func main() {
	ctx := context.Background()
	db, err := postgres.OpenPool(ctx, "postgres://user:pass@localhost:5432/app?pool_max_conns=10")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	if err := postgres.PoolOf(db).Ping(ctx); err != nil {
		log.Fatal(err)
	}
}

func PoolOf added in v0.3.0

func PoolOf(db *rio.DB) *pgxpool.Pool

PoolOf returns the pool managed by a pool-backed or native rio DB. It returns nil for other constructions and for a nil DB.

func TxOf added in v0.3.0

func TxOf(tx *rio.Tx) pgx.Tx

TxOf returns the pgx transaction behind a native rio transaction. It returns nil for other constructions and for a nil transaction. Nested savepoints share the root pgx transaction.

Example

TxOf exposes the pgx transaction for session settings the rio API does not cover.

package main

import (
	"context"
	"log"

	"github.com/go-rio/postgres"
	"github.com/go-rio/rio"
)

type User struct {
	ID    int64
	Email string
	Age   int
}

func main() {
	ctx := context.Background()
	db, err := postgres.OpenNative(ctx, "postgres://user:pass@localhost:5432/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	err = db.Tx(ctx, func(tx *rio.Tx) error {
		if _, err := postgres.TxOf(tx).Exec(ctx, "SET LOCAL statement_timeout = '5s'"); err != nil {
			return err
		}
		_, err := rio.From[User]().Where("id = ?", 1).UpdateAll(ctx, tx, rio.Set{"age": 31})
		return err
	})
	if err != nil {
		log.Fatal(err)
	}
}

Types

This section is empty.

Jump to

Keyboard shortcuts

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