Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog

All notable changes to pgevolve are documented in this file.

The format is loosely based on Keep a Changelog, and the project follows Semantic Versioning.

[0.4.6] — 2026-06-23

Fixed

  • String-literal CHECK constraints diffed spuriously on PG 14–17 (#47). A CHECK whose expression contained a string literal — e.g. CHECK (email <> '') — produced a perpetual spurious diff against a live database, because pg_get_constraintdef deparses the literal with an explicit ::text cast (email <> ''::text) on PG 14–17 (PG 18 dropped it) while the source parse keeps email <> ''. The expression canonicalizer (normalize_expr) now recursively strips the always-redundant string-literal→text cast ('x'::text) throughout an expression tree, so both paths converge. The rule is precise — a string-literal A_Const cast to a bare text-family type with no typmod — so meaningful coercions like '5'::int, '2024-01-01'::date, and 'x'::varchar(5) are preserved. Verified by a live-PG apply round-trip across PG 14–18.
  • Unnamed CHECK constraint naming (#48). An inline unnamed CHECK was named {table}_check, but Postgres names a single-column check {table}_{col}_check, forcing a spurious diff against the live catalog. Unnamed checks are now named the Postgres-faithful way via the ChooseIndexName port: a column-level check → {table}_{col}_check, a table-level check → {table}_check. This also corrects the v0.4.5 LIKE INCLUDING CONSTRAINTS behavior: Postgres copies CHECK constraint names verbatim on LIKE (it does not re-derive), so the LIKE path now copies the source name verbatim rather than re-deriving (reverting the #45 heuristic). Verified against live PG 14–18 with unnamed-CHECK round-trip tests.
  • Over-long generated index/constraint names truncated incorrectly (#49). choose_name truncated only the table-name component when a generated name exceeded NAMEDATALEN (63 bytes); Postgres's makeObjectName shrinks the table-name and column-addition components alternately. For an index/constraint driven by long column names this produced a wrong (possibly over-length) name. The port now mirrors Postgres's alternating-shrink algorithm. The common case (long table, short columns) is unchanged; verified by a long-column live-PG round-trip across PG 14–18.

[0.4.5] — 2026-06-23

Added

  • CREATE TABLE … (LIKE source [INCLUDING …]) support (#43). LIKE clauses are now expanded into concrete IR during a deferred pass (crates/pgevolve-core/src/parse/builder/table_like.rs) that runs after every table is assembled. The following INCLUDING options are supported: DEFAULTS, IDENTITY, GENERATED, STORAGE, COMPRESSION, COMMENTS (table and column), CONSTRAINTS (CHECK only — PK/UNIQUE belong to INCLUDING INDEXES), INDEXES (PK/UNIQUE constraints plus plain CREATE INDEX indexes), STATISTICS (extended statistics), and INCLUDING ALL. Chained or interleaved LIKE clauses work regardless of declaration order; a cycle or self-reference is detected and reported as a parse error. Using a view or materialized view as a LIKE source is rejected with a clear diagnostic. Auto-derived names for constraint-backing and plain indexes are generated via a Rust port of Postgres's ChooseIndexName (choose_name.rs), producing names that match live PG 14–18 output (verified by conformance fixtures). Known limitations: (1) EXCLUDE constraints are not modeled in pgevolve's IR and therefore cannot appear on a LIKE source — nothing to copy. (2) Unnamed source CHECK constraints are re-derived to the clone's name ({clone}_check, matching pgevolve's inline convention; see #45) — full Postgres column-form fidelity ({table}_{col}_check) is tracked with the broader constraint-naming work in #44/#46. Conformance coverage: objects/tables/create-like-bare, create-like-including-all, create-like-multiple, create-like-interleaved.

Fixed

  • interval(N) / interval <fields> type modifiers misclassified to Other (#41). A column declared interval(6), interval hour, or interval hour to minute parsed without error but was silently stored as ColumnType::Other, producing a perpetual spurious ALTER COLUMN TYPE diff against a live database (the catalog's format_type emits the canonical form, which does map to Interval). pg_query injects the PG INTERVAL_MASK fields bitmask as the first typmod argument (interval(6)[32767, 6]); the renderer now decodes that bitmask and emits the canonical string, and parse_canonical gained arms for the interval <fields> and interval <fields>(p) forms, so the source and catalog paths converge to the same ColumnType::Interval.
  • Schema-qualified PostGIS types dropped their typmod (#42). public.geometry(Point,4326) took the user-defined-type branch and discarded the (Point,4326) modifier, so the source UserDefined could never match the catalog's Other. Schema-qualified geometry/geography declarations that carry a typmod are now routed through the canonical stringify path (preserving the schema), and the casing-normalization helper recognizes a schema-qualified head, so both sides converge. Non-geo user-defined types and modifier-free declarations are unchanged.
  • Inline unnamed UNIQUE constraint naming (#44). An inline UNIQUE (a, b) was auto-named {table}_key, which never matches Postgres's {table}_{col}_key, forcing a spurious diff. Unnamed unique constraints are now named via the same ChooseIndexName port used by the LIKE path, with per-table collision counters. PRIMARY KEY ({table}_pkey) and explicitly-named constraints are unchanged.
  • LIKE INCLUDING CONSTRAINTS re-derives unnamed CHECK names (#45). A copied unnamed CHECK previously kept the source table's name (base_check) on the clone; it is now re-derived to the clone's name (clone_check) so a LIKE clone matches an equivalent hand-written table.

Internal

  • Extracted a shared resolve_order helper for the LIKE dependency-ordering passes and locked choose_name truncation behavior at the NAMEDATALEN boundary (#46).

[0.4.4] — 2026-06-18

Added

  • Recursive views (WITH RECURSIVE). Confirmed support for CREATE RECURSIVE VIEW, CREATE VIEW … WITH RECURSIVE, recursive materialized views, and CREATE OR REPLACE of a recursive body, with conformance coverage across PG 14–18. No code change was required: a recursive CTE's self-reference is unqualified, and the view body-dependency walkers only emit edges for schema-qualified references, so no dependency self-edge is ever produced; the body round-trips via the canonical deparse. The earlier roadmap assumption that recursive views needed cycle-aware dep-graph work was incorrect.

Fixed

  • Parameterized PostGIS types (geometry/geography(<subtype>,<srid>)). A column declared geometry(Point,4326) failed to parse with "could not stringify type name" (#40), blocking any PostGIS-backed schema at pgevolve validate. Type modifiers are an expr_list, so the subtype token (Point/MultiPolygon/…) is parsed as a bareword (ColumnRef), not a constant; the type-name stringifier now handles bareword typmod arguments in addition to literals. The geometry/geography subtype is also normalized to lowercase on both the source-parse and catalog paths so it does not produce a perpetual spurious ALTER COLUMN TYPE diff (the source parser lowercases the subtype while pg_catalog.format_type emits canonical TitleCase, and the catch-all type compares by exact string). Bare geometry/geography, the array form geometry(Point,4326)[], and composite/domain/function-argument declarations are covered. Two adjacent pre-existing limitations were filed as follow-ups: interval(N) typmods silently misclassify (#41) and schema-qualified public.geometry(…) drops its typmod (#42).

[0.4.3] — 2026-06-08

Added

  • Table TABLESPACE placement. CREATE TABLE … TABLESPACE <ts> and CREATE TABLE … PARTITION OF … TABLESPACE <ts> on regular tables, partitioned parents, and partition children. ALTER TABLE … SET TABLESPACE is RequiresApproval on a leaf table (full rewrite + ACCESS EXCLUSIVE lock) and Safe on a partitioned parent (metadata-only, no rewrite). pg_default is normalized to the implicit default — declaring TABLESPACE pg_default produces no spurious diff. Per-partition TABLESPACE overrides are tracked independently of the parent on the Table.tablespace field.
  • TEXT SEARCH DICTIONARY and TEXT SEARCH CONFIGURATION support. Managed TEXT SEARCH DICTIONARY (TEMPLATE reference + OPTIONS list; CREATE, ALTER … (options), ALTER … OWNER TO, DROP, COMMENT ON) and TEXT SEARCH CONFIGURATION (PARSER reference + token→dictionary MAPPING list; CREATE, ALTER … ADD/ALTER/DROP MAPPING, ALTER … OWNER TO, DROP, COMMENT ON). Identity is (schema, name); a template/parser change reads as drop + create (Postgres has no in-place ALTER for those), and owner is lenient. PARSER and TEMPLATE are unmanaged environment references (C-language functions); pgevolve never auto-creates or drops them, and unqualified template/parser/dictionary names resolve to pg_catalog. COPY= on CREATE TEXT SEARCH CONFIGURATION is out of scope. Known limitation: a functional index or generated column whose expression calls to_tsvector('schema.config', …) carries an implicit dependency on that text-search configuration that the dep-graph does not track (no expression-level TS-config dep edges); such an index may be ordered before its configuration at apply time. The TS objects themselves round-trip correctly; this is a planner gap to address in a future release.

[0.4.2] — 2026-06-08

Added

  • CAST support. Custom casts via CREATE CAST (WITH FUNCTION / WITHOUT FUNCTION / WITH INOUT; EXPLICIT / ASSIGNMENT / IMPLICIT contexts), DROP, and COMMENT ON. Managed: casts are auto-dropped when removed from source. WITH FUNCTION is constrained to managed SQL/plpgsql functions — source rejects references to unmanaged or built-in functions via the cast-references-unmanaged-function lint. Built-in/system casts (pg_cast.oid < 16384) and extension-owned casts are excluded from introspection. No ALTER CAST in Postgres, so any structural change is drop + create; identity is (source_type, target_type).

[0.4.1] — 2026-06-07

Added

  • AGGREGATE support. User-defined aggregates via CREATE AGGREGATE (ordinary form: SFUNC + STYPE + optional FINALFUNC/INITCOND), ALTER … OWNER TO, DROP, and COMMENT ON. State and final functions must be managed SQL/plpgsql functions; source rejects references to unmanaged or built-in functions via the aggregate-references-unmanaged-function lint. The reader skips ordered-set aggregates, moving aggregates, and aggregates whose state function is in an unreadable language. Rename is drop + create; identity is (schema, name, arg_types).

0.4.0 — 2026-06-06

Added

  • EVENT TRIGGER support. CREATE EVENT TRIGGER (with ON <event> and WHEN TAG IN (...) filter), ALTER … ENABLE/DISABLE/ENABLE REPLICA/ENABLE ALWAYS, ALTER … OWNER TO, DROP, and COMMENT ON. Database-global with a lenient drop policy (unmanaged event triggers surface via the unmanaged-event-trigger lint, never auto-dropped), mirroring publications/subscriptions. Extension-owned event triggers are excluded from introspection.
  • TABLESPACE support (cluster object). Cluster-level TABLESPACE management via the pgevolve cluster … surface: CREATE (with OWNER, LOCATION, WITH (options)), ALTER … OWNER TO, ALTER … SET (options), DROP (intent-gated), and COMMENT ON. Lenient owner + lenient options. LOCATION is immutable, so a location drift surfaces via the tablespace-location-drift advisory rather than a destructive recreate. Filesystem-layout management (directory creation, mount points) stays out of scope. Tablespaces are declared in a tablespaces/ cluster-source directory.
  • Table access method support. CREATE TABLE … USING <am> is parsed and rendered, with the access method read from pg_class.relam. The built-in heap is normalized to the implicit default (declaring USING heap is a no-op). An access-method change on an existing table surfaces via the table-access-method-change advisory — pgevolve does not auto-rewrite tables (ALTER TABLE … SET ACCESS METHOD is PG 15+ and implies a full rewrite).
  • SubscriptionOptions.connect — CREATE-only directive. New IR field that maps to PG's CREATE SUBSCRIPTION ... WITH (connect = ...) option. connect = false creates the subscription without dialing the publisher (the only way to construct a subscription against a publisher that may not be reachable). The option is CREATE-only — pg_subscription doesn't store it — so the catalog reader always returns None and the diff never emits an ALTER for it. Closes #14.

Changed

  • Cluster apply reaches per-DB parity. pgevolve cluster apply now bootstraps pgevolve metadata, acquires the singleton advisory lock, runs cluster preflight (identity match + intent approval), writes an apply_log row, executes via the per-DB group executor, and closes the audit row. pgevolve cluster plan writes the canonical 3-file plan layout (structured plan.sql headers + intent.toml + manifest.toml with target_identity). The cluster target_identity format is cluster:{system_identifier_hex16}. Closes #7.

Fixed

Diff + planner

  • New tables, sequences, schemas, and indexes carry their full attribute set on CREATE. The diff previously emitted only the bare CREATE for newly-declared objects; owner, grants, policies, storage options, and the RLS-forced bit were silently dropped and required a subsequent ALTER cycle to land. Fresh applies and empty-DB bootstraps now produce live state matching the source IR on the first apply.
  • CREATE INDEX emits the WITH (...) storage clause. The clause was previously dropped at render time; index reloptions (fillfactor, fastupdate, etc.) needed a follow-up ALTER INDEX SET (...) to land. Now emitted inline.
  • Column STORAGE is no longer inline-rendered in CREATE TABLE / ADD COLUMN. Inline STORAGE is PG 16+ syntax. The renderer now emits ALTER COLUMN ... SET STORAGE as a follow-up step, restoring PG 14/15 compatibility.
  • CREATE SUBSCRIPTION runs outside transaction when create_slot != false. PG forbids tx-block CREATE SUBSCRIPTION with create_slot = true (the default). Planner now sets TransactionConstraint::OutsideTransaction based on subscription options. Closes #11.
  • DROP SUBSCRIPTION runs outside transaction. PG forbids tx-block DROP when the subscription has an attached slot. The IR can't tell at diff time whether a slot is attached; conservative path always uses out-of-transaction. Closes #26.
  • streaming subscription option emits boolean form on PG ≤15. PG ≤15 accepts only true/false for streaming; the parallel keyword is PG 16+. Renderer no longer emits string forms. Closes #24.
  • Subscription reader accepts boolean substream column values. Companion to the renderer fix above; PG's pg_subscription.subsubstream is returned via ::text cast as f/t/false/true/parallel. Closes #28.
  • REVOKE statements emit before GRANT in the same diff step. PG's REVOKE priv FROM role removes the privilege regardless of grant_option; ordering GRANT-before-REVOKE silently cancelled a just-emitted grant. Affects with_grant_option upgrades and downgrades on tables, sequences, schemas, views, materialized views, functions, procedures, and types. Closes #33.
  • default_privileges diff emits REVOKE when grants shrink. Previously, removing a grant from an existing rule left the live database with the old grant intact. Closes #23.
  • default_privileges rules carry their grant set when newly declared. Was previously a no-op in the source-only diff branch. Closes #25.
  • Column-level REVOKE elided when the column is dropped in the same plan. PG cascade-revokes column ACLs during column drop; the explicit REVOKE then failed with column does not exist. Closes #39.
  • Column-grant diff ignores columns being dropped. When a column carrying a multi-column grant is dropped (live GRANT SELECT (id, price), source keeps GRANT SELECT (id)), the diff previously emitted REVOKE SELECT (id, price) — naming the dropped price — which failed with [42703] column "price" of relation "X" does not exist. The table grant diff now strips dropped columns from the target grants before diffing, so the column drop alone converges the privilege and no spurious REVOKE/GRANT is emitted. (Generalises #39, which only covered grants whose entire column list was dropped.)
  • DROP SCHEMA cascade-emits DROP COLLATION. The diff previously left dependent collations in catalog.collations, tripping PG error 2BP01 on apply. The dependency graph now also carries a Collation → Schema edge so the drops sort correctly. Closes #38.
  • change_node() returns the correct NodeId variant per object kind. Owner/grant changes on sequences/schemas/views/types/MVs/procedures/collations previously defaulted to NodeId::Table(qname), causing incorrect interleaving across object kinds when sorting plan steps. Closes #36 (one of two root causes).

Reader + canonicalization

  • PG-implicit grants stripped from default_privileges canonical IR. PG silently injects (grantee = target_role, full self-priv) into every pg_default_acl.defaclacl, and additionally injects (Public, USAGE) on TYPES and (Public, EXECUTE) on FUNCTIONS. Both the catalog reader and the source parser now strip these implicit entries so source and live IRs match. Without this, the diff emitted spurious REVOKEs that deleted entire pg_default_acl rows, manifesting as present vs removed divergences in round-trip tests. Closes #34 + #37.
  • default_privileges implicit-PUBLIC strip now runs in canon too. The reader/parser strip above did not cover IR built by the testkit generator (which bypasses the SQL parser): an implicit (Public, USAGE) on TYPES — or (Public, EXECUTE) on FUNCTIONS — survived on the source side while the live reader stripped it, reappearing as default_privileges.*.TYPES/FUNCTIONS: present vs removed. The canon pass now applies the same strip, then drops any rule it empties out, so every source path (parser and generator) normalises identically to live.
  • Empty publications are skipped on read. PG permits an empty publication — CREATE PUBLICATION p;, or a selective publication whose last table/schema was dropped (PG silently empties it rather than dropping it). pgevolve cannot model one (the parser and canon reject empty Selective scopes, so it can never appear in source). The catalog reader previously failed the entire introspection with empty Selective scope (no tables, no schemas); it now skips the unmanaged empty publication, keeping the database readable. (Surfaced by the lenient drop policy: dropping a publication's last table leaves the publication empty because the publication itself is never auto-dropped.)
  • Range type subtype_opclass and collation canonicalize to None when matching PG defaults. PG resolves and stores the default opclass (e.g. timestamptz_ops) and the pg_catalog.default collation even when the user omits them; canon now strips the resolved value back to None so source IR matches live IR after read-back. Closes #35.
  • Policy roles [PUBLIC, X] canonicalize to [PUBLIC]. PG accepts the list at CREATE POLICY time but silently drops named roles when PUBLIC is present (PUBLIC includes all roles). Canon now aligns source IR with PG's stored form. Closes #31.
  • Owner self-grants stripped from source IR. The live catalog reader has long stripped grants where grantee == owner from tables/sequences/schemas/views/etc.; source IR now matches symmetrically. The asymmetry caused diff(live, source) to be non-empty whenever the IR generator (or a user) happened to write the owner into the grants list. Closes #36 (second root cause).
  • default_privileges rules with empty grant sets stripped in canon. PG has no DDL form for a zero-grant default-privilege rule; the rule only materializes in pg_default_acl when at least one grant is in effect. Canon now removes empty-grant rules from source IR so they don't show as present vs removed divergences.
  • RLS policy predicates respect PG's per-command matrix. FOR INSERT policies use only WITH CHECK (PG rejects USING for INSERT with error 42601); FOR SELECT/DELETE use only USING; FOR UPDATE/ALL use both. Closes #22.

Test infrastructure (no user-visible change)

  • Soak workflow now sets TEST_PWD so subscription apply doesn't trip the env-var preflight.
  • Testkit IR generator tightened across many shapes: subscription options gated by PG version (origin, failover, two_phase, disable_on_error); RLS policy predicates gated by command; default_privileges restricted to valid (grantee, object_kind) and (schema, object_type) matrices; index storage options gated by access method; STATISTICS columns restricted to types with a default B-tree opclass; publication scope guards empty Selective output and PG 15+ FOR ALL TABLES IN SCHEMA form; mutation cascades clean dependent references on drop_schema (default privileges, types, statistics, views, collations) and drop_column (constraints, grants, statistics, indexes); grant generator never combines WITH GRANT OPTION with PUBLIC.
  • Mutator drop cascades extended to match PG's automatic dependency handling, eliminating spurious Statistic(Create) divergences and empty-publication read errors in the soak: drop_table now also drops extended statistics targeting the table and removes the table from publications (dropping any publication left empty); drop_column drops a statistic entirely when one of its columns is dropped (PG removes the whole object, it does not shrink the column list — verified empirically) and removes the table from any publication whose column list names the dropped column; drop_schema additionally drops statistics whose target table lived in the schema and prunes publications referencing the schema or its tables.
  • cargo xtask soak-streak tracks the consecutive-clean-soak-day counter feeding the v1.0 release gate (sub-project C).
  • Plan::from_grouped_with_id constructor lets callers supply a pre-computed PlanId (used by cluster apply, which hashes ClusterCatalog rather than per-DB Catalog).

Removed

  • pgevolve::executor::apply_cluster_steps (public API). Callers that previously built a Vec<RawStep> and applied it directly should now build a Plan and use apply_cluster_plan instead.

[0.3.9] — 2026-05-28

Patch for the broken v0.3.8 — no new features.

Fixed

  • Collation catalog reader on PG 15 and PG 16. v0.3.8 shipped with SQL that used pg_collation.colllocale for "PG 16+" — but colllocale was introduced in PG 17, not PG 16. PG 15 added colliculocale (ICU-only) and PG 17 renamed it to colllocale (generic, since the new builtin provider also uses it). ICU rows on PG 15 and PG 16 left collcollate NULL, so pgevolve returned empty lc_collate strings and either crashed on decode or triggered a "column c.colllocale does not exist" SQL error. Three-way per-version SQL: PG 14 (legacy collcollate), PG 15/16 (colliculocale), PG 17/18 (colllocale). Commits 09cd563
    • a30d7f3.
  • Tier-3 catalog round-trip snapshots re-blessed. Stage 2 of the v0.3.8 plan added Catalog::collations: Vec<Collation> but the JSON snapshot fixtures under crates/pgevolve-core/tests/fixtures/catalog/pg{14,15,16,17}/ were never re-blessed for the new field. Stage 11's pre-release verify ran the conformance suite (-p pgevolve-conformance) rather than the tier-3 round-trip in pgevolve-core, so the gap surfaced only in CI on the v0.3.8 push. All 28 snapshots re-blessed via cargo xtask bless. Commit 09cd563.

Yanked

  • v0.3.8 yanked from crates.io. ICU collation reads against PG 15 or PG 16 fail; users should upgrade to v0.3.9.

[0.3.8] — 2026-05-28

Added

  • CREATE COLLATION as a first-class IR object. libc / ICU / PG 17+ builtin providers with the deterministic toggle, RENAME, and COMMENT ON COLLATION all managed. Source may use the locale = 'X' shorthand or explicit lc_collate + lc_ctype; the IR always stores the latter and the renderer collapses back to the shorthand when they match. pg_collation.collversion is read-only (differ ignores it); ALTER COLLATION … REFRESH VERSION and the matching collation-version-drift lint are deferred to v0.3.9.
  • CREATE TYPE … AS RANGE — additive UserTypeKind::Range variant on the existing user-type machinery. Models subtype, subtype opclass, collation, canonical fn, subtype_diff fn, and an optional custom multirange type name. Any structural change goes through the existing ReplaceWithCascade path — Postgres has no in-place ALTER for range fields. Auto-generated multirange types filtered from pg_type via typtype != 'm'.
  • 5 new lint rules: unmanaged-collation, column-references-unmanaged-collation, range-type-references-unmanaged-subtype, nondeterministic-collation-requires-pg-12, builtin-provider-requires-pg-17.
  • 5 new StepKind variants for collation lifecycle: CreateCollation, DropCollation, RenameCollation, ReplaceCollation, CommentOnCollation. Dep-graph gains NodeId::Collation plus 4 edge types (Column → Collation, Domain → Collation, Range → Collation, CompositeAttribute → Collation).
  • 11 conformance fixtures: 6 under objects/collations/ (create-libc, create-icu, create-nondeterministic, drop, comment-on, replace-on-locale-change) and 5 under objects/ranges/ (create-simple-int4range, create-with-opclass, create-with-subtype-diff-fn, drop, column-with-range-type), plus the scenarios/column-references-managed-collation cross-kind fixture. The originally-planned objects/collations/rename was substituted to replace-on-locale-change (rename is exercised in unit + property tests; replace-on-structural-change is the higher-value end-to-end path). The originally-planned objects/ranges/create-with-canonical-fn was substituted to create-with-subtype-diff-fn (canonical-fn requires authoring a matching C/PLpgSQL function, which is out of scope for v0.3.8 fixtures).

Fixed

  • Range-type round-trip — the differ now treats source-side None for range-type optional fields (opclass, collation, canonical, subtype_diff, multirange_type_name) leniently, matching the established "source None means unmanaged" pattern. Previously any catalog-side default would spuriously diff against an unpinned source. Surfaces a new resolve_user_defined_types canon pass that resolves ColumnType::Other(qname) references against Catalog::types after the source parse pass — applies to any user-defined type, not just ranges. (054364a)
  • ICU collation locale reader on PG 16+ — pg_collation.colllocale replaced collcollate / collctype for ICU rows in PG 16+. The reader now selects the right column per PG major; previously ICU collations dumped on PG 16+ surfaced as empty locales. (51fc476)

Out of scope (deferred to v0.3.9+)

  • CREATE COLLATION FROM existing_collation form.
  • ALTER COLLATION … REFRESH VERSION and the matching collation-version-drift lint.
  • Multirange-type customization beyond multirange_type_name (no per-multirange opclass / canonical fn surface yet).
  • A first-class Multirange IR object distinct from Range — multiranges are still modeled implicitly via the parent range.

[0.3.7] — 2026-05-27

Added

  • CREATE STATISTICS — multi-column statistics objects (ndistinct, dependencies, mcv) with PG 14+ expression statistics. Explicit names required (anonymous form rejected, mirroring index-naming policy). Granular differ: AlterStatisticSetTarget for the cheap SET STATISTICS n path; ReplaceStatistic (DROP + CREATE) for any other change since PG has no in-place ALTER for column lists or kinds.
  • CREATE VIEW … WITH CHECK OPTION — per-view check_option: Option<CheckOption> (Local | Cascaded). Parser folds both SQL-clause and WITH-options forms; differ emits CREATE OR REPLACE VIEW.
  • 5 new StepKind variants for STATISTICS + 1 for views: CreateStatistic, DropStatistic, ReplaceStatistic, AlterStatisticSetTarget, CommentOnStatistic, AlterViewSetCheckOption.
  • unmanaged-statistic lint (Warning, waivable) — standard v0.3.x lenient-drift surface.
  • 9 conformance fixtures (3 views + 6 statistics).

Closes

Third and fourth items from the post-v0.3.3 agreed roadmap (STATISTICS was 📋 Planned in objects.md; CREATE VIEW … WITH CHECK OPTION was 🔮 Future).

[0.3.6] — 2026-05-27

Added

  • Postgres 18 catalog support. PgVersion::Pg18 variant; catalog/queries/pg18.rs thin re-export of shared (PG 18 is fully backward-compatible with PG 17 catalog queries — no divergences found). Tier-2/3/C suites green under PG 18. CI matrix exercises PG 18 on every push.
  • [managed].min_pg_version now accepts 18.

Notes

  • v0.3.6 is catalog-read + conformance only. New PG 18 IR features (virtual generated columns, etc.) are explicitly deferred to v0.4.1 per the roadmap.
  • Constitution §6 now reads "14, 15, 16, 17, and 18" as actively-maintained PG majors.

[0.3.5] — 2026-05-26

Added

  • SUBSCRIPTION as a first-class IR object. Per-field lenient SubscriptionOptions (enabled, binary, streaming Off/On/Parallel, two_phase, disable_on_error PG15+, password_required + run_as_owner
    • origin PG16+, failover PG17+). Opaque CONNECTION string with ${VAR} env-var interpolation.
  • Apply-time ${VAR} resolution. Source IR and plan.sql store unresolved ${VAR} placeholders. Preflight scans every step's SQL, resolves against process env, fails before any DB connection if a reference is unset. Secrets never persist to disk.
  • 8 new StepKind variants for subscription operations.
  • 4 lint rules: unmanaged-subscription (Warning), subscription-references-undeclared-publication (Warning), subscription-feature-requires-pg-version (Error, not waivable), subscription-password-in-source (Error, not waivable) — catches plaintext password= at parse time.
  • [fixture] apply flag in the conformance harness so fixtures with cross-cluster side-effects (subscriptions) can validate parse/diff/plan/lint without applying.
  • 12 conformance fixtures under objects/subscriptions/.

Closes

Second item from the post-v0.3.3 agreed roadmap (next: CREATE VIEW WITH CHECK OPTION).

[0.3.4] — 2026-05-26

Added

  • PUBLICATION as a first-class IR object. All 5 PG syntactic forms (explicit FOR TABLE, FOR ALL TABLES, FOR TABLES IN SCHEMA PG15+, row filters PG15+, column lists PG15+). PublicationScope sum-type encodes PG's mutual exclusion of AllTables vs Selective.
  • Granular ALTER PUBLICATION semantics. 11 new StepKind variants (add/drop/set per table, add/drop per schema, set publish, etc.) — each plan step is independently auditable and rollback-safe.
  • [managed].min_pg_version config key. Defaults to 14; raise to 15+ to use row filters, column lists, or schema-scope. PG-version-gated source features fail at lint time (publication-feature-requires-pg-version, Error) instead of at apply with a Postgres syntax error.
  • 4 lint rules: unmanaged-publication (Warning), publication-captures-unmanaged-table (Warning), publication-row-filter-references-unmanaged-column (Warning), publication-feature-requires-pg-version (Error, not waivable).
  • 12 conformance fixtures under objects/publications/.

Closes

Slipped from the v0.3 roadmap commitment (next: v0.3.5 SUBSCRIPTION).

[0.3.3] — 2026-05-23

Added

  • Storage parameters / reloptions on tables, indexes, materialized views. Typed Option<T> fields for the well-known keys (fillfactor, autovacuum_*, parallel_workers, fastupdate, buffering, pages_per_range, etc.) plus extra: BTreeMap<String, String> for extension-registered or unknown keys. Tables and MVs share the autovacuum substruct since PG documents identical key sets.
  • Per-AM fillfactor validation at parse time: B-tree 50..=100, GiST 10..=100, SP-GiST 90..=100, BRIN/GIN reject fillfactor.
  • Lenient drift policy: source None always means "unmanaged" — never triggers RESET. unmanaged-reloption lint surfaces catalog reloptions not in source.
  • 3 new StepKind variants: SetTableStorage, SetIndexStorage, SetMaterializedViewStorage. One ALTER step per relkind per diff (batches multiple keys into one SET).
  • unmanaged-reloption lint (warning, waivable).
  • Source parser for WITH (...) on CREATE TABLE/INDEX/MATERIALIZED VIEW and ALTER ... SET (...). RESET (...) rejected in source.
  • Catalog reader decodes pg_class.reloptions::text[] into typed structs.
  • 11 conformance fixtures.

Known limitations

  • CREATE TABLE/INDEX/MATERIALIZED VIEW … WITH (…) against a brand-new object emits the CREATE without the inline WITH. Convergent on the next plan run via ALTER … SET. Same gap as owner/grants/policies on new objects in v0.3.x; will be closed uniformly in a follow-up.

Closes

Slipped v0.2 commitment from docs/spec/objects.md (table reloptions row marked 🟡 Partial). Per-partition storage parameters also satisfied (partitions inherit since they're Table in IR).

[0.3.2] — 2026-05-22

Added

  • Row-level security policiesTable gains rls_enabled, rls_forced, and policies: Vec<Policy>. Policies carry permissive, command, roles, using, with_check. USING / WITH CHECK reuse NormalizedExpr canonicalization shared with check constraints.
  • Source parser: CREATE POLICY + four ALTER TABLE ... { ENABLE | DISABLE | FORCE | NO FORCE } ROW LEVEL SECURITY subcommands. ALTER POLICY and DROP POLICY rejected in source (diff-driven).
  • Differ: 5 new Change variants (CreatePolicy, DropPolicy, AlterPolicy, SetTableRowSecurity, SetTableForceRowSecurity). Command-kind changes recreate (DROP + CREATE) because PG doesn't allow ALTER POLICY to change the command.
  • Catalog reader: new pg_policies query + relrowsecurity / relforcerowsecurity on the tables query.
  • Two lint additions:
    • grant-references-unknown-role (existing) now also walks policy TO clauses.
    • force-rls-without-policies (new, Warning) — fires when a table has FORCE RLS enabled but no policies defined (PG would deny all rows).
  • Conformance: 11 new fixtures under objects/policies/.

Closes

v0.3 security/permissions trilogy: roles (v0.3.0) → grants (v0.3.1) → policies (v0.3.2).

[0.3.1] — 2026-05-22

Added

  • Object grants + ownership — all 8 grantable IR types (Schema, Sequence, Table, View, MaterializedView, Function, Procedure, UserType) gain owner: Option<Identifier> + grants: Vec<Grant>. Column-level grants on tables/views/MVs.
  • Default privilegesCatalog.default_privileges: Vec<DefaultPrivilegeRule> mirroring pg_default_acl. Supports FOR ROLE x IN SCHEMA y GRANT/REVOKE ... ON {TABLES, SEQUENCES, FUNCTIONS, TYPES, SCHEMAS}.
  • Lenient drift policy — catalog grants to roles outside source surface as grants-to-unmanaged-role warning, never silently revoked.
  • Optional cluster-link[cluster].project in pgevolve.toml validates grantee role names against the linked cluster project's roles/*.sql via the grant-references-unknown-role lint (Error severity).
  • Three new lint rules:
    • grant-references-unknown-role (Error, cluster-aware) — grantee not in linked cluster source.
    • grants-to-unmanaged-role (Warning) — catalog grants to roles not declared in source.
    • revoke-from-owner (Error) — REVOKE would target object's owner.
  • Six new StepKind variants: AlterObjectOwner, GrantObjectPrivilege, RevokeObjectPrivilege, GrantColumnPrivilege, RevokeColumnPrivilege, AlterDefaultPrivileges.

Catalog reader

  • New catalog::grants module decodes PG aclitem text format. All 6 family queries gain <obj>owner + <obj>acl::text[]. Tables/views/MVs also decode pg_attribute.attacl for column-level grants. Owner self-grants stripped (PG materializes them when any explicit grant exists; they're implicit by ownership).

Source parser

  • Three new builder modules: object-level GRANT, ALTER ... OWNER TO, ALTER DEFAULT PRIVILEGES. GRANT ALL expands per object type. Column-level grants extracted from AccessPriv.cols. REVOKE rejected in source.

Shadow validate

  • validate --shadow now respects "unmanaged owner" semantics: when source declares owner = None (no OWNER TO), shadow-validate ignores any catalog-side owner. Similarly for grants — only managed grantees compared.

Conformance

  • 13 new fixtures under objects/grants/ covering table/schema/function/sequence/owner/default-privs/lint sub-roots. Two cluster-link fixtures deferred (harness extension out of scope for v0.3.1).

[0.3.0] — 2026-05-22

Added

  • Cluster-level surface — new project type (pgevolve-cluster.toml + roles/), new command family (pgevolve cluster init/diff/plan/apply/status), new executor running against a superuser DSN. Per Decision 23 of the v0.2 architecture review.
  • ROLE / CREATE USER fully managedClusterCatalog.roles with full PG attribute matrix (superuser, createdb, createrole, inherit, login, replication, bypass_rls, connection_limit, valid_until), plus role membership via inline IN ROLE or GRANT role TO target. Passwords intentionally not modeled.
  • Two new universal lint rules:
    • role-loses-superuser (warning) — fires on ALTER ROLE … NOSUPERUSER when the role had superuser.
    • role-membership-cycle (error) — detects cycles in the projected post-apply membership graph; pre-empts PG's apply-time rejection.
  • Conformance harness — new authoring = "cluster" mode + seven fixtures under cases/cluster/roles/.
  • Property testsarbitrary_role_attributes, arbitrary_cluster_catalog generators with cycle-free membership; diff round-trip invariant: diff_cluster(A, B) applied to A yields B modulo canonicalization.

Catalog reader

  • New read_cluster_catalog(querier, bootstrap_roles) querying pg_authid + pg_auth_members. Filters pg_* predefined roles and caller-supplied bootstrap roles.

Known v0.3.0 gaps (closing in 0.3.x)

  • Cluster apply: reads plan.sql and runs each statement in a transaction. intent.toml destructive-step gates and apply-log tracking are deferred.
  • No advisory lock during cluster apply; concurrent applies are not protected.
  • Object-level GRANT/REVOKE (per-DB) lands in v0.3.1.
  • RLS policies (per-DB) land in v0.3.2.

[0.2.1] — 2026-05-21

Added

  • Per-column TOAST storageSTORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } is now a managed Column attribute. Source parser accepts both inline (col text STORAGE EXTERNAL, PG 16+ syntax) and ALTER COLUMN SET STORAGE forms. Differ emits non-destructive SET STORAGE steps; canon strips type-default values so explicit and implicit defaults are equivalent.
  • Per-column TOAST compressionCOMPRESSION { pglz | lz4 } is now a managed attribute. None preserves the cluster default_toast_compression GUC; explicit pglz or lz4 overrides it. SET COMPRESSION DEFAULT round-trips through the parser as None.
  • Two new lint rules (surfaced as Plan.advisory_findings and printed by pgevolve plan to stderr):
    • storage-downgrade-not-retroactive — warns when a SET STORAGE change reduces toastability (e.g. EXTERNAL → MAIN), since existing TOASTed values aren't rewritten until UPDATE or VACUUM FULL.
    • compression-change-not-retroactive — warns on any compression change for the same reason.

Catalog reader

  • COLUMNS_QUERY now selects attstorage and attcompression from pg_attribute. No per-version split; both columns are present in PG 14+ (the project MSRV).

Conformance

  • Five new fixtures under objects/columns/: set-storage-external, set-storage-plain-warning, set-compression-lz4, create-table-with-storage, set-storage-type-default-noop.

0.2.0 — 2026-05-21

Extends the v0.1 surface with views, materialized views, user-defined types, functions/procedures, extensions, triggers, and declarative partitioning as fully-managed objects. The differ, planner, linter, conformance suite, and property tests all cover the new object kinds. Ships alongside a project constitution (docs/CONSTITUTION.md), cargo-deny-enforced license + advisory policy (deny.toml), CLAUDE.md agent guidance, and shadow validation for view bodies (T13).

Added — internal architecture (2026-05-19)

  • pgevolve-core-macros crate — internal proc-macro crate exposing #[derive(DiffMacro)]. Most IR structs (Schema, Sequence, Column, Constraint, ForeignKey, Procedure, Index) now derive their Diff impl with #[diff(skip)] / #[diff(via_debug)] / #[diff(nested)] field attributes. Hand-written impls retained where they have non-trivial logic (Catalog, Function, Table, View, MaterializedView, UserType, and the enum impls). Removes ~250 lines of mechanical boilerplate.
  • ir::canon pipeline — every IR-value normalization rule moved into a single ordered pipeline. Four named passes: filter_pg_defaults (sequence min/max, function cost/rows, pg_catalog.default collation → None); sentinel_view_columns (view/MV column types → shared sentinel); renumber_enum_sort_orders (enum sort_order → 1.0, 2.0, 3.0, …); sort_and_dedupe (canonical-key sort + duplicate detection). Catalog::canonicalize is now a thin wrapper. Catalog reader and source builders are kept "raw" — they no longer filter PG defaults. The rule for the next PG-default surprise lives in one place.
  • pgevolve::api::build_plan — library entry point that runs the full parse→introspect→diff→order→rewrite→group→assemble pipeline and returns a Plan value. No println!, no waiver-prompt UX, no --shadow-validate, no on-disk plan directory.
  • pgevolve::executor::apply_plan(&Plan, …) — sibling to apply(plan_dir, …) that takes an in-memory Plan. The disk-based apply is now a thin shim that calls read_plan_dir then delegates. CLI plan/apply commands are thin wrappers over api::build_plan / executor::apply_plan plus CLI UX.
  • Plan::approve_all_intents — helper on pgevolve_core::plan::Plan for test harnesses building plans programmatically. Production apply still requires explicit intent.toml approval.

Changed — conformance suite (2026-05-19)

  • Conformance Layer 4 (apply roundtrip) now runs in-process via pgevolve::api::build_plan + pgevolve::executor::apply_plan. The subprocess scaffolding (cargo_bin, run_pgevolve, plan_and_locate, patch_intent_toml_approve_all, write_project) is gone — ~150 fewer lines in crates/pgevolve-conformance/src/assertions/apply.rs. Faster (no per-fixture binary rebuild + spawn) and easier to debug (assertions can inspect the Plan value rather than its on-disk rendering).

Added — shadow validation for view bodies (T13)

  • --shadow-validate now cross-checks view + materialized view bodies against an ephemeral Postgres. render_catalog emits views/MVs (after sequences); cross_check queries pg_get_viewdef for each declared view/MV, re-parses through the source canonicalizer, and compares against the source IR's body canonical text + body_dependencies (the latter walked via pg_rewrite → pg_depend → pg_class). Defaults to warnings; --shadow-strict promotes mismatches to errors.
  • New crates/pgevolve-core/src/render/view.rs (render_view, render_materialized_view).
  • Docker-gated integration tests in crates/pgevolve/tests/shadow_validate_views.rs.
  • Closes the deferred T13 plan from sub-spec #1 (views/MVs).

Added — IR (functions and procedures)

  • Function { qname, args, arg_types_normalized, return_type, language, body, body_dependencies, volatility, strict, security, parallel, leakproof, cost, rows, comment } flat IR type in pgevolve-core::ir::function.
  • Procedure { qname, args, language, body, body_dependencies, security, commits_in_body, comment } flat IR type in pgevolve-core::ir::procedure.
  • FunctionArg { name, mode: ArgMode, ty, default } — argument declaration with IN/OUT/INOUT/VARIADIC modes.
  • NormalizedArgTypes { types, canonical_hash } — BLAKE3 hash over comma-joined IN/INOUT/VARIADIC type strings; the function identity disambiguator for overloads.
  • ReturnTypeScalar, SetOf, Table { columns }, Trigger, EventTrigger, Void.
  • FunctionLanguageSql | PlPgSql.
  • Catalog::functions: Vec<Function> and Catalog::procedures: Vec<Procedure> — flat collections, sorted by (qname, arg_types_normalized) / qname after canonicalize().

Added — pipeline (functions and procedures)

  • Source parserCREATE FUNCTION and CREATE PROCEDURE parse into the Function / Procedure IR. Full attribute matrix (volatility, strict, security, parallel, leakproof, cost, rows). Dollar-quote body extraction for both SQL and PL/pgSQL languages.
  • PL/pgSQL body parser (parse/builder/plpgsql.rs) — wraps the body in a synthetic CREATE FUNCTION and calls pg_query::parse_plpgsql. Extracts static embedded SQL dep edges (PLpgSQL_stmt_execsql), detects COMMIT/ROLLBACK nodes for commits_in_body, and scans -- @pgevolve dep: directives for dynamic SQL.
  • AST resolution — validates routine body dep edges against the catalog; unresolved managed-schema references surface as warnings.
  • Catalog reader — queries pg_proc (with pg_language, pg_type, pg_namespace) to reconstruct Function and Procedure from a live database. Handles multi-arg functions, OUT args, overloads, and all attribute columns.
  • DifferFunctionChange variants: Create, Drop, OrReplace, ReplaceWithCascade, CommentOn. ProcedureChange variants: Create, Drop, OrReplace, CommentOn.
  • OR-REPLACE compatibility predicate (function_can_or_replace) — returns true when language, return type, and OUT/INOUT parameters are all unchanged; falls back to ReplaceWithCascade otherwise.
  • Planner — 6 new step kinds: CreateOrReplaceFunction, DropFunction, CommentOnFunction, CreateOrReplaceProcedure, DropProcedure, CommentOnProcedure. Procedures with commits_in_body = true are placed in non-transactional steps.
  • NodeId::Function / NodeId::Procedure — added to the dep graph; body dep edges drive correct creation/drop ordering relative to their referenced tables, views, and types.

Added — lint rules (functions and procedures)

  • plpgsql-dynamic-sql (Error) — PL/pgSQL body uses EXECUTE without a -- @pgevolve dep: directive.
  • procedure-contains-commit (Warning) — procedure body contains COMMIT or ROLLBACK; runs with transactional=OutsideTransaction.
  • function-references-unmanaged-schema (Warning) — routine body dep edge targets an unmanaged schema.

Added — tests (functions and procedures)

  • ~22 conformance fixtures (Tier C): objects/functions/ and objects/procedures/ covering SQL functions, PL/pgSQL functions, procedures, overloads, dep-edge extraction, ReplaceWithCascade, and all three lint rules.
  • Property test plpgsql_canonicalization_is_idempotent (#[ignore], pure, no Docker) — for each body in the representative PLPGSQL_BODIES corpus, parse_routine_body → canonical_text → re-parse → canonical_text produces byte-identical output. Closes the round-trip invariant the differ relies on.

Added — IR (triggers)

  • Trigger { qname, table_name, function_name, timing: TriggerTiming, events: Vec<TriggerEvent>, for_each: ForEach, when_clause: Option<NormalizedExpr>, update_columns: Vec<Identifier>, referencing: Option<TransitionTables>, constraint: bool, deferrable: bool, initially_deferred: bool, comment: Option<String> } flat IR type in pgevolve-core::ir::trigger.
  • TriggerTimingBefore | After | InsteadOf.
  • TriggerEventInsert | Update | Delete | Truncate.
  • ForEachRow | Statement.
  • TransitionTables { old_table: Option<Identifier>, new_table: Option<Identifier> } — REFERENCING clause for transition tables.
  • Catalog::triggers: Vec<Trigger> flat collection, sorted by (table_name, qname) after canonicalize().

Added — pipeline (triggers)

  • Source parserCREATE [CONSTRAINT] TRIGGER parses into the Trigger IR. BEFORE/AFTER/INSTEAD OF timing, INSERT/UPDATE/DELETE/TRUNCATE events, FOR EACH ROW/STATEMENT, WHEN clause (as NormalizedExpr), UPDATE OF column list, and REFERENCING transition tables all modeled. ALTER TRIGGER in source files rejected at statement classification.
  • COMMENT ON TRIGGER parser arm — recognized alongside COMMENT ON FUNCTION and COMMENT ON EXTENSION in the comment-statement path.
  • Catalog reader — queries pg_trigger joined with pg_class, pg_namespace, and pg_description. Filters: NOT tgisinternal (system-generated triggers excluded); NOT EXISTS (pg_depend deptype='e') (extension-owned triggers excluded). Reconstructs all modeled fields including constraint, deferrable, and initially-deferred flags.
  • DifferTriggerChange variants: Create, Drop, CommentOn. Any structural difference (timing, events, for-each, when-clause, function, columns, transition tables, constraint flags) emits Drop + Create — there is no ALTER TRIGGER for body-level changes. CommentOn is comment-only.
  • Planner — 3 new step kinds: CreateTrigger, DropTrigger (destructive; intent required), CommentOnTrigger. DropTrigger is placed in the same destructive bucket as DropTable and DropFunction.
  • NodeId::Trigger — added to the dep graph; Trigger → Table/View/MV edges ensure the target relation exists before the trigger is created; Trigger → Function edges ensure the trigger function exists before the trigger fires.

Added — lint rules (triggers)

  • trigger-references-unmanaged-table (Warning) — trigger's target relation is not in any managed schema.
  • trigger-references-unmanaged-function (Warning) — trigger function is not in any managed schema.

Added — tests (triggers)

  • Conformance fixtures (Tier C): objects/triggers/ covering create/drop/comment, BEFORE/AFTER/INSTEAD OF variants, ROW vs STATEMENT, WHEN clause, UPDATE OF columns, REFERENCING transition tables, CONSTRAINT TRIGGER with DEFERRABLE, and both lint rules.

Added — IR (partitioning)

  • partition_by: Option<PartitionBy> and partition_of: Option<PartitionOf> fields added to Table in pgevolve-core::ir::table.
  • New ir/partition.rs module:
    • PartitionBy { strategy: PartitionStrategy, columns: Vec<PartitionColumn> } — the PARTITION BY clause on a partitioned parent.
    • PartitionStrategyRange | List | Hash.
    • PartitionColumn { kind: PartitionColumnKind, collation: Option<QualifiedName>, opclass: Option<QualifiedName> } — a single partition key element with optional collation and opclass overrides.
    • PartitionColumnKindColumn(Identifier) | Expr(NormalizedExpr).
    • PartitionOf { parent: QualifiedName, bounds: PartitionBounds } — the PARTITION OF parent FOR VALUES … clause on a partition child.
    • PartitionBoundsRange { from, to } | List { values } | Hash { modulus, remainder } | Default.
    • BoundDatumLiteral(NormalizedExpr) | MinValue | MaxValue.

Added — pipeline (partitioning)

  • Source Form 1CREATE TABLE child PARTITION OF parent FOR VALUES … parsed directly into Table { partition_of: Some(…) }. The parent's key is inferred from its partition_by.
  • Source Form 2 — standalone CREATE TABLE child PARTITION OF parent FOR VALUES … in a separate file. Identical IR as Form 1.
  • Source Form 3ALTER TABLE parent ATTACH PARTITION child FOR VALUES … combined with a standalone child CREATE TABLE (no inline PARTITION OF). The parser merges the attach statement into the child's partition_of, producing the same IR as Form 2. Equivalence of Form 2 and Form 3 is verified by a conformance fixture.
  • Sub-partitioning — a Table may have both partition_by (it is itself a partitioned parent) and partition_of (it is a partition of another parent).
  • Catalog reader — two new queries: SELECT_PARTITIONED_TABLES (pg_class.relkind='p' + pg_get_partkeydef) reads partitioned-parent keys; SELECT_PARTITIONS (relispartition=true + pg_get_expr(relpartbound, oid)) reads partition children and re-parses the bounds text. Both filters apply NOT EXISTS (pg_depend deptype='e').
  • DifferTableChange::AttachPartition { parent, child, bounds } and TableChange::DetachPartition { parent, child } variants. Bounds change on a stable parent → DetachPartition + AttachPartition. Parent partition_by rekey → UnsupportedDiff (no safe in-place path in Postgres). Column and constraint diff is suppressed when either the source or target side is a partition (columns are inherited from the parent).
  • Planner — 2 new step kinds: AttachPartition (non-destructive) and DetachPartition (destructive; intent required). AttachPartition is placed in the same post-create ordering bucket as CreateIndex; DetachPartition is placed in the same destructive bucket as DropTable.
  • NodeId dep edge — child partition → parent table (DepSource::Structural). Ensures the parent exists (and has its partition_by applied) before the child is attached.

Added — lint rules (partitioning)

  • partition-references-unmanaged-parent (Error) — a partition child's partition_of.parent schema is not in [managed].schemas. Prevents silent attach failures when the parent table is outside pgevolve's control.

Added — tests (partitioning)

  • 14 conformance fixtures (Tier C): objects/partitions/ covering:
    • create-range-parent-and-two-partitions — RANGE parent with FOR VALUES FROM … TO ….
    • create-list-parent — LIST parent.
    • create-hash-parent-and-partitions — HASH parent with FOR VALUES WITH (MODULUS m, REMAINDER r).
    • create-default-partitionDEFAULT partition.
    • add-partition — attaching a new partition to an existing parent.
    • drop-partition — detach + destructive intent path.
    • replace-bounds — bounds change → detach + reattach.
    • attach-existing-standalone — Form 3 attach of a pre-existing standalone table.
    • attach-form-vs-declarative-form-equivalent — Form 2 vs Form 3 produce identical plans.
    • detach-to-standalone — detach leaves the child as a standalone table.
    • subpartitioned — sub-partitioned child (both partition_by and partition_of set).
    • lint-unmanaged-parentpartition-references-unmanaged-parent fires when parent is in an unmanaged schema.
    • Reject path fixtures for invalid bounds expressions.

Changed — differ (partitioning)

  • diff_tables now skips column and constraint diffing when either the source or the target side of a table pair is a partition (partition_of.is_some()). Partition children inherit their column list from the parent; diffing inherited columns produces spurious changes. The partition bounds themselves are diffed via AttachPartition/DetachPartition instead.

Added — IR (extensions)

  • Extension { name, schema: Option<Identifier>, version: Option<String>, comment: Option<String> } flat IR type in pgevolve-core::ir::extension.
  • Catalog::extensions: Vec<Extension> flat collection. canon::sort_and_dedupe rejects duplicate extension names.

Added — pipeline (extensions)

  • Source parserCREATE EXTENSION [IF NOT EXISTS] name [WITH SCHEMA s] [VERSION 'v'] parses into the Extension IR. CASCADE, FROM old_version, and unknown options rejected with structural errors. ALTER EXTENSION in source files rejected at statement classification. COMMENT ON EXTENSION parsing added.
  • Catalog reader — queries pg_extension joined with pg_namespace and pg_description. The reader for every other object kind (tables, indexes, sequences, functions, types, views, MVs) gains a NOT EXISTS (pg_depend deptype='e') filter so extension-owned objects never appear as drift.
  • DifferExtensionChange variants: Create, Drop, AlterUpdate, ReplaceWithCascade, CommentOn. Source-None for schema, version, or comment means "any catalog value", so unpinned source declarations don't diff against any installed version.
  • Planner — 4 new step kinds: CreateExtension, DropExtension (destructive), AlterExtensionUpdate, CommentOnExtension. Schema changes go through DropExtension + CreateExtension with linked intent.
  • NodeId::Extension — added to the dep graph; Extension → Schema edges force the schema to exist before the extension is created.

Added — lint rules (extensions)

  • extension-version-unpinned (Warning) — CREATE EXTENSION foo; without a VERSION clause.
  • extension-references-unmanaged-schema (Error) — WITH SCHEMA gis but gis isn't in the source catalog.

Added — tests (extensions)

  • 11 conformance fixtures (Tier C): objects/extensions/ covering create/drop/replace/comment paths plus version-pin and version-unpinned no-op cases. scenarios/extension-owned-objects-ignored exercises the pg_depend deptype='e' filter. scenarios/create-order-schema-first verifies the Extension → Schema dep ordering.

Changed — conformance suite (extensions)

  • Apply-layer post-check (assertions::apply) switched from strict canonical_eq to a differ-based convergence check. Source IR with None for schema/version/comment now correctly converges against any catalog reading that has concrete values for those fields.

Added — IR (user-defined types)

  • UserType { qname, kind: UserTypeKind, comment } flat IR type in pgevolve-core::ir::user_type.
  • UserTypeKind::Enum { values: Vec<EnumValue> } — ordered label list with sort_order: f32 mirroring pg_enum.enumsortorder.
  • UserTypeKind::Domain { base, nullable, default, check_constraints, collation } — domain defaults and CHECK expressions use NormalizedExpr for canonical comparison.
  • UserTypeKind::Composite { attributes: Vec<CompositeAttribute> } — each attribute carries name, type, and optional collation.
  • Catalog::types: Vec<UserType> — flat collection, sorted by qname after canonicalize().

Added — pipeline (user-defined types)

  • Source parserCREATE TYPE … AS ENUM, CREATE DOMAIN, CREATE TYPE … AS (…) all parse into the UserType IR. Duplicate labels / attributes rejected at parse time.
  • AST resolutionUserDefined(QualifiedName) column type references resolved against Catalog::types after the source parse pass.
  • Catalog reader — queries pg_type, pg_enum, pg_attribute (for composites), and pg_constraint / pg_attrdef (for domains) to reconstruct UserType from a live database.
  • DifferUserTypeChange variants: Create, Drop, EnumAddValue, EnumRenameValue, DomainAddCheck, DomainDropCheck, DomainSetDefault, DomainSetNotNull, CompositeAddAttribute, CompositeDropAttribute, CompositeAlterAttributeType, CommentOn, ReplaceWithCascade.
  • Compatibility predicatesenum_can_alter_in_place (preserved labels maintain relative order; renames position-paired) and composite_can_alter_in_place (preserved attributes maintain relative order). Both fall back to ReplaceWithCascade when the predicate returns false.
  • Planner — 12 new step kinds: CreateType, DropType, AlterTypeAddValue, AlterTypeRenameValue, AlterDomainAddConstraint, AlterDomainDropConstraint, AlterDomainSetDefault, AlterDomainSetNotNull, AlterTypeAddAttribute, AlterTypeDropAttribute, AlterTypeAlterAttributeType, CommentOnType.
  • NodeId::Type — added to the dep graph; edges from type → column (column's ColumnType::UserDefined) and type → type (domain base type) drive correct creation/drop ordering.

Added — lint rules (user-defined types)

  • type-shadows-table (Error) — a user-defined type shares a qualified name with a table, view, or MV.
  • enum-value-collision (Error) — an enum type declares duplicate value labels.
  • composite-attribute-collision (Error) — a composite type declares duplicate attribute names.
  • domain-check-references-unmanaged-type (Warning) — a domain's CHECK expression references a schema outside [managed].schemas.

Added — tests (user-defined types)

  • 20 conformance fixtures (Tier C): objects/enums/ (8), objects/domains/ (6), objects/composites/ (4), objects/user_type_lints/ (2).
  • Property test enum_add_value_preserves_existing_values (#[ignore], pure, no Docker) — for any random initial label list and a new distinct label, diff_user_types emits exactly one EnumAddValue change.

Added — IR (views and materialized views)

  • View and MaterializedView flat IR types in pgevolve-core::ir::view.
  • ViewColumn — named column with resolved type and optional comment; used by both views and MVs.
  • body_canonical: NormalizedBody — parsed-and-deparsed SELECT body in canonical form. Enables cosmetically-different but semantically-identical view bodies to diff equal.
  • body_dependencies: Vec<DepEdge> — dependency edges extracted from the body AST with DepSource::AstExtracted provenance. Powers the dependent-recreation walk and the view-body-references-unmanaged-schema lint.
  • security_barrier and security_invoker reloptions on View.

Added — pipeline (views and materialized views)

  • AST canonicalization pass (parse/ast_canon.rs) — runs after source parse; calls NormalizedBody::from_sql on each view body, extracts DepEdges, resolves references against the provisional catalog, and fills in column types.
  • Catalog readerread_views and read_materialized_views query pg_views / pg_matviews, call pg_get_viewdef, and feed the result through NormalizedBody::from_sql. Source-side and catalog-side canonical texts are directly comparable.
  • DifferViewChange and MvChange variants. OR-REPLACE compatibility predicate (body_is_or_replace_compatible) determines whether a body change emits CREATE OR REPLACE VIEW (compatible) or DROP + CREATE (incompatible).
  • Planner — 7 new step kinds: CreateView, DropView, CreateMaterializedView, DropMaterializedView, RefreshMaterializedView, AlterViewSetReloption, CommentOnView.
  • Online rewritesREFRESH MATERIALIZED VIEW CONCURRENTLY upgrade (when unique index present); dependent-view recreation cascade (recreate_views::extend_with_dependent_recreations).

Added — configuration

  • [planner.online_rewrites].refresh_mv_concurrently (default true) — upgrade REFRESH to REFRESH CONCURRENTLY when the MV has a unique index.
  • [planner.online_rewrites].view_drop_create_dependents (default true) — cascade dependent-view recreations; set false to error instead of auto-cascading.
  • [[step_override]] rows in intent.toml — suppress individual plan steps by kind + target.

Added — lint rules (views and materialized views)

  • view-shadows-table (Error) — a view or MV shares a qualified name with a managed table.
  • mv-no-unique-index (Warning) — an MV has no unique index; REFRESH CONCURRENTLY unavailable.
  • view-body-references-unmanaged-schema (Warning) — a view body dependency edge points to an unmanaged schema.

Added — tests (views and materialized views)

  • 15 conformance fixtures (Tier C): objects/views/ (8), objects/materialized_views/ (6), intent/drop-view-requires-intent (1), scenarios/dependency-chains/ (2).
  • Property test view_canonicalization_closed_under_pg_rewrite (#[ignore], Docker-gated) — verifies NormalizedBody::from_sql closure under the PG rewrite for a fixed set of representative view bodies.
  • Property test arb_view_dependency_graph (#[ignore], Docker-free) — generates random view DAGs over a generated table corpus, mutates a leaf-table column, and asserts the resulting plan recreates exactly the transitively-dependent views in valid topological order. Closes the deferred test from sub-spec #1 §12.2. New arbitrary_view_catalog generator in pgevolve-testkit.

0.1.0 — 2026-05-17

First tagged release. The v0.1 surface manages schemas, tables (with columns/constraints/comments), indexes, and sequences against Postgres 14, 15, 16, and 17.

Added — pipeline

  • Parser (pgevolve-core::parse) — *.sql → IR via pg_query. Tracks source locations for every parsed object. Recognises -- @pgevolve directives (schema=…, dep:…).
  • AST resolution pass — runs between parse and canonicalize. Validates structural references (FKs against declared tables; default-using sequences against declared sequences). Surfaces unresolved references with source-located errors before any DB touch.
  • Catalog reader (pgevolve-core::catalog) — live PG → IR via per-PG-major SQL strings and a sync CatalogQuerier trait. Returns (Catalog, DriftReport). The drift report captures NOT VALID constraints and INVALID indexes for auto-recovery.
  • Differ (pgevolve-core::diff) — pair-by-qname, structural; ChangeSet plus higher-level Change enum. Drift entries fold into Change::ValidateConstraint / Change::RecreateIndex.
  • Planner (pgevolve-core::plan) — order → rewrite → group → wrap. Deterministic topo sort (Kahn + min-heap tiebreak). FK cycle extraction via DeferredFkAdd. Four online rewrites: CREATE INDEX CONCURRENTLY, FK NOT VALID + VALIDATE, CHECK NOT VALID + VALIDATE, SET NOT NULL via CHECK pattern.
  • Plan format — three-file directory (plan.sql, intent.toml, manifest.toml); deterministic PlanId (BLAKE3 over bincoded canonical IRs); [[intent]] rows with approved: bool; [[lint_waiver]] rows to acknowledge LintAtPlan findings; RecordedFinding rows in manifest.toml for apply-time waiver recheck.
  • Executor (pgevolve::executor) — bootstrap, advisory lock, per-step audit, preflight (target identity, drift recheck, intent approval, lint-waiver recheck). Per-group transactional or autocommit execution.
  • Linter (pgevolve-core::lint) — universal rules + four built-in layout profiles (schema-mirror, kind-grouped, feature-grouped, free-form) plus a regex+assertion custom-profile mechanism. New Severity::LintAtPlan tier (gates plan with exit code 2 unless waived) and a new column-position-drift rule.

Added — IR

  • Top-level types: Catalog, Schema, Table, Column, Constraint, Index, Sequence, plus ColumnType, DefaultExpr, NormalizedExpr, NormalizedBody (the statement-scope counterpart for v0.2 body-bearing objects).
  • Dep-graph types: DepEdge { from, to, source: DepSource } with Structural (v0.1) + AstExtracted / AstDeclared (v0.2) provenance.

Added — CLI

  • pgevolve init — scaffold project files.
  • pgevolve lint [--format human|json] — universal + layout-profile rules.
  • pgevolve validate [--shadow] [--shadow-validate] [--shadow-strict] — source-tree validation.
  • pgevolve diff --db <env> [--format human|json|sql] [--shadow-validate] — print the change set.
  • pgevolve plan --db <env> [-o <dir>] [--shadow-validate] — write a plan directory. Refuses with exit 2 on unwaived LintAtPlan findings.
  • pgevolve apply <plan-dir> --db <env> — execute a plan.
  • pgevolve status --db <env> — recent applies and per-step state.
  • pgevolve dump --db <env> -o <dir> — introspect a live DB and write a fully-populated schema/ tree via the new IR → SQL emitter (pgevolve-core::render).
  • pgevolve bootstrap --db <env> — install/upgrade the metadata schema.
  • pgevolve graph [--graph-format dot|mermaid] [-o <path>] — render the dep graph.
  • pgevolve doctor --db <env> — project health check (drift, dangling intents, recent apply failures).
  • pgevolve rewrite-table <qname> --db <env> --confirm-rewrite — skeleton; full implementation lands with a v0.2 sub-spec.

Added — config

  • pgevolve.toml with [project], [managed], [planner], [planner.online_rewrites], [environments.<env>], and a new [shadow] block (backend = auto | testcontainers | dsn; per-backend url, url_env, reset, extensions, postgres_version).

Added — test infrastructure

  • pgevolve-testkitEphemeralPostgres, PgCatalogQuerier, MigrationFixture, IR generator + mutator, TestPgBackend pluggable backend trait with testcontainers / compose / dsn impls (selected via PGEVOLVE_TEST_PG_MODE).
  • pgevolve-conformance — Tier C suite with five fixture authoring subtrees (objects/, scenarios/, intent/, failure/, regressions/) and nine assertion layers (L1 diff, L2 plan structural, L3 plan-SQL golden, L4 apply roundtrip, L5 minimality, L6 no-collateral-damage, L7 intent shape, L8 dep-graph golden, L9 topological order). Runtime budgets enforced per-fixture and suite-total.
  • dev/docker-compose.pg.yml — PG 14/15/16/17 on stable ports for fast local test iteration in compose mode.
  • cargo xtask subcommands: bless --conformance, coverage --check|--gaps, fixture-cost, capture-regression, verify-regression, property-status, diagnose-pg-version.

Added — workflows

  • ci.yml — fmt, clippy, unit + tier-2 tests, conformance matrix across PG 14/15/16/17, property-status compliance gate.
  • property-tests.yml — nightly property test runs with auto-capture of failures into regressions/.
  • soak.yml — weekly high-case property runs.

Known limitations of v0.1

  • pgevolve rewrite-table is a CLI skeleton — invoking it errors with "not yet implemented." The implementation lands with a v0.2 sub-spec (partitioning / column-type-change).
  • pgevolve dump writes a single schema.sql file. Multi-file layout following [project].layout_profile is deferred to v0.1.x+.
  • Views, materialized views, functions, procedures, triggers, user-defined types, extensions, and declarative partitioning are not in v0.1; they land per v0.2 sub-spec series.
  • --shadow-validate is a scaffold cross-check. v0.1 has no body- bearing objects so the cross-check has nothing to do beyond a trivial structural-edge count; v0.2 sub-specs deepen it.