Documentation
¶
Overview ¶
Package postgres is Forge's PostgreSQL adapter: a configured pgx connection pool, explicit transactions, error translation into fault errors, advisory locks and SQL-file migrations.
Forge is SQL-first. Repositories hold explicit SQL (hand-written or generated by sqlc, whose pgx/v5 DBTX interface matches DBTX here); there is no ORM and no query builder.
Index ¶
- Constants
- Variables
- func AdvisoryXactLock(ctx context.Context, tx pgx.Tx, key int64) error
- func Commands(migrations fs.FS) []command.Command
- func CommandsFromSets(migrationSets ...fs.FS) []command.Command
- func ConstraintName(err error) string
- func EncryptedColumn(db DBTX, spec encryption.SQLColumn, opts ...EncryptedColumnOption) (encryption.Rewriter, error)
- func IsolatedConnConfig(raw string, allowedKeys []string) (*pgx.ConnConfig, error)
- func LockKey(name string) int64
- func NewMigrator(cfg config.Database, migrations []migrate.Migration, logger *slog.Logger) (*migrate.Migrator, error)
- func SQLState(err error) string
- func SeedCommand(seed SeedFunc) command.Command
- func Translate(err error) error
- func TryAdvisoryXactLock(ctx context.Context, tx pgx.Tx, key int64) (bool, error)
- type DB
- func (db *DB) Close()
- func (db *DB) Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
- func (db *DB) InTenantTx(ctx context.Context, fn func(tx pgx.Tx) error) error
- func (db *DB) InTenantTxWith(ctx context.Context, opts TxOptions, fn func(tx pgx.Tx) error) error
- func (db *DB) InTx(ctx context.Context, fn func(tx pgx.Tx) error) error
- func (db *DB) InTxWith(ctx context.Context, opts TxOptions, fn func(tx pgx.Tx) error) error
- func (db *DB) Ping(ctx context.Context) error
- func (db *DB) Pool() *pgxpool.Pool
- func (db *DB) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
- func (db *DB) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
- func (db *DB) Shutdown(ctx context.Context) error
- type DBTX
- type EncryptedColumnOption
- type Option
- type SeedFunc
- type TxOptions
- type URLRefusedError
Constants ¶
const ( ReadCommitted = pgx.ReadCommitted RepeatableRead = pgx.RepeatableRead Serializable = pgx.Serializable )
Isolation levels accepted by TxOptions.
const DefaultPoolName = sqldb.DefaultPoolName
DefaultPoolName is the pool name reported when WithPoolName is not used.
Variables ¶
var ErrRowSecurityBypass = errors.New("the database role bypasses row-level security")
ErrRowSecurityBypass is returned by Open, outside development and test, when the connection's role would not be subject to row-level security (see RequireRowSecurity).
Functions ¶
func AdvisoryXactLock ¶
AdvisoryXactLock blocks until it holds the transaction-scoped advisory lock for key. PostgreSQL releases it at commit or rollback, so it cannot leak. Use it to serialize work across processes, such as a scheduled job that must not run twice.
func Commands ¶
Commands returns the migrate and rollback subcommands for an application binary with one migration set; see migrate.Commands.
func CommandsFromSets ¶
CommandsFromSets returns migrate and rollback commands over one globally ordered history composed from independent migration filesystems.
func ConstraintName ¶
ConstraintName returns the violated constraint in err's chain, or "". Use it to turn a specific unique violation into a domain error, for example "tasks_title_key" into "a task with this title already exists".
func EncryptedColumn ¶
func EncryptedColumn(db DBTX, spec encryption.SQLColumn, opts ...EncryptedColumnOption) (encryption.Rewriter, error)
EncryptedColumn returns the encryption.Rewriter for one encrypted TEXT column, used by Cipher.Reencrypt to move a table to the active key. The primary key must be a uuid or text column (UUIDv7 in Forge applications); other types, such as bigint, are refused with an error naming the requirement. The spec's placeholder style is always PostgreSQL's.
A table under row-level security (the generated tenant tables: ENABLE and FORCE ROW LEVEL SECURITY on forge.tenant_id, read by NOBYPASSRLS roles) is rotated one tenant at a time with WithTenant. Without a tenant scope every page first checks the catalog and fails with encryption.ErrConfiguration when the relation is missing or is not a table (a view hides the security of the table under it), or when its policies apply to the connected role, instead of seeing no row and reporting an empty, successful rotation.
func IsolatedConnConfig ¶
func IsolatedConnConfig(raw string, allowedKeys []string) (*pgx.ConnConfig, error)
IsolatedConnConfig parses raw, a postgres:// or postgresql:// URL, with pgx without letting the process environment decide anything: pgx reads PGSSLMODE, PGOPTIONS, PGTARGETSESSIONATTRS, PGSERVICE, PGUSER and the other PG* variables as defaults for what the URL leaves unset and has no option to ignore them. The environment is not edited (that would tear it under every concurrent reader): the libpq default of each parameter the environment sets is written into the URL, which pgx prefers, a service named by PGSERVICE is replaced by an empty one, and the session settings the URL does not name (PGOPTIONS, PGTZ, PGAPPNAME) are removed from the result. Every query parameter of raw must be in allowedKeys (dbname is accepted wherever database is listed); allowedKeys must not be empty.
A refusal is a *URLRefusedError. Any other error either says the URL is not valid or comes from pgx, whose parse errors can quote raw: callers must not show them as they are. The forge db commands use it for their administration connections.
func LockKey ¶
LockKey derives a stable advisory lock key from a descriptive name, such as "billing:invoice-run". Names are hashed with 64-bit FNV-1a; use distinct, namespaced names to keep collisions negligible.
func NewMigrator ¶
func NewMigrator(cfg config.Database, migrations []migrate.Migration, logger *slog.Logger) (*migrate.Migrator, error)
NewMigrator returns a migrator for the database cfg points at. It opens its own dedicated connections, never ones from an application pool; close it when done.
func SeedCommand ¶
SeedCommand returns the "seed" subcommand: it runs seed in one READ COMMITTED transaction on the configured database and commits only when seed returns nil, so a failed seed changes nothing. Generated applications register it with their db.Seed function.
Types ¶
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a PostgreSQL connection pool.
func Open ¶
Open creates a connection pool from validated configuration and verifies connectivity within cfg.ConnectTimeout, so misconfiguration fails at startup rather than on the first request.
Outside development and test (cfg.Environment, which config.Load and forge.New set from FORGE_ENV) it also refuses, with ErrRowSecurityBypass, a role that could bypass row-level security; see RequireRowSecurity for the rules and AllowRowSecurityBypass for the explicit opt-out. With Lazy(true) it connects on the first query instead.
Open refuses, lazily too, while PGOPTIONS is set (non-empty): pgx would add it to every connection, and PGOPTIONS='-c forge.tenant_id=...' would give every session a default tenant. The error names the variable, never its value. PGTZ and PGAPPNAME are ignored: sessions are in UTC and the application_name is the URL's, else "forge". Only the migrator (NewMigrator) reads PGOPTIONS, as for psql.
func (*DB) Close ¶
func (db *DB) Close()
Close waits for acquired connections to be released and closes the pool.
func (*DB) InTenantTx ¶
InTenantTx runs fn in a transaction whose PostgreSQL-local tenant setting comes from ctx. It fails before acquiring a connection when ctx has no tenant. The setting is transaction-local, so it cannot leak through the pool. RLS policies should read current_setting('forge.tenant_id', true).
func (*DB) InTenantTxWith ¶
InTenantTxWith is InTenantTx with explicit transaction options. The tenant setting is installed again for every serialization/deadlock retry.
func (*DB) InTx ¶
InTx runs fn in a READ COMMITTED transaction. The transaction commits when fn returns nil and rolls back when it returns an error or panics. fn's error is returned unchanged.
func (*DB) Ping ¶
Ping verifies a connection can be used. Its signature matches health.Check, so it can be registered directly as a readiness check.
func (*DB) Pool ¶
Pool exposes the underlying pgx pool for capabilities Forge does not wrap, such as COPY or LISTEN/NOTIFY. It is the explicit escape hatch.
func (*DB) Query ¶
Query runs a query that returns rows, on the pool. The caller must close the rows.
type DBTX ¶
type DBTX interface {
Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
DBTX is satisfied by *DB and pgx.Tx, so a repository written against it runs both standalone and inside a transaction.
type EncryptedColumnOption ¶
type EncryptedColumnOption func(*encryptedColumn)
EncryptedColumnOption configures EncryptedColumn.
func WithTenant ¶
func WithTenant(tenant tenancy.Tenant) EncryptedColumnOption
WithTenant scopes every page read and compare-and-set update of an EncryptedColumn to tenant: each runs in its own transaction that first installs the tenant as forge.tenant_id (set_config(..., true), as InTenantTx does), so the table's row-level security policies show and accept that tenant's rows only. Rotate a tenant table once per tenant. db must then be a *DB or a pgx.Tx; inside a pgx.Tx each step is a savepoint, and the transaction's own forge.tenant_id is restored after every step, so the caller's later statements stay in the caller's scope.
type Option ¶
type Option func(*openOptions)
Option configures Open.
func AllowRowSecurityBypass ¶
func AllowRowSecurityBypass() Option
AllowRowSecurityBypass is the explicit opt-out of the check Open applies outside development and test (see RequireRowSecurity): Open then accepts any role. Pass it only for a pool that must connect as such a role on purpose, such as an administration or reporting tool, never for the pool an application serves tenants with. It is the only opt-out, and RequireRowSecurity for a staging or production environment overrides it.
func Lazy ¶
Lazy makes Open, when lazy is true, return without connecting: the pool connects on its first query, and each connection it opens is checked for row-level security (RequireRowSecurity) before its first use, where Open would check the pool. A wrong URL or an unreachable database is then reported by that query, not by Open. Generated applications pass Lazy(!app.Serving()): `routes` and `openapi` compose the application without serving it and need no reachable database. Lazy(false) is Open's default. A set PGOPTIONS is refused lazily too (see Open).
func RequireRowSecurity ¶
func RequireRowSecurity(environment config.Environment) Option
RequireRowSecurity makes Open check the role, as described below, when environment is neither development nor test (so in staging and production), even if cfg.Environment says development or test. It only ever adds the check: RequireRowSecurity(config.Development) leaves Open's default, which follows cfg.Environment, and it also wins over AllowRowSecurityBypass. Open already checks for cfg.Environment, which config.Load and forge.New set; pass this option only for a config.Database built in code whose Environment is not the application's.
The check covers both the role FORGE_DATABASE_URL logs in as (session_user) and the role it acts as (current_user), and Open fails when either
- is a superuser or has BYPASSRLS or REPLICATION (logical decoding reads every tenant's changes),
- is a member of such a role (it can SET ROLE to it),
- has CREATEROLE (before PostgreSQL 16 it can grant itself such a membership),
- is a member of pg_execute_server_program, pg_write_server_files or pg_read_server_files (paths to superuser or to the data files),
- owns, or is a member of the owner of, a table of this database with row-level security enabled (an owner can ALTER TABLE ... NO FORCE ROW LEVEL SECURITY), or
- holds TRUNCATE, REFERENCES (on the table or on any of its columns) or TRIGGER on such a table, itself, through PUBLIC or through a role it is a member of (none of the three is subject to row-level security: TRUNCATE empties every tenant's rows, a foreign key probes them and a trigger sees them; the usual cause is a GRANT ALL), or
- holds any privilege on a partition that has no row-level security of its own but belongs to a partitioned table with it (querying the partition directly skips the parent's policies).
Membership counts whatever its INHERIT and SET options: a role granted WITH INHERIT FALSE, SET FALSE cannot use the other role's privileges, and is refused all the same. That over-refusal is deliberate (it fails closed) and has a simple fix: do not grant the serving role such memberships.
It also fails when the two differ: a role setting (`options=-c role=...`, ALTER ROLE ... SET role) makes the session act as another role, and `SET ROLE NONE` returns it to the login role, so only the login role counts and it must be the serving role itself. Tenant isolation through RLS policies is otherwise only as strong as every query's own predicate, so the application refuses to serve instead. Outside these environments, and without this option, development and test are not checked, so local tools may connect as an administrator.
Serve as a NOSUPERUSER NOBYPASSRLS NOCREATEROLE NOREPLICATION runtime role that owns no table and migrate as a separate role (FORGE_MIGRATION_DATABASE_URL); see docs/DATABASE.md.
func WithPoolName ¶
WithPoolName names the pool in the connection metrics' pool name label (default "default"). Name each pool when an application opens more than one PostgreSQL database; a name used twice replaces the earlier pool's series.
func WithTelemetry ¶
WithTelemetry instruments the pool: its connection statistics are exported (db.client.connection.count and the other pool metrics, see docs/OBSERVABILITY.md), and through pgx's query tracer every statement, including those inside transactions, is measured in db.client.operation.duration and, inside a traced operation, gets a span with db.system.name "postgresql" and db.operation.name. Statement text and arguments are never recorded. Batches (SendBatch) and COPY are not traced. A nil Telemetry leaves the pool uninstrumented, so Open(ctx, cfg, WithTelemetry(app.Telemetry())) is always safe.
type SeedFunc ¶
SeedFunc loads the rows every environment of an application needs, such as lookup tables and default settings, inside the transaction SeedCommand opens. It must be idempotent (INSERT ... ON CONFLICT DO NOTHING, or check before writing), because seeding is meant to be run again, for example after every deploy. Tenant-scoped tables behind row-level security need SELECT set_config('forge.tenant_id', $1, true) before their rows.
type TxOptions ¶
type TxOptions struct {
// Isolation defaults to PostgreSQL's default, READ COMMITTED.
Isolation pgx.TxIsoLevel
ReadOnly bool
// Attempts is how many times the whole transaction may run when it fails
// with a serialization failure or deadlock (SQLSTATE 40001 / 40P01),
// which SERIALIZABLE transactions must expect. Values below 1 mean 1.
// Retried functions run again from the start, so they must not have
// effects outside the transaction.
Attempts int
}
TxOptions configures InTxWith.
type URLRefusedError ¶
type URLRefusedError = pgurl.RefusedError
URLRefusedError is how IsolatedConnConfig refuses a URL it cannot make independent of the environment: one net/url and pgx would read differently, or a set PG* TLS file variable the URL does not name. Its message names variables and parameters, never a value or the URL, so it is safe to show.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
argon2cost
Package argon2cost tests, with the real golang.org/x/crypto/argon2, that auth refuses an Argon2id implementation that changes Forge's default cost (ADR 0053).
|
Package argon2cost tests, with the real golang.org/x/crypto/argon2, that auth refuses an Argon2id implementation that changes Forge's default cost (ADR 0053). |
|
integration
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the PostgreSQL driver.
|
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the PostgreSQL driver. |
|
pgurl
Package pgurl makes a PostgreSQL URL independent of the process environment before pgx parses it.
|
Package pgurl makes a PostgreSQL URL independent of the process environment before pgx parses it. |
|
Package postgrestest gives each test its own empty PostgreSQL database.
|
Package postgrestest gives each test its own empty PostgreSQL database. |