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 ¶
- 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.
- 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.
- 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.
- 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 ¶
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.
const MaxParams = 65535
MaxParams is the most $n parameters a statement may reference; Postgres' own limit on bind parameters.
const MaxSQLBytes = 16 << 10
MaxSQLBytes caps the statement text.
Variables ¶
This section is empty.
Functions ¶
func Check ¶
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
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 Auditor ¶
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 ¶
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
}
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 Kind = "unavailable" // KindAudit: the [Auditor] failed. The rows are withheld. KindAudit Kind = "audit_failed" )
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.
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. |