plan

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package plan defines the machine-readable dry-run plan report: the stable JSON contract an operator or orchestrator consumes to decide whether and how a change would execute. Both front doors emit it — the imperative migrate --dry-run path and the declarative diff path — so a consumer parses one shape regardless of how the plan was derived.

Index

Constants

View Source
const FormatVersion = 5

FormatVersion identifies the report contract. A consumer must reject a report whose version it does not understand instead of guessing at the field semantics. Version 2 added the guidance field on rewrite-required statements. Version 3 added the cause field on greenfield statements the create path refuses by shape. Version 4 added the class and owner fields on the report and on refused statements, and closed their vocabularies. Version 5 added blocking_passthrough_eligible to refused statements.

Variables

This section is empty.

Functions

func CreateShapeRefusal added in v0.3.3

func CreateShapeRefusal(cause executor.CreateShapeCause) (verdict.Refusal, bool)

CreateShapeRefusal classifies the create path's shape refusal of a statement in a greenfield plan. The closed key set is executor.CreateShapeCauses(); ok is false for a cause outside it, which callers treat as an invariant violation rather than classify.

func DiscloseGreenfieldExecution added in v0.3.0

func DiscloseGreenfieldExecution(report *Report)

DiscloseGreenfieldExecution makes executable statements describe the plain, bounded builds used for a table born in this run. The report must describe a table that does not exist yet; the function returns without mutation unless the report establishes that precondition. Each create step commits in its own transaction under the brief lock_timeout and statement_timeout budget, so CREATE TABLE is visible before its indexes build and a concurrent writer that already knows the name makes the step fail fast rather than block. The build is classified metadata-only because its cost is bounded by that budget on a table born in the run. Reclassifying keeps the statement in a state router.Route can produce: an execute disposition never carries a safer-idiom decision without its rewrite.

func Fingerprint

func Fingerprint(statements []Statement) string

Fingerprint computes the plan's stable identity: "sha256:" plus the hex digest over what would execute — each statement's canonical SQL, route, backend, disposition, and exec_sql, in plan order. Explanatory fields (decisions, kind, destructive, reason, cause, guidance) are excluded, so a reworded reason does not change identity but a rerouted or resequenced plan does. The exact serialization is part of the contract (docs/plan-report.md) and changes only with a format_version bump. This is a plan identity, not a schema fingerprint: it never participates in schema-state comparison.

func PartitionRefusal added in v0.3.3

func PartitionRefusal(cause preflight.PartitionRefusalCause) (verdict.Refusal, bool)

PartitionRefusal classifies a partitioned-parent refusal by its cause. The closed key set is preflight.PartitionRefusalCauses(); ok is false outside it. The four causes span three classes, which is why the plan carries the cause rather than a bare refused flag. The refusal keeps its typed cause, so every consumer of the proof — both front doors and the accepted-blocking eligibility registry — reads one narrowing instead of re-deriving it from the preflight error.

func RefuseUnsupportedCreateShape added in v0.3.0

func RefuseUnsupportedCreateShape(report *Report, refused []error) error

RefuseUnsupportedCreateShape marks the create-path statements whose connection-free shape checks refuse them and stamps each one with the executor's typed cause. refused is positional over report.Statements — one entry per planned statement, nil where the statement is admitted. A length mismatch means the two sides no longer agree on what the plan contains, so no positional marking is safe and the report is left untouched. A refusal the cause vocabulary does not name is a contract violation: the report would carry a refusal it cannot explain, so it fails closed before any statement is marked.

func RefuseUnsupportedPartitionedParent

func RefuseUnsupportedPartitionedParent(report *Report, causes []preflight.PartitionRefusalCause) error

RefuseUnsupportedPartitionedParent marks statements as refused when partition-aware admission rejects their execution steps. causes is positional over report.Statements — one entry per planned statement, empty where the statement is admitted — because the class of the refusal is a property of the cause (docs/refusal-classes.md), not of the reason. A length mismatch, or a cause the classification registry does not name, is a contract violation: the report would carry a refusal it cannot explain, so it fails closed before any statement is marked.

func RouteRefusal added in v0.3.3

func RouteRefusal() verdict.Refusal

RouteRefusal classifies a routed statement the planner has no safe path for: an admitted ALTER TABLE operation with no route. The key is the site.

Types

type Report

type Report struct {
	// FormatVersion is the report contract version; always FormatVersion.
	FormatVersion int `json:"format_version"`
	// Source is the front door that derived the plan.
	Source Source `json:"source"`
	// Schema is the target schema; empty when the submitted statement did
	// not qualify one.
	Schema string `json:"schema,omitempty"`
	// Table is the target table; empty when the statement has no single
	// table target (index maintenance).
	Table string `json:"table,omitempty"`
	// ServerVersion is the PostgreSQL server_version the plan was derived
	// against. Classification is version-sensitive, so a stored or
	// forwarded report names the server whose rules produced it; empty
	// only for sources that never connected.
	ServerVersion string `json:"server_version,omitempty"`
	// TableExists reports whether the live table was found. Set by every
	// source that introspects the target (diff, and the alter dry run);
	// nil means the plan has no single table target to introspect. For
	// diff, false means the statements are the full desired schema; for
	// an alter dry run, false means the plan was classified from zero
	// facts and executing it would fail.
	TableExists *bool `json:"table_exists,omitempty"`
	// Disposition is the aggregate disposition across all statements:
	// what would happen if the engine executed this plan now.
	Disposition router.Disposition `json:"disposition"`
	// Reason is the typed refusal cause when target facts make an otherwise
	// executable routed plan unsafe.
	Reason verdict.Reason `json:"reason,omitempty"`
	// Class identifies how a consumer routes an aggregate refusal.
	Class verdict.Class `json:"class,omitempty"`
	// Owner identifies who owns aggregate work with no online-safety problem.
	Owner verdict.Owner `json:"owner,omitempty"`
	// Fingerprint is the plan's stable identity (see Fingerprint). An
	// approver pins it when the plan is reviewed; an executor recomputes it
	// at apply time and refuses on mismatch — that is how "the plan a
	// reviewer approves is the plan that executes" is enforced across
	// storage and forwarding.
	Fingerprint string `json:"fingerprint"`
	// Statements is the ordered plan; empty means there is nothing to do.
	Statements []Statement `json:"statements"`
}

