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

dbquery

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package dbquery runs one read-only SQL statement against a Postgres and returns typed, bounded, JSON-encodable results. It is the execution core behind `forge db query`: forge's local transport and the control plane's hosted QueryDatabase both call Run, so a query means the same thing wherever it runs.

What every query does

  1. Check, the lexical guard: exactly one statement, and one that starts SELECT, WITH, TABLE, VALUES, SHOW or EXPLAIN. It exists for good error messages and is NOT the security boundary.
  2. BEGIN TRANSACTION READ ONLY ISOLATION LEVEL READ COMMITTED, then statement_timeout, lock_timeout and idle_in_transaction_session_timeout set for that transaction only, and standard_conforming_strings on, so the server reads string literals the way the guard did.
  3. The statement goes over the extended protocol, which refuses a multi-statement string on its own, and its rows are read up to the cap. Options.Args bind the $n parameters the statement references.
  4. ROLLBACK, always.

Why READ COMMITTED, and why there is no option

A query is exactly one statement, and one statement sees one snapshot at every isolation level: READ COMMITTED takes its snapshot when the statement starts, REPEATABLE READ when the transaction's first statement does, and here those are the same moment. REPEATABLE READ buys nothing and costs a transaction-long snapshot that holds back vacuum and can raise serialization failures against the writers a live dashboard must never disturb. A caller that wants a multi-statement consistent read is asking for a different feature.

The boundary is the read-only transaction, which refuses every write, nextval() and a data-modifying CTE, plus the role's privileges, which the caller owns (a role that cannot SELECT a column cannot read it through any expression). The context deadline is a client-side backstop to the server-side timeout: the guard refuses set_config(), but a denylist is not a boundary, and a statement that reached set_config('statement_timeout', '0') some other way is still stopped.

Bounds

Rows are capped (default 200, at most 10,000), the encoded response is capped at 4 MiB, a single cell at 4 KiB, and a query's time at 5 s by default and 30 s at most. A result cut by a cap is not an error: it carries Truncated and says which cap.

Audit

An Auditor sees every attempt that gets past argument validation, the ones the guard refuses included, so an attempted write against production is on record too. A failing Auditor withholds the rows.

This package opens no connections and owns no credentials. The caller passes anything with a pgx BeginTx (a *pgx.Conn, a *pgxpool.Pool).

Index

Constants

View Source
const (
	DefaultMaxRows = 200
	MaxRowsLimit   = 10_000

	DefaultTimeout = 5 * time.Second
	MaxTimeout     = 30 * time.Second

	// MaxCellBytes is the longest text a single cell keeps; the rest is
	// replaced by a marker.
	MaxCellBytes = 4 << 10
	// MaxResponseBytes caps the encoded size of all cells in a result.
	MaxResponseBytes = 4 << 20

	// DefaultLockTimeout is how long a statement waits for a lock; above
	// MaxLockTimeout is refused. A query that has to wait for a writer's lock
	// should give up fast rather than queue behind it.
	DefaultLockTimeout = time.Second
	MaxLockTimeout     = 5 * time.Second

	// DefaultIdleTimeout is idle_in_transaction_session_timeout; above
	// MaxTimeout is refused.
	DefaultIdleTimeout = 10 * time.Second
)

Limits. The Default* values apply to a zero Options field; a value above its Max* is refused, not clamped, so a caller learns the cap it asked past.

View Source
const MaxParams = 65535

MaxParams is the most $n parameters a statement may reference; Postgres' own limit on bind parameters.

View Source
const MaxSQLBytes = 16 << 10

MaxSQLBytes caps the statement text.

Variables

This section is empty.

Functions

func Check

func Check(sql string) (leader string, err error)

Check is the lexical guard: the first line of defence, and the source of good error messages. It accepts exactly one statement that starts with SELECT, WITH, TABLE, VALUES, SHOW or EXPLAIN, and refuses EXPLAIN ANALYZE over anything but a SELECT, because ANALYZE executes what it explains.

