preflight

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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

View Source
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

View Source
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).

View Source
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).

Functions

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.

Types

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 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.

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.

func (*SizeError) Error

func (e *SizeError) Error() string

Error implements the error interface.

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
)

The contract's tiers, lowest to highest.

func RequiredTier

func RequiredTier(execSQL []string) (Tier, error)

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.

func (Tier) String

func (t Tier) String() string

String names the tier's capability for refusal messages.

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

Error implements the error interface.

Jump to

Keyboard shortcuts

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