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
CHECKwhose expression contained a string literal — e.g.CHECK (email <> '')— produced a perpetual spurious diff against a live database, becausepg_get_constraintdefdeparses the literal with an explicit::textcast (email <> ''::text) on PG 14–17 (PG 18 dropped it) while the source parse keepsemail <> ''. 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-literalA_Constcast 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
CHECKwas 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 theChooseIndexNameport: a column-level check →{table}_{col}_check, a table-level check →{table}_check. This also corrects the v0.4.5LIKE INCLUDING CONSTRAINTSbehavior: Postgres copies CHECK constraint names verbatim onLIKE(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_nametruncated only the table-name component when a generated name exceededNAMEDATALEN(63 bytes); Postgres'smakeObjectNameshrinks 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).LIKEclauses 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 followingINCLUDINGoptions are supported:DEFAULTS,IDENTITY,GENERATED,STORAGE,COMPRESSION,COMMENTS(table and column),CONSTRAINTS(CHECK only — PK/UNIQUE belong toINCLUDING INDEXES),INDEXES(PK/UNIQUE constraints plus plainCREATE INDEXindexes),STATISTICS(extended statistics), andINCLUDING 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'sChooseIndexName(choose_name.rs), producing names that match live PG 14–18 output (verified by conformance fixtures). Known limitations: (1)EXCLUDEconstraints 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 toOther(#41). A column declaredinterval(6),interval hour, orinterval hour to minuteparsed without error but was silently stored asColumnType::Other, producing a perpetual spuriousALTER COLUMN TYPEdiff against a live database (the catalog'sformat_typeemits the canonical form, which does map toInterval).pg_queryinjects the PGINTERVAL_MASKfields bitmask as the first typmod argument (interval(6)→[32767, 6]); the renderer now decodes that bitmask and emits the canonical string, andparse_canonicalgained arms for theinterval <fields>andinterval <fields>(p)forms, so the source and catalog paths converge to the sameColumnType::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 sourceUserDefinedcould never match the catalog'sOther. Schema-qualifiedgeometry/geographydeclarations 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
UNIQUEconstraint naming (#44). An inlineUNIQUE (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 sameChooseIndexNameport used by the LIKE path, with per-table collision counters.PRIMARY KEY({table}_pkey) and explicitly-named constraints are unchanged. LIKE INCLUDING CONSTRAINTSre-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 aLIKEclone matches an equivalent hand-written table.
Internal
- Extracted a shared
resolve_orderhelper for the LIKE dependency-ordering passes and lockedchoose_nametruncation behavior at theNAMEDATALENboundary (#46).
[0.4.4] — 2026-06-18
Added
- Recursive views (
WITH RECURSIVE). Confirmed support forCREATE RECURSIVE VIEW,CREATE VIEW … WITH RECURSIVE, recursive materialized views, andCREATE OR REPLACEof 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 declaredgeometry(Point,4326)failed to parse with "could not stringify type name" (#40), blocking any PostGIS-backed schema atpgevolve validate. Type modifiers are anexpr_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 spuriousALTER COLUMN TYPEdiff (the source parser lowercases the subtype whilepg_catalog.format_typeemits canonical TitleCase, and the catch-all type compares by exact string). Baregeometry/geography, the array formgeometry(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-qualifiedpublic.geometry(…)drops its typmod (#42).
[0.4.3] — 2026-06-08
Added
- Table
TABLESPACEplacement.CREATE TABLE … TABLESPACE <ts>andCREATE TABLE … PARTITION OF … TABLESPACE <ts>on regular tables, partitioned parents, and partition children.ALTER TABLE … SET TABLESPACEis RequiresApproval on a leaf table (full rewrite +ACCESS EXCLUSIVElock) and Safe on a partitioned parent (metadata-only, no rewrite).pg_defaultis normalized to the implicit default — declaringTABLESPACE pg_defaultproduces no spurious diff. Per-partitionTABLESPACEoverrides are tracked independently of the parent on theTable.tablespacefield. TEXT SEARCH DICTIONARYandTEXT SEARCH CONFIGURATIONsupport. ManagedTEXT SEARCH DICTIONARY(TEMPLATE reference + OPTIONS list;CREATE,ALTER … (options),ALTER … OWNER TO,DROP,COMMENT ON) andTEXT 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-placeALTERfor 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 topg_catalog.COPY=onCREATE TEXT SEARCH CONFIGURATIONis out of scope. Known limitation: a functional index or generated column whose expression callsto_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
CASTsupport. Custom casts viaCREATE CAST(WITH FUNCTION/WITHOUT FUNCTION/WITH INOUT;EXPLICIT/ASSIGNMENT/IMPLICITcontexts),DROP, andCOMMENT ON. Managed: casts are auto-dropped when removed from source.WITH FUNCTIONis constrained to managed SQL/plpgsql functions — source rejects references to unmanaged or built-in functions via thecast-references-unmanaged-functionlint. Built-in/system casts (pg_cast.oid < 16384) and extension-owned casts are excluded from introspection. NoALTER CASTin Postgres, so any structural change is drop + create; identity is(source_type, target_type).
[0.4.1] — 2026-06-07
Added
AGGREGATEsupport. User-defined aggregates viaCREATE AGGREGATE(ordinary form: SFUNC + STYPE + optional FINALFUNC/INITCOND),ALTER … OWNER TO,DROP, andCOMMENT ON. State and final functions must be managed SQL/plpgsql functions; source rejects references to unmanaged or built-in functions via theaggregate-references-unmanaged-functionlint. 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 TRIGGERsupport.CREATE EVENT TRIGGER(withON <event>andWHEN TAG IN (...)filter),ALTER … ENABLE/DISABLE/ENABLE REPLICA/ENABLE ALWAYS,ALTER … OWNER TO,DROP, andCOMMENT ON. Database-global with a lenient drop policy (unmanaged event triggers surface via theunmanaged-event-triggerlint, never auto-dropped), mirroring publications/subscriptions. Extension-owned event triggers are excluded from introspection.TABLESPACEsupport (cluster object). Cluster-levelTABLESPACEmanagement via thepgevolve cluster …surface:CREATE(withOWNER,LOCATION,WITH (options)),ALTER … OWNER TO,ALTER … SET (options),DROP(intent-gated), andCOMMENT ON. Lenient owner + lenient options.LOCATIONis immutable, so a location drift surfaces via thetablespace-location-driftadvisory rather than a destructive recreate. Filesystem-layout management (directory creation, mount points) stays out of scope. Tablespaces are declared in atablespaces/cluster-source directory.- Table access method support.
CREATE TABLE … USING <am>is parsed and rendered, with the access method read frompg_class.relam. The built-inheapis normalized to the implicit default (declaringUSING heapis a no-op). An access-method change on an existing table surfaces via thetable-access-method-changeadvisory — pgevolve does not auto-rewrite tables (ALTER TABLE … SET ACCESS METHODis PG 15+ and implies a full rewrite). SubscriptionOptions.connect— CREATE-only directive. New IR field that maps to PG'sCREATE SUBSCRIPTION ... WITH (connect = ...)option.connect = falsecreates 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_subscriptiondoesn't store it — so the catalog reader always returnsNoneand the diff never emits an ALTER for it. Closes #14.
Changed
- Cluster apply reaches per-DB parity.
pgevolve cluster applynow bootstrapspgevolvemetadata, acquires the singleton advisory lock, runs cluster preflight (identity match + intent approval), writes anapply_logrow, executes via the per-DB group executor, and closes the audit row.pgevolve cluster planwrites the canonical 3-file plan layout (structuredplan.sqlheaders +intent.toml+manifest.tomlwithtarget_identity). The clustertarget_identityformat iscluster:{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 INDEXemits theWITH (...)storage clause. The clause was previously dropped at render time; index reloptions (fillfactor,fastupdate, etc.) needed a follow-upALTER 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 emitsALTER COLUMN ... SET STORAGEas a follow-up step, restoring PG 14/15 compatibility. CREATE SUBSCRIPTIONruns outside transaction whencreate_slot != false. PG forbids tx-blockCREATE SUBSCRIPTIONwithcreate_slot = true(the default). Planner now setsTransactionConstraint::OutsideTransactionbased on subscription options. Closes #11.DROP SUBSCRIPTIONruns 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.streamingsubscription option emits boolean form on PG ≤15. PG ≤15 accepts onlytrue/falseforstreaming; theparallelkeyword 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.subsubstreamis returned via::textcast asf/t/false/true/parallel. Closes #28. - REVOKE statements emit before GRANT in the same diff step. PG's
REVOKE priv FROM roleremoves the privilege regardless ofgrant_option; ordering GRANT-before-REVOKE silently cancelled a just-emitted grant. Affectswith_grant_optionupgrades and downgrades on tables, sequences, schemas, views, materialized views, functions, procedures, and types. Closes #33. default_privilegesdiff 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_privilegesrules 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 keepsGRANT SELECT (id)), the diff previously emittedREVOKE SELECT (id, price)— naming the droppedprice— 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 SCHEMAcascade-emitsDROP COLLATION. The diff previously left dependent collations incatalog.collations, tripping PG error2BP01on apply. The dependency graph now also carries a Collation → Schema edge so the drops sort correctly. Closes #38.change_node()returns the correctNodeIdvariant per object kind. Owner/grant changes on sequences/schemas/views/types/MVs/procedures/collations previously defaulted toNodeId::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_privilegescanonical IR. PG silently injects(grantee = target_role, full self-priv)into everypg_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 entirepg_default_aclrows, manifesting aspresent vs removeddivergences in round-trip tests. Closes #34 + #37. default_privilegesimplicit-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 asdefault_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 emptySelectivescopes, so it can never appear in source). The catalog reader previously failed the entire introspection withempty 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.) Rangetypesubtype_opclassandcollationcanonicalize to None when matching PG defaults. PG resolves and stores the default opclass (e.g.timestamptz_ops) and thepg_catalog.defaultcollation 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 == ownerfrom tables/sequences/schemas/views/etc.; source IR now matches symmetrically. The asymmetry causeddiff(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_privilegesrules with empty grant sets stripped in canon. PG has no DDL form for a zero-grant default-privilege rule; the rule only materializes inpg_default_aclwhen at least one grant is in effect. Canon now removes empty-grant rules from source IR so they don't show aspresent vs removeddivergences.- RLS policy predicates respect PG's per-command matrix.
FOR INSERTpolicies use onlyWITH CHECK(PG rejectsUSINGfor INSERT with error42601);FOR SELECT/DELETEuse onlyUSING;FOR UPDATE/ALLuse both. Closes #22.
Test infrastructure (no user-visible change)
- Soak workflow now sets
TEST_PWDso 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 SCHEMAform; mutation cascades clean dependent references ondrop_schema(default privileges, types, statistics, views, collations) anddrop_column(constraints, grants, statistics, indexes); grant generator never combinesWITH GRANT OPTIONwithPUBLIC. - 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_tablenow also drops extended statistics targeting the table and removes the table from publications (dropping any publication left empty);drop_columndrops 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_schemaadditionally drops statistics whose target table lived in the schema and prunes publications referencing the schema or its tables. cargo xtask soak-streaktracks the consecutive-clean-soak-day counter feeding the v1.0 release gate (sub-project C).Plan::from_grouped_with_idconstructor lets callers supply a pre-computedPlanId(used by cluster apply, which hashesClusterCatalograther than per-DBCatalog).
Removed
pgevolve::executor::apply_cluster_steps(public API). Callers that previously built aVec<RawStep>and applied it directly should now build aPlanand useapply_cluster_planinstead.
[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.colllocalefor "PG 16+" — butcolllocalewas introduced in PG 17, not PG 16. PG 15 addedcolliculocale(ICU-only) and PG 17 renamed it tocolllocale(generic, since the newbuiltinprovider also uses it). ICU rows on PG 15 and PG 16 leftcollcollateNULL, sopgevolvereturned emptylc_collatestrings and either crashed on decode or triggered a "column c.colllocale does not exist" SQL error. Three-way per-version SQL: PG 14 (legacycollcollate), PG 15/16 (colliculocale), PG 17/18 (colllocale). Commits09cd563a30d7f3.
- 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 undercrates/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 inpgevolve-core, so the gap surfaced only in CI on the v0.3.8 push. All 28 snapshots re-blessed viacargo xtask bless. Commit09cd563.
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 COLLATIONas a first-class IR object. libc / ICU / PG 17+builtinproviders with thedeterministictoggle, RENAME, andCOMMENT ON COLLATIONall managed. Source may use thelocale = 'X'shorthand or explicitlc_collate+lc_ctype; the IR always stores the latter and the renderer collapses back to the shorthand when they match.pg_collation.collversionis read-only (differ ignores it);ALTER COLLATION … REFRESH VERSIONand the matchingcollation-version-driftlint are deferred to v0.3.9.CREATE TYPE … AS RANGE— additiveUserTypeKind::Rangevariant 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 existingReplaceWithCascadepath — Postgres has no in-place ALTER for range fields. Auto-generated multirange types filtered frompg_typeviatyptype != '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
StepKindvariants for collation lifecycle:CreateCollation,DropCollation,RenameCollation,ReplaceCollation,CommentOnCollation. Dep-graph gainsNodeId::Collationplus 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 underobjects/ranges/(create-simple-int4range,create-with-opclass,create-with-subtype-diff-fn,drop,column-with-range-type), plus thescenarios/column-references-managed-collationcross-kind fixture. The originally-plannedobjects/collations/renamewas substituted toreplace-on-locale-change(rename is exercised in unit + property tests; replace-on-structural-change is the higher-value end-to-end path). The originally-plannedobjects/ranges/create-with-canonical-fnwas substituted tocreate-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
Nonefor range-type optional fields (opclass, collation, canonical, subtype_diff, multirange_type_name) leniently, matching the established "sourceNonemeans unmanaged" pattern. Previously any catalog-side default would spuriously diff against an unpinned source. Surfaces a newresolve_user_defined_typescanon pass that resolvesColumnType::Other(qname)references againstCatalog::typesafter the source parse pass — applies to any user-defined type, not just ranges. (054364a) - ICU collation locale reader on PG 16+ —
pg_collation.colllocalereplacedcollcollate/collctypefor 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_collationform.ALTER COLLATION … REFRESH VERSIONand the matchingcollation-version-driftlint.- Multirange-type customization beyond
multirange_type_name(no per-multirange opclass / canonical fn surface yet). - A first-class
MultirangeIR object distinct fromRange— 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:AlterStatisticSetTargetfor the cheapSET STATISTICS npath;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-viewcheck_option: Option<CheckOption>(Local|Cascaded). Parser folds both SQL-clause and WITH-options forms; differ emitsCREATE OR REPLACE VIEW.- 5 new StepKind variants for STATISTICS + 1 for views:
CreateStatistic,DropStatistic,ReplaceStatistic,AlterStatisticSetTarget,CommentOnStatistic,AlterViewSetCheckOption. unmanaged-statisticlint (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::Pg18variant;catalog/queries/pg18.rsthin 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_versionnow accepts18.
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.
- origin PG16+, failover PG17+). Opaque CONNECTION string with
- 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 plaintextpassword=at parse time. [fixture] applyflag 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+).
PublicationScopesum-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_versionconfig 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.) plusextra: 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
Nonealways means "unmanaged" — never triggersRESET.unmanaged-reloptionlint 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-reloptionlint (warning, waivable).- Source parser for
WITH (...)on CREATE TABLE/INDEX/MATERIALIZED VIEW andALTER ... 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 inlineWITH. Convergent on the next plan run viaALTER … 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 policies —
Tablegainsrls_enabled,rls_forced, andpolicies: Vec<Policy>. Policies carrypermissive,command,roles,using,with_check. USING / WITH CHECK reuseNormalizedExprcanonicalization shared with check constraints. - Source parser:
CREATE POLICY+ fourALTER TABLE ... { ENABLE | DISABLE | FORCE | NO FORCE } ROW LEVEL SECURITYsubcommands.ALTER POLICYandDROP POLICYrejected in source (diff-driven). - Differ: 5 new Change variants (
CreatePolicy,DropPolicy,AlterPolicy,SetTableRowSecurity,SetTableForceRowSecurity). Command-kind changes recreate (DROP + CREATE) because PG doesn't allowALTER POLICYto change the command. - Catalog reader: new
pg_policiesquery +relrowsecurity/relforcerowsecurityon the tables query. - Two lint additions:
grant-references-unknown-role(existing) now also walks policyTOclauses.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 privileges —
Catalog.default_privileges: Vec<DefaultPrivilegeRule>mirroringpg_default_acl. SupportsFOR 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-rolewarning, never silently revoked. - Optional cluster-link —
[cluster].projectin pgevolve.toml validates grantee role names against the linked cluster project'sroles/*.sqlvia thegrant-references-unknown-rolelint (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::grantsmodule decodes PG aclitem text format. All 6 family queries gain<obj>owner+<obj>acl::text[]. Tables/views/MVs also decodepg_attribute.attaclfor 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 ALLexpands per object type. Column-level grants extracted fromAccessPriv.cols. REVOKE rejected in source.
Shadow validate
validate --shadownow respects "unmanaged owner" semantics: when source declaresowner = None(noOWNER 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 USERfully managed —ClusterCatalog.roleswith full PG attribute matrix (superuser, createdb, createrole, inherit, login, replication, bypass_rls, connection_limit, valid_until), plus role membership via inlineIN ROLEorGRANT role TO target. Passwords intentionally not modeled.- Two new universal lint rules:
role-loses-superuser(warning) — fires onALTER ROLE … NOSUPERUSERwhen 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 undercases/cluster/roles/. - Property tests —
arbitrary_role_attributes,arbitrary_cluster_cataloggenerators with cycle-free membership; diff round-trip invariant:diff_cluster(A, B)applied toAyieldsBmodulo canonicalization.
Catalog reader
- New
read_cluster_catalog(querier, bootstrap_roles)queryingpg_authid+pg_auth_members. Filterspg_*predefined roles and caller-supplied bootstrap roles.
Known v0.3.0 gaps (closing in 0.3.x)
- Cluster apply: reads
plan.sqland runs each statement in a transaction.intent.tomldestructive-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 storage —
STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN }is now a managedColumnattribute. Source parser accepts both inline (col text STORAGE EXTERNAL, PG 16+ syntax) andALTER COLUMN SET STORAGEforms. Differ emits non-destructiveSET STORAGEsteps; canon strips type-default values so explicit and implicit defaults are equivalent. - Per-column TOAST compression —
COMPRESSION { pglz | lz4 }is now a managed attribute.Nonepreserves the clusterdefault_toast_compressionGUC; explicitpglzorlz4overrides it.SET COMPRESSION DEFAULTround-trips through the parser asNone. - Two new lint rules (surfaced as
Plan.advisory_findingsand printed bypgevolve planto 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_QUERYnow selectsattstorageandattcompressionfrompg_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-macroscrate — internal proc-macro crate exposing#[derive(DiffMacro)]. Most IR structs (Schema,Sequence,Column,Constraint,ForeignKey,Procedure,Index) now derive theirDiffimpl 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::canonpipeline — 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.defaultcollation →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::canonicalizeis 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 aPlanvalue. Noprintln!, no waiver-prompt UX, no--shadow-validate, no on-disk plan directory.pgevolve::executor::apply_plan(&Plan, …)— sibling toapply(plan_dir, …)that takes an in-memoryPlan. The disk-basedapplyis now a thin shim that callsread_plan_dirthen delegates. CLIplan/applycommands are thin wrappers overapi::build_plan/executor::apply_planplus CLI UX.Plan::approve_all_intents— helper onpgevolve_core::plan::Planfor test harnesses building plans programmatically. Production apply still requires explicitintent.tomlapproval.
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 incrates/pgevolve-conformance/src/assertions/apply.rs. Faster (no per-fixture binary rebuild + spawn) and easier to debug (assertions can inspect thePlanvalue rather than its on-disk rendering).
Added — shadow validation for view bodies (T13)
--shadow-validatenow cross-checks view + materialized view bodies against an ephemeral Postgres.render_catalogemits views/MVs (after sequences);cross_checkqueriespg_get_viewdeffor 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 viapg_rewrite → pg_depend → pg_class). Defaults to warnings;--shadow-strictpromotes 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 inpgevolve-core::ir::function.Procedure { qname, args, language, body, body_dependencies, security, commits_in_body, comment }flat IR type inpgevolve-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.ReturnType—Scalar,SetOf,Table { columns },Trigger,EventTrigger,Void.FunctionLanguage—Sql|PlPgSql.Catalog::functions: Vec<Function>andCatalog::procedures: Vec<Procedure>— flat collections, sorted by(qname, arg_types_normalized)/qnameaftercanonicalize().
Added — pipeline (functions and procedures)
- Source parser —
CREATE FUNCTIONandCREATE PROCEDUREparse into theFunction/ProcedureIR. 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 syntheticCREATE FUNCTIONand callspg_query::parse_plpgsql. Extracts static embedded SQL dep edges (PLpgSQL_stmt_execsql), detectsCOMMIT/ROLLBACKnodes forcommits_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(withpg_language,pg_type,pg_namespace) to reconstructFunctionandProcedurefrom a live database. Handles multi-arg functions, OUT args, overloads, and all attribute columns. - Differ —
FunctionChangevariants:Create,Drop,OrReplace,ReplaceWithCascade,CommentOn.ProcedureChangevariants:Create,Drop,OrReplace,CommentOn. - OR-REPLACE compatibility predicate (
function_can_or_replace) — returnstruewhen language, return type, and OUT/INOUT parameters are all unchanged; falls back toReplaceWithCascadeotherwise. - Planner — 6 new step kinds:
CreateOrReplaceFunction,DropFunction,CommentOnFunction,CreateOrReplaceProcedure,DropProcedure,CommentOnProcedure. Procedures withcommits_in_body = trueare 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 usesEXECUTEwithout a-- @pgevolve dep:directive.procedure-contains-commit(Warning) — procedure body containsCOMMITorROLLBACK; runs withtransactional=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/andobjects/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 representativePLPGSQL_BODIEScorpus,parse_routine_body → canonical_text → re-parse → canonical_textproduces 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 inpgevolve-core::ir::trigger.TriggerTiming—Before|After|InsteadOf.TriggerEvent—Insert|Update|Delete|Truncate.ForEach—Row|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)aftercanonicalize().
Added — pipeline (triggers)
- Source parser —
CREATE [CONSTRAINT] TRIGGERparses into theTriggerIR. BEFORE/AFTER/INSTEAD OF timing, INSERT/UPDATE/DELETE/TRUNCATE events, FOR EACH ROW/STATEMENT, WHEN clause (asNormalizedExpr), UPDATE OF column list, and REFERENCING transition tables all modeled.ALTER TRIGGERin source files rejected at statement classification. COMMENT ON TRIGGERparser arm — recognized alongsideCOMMENT ON FUNCTIONandCOMMENT ON EXTENSIONin the comment-statement path.- Catalog reader — queries
pg_triggerjoined withpg_class,pg_namespace, andpg_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. - Differ —
TriggerChangevariants:Create,Drop,CommentOn. Any structural difference (timing, events, for-each, when-clause, function, columns, transition tables, constraint flags) emitsDrop+Create— there is noALTER TRIGGERfor body-level changes.CommentOnis comment-only. - Planner — 3 new step kinds:
CreateTrigger,DropTrigger(destructive; intent required),CommentOnTrigger.DropTriggeris placed in the same destructive bucket asDropTableandDropFunction. NodeId::Trigger— added to the dep graph;Trigger → Table/View/MVedges ensure the target relation exists before the trigger is created;Trigger → Functionedges 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>andpartition_of: Option<PartitionOf>fields added toTableinpgevolve-core::ir::table.- New
ir/partition.rsmodule:PartitionBy { strategy: PartitionStrategy, columns: Vec<PartitionColumn> }— thePARTITION BYclause on a partitioned parent.PartitionStrategy—Range|List|Hash.PartitionColumn { kind: PartitionColumnKind, collation: Option<QualifiedName>, opclass: Option<QualifiedName> }— a single partition key element with optional collation and opclass overrides.PartitionColumnKind—Column(Identifier)|Expr(NormalizedExpr).PartitionOf { parent: QualifiedName, bounds: PartitionBounds }— thePARTITION OF parent FOR VALUES …clause on a partition child.PartitionBounds—Range { from, to }|List { values }|Hash { modulus, remainder }|Default.BoundDatum—Literal(NormalizedExpr)|MinValue|MaxValue.
Added — pipeline (partitioning)
- Source Form 1 —
CREATE TABLE child PARTITION OF parent FOR VALUES …parsed directly intoTable { partition_of: Some(…) }. The parent's key is inferred from itspartition_by. - Source Form 2 — standalone
CREATE TABLE child PARTITION OF parent FOR VALUES …in a separate file. Identical IR as Form 1. - Source Form 3 —
ALTER TABLE parent ATTACH PARTITION child FOR VALUES …combined with a standalone childCREATE TABLE(no inlinePARTITION OF). The parser merges the attach statement into the child'spartition_of, producing the same IR as Form 2. Equivalence of Form 2 and Form 3 is verified by a conformance fixture. - Sub-partitioning — a
Tablemay have bothpartition_by(it is itself a partitioned parent) andpartition_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 applyNOT EXISTS (pg_depend deptype='e'). - Differ —
TableChange::AttachPartition { parent, child, bounds }andTableChange::DetachPartition { parent, child }variants. Bounds change on a stable parent → DetachPartition + AttachPartition. Parentpartition_byrekey →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) andDetachPartition(destructive; intent required).AttachPartitionis placed in the same post-create ordering bucket asCreateIndex;DetachPartitionis placed in the same destructive bucket asDropTable. NodeIddep edge — child partition → parent table (DepSource::Structural). Ensures the parent exists (and has itspartition_byapplied) before the child is attached.
Added — lint rules (partitioning)
partition-references-unmanaged-parent(Error) — a partition child'spartition_of.parentschema 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 withFOR VALUES FROM … TO ….create-list-parent— LIST parent.create-hash-parent-and-partitions— HASH parent withFOR VALUES WITH (MODULUS m, REMAINDER r).create-default-partition—DEFAULTpartition.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 (bothpartition_byandpartition_ofset).lint-unmanaged-parent—partition-references-unmanaged-parentfires when parent is in an unmanaged schema.- Reject path fixtures for invalid bounds expressions.
Changed — differ (partitioning)
diff_tablesnow 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 viaAttachPartition/DetachPartitioninstead.
Added — IR (extensions)
Extension { name, schema: Option<Identifier>, version: Option<String>, comment: Option<String> }flat IR type inpgevolve-core::ir::extension.Catalog::extensions: Vec<Extension>flat collection.canon::sort_and_deduperejects duplicate extension names.
Added — pipeline (extensions)
- Source parser —
CREATE EXTENSION [IF NOT EXISTS] name [WITH SCHEMA s] [VERSION 'v']parses into theExtensionIR.CASCADE,FROM old_version, and unknown options rejected with structural errors.ALTER EXTENSIONin source files rejected at statement classification.COMMENT ON EXTENSIONparsing added. - Catalog reader — queries
pg_extensionjoined withpg_namespaceandpg_description. The reader for every other object kind (tables, indexes, sequences, functions, types, views, MVs) gains aNOT EXISTS (pg_depend deptype='e')filter so extension-owned objects never appear as drift. - Differ —
ExtensionChangevariants:Create,Drop,AlterUpdate,ReplaceWithCascade,CommentOn. Source-Nonefor 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 throughDropExtension+CreateExtensionwith linked intent. NodeId::Extension— added to the dep graph;Extension → Schemaedges force the schema to exist before the extension is created.
Added — lint rules (extensions)
extension-version-unpinned(Warning) —CREATE EXTENSION foo;without aVERSIONclause.extension-references-unmanaged-schema(Error) —WITH SCHEMA gisbutgisisn'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-ignoredexercises thepg_depend deptype='e'filter.scenarios/create-order-schema-firstverifies theExtension → Schemadep ordering.
Changed — conformance suite (extensions)
- Apply-layer post-check (
assertions::apply) switched from strictcanonical_eqto a differ-based convergence check. Source IR withNonefor 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 inpgevolve-core::ir::user_type.UserTypeKind::Enum { values: Vec<EnumValue> }— ordered label list withsort_order: f32mirroringpg_enum.enumsortorder.UserTypeKind::Domain { base, nullable, default, check_constraints, collation }— domain defaults and CHECK expressions useNormalizedExprfor canonical comparison.UserTypeKind::Composite { attributes: Vec<CompositeAttribute> }— each attribute carries name, type, and optional collation.Catalog::types: Vec<UserType>— flat collection, sorted byqnameaftercanonicalize().
Added — pipeline (user-defined types)
- Source parser —
CREATE TYPE … AS ENUM,CREATE DOMAIN,CREATE TYPE … AS (…)all parse into theUserTypeIR. Duplicate labels / attributes rejected at parse time. - AST resolution —
UserDefined(QualifiedName)column type references resolved againstCatalog::typesafter the source parse pass. - Catalog reader — queries
pg_type,pg_enum,pg_attribute(for composites), andpg_constraint/pg_attrdef(for domains) to reconstructUserTypefrom a live database. - Differ —
UserTypeChangevariants:Create,Drop,EnumAddValue,EnumRenameValue,DomainAddCheck,DomainDropCheck,DomainSetDefault,DomainSetNotNull,CompositeAddAttribute,CompositeDropAttribute,CompositeAlterAttributeType,CommentOn,ReplaceWithCascade. - Compatibility predicates —
enum_can_alter_in_place(preserved labels maintain relative order; renames position-paired) andcomposite_can_alter_in_place(preserved attributes maintain relative order). Both fall back toReplaceWithCascadewhen the predicate returnsfalse. - 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'sColumnType::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_typesemits exactly oneEnumAddValuechange.
Added — IR (views and materialized views)
ViewandMaterializedViewflat IR types inpgevolve-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 withDepSource::AstExtractedprovenance. Powers the dependent-recreation walk and theview-body-references-unmanaged-schemalint.security_barrierandsecurity_invokerreloptions onView.
Added — pipeline (views and materialized views)
- AST canonicalization pass (
parse/ast_canon.rs) — runs after source parse; callsNormalizedBody::from_sqlon each view body, extractsDepEdges, resolves references against the provisional catalog, and fills in column types. - Catalog reader —
read_viewsandread_materialized_viewsquerypg_views/pg_matviews, callpg_get_viewdef, and feed the result throughNormalizedBody::from_sql. Source-side and catalog-side canonical texts are directly comparable. - Differ —
ViewChangeandMvChangevariants. OR-REPLACE compatibility predicate (body_is_or_replace_compatible) determines whether a body change emitsCREATE OR REPLACE VIEW(compatible) orDROP + CREATE(incompatible). - Planner — 7 new step kinds:
CreateView,DropView,CreateMaterializedView,DropMaterializedView,RefreshMaterializedView,AlterViewSetReloption,CommentOnView. - Online rewrites —
REFRESH MATERIALIZED VIEW CONCURRENTLYupgrade (when unique index present); dependent-view recreation cascade (recreate_views::extend_with_dependent_recreations).
Added — configuration
[planner.online_rewrites].refresh_mv_concurrently(defaulttrue) — upgradeREFRESHtoREFRESH CONCURRENTLYwhen the MV has a unique index.[planner.online_rewrites].view_drop_create_dependents(defaulttrue) — cascade dependent-view recreations; setfalseto error instead of auto-cascading.[[step_override]]rows inintent.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 CONCURRENTLYunavailable.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) — verifiesNormalizedBody::from_sqlclosure 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. Newarbitrary_view_cataloggenerator inpgevolve-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 viapg_query. Tracks source locations for every parsed object. Recognises-- @pgevolvedirectives (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 syncCatalogQueriertrait. Returns(Catalog, DriftReport). The drift report captures NOT VALID constraints and INVALID indexes for auto-recovery. - Differ (
pgevolve-core::diff) — pair-by-qname, structural;ChangeSetplus higher-levelChangeenum. Drift entries fold intoChange::ValidateConstraint/Change::RecreateIndex. - Planner (
pgevolve-core::plan) — order → rewrite → group → wrap. Deterministic topo sort (Kahn + min-heap tiebreak). FK cycle extraction viaDeferredFkAdd. 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); deterministicPlanId(BLAKE3 over bincoded canonical IRs);[[intent]]rows withapproved: bool;[[lint_waiver]]rows to acknowledgeLintAtPlanfindings;RecordedFindingrows inmanifest.tomlfor 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. NewSeverity::LintAtPlantier (gates plan with exit code 2 unless waived) and a newcolumn-position-driftrule.
Added — IR
- Top-level types:
Catalog,Schema,Table,Column,Constraint,Index,Sequence, plusColumnType,DefaultExpr,NormalizedExpr,NormalizedBody(the statement-scope counterpart for v0.2 body-bearing objects). - Dep-graph types:
DepEdge { from, to, source: DepSource }withStructural(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 unwaivedLintAtPlanfindings.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-populatedschema/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.tomlwith[project],[managed],[planner],[planner.online_rewrites],[environments.<env>], and a new[shadow]block (backend = auto | testcontainers | dsn; per-backendurl,url_env,reset,extensions,postgres_version).
Added — test infrastructure
pgevolve-testkit—EphemeralPostgres,PgCatalogQuerier,MigrationFixture, IR generator + mutator,TestPgBackendpluggable backend trait with testcontainers / compose / dsn impls (selected viaPGEVOLVE_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 xtasksubcommands: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 intoregressions/.soak.yml— weekly high-case property runs.
Known limitations of v0.1
pgevolve rewrite-tableis a CLI skeleton — invoking it errors with "not yet implemented." The implementation lands with a v0.2 sub-spec (partitioning / column-type-change).pgevolve dumpwrites a singleschema.sqlfile. Multi-file layout following[project].layout_profileis 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-validateis 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.