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

dbx

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package dbx is the thin layer between generated repositories and pgx.

It exists for one reason: a repository method has to work the same whether it was called directly or inside a transaction someone else opened. Taking a narrow interface rather than a concrete pool is what makes an Update able to write a snapshot and the row itself atomically without the caller arranging anything.

Index

Constants

View Source
const MaxCascadeDepth = 10

MaxCascadeDepth bounds how far a delete may propagate through child hooks.

A child that deletes its own rows by calling its own Delete triggers their children the same way, so depth is the call stack and recursion falls out rather than being designed. What does not fall out is termination: a cycle in the schema — a table that reaches itself through two relations — would exhaust the stack, and the failure would arrive as a crash rather than as an error somebody can read.

It is the same shape as the generated MaxFilterDepth and the same kind of number: high enough that no schema anybody writes on purpose reaches it, low enough that the one that does says so.

Variables

This section is empty.

Functions

func AfterCommit

func AfterCommit(ctx context.Context, fn func())

AfterCommit runs fn once the transaction this context belongs to has committed, or immediately when there is no transaction.

The distinction matters for anything outside the database: an email sent from inside a transaction that later rolls back has told somebody about a change that did not happen. Registering the work here moves it past the commit without the caller having to know whether it is the one that opened the transaction — and when a repository call is nested inside a larger unit of work, "committed" correctly means the outer one.

A panic in fn is recovered. The write has already landed, and unwinding a request that succeeded would report a failure that did not occur.

func ConstraintName

func ConstraintName(err error) string

ConstraintName returns the constraint an error names, or empty. It is what lets a handler turn "duplicate key" into a message about the field the user actually typed.

func Deref

func Deref[T any](p *T) T

Deref is what p points at, or T's zero value when p is nil. The other half of Null, for reading a nullable column back.

func EnterDelete

func EnterDelete(ctx context.Context, table, id string) (context.Context, bool, error)

EnterDelete records that a row is being deleted, and reports whether to go on.

False means this row is already being deleted further up the same transaction — a cycle — and the second visit is a no-op rather than an error: the row is going, which is what the caller asked for, and refusing would turn a legal schema into a runtime failure. The returned context carries the depth for anything nested inside this delete, so [LeaveDelete] is not needed and does not exist: the depth is the context's, and it unwinds with the call.

The error is the depth cap, which is a different answer from the visited set and deserves to be: reaching it means the propagation is deeper than rig will follow, and finishing halfway through would leave the transaction in a state nobody asked for.

A delete outside any transaction has nothing to guard — there is no parent to have been visited — so it always proceeds.

func InTx

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

InTx runs fn inside a transaction, reusing one already on the context.

Reuse is what makes nesting safe. A repository method that opens its own transaction unconditionally cannot be composed: two of them in one unit of work would commit independently, and the second failing would leave the first applied.

func InTxIf

func InTxIf(ctx context.Context, db Beginner, conn Conn, need bool, fn func(ctx context.Context, tx Conn) error) error

InTxIf runs fn inside a transaction only when one is called for.

A single statement is already atomic, and wrapping it in BEGIN and COMMIT costs two round trips to protect nothing. What does call for a transaction is a hook that has to be able to undo the write — so the caller decides, and gets the same body either way.

conn is what fn runs against when there is no transaction: the pool, or the transaction already on the context.

func IsCheckViolation

func IsCheckViolation(err error) bool

IsCheckViolation reports whether an error is a failed CHECK constraint.

func IsForeignKeyViolation

func IsForeignKeyViolation(err error) bool

IsForeignKeyViolation reports whether an error is a missing or still- referenced row.

func IsNoRows

func IsNoRows(err error) bool

IsNoRows reports whether an error means the query matched nothing.

func IsNotNullViolation

func IsNotNullViolation(err error) bool

IsNotNullViolation reports whether an error is a missing required value.

func IsUniqueViolation

func IsUniqueViolation(err error) bool

IsUniqueViolation reports whether an error is a duplicate-key failure.

func Null

func Null[T comparable](v T) *T

Null is a pointer to v, or nil when v is T's zero value.

For a nullable column whose Go type is not a pointer: an empty string and an absent value are the same thing to the caller and different things to Postgres. Without this, every optional column is a three-line branch at the call site, and the branch that gets it wrong writes `”` where the column means "nothing" — which then compares unequal to NULL in every query looking for one.

