dbx

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package dbx provides Postgres helpers on top of jmoiron/sqlx and the pgx driver: connection setup, named query/exec wrappers with query logging, transactions, and bulk insert/upsert.

Open builds a *sqlx.DB from a Config; StatusCheck waits for it to become reachable. The Named* helpers run named queries/execs, log the expanded SQL at debug level, and translate well-known Postgres errors into the package sentinels (ErrDBNotFound, ErrDBDuplicatedEntry, ErrUndefinedTable) so callers match them with errors.Is instead of inspecting driver codes. WithinTran runs a function inside a transaction, committing on success and rolling back on error or panic. Bulk* batch large multi-row writes within Postgres's parameter limit.

Usage

cfg := dbx.Config{
    User: "postgres", Password: "postgres",
    Host: "localhost:5432", Name: "app",
    Schema: "public", MaxIdleConns: 2, MaxOpenConns: 5,
    DisableTLS: true,
}
db, err := dbx.Open(cfg)
if err != nil {
    return err
}
if err := dbx.StatusCheck(ctx, db); err != nil {
    return err
}

// Named query into a slice.
var widgets []Widget
const q = `SELECT id, name FROM widgets WHERE name = :name`
arg := struct {
    Name string `db:"name"`
}{Name: "gadget"}
if err := dbx.NamedQuerySlice(ctx, log, db, q, arg, &widgets); err != nil {
    return err
}

// Pass a slice as a single array parameter: pgx binds it as a Postgres
// array, so ANY(:ids) filters without expanding placeholders.
var out []Widget
err := dbx.NamedQuerySlice(ctx, log, db,
    `SELECT * FROM widgets WHERE id = ANY(:ids)`,
    map[string]any{"ids": []int64{235, 401, 512}}, // slice -> one $1 -> int8[]
    &out,
)

// Write inside a transaction.
err = dbx.WithinTran(ctx, log, db, func(tx *sqlx.Tx) error {
    const ins = `INSERT INTO widgets (id, name) VALUES (:id, :name)`
    return dbx.NamedExecContext(ctx, log, tx, ins, w)
})
if errors.Is(err, dbx.ErrDBDuplicatedEntry) {
    // unique violation
}

Querying

  • ExecContext / NamedExecContext run statements; the RowsAffected variant returns the affected-row count.
  • QueryStruct / NamedQueryStruct scan exactly one row (ErrDBNotFound when none).
  • QuerySlice / NamedQuerySlice scan every row into a *[]T.
  • NamedQuerySliceUsingIn / NamedQueryStructUsingIn handle a query with an IN clause: they expand a slice parameter (sqlx.In) and rebind before running.

For an IN-style filter, prefer binding the slice as a single array parameter and matching with ANY(:ids) (see Usage): pgx encodes []int64/[]string as a Postgres array, so the query text stays constant regardless of list length. Reach for the …UsingIn helpers only when you need driver-agnostic placeholder expansion instead.

The plain (non-Named) variants take no parameters; the Named variants bind from a struct via its `db` tags.

Dialects

Subpackage dbx/dialect captures the few SQL fragments that differ between engines (currently pagination) behind a small Dialect interface, with Postgres and SQLite implementations, so a store can compose portable SQL.

Bulk writes

BulkInsert writes len(values)/len(columns) rows, splitting into batches that stay under Postgres's bind-parameter cap, with values laid out row-major. An optional conflictAction (e.g. "ON CONFLICT DO NOTHING") is appended verbatim. BulkUpsert builds the ON CONFLICT … DO UPDATE clause for you from the conflict columns.

Transactions

WithinTran is the common path. For stores that must run against either a pool or an outer transaction, depend on the Beginner / CommitRollbacker seam: NewBeginner adapts a *sqlx.DB, and ExtContext extracts the query surface from a started transaction.

Schema provisioning

EnsureSchema applies idempotent DDL at startup under a transaction-scoped Postgres advisory lock, so several service replicas can provision the same table concurrently without racing — this is how the SDK storage packages (outbox, queue, auditlog, translation) create their own tables without a hand-written migration. AdvisoryKey derives a stable, collision-resistant lock key from a namespaced name (e.g. "skit.outbox.schema").

Config

Config fields: User, Password, Host, Name, Schema (sets search_path), MaxIdleConns, MaxOpenConns, DisableTLS (DisableTLS=true selects sslmode=disable, otherwise sslmode=require). Open always sets timezone=utc.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDBNotFound        = sql.ErrNoRows
	ErrDBDuplicatedEntry = errors.New("duplicated entry")
	ErrUndefinedTable    = errors.New("undefined table")
)

Sentinel errors returned by this package; match them with errors.Is.

Functions

func AdvisoryKey added in v0.4.0

func AdvisoryKey(name string) int64

AdvisoryKey derives a stable Postgres advisory-lock key from name using FNV-1a. It lets a package claim a collision-resistant lock key without hand-picking a magic integer that might clash with the application's own advisory locks — advisory locks share one namespace per database. Use a namespaced name such as "skit.outbox.schema".

func BulkInsert

func BulkInsert(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, table string, columns []string, values []any, conflictAction string) error