Report is the dry-run plan for one change against one table.

func NewReport

func NewReport(source Source) Report

NewReport returns an empty report for source with the contract version stamped and Statements non-nil, so an empty plan serializes as [] rather than null.

type Source

type Source string

Source identifies which front door derived the plan.

const (
	// SourceAlter marks a plan derived from a submitted DDL statement
	// (migrate --alter --dry-run): the classify-and-route pipeline with
	// the diff step skipped.
	SourceAlter Source = "alter"
	// SourceDiff marks a plan derived from a desired-state schema diff
	// (diff --desired): the ordered statements that converge the live
	// table on the desired schema.
	SourceDiff Source = "diff"
)

func Sources

func Sources() []Source

Sources returns the closed set of Source 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 report as unknown and refuse it.

type Statement

type Statement struct {
	// SQL is the statement in the engine's canonical rendering: parsed and
	// reprinted through the PostgreSQL deparser, whichever front door
	// derived it. It is never a verbatim echo of the submitted text, so
	// the same change carries the same string through either door.
	SQL string `json:"sql"`
	// Kind classifies a diff-derived statement so a consumer can gate
	// whole classes of change (see schemadiff.ChangeKind). Empty for the
	// alter source: a submitted statement may carry several operations and
	// has no single kind.
	Kind schemadiff.ChangeKind `json:"kind,omitempty"`
	// Destructive marks statements that discard live structure — a dropped
	// column, constraint, index, or NOT NULL. It is derived from the classifier's
	// decisions, so both sources report it identically; it is always
	// emitted, never omitted, because a safety flag a consumer gates on
	// must be explicit even when false.
	Destructive bool `json:"destructive"`
	// Route is the planner's aggregate route for the statement.
	Route planner.Route `json:"route"`
	// Backend is the assigned execution strategy; empty for refusals.
	Backend router.Backend `json:"backend,omitempty"`
	// Disposition is what execution would do with the statement now.
	Disposition router.Disposition `json:"disposition"`
	// Reason is the typed cause when target facts refuse this statement.
	Reason verdict.Reason `json:"reason,omitempty"`
	// Class identifies how a consumer routes a refused statement.
	Class verdict.Class `json:"class,omitempty"`
	// Owner identifies who owns work with no online-safety problem.
	Owner verdict.Owner `json:"owner,omitempty"`
	// BlockingPassthroughEligible reports whether an operator may request the
	// bounded blocking path for this refusal. Present exactly on refusals.
	BlockingPassthroughEligible *bool `json:"blocking_passthrough_eligible,omitempty"`
	// Cause is the create path's typed shape refusal for a statement of a
	// greenfield plan (executor.CreateShapeCause): why a table born in the
	// run cannot carry this statement. Present exactly when the create path
	// refused the statement by shape — on a diff-source plan whose table
	// does not exist, that is every statement with Disposition refuse and
	// Reason unsupported-statement. Absent for every other refusal,
	// including an alter-source refusal against an absent table. Stored
	// rather than derived because a JSON consumer has no desired schema to
	// recompute the shape check from; it is the executor's own explanation,
	// so a renderer prints Description() instead. Explanatory, so it is
	// excluded from the fingerprint.
	Cause executor.CreateShapeCause `json:"cause,omitempty"`
	// Decisions are the planner's per-operation classifications.
	Decisions []planner.Decision `json:"decisions"`
	// ExecSQL is the ordered SQL the native backend would run — the safer
	// sequence when the planner constructed one. Empty for non-native
	// routes.
	ExecSQL []string `json:"exec_sql,omitempty"`
	// Execution is the typed execution contract for ExecSQL
	// (planner.Execution), present exactly when ExecSQL is. A consumer
	// that runs the statements itself branches on it — it is what says
	// each step runs in its own implicit transaction, never inside an
	// enclosing transaction block. It is derived from ExecSQL's presence,
	// so it is excluded from the fingerprint like the other explanatory
	// fields.
	Execution planner.Execution `json:"execution,omitempty"`
	// Guidance is the typed manual path for a rewrite-required refusal,
	// drawn from the suggest contract's Guidance vocabulary
	// (docs/suggest-report.md): the engine will not run the statement, and
	// this names what to do instead. Present exactly when Disposition is
	// rewrite-required. Explanatory, so it is excluded from the
	// fingerprint.
	Guidance suggest.Guidance `json:"guidance,omitempty"`
}

Statement is one planned statement: the SQL, its classification, and what execution would do with it now.

func FromRouted

func FromRouted(rs router.Statement) (Statement, error)

FromRouted converts one routed statement into a plan statement. Destructive is derived from the classifier's decisions — one destructive operation makes the statement destructive — so every source that routes through the planner reports it identically by construction. A rewrite-required statement additionally carries the typed manual path (Guidance), derived through the same mapping the suggest report uses; a rewrite-required decision with no known guidance is a contract violation and fails closed.

Jump to

Keyboard shortcuts

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