It also refuses SET and RESET by name, and any identifier that names a function in [deniedFunctions]: the ones that change the session (set_config, which a read-only transaction allows and through which a query can raise its own work_mem or drop its statement_timeout), run SQL text the guard never saw (ts_stat, ts_rewrite, query_to_xml, dblink, crosstab), take locks that outlive the statement, or reach the server's administration. The name is refused wherever it appears, schema-qualified or quoted alike, since attribute notation calls a function without parentheses; and every Unicode-escaped identifier (U&"...", with or without UESCAPE) is refused, since its spelling can hide the name. See [deniedIdentifier].

IT IS NOT THE SECURITY BOUNDARY. A WITH can still carry a data-modifying CTE and a SELECT can still call nextval(); the read-only transaction Run opens is what refuses them, and the role's privileges are what refuse everything else. A denylist over SQL can only ever be as complete as the day it was written; the client-side deadline in Run bounds a query whatever it managed to set. The guard is pure Go (no cgo, no parser dependency) so the forge binary stays cross-compilable; it understands comments, quoted identifiers, string literals (including E” and dollar quoting) well enough to find the statement's end, and no more.

On success it returns the statement's leading keyword, upper-cased.

func ParamRefs added in v1.0.7

func ParamRefs(sql string) (max int, err error)

ParamRefs returns the highest $n parameter the statement references, 0 when it has none. It reads tokens, not text: a $1 inside a string literal, a quoted identifier, a comment or a dollar-quoted body is not a reference. $0 and a number past MaxParams are refused, as Postgres refuses them.

Types

type AuditFunc

type AuditFunc func(ctx context.Context, e Event) error

AuditFunc adapts a function to Auditor.

func (AuditFunc) Audit

func (f AuditFunc) Audit(ctx context.Context, e Event) error

type Auditor

type Auditor interface {
	Audit(ctx context.Context, e Event) error
}

Auditor records one query attempt. It runs after the attempt, once, on a context that survives the caller's cancellation. If it returns an error, Run returns KindAudit and no rows, unless the attempt itself already failed, in which case that failure is returned.

type Beginner

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

Beginner is what Run needs from a connection. *pgx.Conn and *pgxpool.Pool both satisfy it.

type Column

type Column struct {
	Name string `json:"name"`
	// PGType is Postgres' name for the type ("int4", "timestamptz", an enum's
	// name).
	PGType string `json:"pg_type"`
}

Column describes one result column.

type Error

type Error struct {
	Kind Kind
	// Reason is a stable machine-readable subcode within Kind
	// ("multiple_statements", "statement_timeout", ...), empty when Kind says
	// it all.
	Reason string
	// Message is safe to show the caller who wrote the SQL.
	Message string
	// Err is the underlying cause, if any.
	Err error
}

Error is every failure Run and Check return.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Event

type Event struct {
	SQL       string
	SQLSHA256 string
	// Leader is the statement's leading keyword, empty when the guard
	// refused before finding one.
	Leader string
	// Outcome is "ok", or the [Kind] of the failure.
	Outcome string
	// Err is the failure, nil on success.
	Err        error
	RowCount   int
	Bytes      int
	Truncated  bool
	StartedAt  time.Time
	Duration   time.Duration
	MaxRows    int
	TimeoutSec float64
	// Args are the values bound to the statement's parameters, in order; nil
	// when none were. Values only: they are never part of SQL.
	Args []any
}

Event is what an Auditor receives.

type Kind

type Kind string

Kind classifies why a query did not return rows. A caller maps it onto its own transport (a Connect code, a CLI exit code) with one switch; the package imports neither.

