Documentation
¶
Overview ¶
Package db provides a single shared, capped connection pool over pathsqlx.DB. It replaces the previous per-request pathsqlx.Connect so that the server holds one bounded pool for the lifetime of the process.
Index ¶
- Variables
- func Close(db *pathsqlx.DB) error
- func OpenPool(driver, dsn string, maxOpen, maxIdle int, connMaxLifetime time.Duration) (*pathsqlx.DB, error)
- func Ping(ctx context.Context, db *pathsqlx.DB) error
- func RunQuery(ctx context.Context, pool *pathsqlx.DB, query string, params interface{}, ...) (interface{}, error)
- func RunWrite(ctx context.Context, pool *pathsqlx.DB, query string, params interface{}, ...) (interface{}, error)
- type HardeningReport
- type PoolParams
- type QueryOptions
- type RolePools
Constants ¶
This section is empty.
Variables ¶
var ErrQueryTooExpensive = errors.New("query rejected: estimated cost or row count exceeds the configured limit")
ErrQueryTooExpensive is returned by RunQuery when the proactive cost ceiling rejects a query: its PostgreSQL planner estimate (total cost or output rows, from EXPLAIN without execution) exceeds the configured bound. The wrapping error carries the specific estimate for server logs; callers map the sentinel to a 4xx with a generic client message.
var ErrTooManyRowsAffected = errors.New("write rejected: affected row count exceeds the configured limit")
ErrTooManyRowsAffected is returned by RunWrite when a write affects more rows than QueryOptions.MaxAffectedRows allows. The write is rolled back before commit, so nothing persists. The wrapping error carries the actual count for server logs; callers map the sentinel to a 4xx with a generic client message.
Functions ¶
func OpenPool ¶
func OpenPool(driver, dsn string, maxOpen, maxIdle int, connMaxLifetime time.Duration) (*pathsqlx.DB, error)
OpenPool opens a shared connection pool (no per-request Connect) and applies caps. It uses pathsqlx.Open (lazy, no ping) so the process can start before the DB is reachable; callers may Ping separately. Returns an error for an unknown driver.
func RunQuery ¶
func RunQuery(ctx context.Context, pool *pathsqlx.DB, query string, params interface{}, hints map[string]string, opts QueryOptions) (interface{}, error)
RunQuery runs query inside a transaction so the SET LOCAL session settings apply on the same connection, then commits. It targets Postgres.
The transaction is opened (optionally READ ONLY), session settings are applied via applySessionSettings, the query runs through pathsqlx.PathQueryTx on that same transaction, and the transaction is committed on success or rolled back on any error.
func RunWrite ¶
func RunWrite(ctx context.Context, pool *pathsqlx.DB, query string, params interface{}, hints map[string]string, opts QueryOptions, hasReturning bool) (interface{}, error)
RunWrite executes a data-modifying statement (INSERT/UPDATE/DELETE, or a WITH wrapping one) inside a read-write transaction. It mirrors RunQuery - same transaction-local session settings (applySessionSettings) and the same proactive EXPLAIN cost ceiling (enforceCostCeiling) - and differs only in the transaction mode and the return shape:
- hasReturning true: the statement is run through pathsqlx, so the RETURNING columns come back as JSON (a flat array by default, or shaped by hints).
- hasReturning false: the statement is executed and the result is {"affected": N}, the rows-affected count.
When opts.MaxAffectedRows > 0 the affected count is checked before commit and the transaction is rolled back with ErrTooManyRowsAffected if it is exceeded. The count is exact for the non-RETURNING path (RowsAffected) and for the default flat RETURNING shape (the number of returned rows); a RETURNING result reshaped by hints into a non-array cannot be counted post hoc, so for that case the cap relies on the pre-execution MaxEstimatedRows ceiling instead.
PostgreSQL is the primary target (RETURNING and the EXPLAIN ceiling are Postgres features); the affected-count path is driver-agnostic.
Types ¶
type HardeningReport ¶
type HardeningReport struct {
// Critical findings break the boundary the whole design rests on: a
// superuser or BYPASSRLS role (bypasses RLS and read-only), a role that can
// write, or - when RLS is the enforced boundary - a readable table with no
// row-level security.
Critical []string
// Warnings are findings to surface but not necessarily fatal: executable
// file/sleep/large-object functions, readable tables without RLS (outside
// enforce mode), owner-bypassable (non-forced) RLS, or RLS enabled with no
// policy.
Warnings []string
}
HardeningReport is the result of the startup database hardening self-check. It separates findings that undermine the core security guarantees (Critical) from weaker ones worth surfacing (Warnings).
func VerifyHardening ¶
func VerifyHardening(ctx context.Context, pool *pathsqlx.DB, driver, authTablePrefix string, noRLSIsCritical, writesEnabled, writeRLSIsCritical bool) (*HardeningReport, error)
VerifyHardening runs read-only catalog queries to check the connected role's posture: it is not a superuser, does not hold the BYPASSRLS attribute, has no write privileges outside the auth tables, cannot execute file/sleep/large-object functions, and that every table it can read has row-level security enabled and forced. It is PostgreSQL-specific; for any other driver it returns an empty report.
authTablePrefix scopes out the server's own auth tables, which it legitimately reads and updates (last_used_at), so the column-level write grant they need does not register as a finding.
noRLSIsCritical decides where a readable table with no row-level security is reported: as a Critical finding (when RLS is the security boundary and the operator asked to enforce it) or a Warning. The caller sets it when identity_kind is login_role and startup_checks is enforce, so a silent full-table exposure aborts startup only where RLS is actually relied upon.
writesEnabled changes how a write privilege is judged. When writes are off the server promises read-only, so any write grant is Critical. When writes are on a write grant is expected and is not itself a finding; instead, where RLS is the enforced boundary (writeRLSIsCritical), a writable table that lacks a WITH CHECK policy is Critical, since RLS without WITH CHECK filters which rows a write can see but not which it can create or change - a silent cross-tenant write path.
func (*HardeningReport) Empty ¶
func (r *HardeningReport) Empty() bool
Empty reports whether the check found nothing.
type PoolParams ¶
type PoolParams struct {
MaxOpen int
MaxIdle int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
}
PoolParams holds the tunable parameters applied to a single role pool. They map directly onto the database/sql pool setters: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime and SetConnMaxIdleTime.
type QueryOptions ¶
type QueryOptions struct {
// ReadOnly opens the transaction READ ONLY when true.
ReadOnly bool
// StatementTimeout, when > 0, sets a transaction-local statement_timeout.
StatementTimeout time.Duration
// IdleInTxTimeout, when > 0, sets a transaction-local
// idle_in_transaction_session_timeout so a stalled transaction cannot pin a
// connection indefinitely.
IdleInTxTimeout time.Duration
// WorkMemKB, when > 0, sets a transaction-local work_mem (in kB) so a single
// sort/hash node cannot consume unbounded memory.
WorkMemKB int
// MaxEstimatedCost, when > 0, makes RunQuery EXPLAIN the query first (without
// executing it) and reject it with ErrQueryTooExpensive if the planner's
// estimated total cost exceeds this. 0 disables. PostgreSQL only.
MaxEstimatedCost float64
// MaxEstimatedRows, when > 0, rejects the query if the planner's estimated
// output row count exceeds this (same EXPLAIN pre-check). 0 disables.
MaxEstimatedRows int64
// MaxAffectedRows, when > 0, is the write blast-radius cap: RunWrite rolls the
// transaction back with ErrTooManyRowsAffected if a write affects (or, for a
// RETURNING write, returns) more rows than this. 0 disables.
MaxAffectedRows int64
}
QueryOptions configures how RunQuery executes a query under Postgres row-level security: whether to use a read-only transaction, and the optional transaction-local resource limits. The caller's identity is the connected database role (current_user), so there is no session variable to bind.
type RolePools ¶
type RolePools struct {
// contains filtered or unexported fields
}
RolePools manages one lazily-created connection pool per database role for the login_role model. It is safe for concurrent use.
A global buffered-channel semaphore caps the number of connections in use across all pools at once, which is the hard ceiling PostgreSQL and database/sql cannot enforce themselves. A warm-pool limit caps how many pools keep idle connections: pools beyond the limit have their idle count set to zero by an LRU so they retain no warm connection, but they are not evicted so in-flight use keeps working.
func NewRolePools ¶
func NewRolePools(driver, baseDSN string, maxTotalBackends, warmLimit int, defaults PoolParams) (*RolePools, error)
NewRolePools creates a RolePools. driver is the sql driver name. baseDSN is a connection string WITHOUT a user (for example "host=db dbname=pathql sslmode=disable"); the role is appended as "user=<role>" when a pool is created. maxTotalBackends bounds the number of connections in use across ALL pools at once via a global semaphore. warmLimit bounds how many pools keep idle connections, enforced by an LRU. defaults is the PoolParams applied to each pool.
It returns an error if baseDSN is empty or maxTotalBackends is less than 1.
func NewRolePoolsWithOpener ¶
func NewRolePoolsWithOpener(driver, baseDSN string, maxTotalBackends, warmLimit int, defaults PoolParams, opener func(driver, dsn string) (*pathsqlx.DB, error)) (*RolePools, error)
NewRolePoolsWithOpener is like NewRolePools but takes the pool opener as an explicit argument so tests can inject a fake that never dials a database. A nil opener falls back to pathsqlx.Open.
func (*RolePools) Acquire ¶
func (p *RolePools) Acquire(ctx context.Context, role string) (pool *pathsqlx.DB, release func(), err error)
Acquire blocks until a global semaphore slot is free or ctx is done. On success it returns the pool authenticated as role, created lazily on first use with the configured params, and a release func that returns the slot to the semaphore. The release func is safe to call exactly once.
role must match ^[A-Za-z_][A-Za-z0-9_]*$ because it is interpolated into the DSN as "user=<role>", otherwise an error is returned and no slot is held.
func (*RolePools) Close ¶
Close closes every pool and removes them. It returns the first close error encountered, if any, after attempting to close all pools.
func (*RolePools) Evict ¶
Evict closes and removes the pool for role. In-flight callers holding a reference to the closed pool will see their queries fail, which is the intended behavior when a role is being removed. It is a no-op if no pool exists for the role.
func (*RolePools) Stats ¶
Stats returns a snapshot of sql.DBStats for each live pool, keyed by role.
func (*RolePools) UseRolePassword ¶
UseRolePassword sets the function that supplies each role's connection password (login_role auth). It must be called once at startup before any Acquire. A nil function appends no password.