pgdesign v0.26.0 /internal/predicate
On this page

Package predicate is the migration precondition IR, with a Go executor and a SQL renderer that must agree on object absence, presence, or match.

#internal/predicate

#internal/predicate

Package predicate is the migration PRECONDITION IR (roadmap 5.5+5.7, law L5's domain check / L1's single ≈_syn / L8).

A Precondition is a structured, per-op-class statement about the world BEFORE an op executes: "this object must be ABSENT" (for creates) or "this object must be PRESENT" — optionally "present AND matching" a recorded expectation (for alters/drops). DML and opaque-SQL ops are precondition-FREE: arbitrary SQL has no catalog precondition.

The IR has TWO BACKENDS that must agree (the conformance obligation):

- the Go EXECUTOR (Check), consuming the shared internal/catalog layer and returning STRUCTURED object/expected/found diagnostics — it exists for those diagnostics, not for DB-freedom; - the SQL RENDERER (RenderAssert), compiling the SAME structures into a DO block that RAISEs on a violated precondition.

The predicate is a second computation of ≈_syn in another language (SQL), which is why the two backends are conformance-matrixed: identical verdicts against live states.

#MustBeAbsent

Go go
const MustBeAbsent Existence = iota

MustBeAbsent: the object must NOT exist (a create's domain check — re-creating an existing object is drift). Match is ignored.

#MustBePresent

Go go
const MustBePresent

MustBePresent: the object must exist (a drop/alter's domain check). When Match is set, the present object must additionally match it.

#ClassTable

Go go
const ClassTable      Class = "table"

#ClassColumn

Go go
const ClassColumn     Class = "column"

#ClassConstraint

Go go
const ClassConstraint Class = "constraint"

#ClassIndex

Go go
const ClassIndex      Class = "index"

#ClassView

Go go
const ClassView       Class = "view"

#ClassMatView

Go go
const ClassMatView    Class = "matview"

#ClassSequence

Go go
const ClassSequence   Class = "sequence"

#ClassEnum

Go go
const ClassEnum       Class = "enum"

#ClassEnumValue

Go go
const ClassEnumValue  Class = "enum_value"

#ClassDomain

Go go
const ClassDomain     Class = "domain"

#ClassComposite

Go go
const ClassComposite  Class = "composite"

#ClassFunction

Go go
const ClassFunction   Class = "function"

#ClassTrigger

Go go
const ClassTrigger    Class = "trigger"

#ClassPolicy

Go go
const ClassPolicy     Class = "policy"

#ClassExtension

Go go
const ClassExtension  Class = "extension"

#Execer

Go go
type Execer interface

Execer is the DB surface the Go executor needs. It extends the read-only catalog.Querier with Exec — the definitional-body round-trip (ClassConstraint def, ClassColumn default) builds and drops a throwaway temp object to canonicalize the MODEL text through the live DB (roadmap 5.5+5.7 matching-strategy resolution). Both *pgx.Conn and pgx.Tx satisfy it, so the precondition runs in whatever transactional scope the apply loop opened.

#Result

Go go
type Result struct

Result is the outcome of evaluating a precondition. OK is true when the world satisfies it. When OK is false, Object/Expected/Found name the violation precisely for a hard error.

#Existence

Go go
type Existence int

Existence is the presence half of a precondition.

#Class

Go go
type Class string

Class is the catalog object class a precondition targets.

#Match

Go go
type Match struct

Match, when non-nil on a MustBePresent precondition, additionally requires the present object's attribute(s) to equal these (present-AND-matching). Only the fields relevant to the class are consulted. Nil means existence-only.

Present-and-matching comparisons are LIVE-PATH comparisons (roadmap 5.5+5.7 matching-strategy resolution): they run against the target DB, so the fields carry MODEL text (alias-spelled, non-canonical) and each is compared with a strategy that is robust to equivalent spellings:

- ColumnType — OID probe via to_regtype (alias-robust, pure-computable); - ColumnNotNull — boolean; - ColumnDefault — in-DB round-trip (temp column SET DEFAULT, pg_get_expr); - ConstraintDef — in-DB round-trip (temp table ADD CONSTRAINT, pg_get_constraintdef); - IndexMustBeValid — boolean (pg_index.indisvalid).

Index DEFINITION bodies are NOT compared (existence + validity only): a clean round-trip cannot reach them because pg_get_indexdef embeds the index name and owning table, which differ for the throwaway temp object. This is the documented existence-only class for indexes.

#Precondition

Go go
type Precondition struct

Precondition is one structured domain-check statement for an op.

#Check

Go go
func Check(ctx context.Context, q Execer, p Precondition) (Result, error)

Check evaluates a precondition against the live catalog via the shared query layer. A DB/query failure returns a non-nil error; a violated precondition is Result{OK:false,...} (not an error), so the caller decides the policy (hard error for the migrate executor).

Matching comparisons are LIVE-PATH: column TYPES compare by OID via to_regtype (alias-robust), definitional BODIES (constraint def, column default) round-trip the model text through the DB and compare PG's own canonical form, and not-null / index-validity are booleans. This is the same strategy the SQL renderer emits, so the two backends are conformant by construction.

#RenderIdempotentCreate

Go go
func RenderIdempotentCreate(p Precondition, createSQL string) string

RenderIdempotentCreate wraps a raw, NON-idempotent create statement (createSQL, terminated by ';') in a DO block that implements create-if-absent-OR-verify semantics — the single idempotent-create renderer that generate --idempotent routes its non-schema/extension creates through (roadmap 5.5+5.7). It is the SQL-side fold of the same predicate IR RenderAssert compiles, so the two share the existence probes and the definitional-body round-trip.

Three shapes, chosen by class per the recorded inventory below:

- DEFINITIONAL-BODY (round-trip) classes — a CHECK/FK/UNIQUE/EXCLUDE constraint clause (Match.ConstraintDef) or a non-empty column default (Match.ColumnDefault): create when absent; when present, canonicalize the MODEL text through a throwaway temp object and RAISE (naming object / expected / found) if PG's own pg_get_* form differs. This is the class that makes re-applying idempotent DDL fail LOUDLY on definition drift instead of silently skipping. - BOOLEAN-MATCH classes — a Match with only pure-catalog dimensions (column TYPE via to_regtype OID probe, NOT NULL, index validity): create when absent, RAISE on drift via a single ELSIF guard. - EXISTENCE-ONLY classes — a nil Match (no sound live match strategy that a clean round-trip can reach): create when absent, otherwise NO-OP. These DEGRADE to create-if-absent by design and NEVER RAISE a false mismatch. The per-class rationale is documented in idempotentInventory below so the silence is deliberate and auditable, never accidental.

PER-CLASS IDEMPOTENT INVENTORY (as shipped):

CONSTRAINT (CHECK/FK/UNIQUE/EXCLUDE) — round-trip RAISE on definition drift. pg_get_constraintdef canonicalizes a temp-applied clone of the MODEL clause; alias-spelled equivalents converge, genuine differences RAISE. COLUMN default — round-trip RAISE on default drift. pg_get_expr over a temp SET DEFAULT; same convergence guarantee. (Not emitted by generate today — generate folds defaults into ADD COLUMN IF NOT EXISTS — but the primitive supports it for the apply-loop executor.) COLUMN type / NOT NULL — boolean-match ELSIF RAISE (OID probe). INDEX — EXISTENCE-ONLY. pg_get_indexdef embeds the index name and owning table, which differ for a throwaway temp object, so a clean round-trip cannot reach the definition body. Create-if-absent. ENUM / DOMAIN / COMPOSITE — EXISTENCE-ONLY. A type's body (labels, CHECK, fields) has no LIKE-cloneable temp carrier the round-trip can use, and CREATE TYPE has no IF NOT EXISTS; create-if-absent via a pg_type probe. ENUM VALUE (ClassEnumValue) — NATIVE IF NOT EXISTS. ALTER TYPE ... ADD VALUE IF NOT EXISTS is idempotent in PG's own state model, so the label add needs no DO-block wrapper: absent -> added, present -> no-op. This is the non-transactional enum-add class the apply loop resumes by simply re-running (executeNonTransactionalOp's default arm). No definitional body to drift — an enum value is its own identity, so there is no false-mismatch to raise. TABLE / VIEW / MATVIEW / SEQUENCE — EXISTENCE-ONLY. Whole-relation bodies are out of round-trip reach (self-referential names); create-if-absent (native IF NOT EXISTS / CREATE OR REPLACE handles these upstream, so generate does not route them here). FUNCTION — CREATE OR REPLACE upstream (natively idempotent, definition-updating); not routed here. POLICY / TRIGGER — EXISTENCE-ONLY. Create-if-absent. SCHEMA / EXTENSION — native IF NOT EXISTS upstream; never routed here (no definitional body).

#RenderAssert

Go go
func RenderAssert(p Precondition) string

RenderAssert compiles a precondition into a DO block that RAISEs EXCEPTION when the precondition is VIOLATED (the SQL backend of the same IR). This is the RAISE-on-mismatch primitive generate --idempotent builds on, and the second computation of the predicate the conformance matrix pins against the Go executor.

Two shapes:

- existence / OID-type / not-null / index-validity are pure boolean catalog reads → a single IF NOT (<ok>) THEN RAISE guard; - definitional bodies (constraint def, non-empty column default) need the in-DB round-trip → a DECLARE block that canonicalizes the MODEL text through a throwaway temp object and compares PG's own pg_get_* form to the live one.

Both compute the SAME verdict as the Go executor, keeping the conformance matrix honest.

#Result.Err

Go go
func (r Result) Err() error

Err renders a violated precondition as an error, naming object/expected/found. It returns nil when OK.

#Existence.String

Go go
func (e Existence) String() string
Search