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

postgres

package module
v0.3.4 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package postgres connects gorbital apps to PostgreSQL: a pgx connection pool with OpenTelemetry tracing, transactions, error classification for repositories, a readiness check, goose migrations (ADR-0005, ADR-0032) and the organisation row-level security policies read (ADR-0061).

Repositories hold a DBTX, so the same store runs on the pool or inside a transaction started with InTx. Transactions are passed explicitly, never stored in a context (ADR-0030).

This package is the pgx adapter, so pgx types appear in its API and its errors wrap pgx errors. Repositories translate them into domain errors with IsNoRows, UniqueViolation and the other helpers before returning.

Stability: stable (ADR-0015, ADR-0054).

Index

Examples

Constants

View Source
const (
	// OrgSetting holds the organisation ID of the acquiring context, or ""
	// when it has none.
	OrgSetting = "gorbital.org_id"
	// ScopeSetting is [OrgSetting] under the name the framework uses for
	// tenancy since v0.3.0 (ADR-0088). There is one session setting whatever
	// an app calls its scope: it is invisible to people, row-level-security
	// policies in live databases name it, and renaming it would change what
	// those policies mean.
	ScopeSetting = OrgSetting
	// BypassSetting is "on" for a context from [WithoutRowLevelSecurity],
	// otherwise "".
	BypassSetting = "gorbital.rls_bypass"
)

Row-level security (ADR-0061). Every connection a pool from Open hands out carries the organisation of the context that acquired it, in two session settings that policies read:

org_id = current_setting('gorbital.org_id', true)
    OR current_setting('gorbital.rls_bypass', true) = 'on'
View Source
const DefaultConnectTimeout = 5 * time.Second

DefaultConnectTimeout bounds each connection attempt and the initial ping.

Variables

This section is empty.

Functions

func CheckViolation

func CheckViolation(err error) (constraint string, ok bool)

CheckViolation reports whether err is a check constraint violation and returns the constraint's name.

func ForeignKeyViolation

func ForeignKeyViolation(err error) (constraint string, ok bool)

ForeignKeyViolation reports whether err is a foreign key violation and returns the constraint's name.

func HealthCheck

func HealthCheck(pool *pgxpool.Pool) health.Check

HealthCheck returns a readiness check that pings the database.

func InTx

func InTx(ctx context.Context, db Beginner, fn func(tx pgx.Tx) error) error

InTx runs fn in a read-write transaction with the server's default isolation level. See InTxWithOptions.

Example
package main

import (
	"context"
	"log"

	"github.com/jackc/pgx/v5"

	"gorbital.dev/config"
	"gorbital.dev/modules/postgres"
)

func main() {
	ctx := context.Background()
	pool, err := postgres.Open(ctx, config.NewSecret("postgres://localhost:5432/acme"))
	if err != nil {
		log.Fatal(err)
	}
	defer pool.Close()

	// Stores built from tx take part in the transaction.
	err = postgres.InTx(ctx, pool, func(tx pgx.Tx) error {
		if _, err := tx.Exec(ctx, "UPDATE accounts SET balance = balance - 10 WHERE id = $1", 1); err != nil {
			return err
		}
		_, err := tx.Exec(ctx, "UPDATE accounts SET balance = balance + 10 WHERE id = $1", 2)
		return err
	})
	if err != nil {
		log.Fatal(err)
	}
}

func InTxWithOptions

func InTxWithOptions(ctx context.Context, db Beginner, opts pgx.TxOptions, fn func(tx pgx.Tx) error) error

InTxWithOptions runs fn in a transaction started with opts. It commits when fn returns nil. Otherwise it rolls back and returns fn's error unchanged, so callers can match domain errors with errors.Is. If fn panics, the transaction is rolled back and the panic continues.

Build tx-bound repositories inside fn (for example NewUserStore(tx)); never keep tx after fn returns. With pgx.Serializable, retry when IsRetryable reports true.

func IsNoRows

func IsNoRows(err error) bool

IsNoRows reports whether err means a query returned no rows, as from QueryRow(...).Scan. Repositories return their not-found domain error.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports whether err is a serialization failure or deadlock, after which the whole transaction can be retried.

func Migrate

func Migrate(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS) ([]int64, error)

Migrate applies every pending goose migration in the root of fsys (files such as 00001_create_users.sql) and returns the versions it applied, in order. It returns nil when fsys has no migrations or none are pending.