const (
	// KindRefused: the request was refused before it reached the database —
	// the lexical guard, an empty or oversized statement, or limits out of
	// range. The request is wrong; retrying it unchanged cannot help.
	KindRefused Kind = "refused"
	// KindReadOnly: the database refused a write inside the read-only
	// transaction. The guard let the statement through (or was bypassed); the
	// transaction is the real boundary, and this is it holding.
	KindReadOnly Kind = "read_only"
	// KindPermission: the role may not read something the statement named
	// (SQLSTATE 42501) — a column marked forge:secret, for example.
	KindPermission Kind = "permission_denied"
	// KindTimeout: the statement outran its timeout, or waited past the lock
	// timeout.
	KindTimeout Kind = "timeout"
	// KindCanceled: the caller's context was canceled.
	KindCanceled Kind = "canceled"
	// KindSQL: the database rejected the statement itself — a syntax error, an
	// unknown column, a multi-statement string reaching the extended protocol.
	KindSQL Kind = "sql_error"
	// KindUnavailable: no connection, or the connection broke.
	KindUnavailable Kind = "unavailable"
	// KindAudit: the [Auditor] failed. The rows are withheld.
	KindAudit Kind = "audit_failed"
)

func KindOf

func KindOf(err error) Kind

KindOf reports the Kind of err, or "" when err is not (or does not wrap) an *Error.

type Options

type Options struct {
	// MaxRows is the row cap. 0 means DefaultMaxRows; above MaxRowsLimit is
	// refused.
	MaxRows int
	// Timeout is the statement timeout. 0 means DefaultTimeout; above
	// MaxTimeout is refused.
	Timeout time.Duration
	// LockTimeout is how long the statement waits for a lock. 0 means
	// DefaultLockTimeout; above MaxLockTimeout is refused.
	LockTimeout time.Duration
	// IdleTimeout is idle_in_transaction_session_timeout. 0 means
	// DefaultIdleTimeout; above MaxTimeout is refused.
	IdleTimeout time.Duration
	// MaxBytes caps the encoded size of the cells kept, below the response
	// cap. 0 means MaxResponseBytes; above it is refused.
	MaxBytes int
	// Args are the values of the statement's $1, $2, ... parameters, bound
	// over the extended protocol so a value is never spliced into the SQL.
	// Only $1..$n for the highest $n the statement references are bound (see
	// [ParamRefs]); a reference past len(Args) is refused. Postgres infers a
	// parameter's type from its use, so cast one that has no context
	// ($1::timestamptz).
	Args []any
	// Auditor, when set, is told how every attempt went.
	Auditor Auditor
	// contains filtered or unexported fields
}

Options bound one query. The zero value is the default policy.

type Result

type Result struct {
	Columns []Column `json:"columns"`
	Rows    [][]any  `json:"rows"`
	// RowCount is len(Rows).
	RowCount int `json:"row_count"`
	// Truncated is true when a cap cut the result; TruncatedBy names it:
	// "rows" or "bytes".
	Truncated   bool   `json:"truncated"`
	TruncatedBy string `json:"truncated_by,omitempty"`
	// CellsTruncated counts cells cut to MaxCellBytes.
	CellsTruncated int   `json:"cells_truncated,omitempty"`
	DurationMS     int64 `json:"duration_ms"`
	// Bytes is the encoded size of the cells kept, the figure the response
	// cap counts.
	Bytes int `json:"bytes"`
}

Result is a completed query. Each row has one cell per column, in column order. A cell is nil (NULL), a bool, a json.Number, a string, or a json.RawMessage (json and jsonb); the whole value marshals with encoding/json as is.

Numbers are exact: int2, int4 and floats are JSON numbers, and int8 and numeric are JSON numbers only up to 15 digits, a string beyond that, since a JSON reader that parses doubles would round them. Everything else, dates and times, uuid, arrays, bytea, enums, is the text Postgres prints for it.

func Run

func Run(ctx context.Context, db Beginner, sql string, opts Options) (*Result, error)

Run executes sql as one bounded read-only query. Every failure is an *Error.

Directories

Path Synopsis
Package kubedial reaches a CloudNativePG database in a Kubernetes namespace through the kube API server, with no network path of its own: it resolves a ready pod behind the database's Service, opens a pods/portforward stream to it and hands the stream to pgx as a net.Conn.
Package kubedial reaches a CloudNativePG database in a Kubernetes namespace through the kube API server, with no network path of its own: it resolves a ready pod behind the database's Service, opens a pods/portforward stream to it and hands the stream to pgx as a net.Conn.

Jump to

Keyboard shortcuts

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