Documentation
¶
Overview ¶
Package schemadiff builds the canonical table model both front-ends share and diffs two models into an ordered statement list. The model always comes from a real PostgreSQL catalog — the live table is introspected directly, and a desired-state file is executed on a transaction-scoped scratch schema and introspected the same way, then the transaction is rolled back (execute-and-introspect; semantics are never derived from the AST). Canonical text (types, defaults, constraint and index definitions) is whatever the server's own decompilers print, so cosmetic differences (type aliases, default formatting, implicit names) never show up as diffs.
This is a periphery package (see SAFETY.md): its output is a plan request, and the core executors re-verify their own preconditions.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrDifferentTables = errors.New("models describe different tables")
ErrDifferentTables is returned when the two models describe different tables — a caller bug, refused rather than diffed.
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 ErrTableNotFound = errors.New("table not found")
ErrTableNotFound is returned when the target table does not exist in the requested schema.
var ErrUnrenderableCollation = errors.New("columns with an explicit collation cannot be rendered as a desired schema")
ErrUnrenderableCollation is returned when a column carries an explicit collation. The declarative model does not manage collations, so a rendered baseline without the COLLATE clause would silently change sort order and index semantics — the renderer refuses instead.
var ErrUnrenderableDefault = errors.New("sequence-backed default cannot be rendered as a desired schema")
ErrUnrenderableDefault is returned when a column carries a sequence-backed default that is not the canonical serial form. A desired schema file cannot define a standalone sequence, so the only sequence-backed default a rendered file can reproduce is the serial shorthand (an owned sequence named <table>_<column>_seq on a NOT NULL integer column). Anything else — a shared sequence, a renamed or truncated sequence name, a nullable column — must be resolved by hand.
var ErrUnrenderableForeignKey = errors.New("tables referenced by foreign keys cannot be rendered as a desired schema")
ErrUnrenderableForeignKey is returned when other tables reference this one with foreign keys. A desired file cannot declare foreign keys, so the single-table model carries no incoming foreign-key topology — a rendered baseline would look complete while silently dropping the table's relationships. The renderer refuses instead. Outgoing foreign keys are refused separately by the desired-file grammar (statement.ErrForeignKey).
var ErrUnrenderableInheritance = errors.New("table inheritance cannot be rendered as a desired schema")
ErrUnrenderableInheritance is returned for either side of a classic PostgreSQL inheritance relationship. The desired model cannot express inheritance edges, so rendering would flatten a child or omit its children.
var ErrUnrenderablePartition = errors.New("partitioned tables cannot be rendered as a desired schema")
ErrUnrenderablePartition is returned for a partitioned parent or a partition. The model does not carry partition bounds or the parent/partition topology, so a rendered file would silently lose the PARTITION BY clause or the partition attachment — the renderer refuses instead of emitting a wrong baseline.
var ErrUnrenderableUnlogged = errors.New("unlogged tables cannot be rendered as a desired schema")
ErrUnrenderableUnlogged is returned for an unlogged table. The declarative model does not manage persistence, so a rendered plain CREATE TABLE would silently change the table's crash-safety and replication behavior — the renderer refuses instead.
var ErrUnsupportedChange = errors.New("unsupported schema change")
ErrUnsupportedChange is returned when converging live onto desired would need a change the engine does not derive: identity, generation, or collation changes on an existing column, a persistence (unlogged) difference, or a partitioning difference (partition key or partition attachment) — table identity that no ALTER converges. The caller surfaces it; nothing is guessed.
Functions ¶
func ListManagedTables ¶ added in v0.3.2
ListManagedTables returns, sorted by name, the tables of a live PostgreSQL schema that a declarative schema directory is expected to account for: the set pull enumerates before it exports each one. Ordinary tables and partitioned parents are listed, INHERITS children among them; partitions are represented by their parent's PARTITION BY and extension members belong to their extension, so neither is a table an owner declares. Views, materialized views, foreign tables, and sequences are outside the declarative model and are not listed. The listing is the candidate set, not the set of files pull writes: a listed table whose shape Render refuses is still undeclared and still the owner's to resolve, and pull reports the refusal by table.
Every catalog relation, operator, and type in the query is pg_catalog qualified (CO-9), so a search_path that lists a user schema ahead of pg_catalog cannot change the answer: an unqualified relation or operator would join nothing and list no tables, while an unqualified regclass cast would stop matching the extension dependency and list extension members as undeclared. Both are silent wrong answers, and each blocks or waves through a table nobody touched.
func Render ¶ added in v0.2.0
Render renders the canonical model into a desired-state schema file: one CREATE TABLE followed by the model's CREATE INDEX statements. The output is proven admissible by parsing it through statement.ParseDesired before it is returned, so anything a desired file refuses (a foreign key, for example) surfaces here as that gate's typed error. Materializing the output with IntrospectDesired reproduces the model's names and definitions, so diffing it against the table it came from yields no changes — the round-trip contract the integration tests enforce. Validity is not rendered: an index the table carries invalid — an unfinished concurrent build — renders as its definition and materializes valid, so the round-trip diff of such a table is exactly the create-index change that rebuilds it.
Types ¶
type Change ¶
type Change struct {
// SQL is the literal statement, without a trailing semicolon.
SQL string `json:"sql"`
// Kind classifies the statement for consumers that gate by class.
Kind ChangeKind `json:"kind"`
// Destructive marks statements that discard data, constraints, or
// indexes (column, constraint, and index drops — dropping a unique
// index discards the same guarantee as dropping a unique constraint).
// Destructive changes are gated by the caller, never executed
// silently.
Destructive bool `json:"destructive,omitempty"`
}
Change is one derived statement of the ordered plan.
func Diff ¶
Diff derives the ordered statement list that converges live onto desired. Order is dependency-correct: drops first (indexes, then constraints, then columns), then column adds and alters, then constraint adds, then index creates — so an added column exists before an index or constraint that references it. Within each bucket the order is deterministic: attribute order for columns, name order for constraints and indexes. schema qualifies the emitted statements' table references. Columns are compared by name only: attribute order carries no semantics in PostgreSQL and is deliberately out of scope for convergence.
type ChangeKind ¶
type ChangeKind string
ChangeKind classifies a derived statement so a consumer can gate whole classes of change (destructive, rewriting, index-building) without parsing SQL.
const ( // ChangeCreateTable creates the table (missing-table plans only). ChangeCreateTable ChangeKind = "create-table" // ChangeDropIndex drops an index. ChangeDropIndex ChangeKind = "drop-index" // ChangeDropConstraint drops a table constraint. ChangeDropConstraint ChangeKind = "drop-constraint" // ChangeDropColumn drops a column. ChangeDropColumn ChangeKind = "drop-column" // ChangeAddColumn adds a column. ChangeAddColumn ChangeKind = "add-column" // ChangeAlterType changes a column's type. ChangeAlterType ChangeKind = "alter-type" // ChangeSetDefault sets or replaces a column default. ChangeSetDefault ChangeKind = "set-default" // ChangeDropDefault drops a column default. ChangeDropDefault ChangeKind = "drop-default" // ChangeSetNotNull adds the NOT NULL attribute. ChangeSetNotNull ChangeKind = "set-not-null" // ChangeDropNotNull removes the NOT NULL attribute. ChangeDropNotNull ChangeKind = "drop-not-null" // ChangeAddConstraint adds a table constraint. ChangeAddConstraint ChangeKind = "add-constraint" // ChangeCreateIndex creates an index. ChangeCreateIndex ChangeKind = "create-index" )
The change kinds a plan can contain.
func ChangeKinds ¶
func ChangeKinds() []ChangeKind
ChangeKinds returns the closed set of ChangeKind 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 statement as unknown and refuse it.
type Column ¶
type Column struct {
// Name is the column name.
Name string
// Type is the canonical type text (format_type), e.g. "character
// varying(50)" — never an alias like varchar(50).
Type string
// NotNull reports the NOT NULL attribute.
NotNull bool
// Default is the canonical default expression (pg_get_expr), empty when
// none. For a generated column it is the generation expression.
Default string
// SequenceDefault reports that the default expression depends on a
// sequence (per pg_depend) — a serial column or a hand-written nextval
// default. In a desired-state model that sequence exists only inside
// the rolled-back scratch transaction, so no derived plan can
// reference it.
SequenceDefault bool
// SequenceOwned reports that the sequence behind the default is owned
// by this exact column (a pg_depend OWNED BY edge, deptype 'a') — what
// the serial shorthand produces. It is false for a hand-written
// nextval default on a standalone sequence or another table's
// sequence, however the sequence is named: sharing distinguishes it
// from ownership, not naming.
SequenceOwned bool
// Collation is the column's explicit collation as a schema-qualified,
// quote_ident-quoted name, empty when the column uses its type's
// default collation. The declarative model does not manage collations yet, so
// the model carries it only to refuse: dropping a COLLATE clause from
// a rendered baseline would silently change sort order and index
// semantics, and a collation delta cannot be converged without a
// table rewrite.
Collation string
// Identity is the identity kind, IdentityNone for plain columns.
Identity Identity
// Generated reports GENERATED ALWAYS AS (...) STORED.
Generated bool
}
Column is one column of the canonical model.
type Constraint ¶
type Constraint struct {
// Name is the constraint name.
Name string
// Def is the canonical definition text.
Def string
}
Constraint is one table constraint: its name plus the server-decompiled definition (pg_get_constraintdef), e.g. "PRIMARY KEY (id)".
type Identity ¶
type Identity string
Identity is a column's identity kind, as pg_attribute.attidentity spells it.
type Index ¶
type Index struct {
// Name is the index name.
Name string
// Def is the canonical CREATE INDEX statement.
Def string
// Invalid reports pg_index.indisvalid = false: the entry carries the
// name and the definition but the planner never uses it, so it does not
// deliver the desired index. On a plain table it is a concurrent build
// that has not finished — abandoned, or still running. On a partitioned
// parent it means not every partition has a matching attached index;
// the server never builds a partitioned index concurrently. A
// desired-side index, materialized on the scratch schema, is never
// invalid, so the zero value is the delivered state.
Invalid bool
}
Index is one non-constraint index: its name plus the server-decompiled CREATE INDEX statement (pg_get_indexdef), unqualified under the introspection search_path, and whether the entry is usable.
type Model ¶
type Model struct {
// Table is the unqualified table name.
Table string
// PartitionKey is the server-decompiled partition key definition
// (pg_get_partkeydef), e.g. "RANGE (created_at)" — empty for a
// non-partitioned table.
PartitionKey string
// IsPartition reports that the table is itself a partition of a
// partitioned parent (pg_class.relispartition).
IsPartition bool
// InheritsParents lists the classic-inheritance parents of this table.
// Declarative partitions use the same pg_inherits catalog but are
// excluded using pg_class.relispartition.
InheritsParents []string
// InheritanceChildren lists the classic-inheritance children of this
// table. The model carries both directions so rendering either side can
// fail closed rather than flattening inherited columns or losing edges.
InheritanceChildren []string
// Unlogged reports that the table is unlogged
// (pg_class.relpersistence 'u'). The declarative model does not manage
// persistence yet — converging it (SET LOGGED / SET UNLOGGED) is a
// full table rewrite — so the model carries it only to refuse:
// rendering an unlogged table as a plain CREATE TABLE would silently
// change its crash-safety and replication behavior, and a persistence
// mismatch fails the diff closed instead of diffing to silence.
Unlogged bool
// ReferencedBy lists the incoming foreign keys — constraints on other
// tables that reference this one — as "table.constraint" strings, in
// that order. Incoming foreign keys are not part of this table's own
// definition and cannot be expressed in a desired file, so the model
// carries them only for the renderer to refuse on.
ReferencedBy []string
// Columns are the table's columns in attribute order.
Columns []Column
// Constraints are the table constraints, name-sorted.
Constraints []Constraint
// Indexes are the non-constraint indexes, name-sorted.
Indexes []Index
}
Model is the canonical, comparison-ready description of one table. It carries no schema qualification: the live and desired sides are introspected under matching search_path settings so their definitions compare textually.
func Introspect ¶
Introspect reads the live table schema.table into the canonical model. It runs inside a read-only transaction whose search_path is set to the target schema (then public), so the server's decompilers print definitions unqualified — directly comparable with a desired-state model introspected the same way.
func IntrospectDesired ¶
func IntrospectDesired(ctx context.Context, db *pgxpool.Pool, desired statement.DesiredSchema) (Model, error)
IntrospectDesired materializes a desired-state schema on a scratch schema and introspects it into the canonical model: execute-and-introspect, the decided way the engine understands DDL semantics. The scratch schema is created inside a single transaction that is always rolled back — nothing the desired file defines ever persists, no CREATEDB privilege is needed, and server-version and extension parity with the live table hold by construction because it runs on the same database.