A PostgreSQL advisory lock serialises concurrent callers, so several instances can run Migrate at once. Migrations run WithoutRowLevelSecurity, so a data migration reaches every organisation's rows (ADR-0061). Apps run migrations from a separate command, never implicitly at startup (ADR-0017).

func MigrateDown

func MigrateDown(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS) (int64, error)

MigrateDown rolls back the most recently applied migration in fsys, for development (ADR-0069): a migration written from the Dev Portal carries a Down section, and undoing it is how a mistake is corrected before it is released. It returns the version rolled back, or 0 when none was applied. Production apps refuse to call it.

func NotNullViolation

func NotNullViolation(err error) (column string, ok bool)

NotNullViolation reports whether err is a not-null violation and returns the column's name.

func Open

func Open(ctx context.Context, url config.Secret, opts ...Option) (*pgxpool.Pool, error)

Open creates a connection pool for url and pings the database, so a wrong URL or unreachable server fails at startup. Errors never include the URL, which may contain a password.

Every query becomes an OpenTelemetry client span carrying the SQL text but never its arguments.

Every connection carries the organisation of the context that acquired it in OrgSetting, and BypassSetting for a context from WithoutRowLevelSecurity, for row-level security policies (ADR-0061). Setting them costs one round trip when an acquire changes them, and nothing otherwise. Don't set these settings, or run RESET ALL or DISCARD ALL, yourself; and don't put the pool behind a pooler in transaction mode, which doesn't keep session settings with a client. Close the pool on shutdown:

cleanup.Add("postgres", func(context.Context) error { pool.Close(); return nil })
Example
package main

import (
	"context"
	"log"

	"gorbital.dev/config"
	"gorbital.dev/modules/postgres"
)

