planner

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: 9 Imported by: 0

Documentation

Overview

Package planner classifies schema-change statements: for each operation it decides whether PostgreSQL can run it online natively (possibly via a safer idiom it suggests), whether it needs the engine's copy-and-swap path, or whether it is refused. The mapping is the "Needs copy-and-swap?" column of docs/postgres-online-ddl-reference.md, applied conservatively: anything the planner cannot prove safe routes to copy-and-swap or refuse. Classification predicts; executors keep their own protections regardless.

In MySQL terms, the planner is PostgreSQL's missing ALGORITHM= / LOCK= declaration: MySQL lets authors assert the cost bracket (INSTANT/INPLACE/COPY) and the lock impact (NONE/SHARED/EXCLUSIVE) and fails closed; PostgreSQL has no such clause, so the planner proves both dimensions before execution and routes to the safest sequence that exists.

The rules assume PostgreSQL 14, the oldest major the test matrix runs; every rule holds unconditionally across the supported range (14–18). Rules that were version-dependent below that floor — fast default (PG 11+), SET NOT NULL proven by a validated CHECK (PG 12+), DETACH PARTITION CONCURRENTLY (PG 14+) — carry no version annotation because the floor makes them unconditional. A rule that varies within the supported range must carry an explicit version fact before it lands.

Index

Constants

View Source
const RulesPostgresVersions = "14-18"

RulesPostgresVersions is the inclusive PostgreSQL major-version range the classification rules are derived for (see the package comment and docs/postgresql-version-support.md). Offline consumers such as the linter stamp it into their reports so a stored result names the assumptions behind it.

Variables

This section is empty.

Functions

This section is empty.

Types

type Decision

type Decision struct {
	// Operation is the operator-facing label (display only).
	Operation string `json:"operation"`
	// Destructive marks operations that discard live structure — a dropped
	// column, constraint, or index. It is derived from the operation shape
	// here, in the one place every front door shares, so a plan reports the
	// same statement as destructive no matter how it was submitted. It is
	// always emitted, never omitted: a safety flag a consumer gates on must
	// be explicit even when false.
	Destructive bool `json:"destructive"`
	// Route is where the operation goes.
	Route Route `json:"route"`
	// Reason is why.
	Reason Reason `json:"reason"`
	// Unverified marks a decision the planner took without the live facts
	// needed to prove a cheaper one — it failed closed to the heavier
	// route. The route is what the engine would do, not a proven property
	// of the change: with facts (a live introspection or a supplied
	// column type) the same operation may classify as native.
	Unverified bool `json:"unverified,omitempty"`
	// SaferSQL is the ordered safer native sequence, present only for
	// safer-idiom decisions where the planner could construct it. It is a
	// safer form of the submitted statement, not a semantic equivalent: it
	// converges on the same declared end state with different locking,
	// transactionality, and failure modes. SaferSQLExecution carries the
	// execution contract: the steps run one at a time, in order, each in
	// its own implicit transaction — never inside an enclosing transaction
	// block, which the CONCURRENTLY forms refuse. Each sequence constructor
	// documents what a failed step leaves behind and how a retry resumes
	// (a failed CONCURRENTLY build leaves an invalid index the runner must
	// detect via pg_index.indisvalid and rebuild).
	SaferSQL []string `json:"safer_sql,omitempty"`
	// SaferSQLExecution is the typed execution contract for SaferSQL,
	// present exactly when SaferSQL is. Automation branches on it instead
	// of prose — it is what tells a consumer the sequence must not be
	// wrapped in a transaction block.
	SaferSQLExecution Execution `json:"safer_sql_execution,omitempty"`
}

Decision is the classification of one operation.

func (Decision) ExecutableAsSubmitted

func (d Decision) ExecutableAsSubmitted() bool

ExecutableAsSubmitted reports whether the operation's submitted form is itself safe to run. It is false exactly for safer-idiom decisions: their submitted form blocks and must be replaced by the safer sequence — whether or not one was constructed. Routing fails closed on the combination of a false ExecutableAsSubmitted and an empty SaferSQL.

type Execution

type Execution string

Execution is the typed execution contract for a planner-produced SQL sequence; automation branches on it, never on prose. It tells a consumer how the steps must run and that a failed step can leave partial state the runner owns detecting and recovering.

const (
	// ExecutionAutocommit: the steps run one at a time, in order, each in
	// its own implicit transaction — never inside an enclosing transaction
	// block. The CONCURRENTLY forms refuse an enclosing block outright,
	// and a multi-step sequence inside one block holds every earlier
	// step's locks across the steps designed to avoid them. A failed step
	// leaves partial state the runner must detect and recover before
	// retrying (a failed CONCURRENTLY build leaves an invalid index,
	// pg_index.indisvalid = false).
	ExecutionAutocommit Execution = "autocommit-each-step"
)

The execution contracts a sequence can carry.

func Executions

func Executions() []Execution

Executions returns the closed set of Execution values. It is part of the plan-report contract (docs/plan-report.md): the set changes only with a format_version bump, and a consumer that meets an unrecognized value must treat the sequence as unknown and refuse to run it.

type Facts