Constrained to comparable so that only a type with a meaningful zero reaches it. A json.RawMessage, where an empty document and no document are genuinely different things, is excluded on purpose.

func Tx

func Tx(ctx context.Context) (pgx.Tx, bool)

Tx returns the transaction on the context, if any.

func UTC

func UTC(t time.Time) time.Time

UTC is how a scanned instant reaches the rest of the program.

A timestamptz column holds no zone — it is microseconds since the epoch, an instant — but pgx decodes one into time.Local, which is the zone of the process rather than of the database or of the row. The instant is right either way, so a comparison is safe; what changes is everything that reads the clock off it. Format, Day, and JSON all follow the location, so the same row is rendered "2026-03-02T00:30:00+01:00" on a laptop in Stockholm and "2026-03-01T23:30:00Z" in a container with no TZ set.

Both are the same moment and neither is wrong, which is exactly why this is worth pinning: a response that changes shape with the host's environment breaks golden files, makes two replicas disagree in their logs, and invites somebody to compare the strings.

Generated repositories call this on every timestamp they scan, so the answer does not depend on how the pool was built. Pinning `TimeZone=UTC` in the connection string is still worth doing, and does a different job: it settles what `::date` and `date_trunc` mean inside SQL, which no amount of care on this side of the wire can affect.

func UTCPtr

func UTCPtr(t *time.Time) *time.Time

UTCPtr is UTC for a nullable column. Nil stays nil: a row with no deleted_at has not been deleted, and inventing a zero time would say it was.

func UTCSlice

func UTCSlice(ts []time.Time) []time.Time

UTCSlice is UTC for a timestamptz[] column.

It exists so the rule has no exceptions. pgx decodes the elements of an array the same way it decodes a scalar, so without this a column of instants would come back in the host's zone while every instant beside it came back in UTC — and an invariant with one silent exception is worse than none, because nobody checks the case they were told was handled.

The slice is modified in place and returned: it was just built by the scan and is not shared with anything. Nil stays nil, which is how a null array arrives.

func WithoutTx

func WithoutTx(ctx context.Context) context.Context

WithoutTx hides the transaction on a context, so that InTx opens one of its own instead of joining it.

The exception to the rule InTx exists for. Reuse is what makes nesting safe and is right for every write that belongs to the caller's unit of work — and wrong for the few that deliberately do not. An entry describing a failed authentication attempt is written outside whatever transaction noticed the failure, because it has to survive that transaction rolling back; anything that has to be atomic with *that* write, rather than with the caller's, needs the same treatment. A `pg_notify` is the case that motivated this: published on a transaction that later rolls back it is discarded along with it, which would leave every replica holding an answer the log already contradicts.

Only the transaction is hidden. A nested InTx under this opens a real one and gets its own after-commit list, so nothing registered inside runs on the outer commit.

Types

type Beginner

type Beginner interface {
	Begin(ctx context.Context) (pgx.Tx, error)
}

Beginner can start a transaction. A pool satisfies it; a transaction does too, through savepoints.

type Conn

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

Conn is what a repository needs from a database. A pool, a connection, and a transaction all satisfy it.

The three methods are pgx's own, with pgx's semantics: QueryRow defers its error to Scan, and Exec reports how many rows a write touched. Restating them here rather than taking *pgxpool.Pool is the whole point — the narrower type is what a transaction can also be.

func ConnFor

func ConnFor(ctx context.Context, fallback Conn) Conn

ConnFor is the transaction on the context, or the fallback.

It is what a store runs every statement through, and that is what lets a write join the caller's unit of work without the caller passing anything: a notification written inside the transaction that caused it, a file finalized alongside the row that owns it. With no transaction on the context it is the pool, which is the ordinary case.

type Pool

type Pool interface {
	Conn
	Beginner
}

Pool is Conn and Beginner together: something a statement can run on, and something a transaction can be opened from.

The two interfaces rather than *pgxpool.Pool, so that a test can hand this a transaction and so a module taking one never imports pgxpool. `notify.DB` and `files.DB` are aliases for it; `presence.DB` is plain Conn, because nothing in presence opens a transaction.

type Scanner

type Scanner interface {
	Scan(dest ...any) error
}

Scanner is what both a single row and a row of a result set satisfy, so one scan function serves a lookup and a listing.

The same shape as github.com/jackc/pgx/v5.Row, and named here rather than used from there so that a module scanning rows does not take pgx as a direct dependency for the sake of one interface.

Jump to

Keyboard shortcuts

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