BulkInsert inserts len(values)/len(columns) rows into table in batches, each batch a single multi-row INSERT. conflictAction, if non-empty, is appended verbatim (e.g. "ON CONFLICT DO NOTHING"). values are laid out row-major: [r0c0, r0c1, r1c0, r1c1, ...].

For upserts prefer BulkUpsert, which builds the conflict clause for you.

func BulkUpsert

func BulkUpsert(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, table string, columns []string, values []any, conflictColumns []string) error

BulkUpsert inserts rows and, on conflict over conflictColumns, updates every non-conflict column from the proposed row.

func EnsureSchema added in v0.4.0

func EnsureSchema(ctx context.Context, log *logger.Logger, db *sqlx.DB, lockKey int64, ddl string) error

EnsureSchema applies idempotent DDL once, safely, at service startup. It runs ddl inside a transaction holding a Postgres transaction-scoped advisory lock keyed by lockKey, so when several replicas boot together they serialize on the lock and concurrent CREATE ... IF NOT EXISTS statements do not race ("tuple concurrently updated" / duplicate-relation errors). The lock releases automatically when the transaction ends — commit, rollback, or a dropped connection — so it cannot leak.

This is how a service auto-provisions the tables an SDK storage package owns (outbox, queue, auditlog, translation, ...) at startup, without hand-writing a migration for them. lockKey must be unique per schema across the database; derive it with AdvisoryKey.

func ExecContext

func ExecContext(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string) error

ExecContext runs a parameterless statement.

func ExtContext

func ExtContext(tx CommitRollbacker) (sqlx.ExtContext, error)

ExtContext extracts the sqlx.ExtContext (the query surface) from a transaction returned by Begin.

func NamedExecContext

func NamedExecContext(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any) error

NamedExecContext runs an INSERT/UPDATE/DELETE with named parameters bound from data. It logs the expanded query and translates well-known Postgres errors.

func NamedExecContextRowsAffected

func NamedExecContextRowsAffected(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any) (int64, error)

NamedExecContextRowsAffected is like NamedExecContext but returns the number of affected rows.

func NamedQuerySlice

func NamedQuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest *[]T) error

NamedQuerySlice runs a named query and scans all rows into *[]T.

func NamedQuerySliceUsingIn

func NamedQuerySliceUsingIn[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data any, dest *[]T) error

NamedQuerySliceUsingIn runs a named query whose data contains a slice bound to an IN clause, scanning all rows into *[]T. It expands the slice parameters (sqlx.In) and rebinds the query for the driver before executing. Use it instead of NamedQuerySlice when the query has an `IN (:ids)` form.

func NamedQueryStruct

func NamedQueryStruct(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data, dest any) error

NamedQueryStruct runs a named query expected to return exactly one row.

func NamedQueryStructUsingIn

func NamedQueryStructUsingIn(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, data, dest any) error

NamedQueryStructUsingIn runs a named query with an IN clause expected to return exactly one row, scanning it into dest. Returns ErrDBNotFound when no row matches.

func Open

func Open(cfg Config) (*sqlx.DB, error)

Open opens a sqlx.DB using the pgx driver. It does not verify connectivity; call StatusCheck for that.

func QuerySlice

func QuerySlice[T any](ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, dest *[]T) error

QuerySlice runs a parameterless query and scans all rows into *[]T.

func QueryStruct

func QueryStruct(ctx context.Context, log *logger.Logger, db sqlx.ExtContext, query string, dest any) error

QueryStruct runs a parameterless query expected to return exactly one row and scans it into dest (a pointer to a struct).

func StatusCheck

func StatusCheck(ctx context.Context, db *sqlx.DB) error

StatusCheck pings the database, retrying until it is reachable or ctx is done.

On timeout it returns errors.Join(ctx.Err(), lastPingErr) rather than a bare "context deadline exceeded", so the real cause — connection refused, TLS failure, password authentication failed — is visible at the call site instead of being masked by the deadline.

func WithinTran

func WithinTran(ctx context.Context, log *logger.Logger, db *sqlx.DB, fn func(tx *sqlx.Tx) error) error

WithinTran runs fn inside a transaction, committing on success and rolling back on error or panic.

Types

type Beginner

type Beginner interface {
	Begin() (CommitRollbacker, error)
}

Beginner starts a transaction. It is the seam stores depend on so they can be driven either by a pool or by an outer transaction (see middleware that opens a transaction per request).

type CommitRollbacker

type CommitRollbacker interface {
	Commit() error
	Rollback() error
}

CommitRollbacker is a transaction that can be committed or rolled back.

type Config

type Config struct {
	User         string
	Password     string
	Host         string
	Name         string
	Schema       string
	MaxIdleConns int
	MaxOpenConns int
	DisableTLS   bool
}

Config holds connection settings.

type DBBeginner

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

DBBeginner adapts a *sqlx.DB to the Beginner interface.

func NewBeginner

func NewBeginner(db *sqlx.DB) *DBBeginner

NewBeginner returns a Beginner backed by db.

func (*DBBeginner) Begin

func (b *DBBeginner) Begin() (CommitRollbacker, error)

Begin starts a new transaction.

Directories

Path Synopsis
Package dialect provides cross-cutting helpers that vary between SQL engines but are otherwise reusable by every domain store.
Package dialect provides cross-cutting helpers that vary between SQL engines but are otherwise reusable by every domain store.

Jump to

Keyboard shortcuts

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