Documentation
¶
Overview ¶
Package preflight verifies preconditions before the engine writes anything (invariant ST-6). In Phase 1 that is the table-size guard in front of the optimistic attempt: a cancelled rewrite attempt is not a free probe — it holds ACCESS EXCLUSIVE and does real rewrite work for the full statement budget — so above a size threshold the attempt is skipped entirely.
This is a safety-critical core package: see SAFETY.md. It returns proof types with package-private constructors; dangerous downstream APIs accept only the proof.
Index ¶
- Constants
- Variables
- func CheckNamesAbsent(ctx context.Context, pool *pgxpool.Pool, at AbsentTarget, names []string) error
- func CheckPartitionSupport(table PreflightedTable, serverMajor int, execSQL []string) error
- func IsNameOccupied(err error) bool
- type AbsentTarget
- type CopySwapTarget
- type CreationRole
- type OwnedRelationNames
- type PKType
- type PartitionRefusalCause
- type PreflightedTable
- type PrivilegeError
- type PrivilegedRole
- type Requirement
- type SizeError
- type TargetFacts
- type Tier
- type UnsupportedPartitionedParentError
Constants ¶
const NoSizeLimit int64 = math.MaxInt64
NoSizeLimit is a size limit no PostgreSQL relation can exceed. Callers pass it when the check should prove only existence and kind — the online sequence path, whose long steps are safe on any size by design (the size guard protects blind attempts, not planner-proven online idioms).
Variables ¶
var ErrCreateOwnerNotFound = errors.New("create owner role not found")
ErrCreateOwnerNotFound is returned when the named create owner is not a role on the server. The name is caller configuration, so the cause is separated from a missing grant: no GRANT provisions a role that does not exist, and nothing has run.
var ErrNoCreationSchema = errors.New("the session's search_path names no creation schema")
ErrNoCreationSchema is returned when an unqualified check cannot resolve the session's creation schema: the search_path names no usable schema, so there is no schema to verify the absence in. Unlike the other refusals this one is not a database fact — the remedy is entirely on the caller's side: qualify the name, or fix the connection's search_path.
var ErrNotTable = errors.New("not an ordinary or partitioned table")
ErrNotTable is returned when the target exists but is not an ordinary or partitioned table (e.g. a view or foreign table).
var ErrRelationExists = errors.New("a relation already exists at the target name")
ErrRelationExists is returned when the target name is already occupied by a relation of any kind — a table, view, index, or sequence all block a CREATE TABLE at that name the same way.
var ErrSchemaNotFound = errors.New("schema not found")
ErrSchemaNotFound is returned when the schema a create would target does not exist. Creating a table cannot fix a missing schema, so the cause is separated from a free name.
var ErrTableNotFound = errors.New("table not found")
ErrTableNotFound is returned when the target table does not exist (or is not visible with the session's search_path).
var ErrTypeExists = errors.New("a type already exists at the target name")
ErrTypeExists is returned when the target name is already occupied by a standalone type — an enum, domain, range, or shell type. Every table gets a composite type of the same name, so a CREATE TABLE collides with these exactly as it does with a relation.
Functions ¶
func CheckNamesAbsent ¶ added in v0.3.0
func CheckNamesAbsent(ctx context.Context, pool *pgxpool.Pool, at AbsentTarget, names []string) error
CheckNamesAbsent verifies that no relation in the proved target's schema occupies any of names. It reads pg_class in one catalog snapshot and reports the first occupied name in lexical order, regardless of input order — the query's ORDER BY decides, so the caller need not sort. It does not probe pg_type: this check protects index, constraint-index, and sequence names; CheckTableAbsent separately protects the CREATE TABLE name and its composite type. The AbsentTarget binds the check to the resolved, existing schema where those names would land.
func CheckPartitionSupport ¶
func CheckPartitionSupport(table PreflightedTable, serverMajor int, execSQL []string) error
CheckPartitionSupport verifies that the execution steps are safe for the target's relation kind. Ordinary tables and leaf partitions pass unchanged. Supported in-place parent ALTER TABLE operations remain available.
func IsNameOccupied ¶ added in v0.2.0
IsNameOccupied reports whether err means the target name is already held, by a relation or by a standalone type. Both block a CREATE TABLE the same way, so a caller routing "name taken" versus "name free" matches this predicate rather than the two sentinels separately — the sentinels stay distinct for messages, where the difference tells an operator what the obstacle actually is.
Types ¶
type AbsentTarget ¶ added in v0.2.0
type AbsentTarget struct {
// contains filtered or unexported fields
}
AbsentTarget proves the target name is free: the schema exists and no relation or standalone type occupies schema.table. It can only be constructed by CheckTableAbsent in this package. The schema it carries is always resolved — an unqualified check records the session's creation schema, so the proof names the exact schema a create would land in.
The proof is time-of-check and session-scoped: mint it inside the apply, in the same session that will run the CREATE TABLE, and never serialize it or carry it across a plan/apply boundary — an absence verified at plan time proves nothing about apply time. The create path must also re-verify it at the point of use, the way ST-7 does for PreflightedTable: reject a proof whose Table() is empty (the zero value is forgeable by any package), and require the CREATE TABLE statement's schema and table to equal the proof's before executing.
func CheckTableAbsent ¶ added in v0.2.0
func CheckTableAbsent(ctx context.Context, pool *pgxpool.Pool, schema, table string) (AbsentTarget, error)
CheckTableAbsent verifies that schema.table names no existing relation or standalone type, so a CREATE TABLE at that name has nothing to collide with. When schema is empty the session's creation schema (current_schema()) is resolved first — the schema an unqualified CREATE TABLE would land in — and the proof carries it. The facts come from one catalog snapshot read directly from pg_class and pg_type, which are visible regardless of privileges, so a missing grant can never masquerade as absence; whether the role may create in the schema is a separate privilege check, not this fact check. The proof is time-of-check: nothing locks the name, so a concurrent create can still take it before the CREATE TABLE runs — the create path must still treat a duplicate-name error as a collision; the proof turns the common case into a clean refusal, not a guarantee.
This check is NOT the complement of CheckTable for an unqualified name: CheckTable resolves across the whole search_path (to_regclass), while this check resolves current_schema() only — the one schema an unqualified CREATE TABLE lands in. A table in a later search_path schema makes both checks succeed for the same arguments: CheckTable finds it, and this check correctly reports the creation schema free. A caller deciding between create and alter on that pairing would create a new table that shadows the one the user meant — so such a caller must pass an explicit schema, where the two checks share one namespace and are true inverses.
func (AbsentTarget) Schema ¶ added in v0.2.0
func (a AbsentTarget) Schema() string
Schema returns the resolved schema the absence was verified in. A proof minted by CheckTableAbsent always carries one: an unqualified check resolves the session's creation schema before verifying.
func (AbsentTarget) Table ¶ added in v0.2.0
func (a AbsentTarget) Table() string
Table returns the verified-absent table name.
type CopySwapTarget ¶ added in v0.3.2
type CopySwapTarget struct {
// contains filtered or unexported fields
}
CopySwapTarget proves ST-6 prerequisites for a copy-and-swap target. Its zero value is forgeable; consumers must reject it when Table is empty.
func (CopySwapTarget) OwnerRole ¶ added in v0.3.2
func (t CopySwapTarget) OwnerRole() string
OwnerRole returns the catalog-resolved owner of the target table: the role the shadow builder runs SET ROLE to so that the shadow table and its dependents are created owner-correct, and whose SET-usable membership the Tier 3 privilege check proved for the connected role.
func (CopySwapTarget) PKColumn ¶ added in v0.3.2
func (t CopySwapTarget) PKColumn() string
PKColumn returns the primary-key column.
func (CopySwapTarget) PKType ¶ added in v0.3.2
func (t CopySwapTarget) PKType() PKType
PKType returns the primary-key type.
func (CopySwapTarget) Schema ¶ added in v0.3.2
func (t CopySwapTarget) Schema() string
Schema returns the target schema.
func (CopySwapTarget) Table ¶ added in v0.3.2
func (t CopySwapTarget) Table() string
Table returns the target table.
type CreationRole ¶ added in v0.2.0
type CreationRole struct {
// contains filtered or unexported fields
}
CreationRole proves the connected role holds every access a greenfield CREATE TABLE in the schema needs: CONNECT on the database, USAGE and CREATE on the schema for the role the table will be owned by, and — when a create owner other than the connected role is named — membership that lets the session SET ROLE to it. It can only be constructed by CheckCreatePrivileges or CheckCreatePrivilegesAs in this package. The schema it carries is always resolved — an unqualified check records the session's creation schema.
Like AbsentTarget, the proof is time-of-check and session-scoped: a grant can be revoked between the check and the CREATE TABLE, in which case the create fails with the server's own insufficient-privilege error rather than a typed refusal.
func CheckCreatePrivileges ¶ added in v0.2.0
func CheckCreatePrivileges(ctx context.Context, pool *pgxpool.Pool, schema string) (CreationRole, error)
CheckCreatePrivileges verifies the connected role can create a table in the schema (the session's creation schema, current_schema(), when schema is empty): CONNECT on the database, USAGE and CREATE on the schema. A missing grant is a *PrivilegeError naming the exact statement that would satisfy it — the grantee is the connected role itself, because a table that does not exist yet has no owning role to inherit from. On success it returns the CreationRole proof.
func CheckCreatePrivilegesAs ¶ added in v0.3.3
func CheckCreatePrivilegesAs(ctx context.Context, pool *pgxpool.Pool, schema, owner string) (CreationRole, error)
CheckCreatePrivilegesAs verifies creation access for owner. When owner is empty, creation remains under the connected role. Otherwise the connected role must be able to SET ROLE to owner, and owner must hold schema access.
SET ROLE consults membership, not inheritance: pg_has_role(..., 'MEMBER') is the predicate on every supported version, and PostgreSQL 16 adds the SET membership option as a second, narrower gate. A non-inheriting member can assume the owner and is admitted; the USAGE mode — whether the owner's privileges are already available without SET ROLE — is not what the mechanism needs, and a refusal keyed on it could not be cleared by the GRANT it prints.
func (CreationRole) Owner ¶ added in v0.3.3
func (c CreationRole) Owner() string
Owner returns the role a created table will be owned by: the named create owner when one was checked, otherwise the connected role.
func (CreationRole) Role ¶ added in v0.2.0
func (c CreationRole) Role() string
Role returns the connected role the checks ran as — the identity whose CONNECT was proved and the grantee of any membership the create needs.
func (CreationRole) Schema ¶ added in v0.2.0
func (c CreationRole) Schema() string
Schema returns the resolved schema the access was verified in.
func (CreationRole) SetsRole ¶ added in v0.3.3
func (c CreationRole) SetsRole() bool
SetsRole reports whether create steps must SET LOCAL ROLE to Owner because it differs from the connected role.
type OwnedRelationNames ¶ added in v0.3.1
OwnedRelationNames contains the relation names a table owns: the indexes backing its own primary-key, unique, and exclusion constraints, and the sequences owned by its columns. Each list is sorted and duplicate-free. A name is present whether the server invented it or the desired file stated it — a named constraint's index and an ALTER SEQUENCE ... OWNED BY sequence are owned just the same.
func LookupOwnedRelationNames ¶ added in v0.3.1
func LookupOwnedRelationNames(ctx context.Context, pool *pgxpool.Pool, schema, table string) (OwnedRelationNames, error)
LookupOwnedRelationNames reads the relation names owned by an ordinary or partitioned table so a caller can verify them after a create step. Schema must be explicit because the caller must inspect the exact schema covered by its earlier absence proof rather than resolve a possibly different search_path target. The read excludes standalone indexes, whose own create steps report duplicate-name SQLSTATEs; the table itself, whose name is covered by the caller's absence proof; and the indexes a foreign key borrows from its referenced table, which belong to that table.
type PKType ¶ added in v0.3.2
type PKType string
PKType identifies a supported single-column integer primary-key type.
type PartitionRefusalCause ¶
type PartitionRefusalCause string
PartitionRefusalCause identifies which unsupported shape triggered a partitioned-parent refusal. The zero value means the steps are supported.
const ( // PartitionCauseConcurrentIndexBuild means a step attempts a concurrent // index build on the parent. PartitionCauseConcurrentIndexBuild PartitionRefusalCause = "parent-concurrent-index-build" // PartitionCauseBlockingIndexBuild means a step would build an index on // the parent while holding ACCESS EXCLUSIVE. PartitionCauseBlockingIndexBuild PartitionRefusalCause = "parent-blocking-index-build" // PartitionCauseIndexAdoption means a step adopts an existing index as a // primary-key or unique constraint on the parent. PartitionCauseIndexAdoption PartitionRefusalCause = "parent-index-adoption" // PartitionCauseNotValidForeignKey means a step adds a NOT VALID // foreign key, which the server version cannot do on a partitioned // table. PartitionCauseNotValidForeignKey PartitionRefusalCause = "parent-not-valid-foreign-key" )
func PartitionRefusalCauses ¶ added in v0.3.2
func PartitionRefusalCauses() []PartitionRefusalCause
PartitionRefusalCauses returns the closed set of partitioned-parent refusal causes, so documentation and consumers can enumerate them instead of maintaining their own list.
func RefusesPartitionedParent ¶
func RefusesPartitionedParent(serverMajor int, execSQL []string) (PartitionRefusalCause, error)
RefusesPartitionedParent reports the cause that makes steps unsupported on a partitioned parent, or the zero value when they are supported. It is the shared static policy used by preflight, plan reporting, and executor admission.
type PreflightedTable ¶
type PreflightedTable struct {
// contains filtered or unexported fields
}
PreflightedTable proves the target table exists, is a table, and is under the size threshold for an optimistic attempt. It can only be constructed by CheckTable in this package.
func CheckTable ¶
func CheckTable(ctx context.Context, pool *pgxpool.Pool, schema, table string, limitBytes int64) (PreflightedTable, error)
CheckTable verifies that schema.table (search_path when schema is empty) exists, is an ordinary or partitioned table, and is at most limitBytes on disk. Above the limit it returns a *SizeError; on success it returns the PreflightedTable proof.
The unqualified lookup is search_path-wide (to_regclass), so success does not mean the name is occupied in the session's creation schema: a table in a later search_path schema satisfies this check while CheckTableAbsent — which resolves current_schema() only — still proves the creation schema free for the same name. The two checks are inverses only when the caller passes an explicit schema.
func (PreflightedTable) Partitioned ¶
func (t PreflightedTable) Partitioned() bool
Partitioned reports whether the verified target is a partitioned parent. Leaf partitions have relkind 'r' and therefore report false.
func (PreflightedTable) RelTuples ¶
func (t PreflightedTable) RelTuples() float64
RelTuples returns the planner's row estimate (-1 when the table has never been vacuumed or analyzed). Reporting only — the size guard's authority is bytes on disk.
func (PreflightedTable) Schema ¶
func (t PreflightedTable) Schema() string
Schema returns the schema qualification the check ran with (empty when the lookup used the session search_path).
func (PreflightedTable) Table ¶
func (t PreflightedTable) Table() string
Table returns the verified table name.
func (PreflightedTable) TotalBytes ¶
func (t PreflightedTable) TotalBytes() int64
TotalBytes returns the measured on-disk size across all partitions, including indexes and TOAST.
type PrivilegeError ¶
type PrivilegeError struct {
// Tier is the access level the failed check belongs to.
Tier Tier
// Check is the catalog predicate that returned false. It is display
// prose: identifiers appear unquoted, so a renderer embedding it in
// structured output (a markdown table, a PR comment) owns escaping it.
Check string
// Grant is the exact statement that would satisfy the check. Every
// identifier in it is Sanitize()-quoted, so it is safe to echo
// verbatim as executable SQL.
Grant string
// Hint explains the remediation when the Grant alone would surprise
// the operator — for example when its grantee differs from the role
// the Check names. Empty when the Grant speaks for itself.
Hint string
}
PrivilegeError reports a failed access check: the tier that needs it, the catalog check that returned false, and the exact statement that would satisfy it. It is a refusal input, not an operational failure — the same fail-closed posture as every other refusal in the engine. Provisioning rationale lives in docs/engine-role.md.
func (*PrivilegeError) Error ¶
func (e *PrivilegeError) Error() string
Error implements the error interface.
type PrivilegedRole ¶
type PrivilegedRole struct {
// contains filtered or unexported fields
}
PrivilegedRole proves the connected role holds every access the requirement's tier needs against the target table. It can only be constructed by CheckPrivileges in this package. The owning role it carries is the catalog-resolved owner the copy-and-swap path will SET ROLE to for shadow objects.
func CheckPrivileges ¶
func CheckPrivileges(ctx context.Context, pool *pgxpool.Pool, schema, table string, req Requirement) (PrivilegedRole, error)
CheckPrivileges verifies the connected role holds the access the requirement needs against schema.table (search_path resolution when schema is empty), per the tiered contract in docs/engine-role.md. A missing requirement is a *PrivilegeError naming the exact statement that would satisfy it; on success it returns the PrivilegedRole proof.
It runs before CheckTable in the preflight order: a role that cannot see the target would otherwise report "table not found" and mask the real cause.
func (PrivilegedRole) Owner ¶
func (p PrivilegedRole) Owner() string
Owner returns the target table's owning role from the catalog.
func (PrivilegedRole) Role ¶
func (p PrivilegedRole) Role() string
Role returns the connected role the checks ran as.
func (PrivilegedRole) Tier ¶
func (p PrivilegedRole) Tier() Tier
Tier returns the tier the role was verified at.
type Requirement ¶
type Requirement struct {
Tier Tier
// LogicalDecoding requires replication access on top of the tier:
// rds_replication membership where that role exists (Aurora/RDS), the
// REPLICATION role attribute otherwise. Only valid with
// TierCopyAndSwap — no other strategy decodes WAL.
LogicalDecoding bool
}
Requirement states the access a schema change's plan needs: the tier, and whether the strategy decodes WAL (logical-decoding CDC, copy-and-swap only), which additionally requires replication access.
type SizeError ¶
type SizeError struct {
// TotalBytes is the table's measured on-disk size (all partitions,
// including indexes and TOAST).
TotalBytes int64
// LimitBytes is the threshold that was exceeded.
LimitBytes int64
}
SizeError reports that the table exceeds the configured size threshold, so the optimistic attempt must be skipped. It is a refusal input, not an operational failure.
type TargetFacts ¶
type TargetFacts struct {
// contains filtered or unexported fields
}
TargetFacts are the cheap target facts needed by planning and executor admission without measuring the relation or its partition tree.
func LookupTargetFacts ¶
func LookupTargetFacts(ctx context.Context, pool *pgxpool.Pool, schema, table string) (TargetFacts, error)
LookupTargetFacts verifies that the target is an ordinary or partitioned table and returns its relation kind and server major in one catalog query.
func (TargetFacts) Partitioned ¶
func (f TargetFacts) Partitioned() bool
Partitioned reports whether the target is a partitioned parent.
func (TargetFacts) ServerMajor ¶
func (f TargetFacts) ServerMajor() int
ServerMajor returns the PostgreSQL server major version.
type Tier ¶
type Tier int
Tier is the access level a schema change's plan requires from the engine role, per the tiered contract in docs/engine-role.md. Each tier includes everything below it; a change is admitted at the tier its plan requires and nothing higher.
const ( // TierConnect covers connecting and resolving the target: CONNECT on // the database and USAGE on the target schema. The CONNECT rung // documents the contract rather than catching live failures — a role // missing it fails at connection time, before any check runs — while // the USAGE rung is load-bearing: without it a qualified target // masquerades as "table not found". TierConnect Tier = iota // TierAlterInPlace covers owner-gated in-place ALTER TABLE (the // instant and fast native paths): inheritable membership in the // owning role. TierAlterInPlace // TierIndexBuild covers CREATE INDEX [CONCURRENTLY]: CREATE on the // target schema on top of owning-role membership. TierIndexBuild // TierCopyAndSwap covers shadow-object creation: membership usable // with SET ROLE, so shadow objects are born with the correct owner. TierCopyAndSwap // TierCreateTable covers greenfield CREATE TABLE and sits off the // ladder above: a table that does not exist yet has no owner to be a // member of, so the create path proves CONNECT on the database plus // USAGE and CREATE on the schema — deliberately not the ownership // membership the ALTER tiers require. It is checked by // CheckCreatePrivileges, never by CheckPrivileges, whose ladder walks // facts about an existing table. TierCreateTable )
The contract's tiers, lowest to highest.
func RequiredTier ¶
RequiredTier derives the engine-role tier a routed change's exec SQL needs: an in-place ALTER TABLE step is owner-gated (TierAlterInPlace), and any step that builds a new index — every CREATE INDEX, and the ALTER TABLE shapes that build one as a side effect — additionally needs CREATE on the schema (TierIndexBuild), so the requirement is the most demanding step's tier — the ladder check covers every rung below it. The mapping from step shape to required access lives here, next to Tier and CheckPrivileges, so every consumer of the routed plan derives the same answer. A step shape the engine does not execute fails closed here, before anything runs.
A CREATE TABLE step derives the off-ladder TierCreateTable — the greenfield create plan's shape: one CREATE TABLE plus CREATE INDEX steps on the table it creates, checked by CheckCreatePrivileges, never by CheckPrivileges, whose ladder states facts about an existing table. A set that mixes CREATE TABLE with ALTER TABLE fails closed: the off-ladder tier proves creation access only and cannot vouch for the ladder rungs an alter on an existing table needs — and no front door produces such a set.
type UnsupportedPartitionedParentError ¶
type UnsupportedPartitionedParentError struct {
// Cause is the unsupported shape that triggered the refusal.
Cause PartitionRefusalCause
}
UnsupportedPartitionedParentError reports that an execution plan contains a step pg-sprite cannot safely run on a partitioned parent. Its rendered message is a fixed English sentence with no interpolated identifiers or server text, so orchestrator-facing surfaces may render it verbatim. This is a deliberate property to preserve.
func (*UnsupportedPartitionedParentError) Error ¶
func (e *UnsupportedPartitionedParentError) Error() string
Error implements the error interface.