type Facts struct {
	// ColumnTypes maps a column name to its live type as rendered by
	// PostgreSQL's format_type (e.g. "character varying(50)").
	ColumnTypes map[string]string
}

Facts are properties of the live table that sharpen classification, and they are trusted as stated: the CLI fills them by introspecting the target database, and a library caller may supply facts it already holds — but they must describe the database the change will run on, because a wrong fact can upgrade a rewrite to native. Missing facts are always safe: the zero value is valid and classifies strictly more conservatively (every type change becomes copy-and-swap).

func FactsFrom

func FactsFrom(live schemadiff.Model) Facts

FactsFrom extracts the facts a live introspection model provides: the canonical type of every live column. Every front door — the declarative diff and the imperative dry-run — extracts facts through this one function so equivalent changes classify identically.

type Plan

type Plan struct {
	// Statement is the submitted SQL.
	Statement string `json:"statement"`
	// Route is the aggregate route.
	Route Route `json:"route"`
	// Decisions are the per-operation classifications, in statement order.
	Decisions []Decision `json:"decisions"`
}

Plan is the classification of one statement: one decision per operation and the aggregate route (the worst of its decisions — one rewrite makes the whole statement a copy, one refusal refuses it).

func Classify

func Classify(sql string, facts Facts) (Plan, error)

Classify parses one statement and routes each of its operations. A parse failure is an error; an unrecognized operation is not — it comes back as a refuse decision so the caller can render the whole plan.

type Reason

type Reason string

Reason is the typed cause of a routing decision; automation branches on it, never on prose.

const (
	// ReasonMetadataOnly: a brief ACCESS EXCLUSIVE catalog change, no scan
	// and no rewrite.
	ReasonMetadataOnly Reason = "metadata-only"
	// ReasonOnlineIdiom: already the safe native form (CONCURRENTLY,
	// NOT VALID, VALIDATE, USING INDEX).
	ReasonOnlineIdiom Reason = "online-idiom"
	// ReasonFastDefault: ADD COLUMN with a constant default — the catalog
	// stores the default, no rewrite (PG 11+).
	ReasonFastDefault Reason = "fast-default"
	// ReasonBinaryCoercible: a type change PostgreSQL relabels without a
	// rewrite (widen varchar, varchar to text, widen numeric precision).
	ReasonBinaryCoercible Reason = "binary-coercible"
	// ReasonSaferIdiom: native, but the submitted form blocks; SaferSQL
	// carries the online rewrite when one can be constructed.
	ReasonSaferIdiom Reason = "safer-idiom"
	// ReasonAppBreakingRename: PostgreSQL executes the rename as a brief
	// metadata-only catalog flip, but it cannot land atomically across
	// running application instances — code still referencing the old
	// column or table name starts erroring the instant it commits. For a
	// column the safe sequence is expand/contract: add the new column,
	// dual-write and backfill, switch reads, then drop the old column as
	// its own reviewed change. For a table, coordinate the rename with
	// the application deploy that adopts the new name.
	ReasonAppBreakingRename Reason = "app-breaking-rename"
	// ReasonVolatileDefault: ADD COLUMN whose default the planner cannot
	// prove constant — PostgreSQL rewrites the table.
	ReasonVolatileDefault Reason = "volatile-default"
	// ReasonGeneratedStored: adding a stored generated column computes
	// every row — a full rewrite.
	ReasonGeneratedStored Reason = "generated-stored"
	// ReasonTypeRewrite: a type conversion PostgreSQL cannot relabel —
	// rewrite plus reindex.
	ReasonTypeRewrite Reason = "type-rewrite"
	// ReasonRelocation: SET TABLESPACE moves the heap — a rewrite-scale
	// copy.
	ReasonRelocation Reason = "relocation"
	// ReasonPartitionParentLock: creating a partition takes a brief ACCESS
	// EXCLUSIVE on the partitioned parent — no scan, but it queues behind
	// and then blocks every query on the parent while held.
	ReasonPartitionParentLock Reason = "partition-parent-lock"
	// ReasonUnsupportedOperation: the planner does not recognize the
	// operation or knows no safe path for it.
	ReasonUnsupportedOperation Reason = "unsupported-operation"
)

The reasons a decision can carry.

func Reasons

func Reasons() []Reason

Reasons returns the closed set of Reason values. It is part of the plan-report contract (docs/plan-report.md): the set changes only with a format_version bump, and a consumer that meets an unrecognized value must treat the decision as unknown and refuse it.

type Route

type Route string

Route is where an operation is sent.

const (
	// RouteNative: PostgreSQL runs it online natively — directly or via
	// the safer idiom in Decision.SaferSQL.
	RouteNative Route = "native"
	// RouteCopyAndSwap: needs a table rewrite; only the engine's shadow
	// copy + cutover can do it online.
	RouteCopyAndSwap Route = "copy-and-swap"
	// RouteRefuse: no known safe path; not executed.
	RouteRefuse Route = "refuse"
)

The three routes.

func Routes

func Routes() []Route

Routes returns the closed set of Route values, in severity order. It is part of the plan-report contract (docs/plan-report.md): the set changes only with a format_version bump, and a consumer that meets an unrecognized value must treat the statement as unknown and refuse it.

Jump to

Keyboard shortcuts

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