func main() {
	ctx := context.Background()
	url, err := config.OS.Secret("DATABASE_URL")
	if err != nil {
		log.Fatal(err)
	}
	pool, err := postgres.Open(ctx, url,
		postgres.WithMaxConns(20),
		postgres.WithApplicationName("acme-api"),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer pool.Close()
}

func UniqueViolation

func UniqueViolation(err error) (constraint string, ok bool)

UniqueViolation reports whether err is a unique constraint violation and returns the constraint's name, so repositories can map it to a domain error such as ErrDuplicateEmail.

func WithOrg

func WithOrg(ctx context.Context, orgID string) context.Context

WithOrg returns a copy of ctx whose connections carry orgID in OrgSetting, so row-level security policies limit them to that organisation's rows. Without it, a connection carries the organisation of the context's actor (actor.Actor.OrgID, set by orgs.RequireMember after checking membership), or none. Use it where code acts in one organisation without an organisation actor, such as a job reading one organisation's rows.

The organisation is set when a connection is acquired: a transaction keeps the one its context had at BeginTx.

func WithScope added in v0.3.0

func WithScope(ctx context.Context, scopeID string) context.Context

WithScope returns a copy of ctx whose connections carry scopeID in ScopeSetting, so row-level security policies limit them to that scope's rows. It is WithOrg under the name the framework uses for tenancy since v0.3.0, and is what gorbital.Scope.Session is usually set to.

Example

WithScope carries one scope into a context's connections, so row-level security policies limit them to that scope's rows. Use it where code acts in one scope without a scoped actor, such as a job working through one merchant's orders.

package main

import (
	"context"
	"log"

	"gorbital.dev/modules/postgres"
)

func main() {
	ctx := postgres.WithScope(context.Background(), "mch_1")

	// Connections acquired from ctx now set postgres.ScopeSetting to
	// mch_1, and policies written against it see only that merchant.
	log.Println(postgres.ScopeSetting, ctx != nil)
}

func WithoutRowLevelSecurity

func WithoutRowLevelSecurity(ctx context.Context, reason string) context.Context

WithoutRowLevelSecurity returns a copy of ctx whose connections set BypassSetting to "on", so policies let them read and write every organisation's rows. It is for system paths that work across organisations, such as migrations and maintenance jobs; reason names the path ("migrate", "job:orgs_purge"). It panics when reason is empty.

Decide it in code, never from request input. The first connection acquired with the returned context is logged at info level through the pool's logger (WithLogger), and every query span it runs carries gorbital.rls_bypass with the reason.

Types

type Beginner

type Beginner interface {
	BeginTx(ctx context.Context, opts pgx.TxOptions) (pgx.Tx, error)
}

Beginner starts transactions. *pgxpool.Pool and *pgx.Conn implement it.

type DBTX

type DBTX interface {
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

DBTX runs queries. *pgxpool.Pool, *pgxpool.Conn, *pgx.Conn and pgx.Tx implement it, so repositories work with or without a transaction.

type MigrationInfo

type MigrationInfo struct {
	Version int64
	// Path is the file's path in fsys.
	Path      string
	Applied   bool
	AppliedAt time.Time
}

MigrationInfo is one migration file and whether the database has it.

func MigrationList

func MigrationList(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS) ([]MigrationInfo, error)

MigrationList reports every migration in fsys with its state, oldest first. It changes nothing.

type MigrationState

type MigrationState struct {
	// Current is the highest applied version, 0 when none are applied.
	Current int64
	// Latest is the highest version in the migration files.
	Latest int64
	// Pending counts migration files not yet applied.
	Pending int
}

MigrationState describes the schema version against a set of migrations.

func Migrations

func Migrations(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS) (MigrationState, error)

Migrations reports the database's migration state for fsys, for readiness reports and doctor commands. It changes nothing.

type Option

type Option interface {
	// contains filtered or unexported methods
}

An Option configures Open.

func WithApplicationName

func WithApplicationName(name string) Option

WithApplicationName sets application_name, shown in pg_stat_activity.

func WithConnectTimeout

func WithConnectTimeout(d time.Duration) Option

WithConnectTimeout bounds each connection attempt and the ping in Open. Default: DefaultConnectTimeout.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger that records uses of WithoutRowLevelSecurity. Default: discard.

func WithMaxConnIdleTime

func WithMaxConnIdleTime(d time.Duration) Option

WithMaxConnIdleTime closes connections idle longer than d. Default: 30 minutes.

func WithMaxConnLifetime

func WithMaxConnLifetime(d time.Duration) Option

WithMaxConnLifetime closes connections older than d, so load moves to new database hosts after failover. Default: one hour.

func WithMaxConns

func WithMaxConns(n int32) Option

WithMaxConns sets the maximum pool size. Default: pgx's default, the greater of 4 and the number of CPUs.

func WithMeterProvider

func WithMeterProvider(mp metric.MeterProvider) Option

WithMeterProvider reports the pool's connection and acquire statistics as metrics of mp, read when metrics are collected: db.client.connection.count (by state, idle or used), db.client.connection.max, pgxpool.acquires, pgxpool.acquire.waits, pgxpool.acquire.wait_time, pgxpool.acquire.canceled and pgxpool.connections.created. Series are labelled with db.client.connection.pool.name, the application name set by WithApplicationName or "postgres". The instruments stay registered for the life of mp. Default: no pool metrics.

func WithMinConns

func WithMinConns(n int32) Option

WithMinConns sets how many connections the pool keeps open when idle. Default: 0.

func WithTracerProvider

func WithTracerProvider(tp trace.TracerProvider) Option

WithTracerProvider sets the provider for query spans. Default: the global OpenTelemetry provider.

type RowLevelSecurityReport

type RowLevelSecurityReport struct {
	// Role is the role queries run as (current_user).
	Role string
	// Bypasses reports a superuser or a role with BYPASSRLS: PostgreSQL
	// applies no policy to it, forced or not.
	Bypasses bool
	// Forced lists tables with row-level security enabled and forced, which
	// limits their owner too.
	Forced []string
	// NotForced lists tables with row-level security enabled but not
	// forced: a role that owns one isn't limited by its policies.
	NotForced []string
	// Unprotected lists tables with a NOT NULL org_id column and row-level
	// security off.
	Unprotected []string
}

RowLevelSecurityReport describes row-level security in the current schema for the role the connection runs as, so apps can warn at startup and in orb doctor when policies exist but don't apply (ADR-0061).

func CheckRowLevelSecurity

func CheckRowLevelSecurity(ctx context.Context, db DBTX) (RowLevelSecurityReport, error)

CheckRowLevelSecurity reports row-level security in the current schema as the role db connects with. It changes nothing.

func (RowLevelSecurityReport) On

On reports whether any table has row-level security enabled.

Directories

Path Synopsis
Package pgtest gives each test its own PostgreSQL database on the Docker PostgreSQL server started with `docker compose up -d --wait` (ADR-0028).
Package pgtest gives each test its own PostgreSQL database on the Docker PostgreSQL server started with `docker compose up -d --wait` (ADR-0028).

Jump to

Keyboard shortcuts

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