Releases: supabase/pg-toolbelt
@supabase/pg-delta@1.0.0-alpha.26
Patch Changes
-
82d4700: feat(pg-delta): emit
VALIDATE CONSTRAINTshortcut when onlyvalidatedflips from false to trueWhen the only difference between main and branch for an existing table constraint is
convalidatedflipping fromfalsetotrue(i.e. the user wants to validate a previouslyNOT VALIDconstraint), pg-delta now emits a singleALTER TABLE ... VALIDATE CONSTRAINT ...instead of dropping and re-adding the constraint.VALIDATE CONSTRAINTonly takesSHARE UPDATE EXCLUSIVEon the table (concurrent reads and writes continue while the row scan runs), whereas drop+add takesACCESS EXCLUSIVEfor the duration of the scan. This matches the standard "ADD CONSTRAINT ... NOT VALID; later VALIDATE CONSTRAINT" two-phase safe-migration pattern.The reverse direction (
validated→NOT VALID) has no equivalent Postgres command, so it still goes through drop+add. Any other field change (expression, key columns, FK target, on_delete, etc.) on top of avalidatedflip also still goes through drop+add — the shortcut applies only when nothing else differs. -
6d49e04: fix(pg-delta): clear the connect-timeout timer when the race settles
createManagedPoolracedpool.connect()against asetTimeoutrejection but never cleared the timer. When the connect won (the normal, fast case), the pendingsetTimeoutkept the event loop alive, so the process hung for the rest ofPGDELTA_CONNECT_TIMEOUT_MSeven though the plan was already done. Raising the timeout for far-away databases made every local run wait that long too. The race now goes through aconnectWithTimeouthelper that clears the timer in a.finally. -
82d4700: fix(pg-delta): stop re-validating NOT VALID constraints
A NOT VALID constraint was followed by a VALIDATE CONSTRAINT step that flipped it back to validated, so the plan never converged. ADD CONSTRAINT already carries the NOT VALID suffix, so the VALIDATE was redundant. It's now dropped from the create, alter, and table-replacement paths.
@supabase/pg-delta@1.0.0-alpha.25
Patch Changes
-
f1704bd: fix(pg-delta): keep user-defined triggers on auth/storage tables through the supabase filter
User-attached triggers on
auth.users,storage.objects, etc. were being dropped fromsupabaseintegration diffs because triggers live in their parent table's schema and inherit its owner — both signals the Supabase managed-schema filter uses to skip Supabase's own objects. The filter now keeps any trigger whose function lives outside the managed schemas, which is the reliable user-defined marker. -
62f39d4: fix(pg-delta): emit valid GRANT/REVOKE syntax for ordered-set, hypothetical-set, and variadic aggregates
GrantAggregatePrivileges/RevokeAggregatePrivileges/
RevokeGrantOptionAggregatePrivilegespreviously serialized the
aggregate signature usingpg_get_function_identity_arguments, which
embedsORDER BYfor ordered-set / hypothetical-set aggregates
(aggkindofo/h) andVARIADICfor variadic aggregates. The
PostgreSQLGRANT ... ON FUNCTIONparser rejects both keywords inside
the argument list, so the generatedGRANT/REVOKEfailed with a
syntax error for any aggregate that wasn't a plainaggkind = 'n'.
The serializer now uses theproargtypes-derivedargument_types
list, matching the signature shape PostgreSQL expects forGRANT/REVOKE. -
ae4c499: fix(pg-delta): skip redundant
ALTER TABLE … ADD CONSTRAINTfor CHECK constraints inherited by partition childrenPreviously the inheritance signal used
pg_constraint.conparentid <> 0, but PostgreSQL only populatesconparentidfor PK / UNIQUE / FK constraints on partitions — CHECK constraints on partitions always haveconparentid = 0. As a result, pg-delta re-emitted every inherited CHECK constraint against each partition, and apply failed with SQLSTATE 42710 ("constraint already exists") because the constraint had already been auto-created on the partition by Postgres when the parent's constraint or the partition itself was created. The extractor now usesconinhcount > 0, the canonical inheritance flag, which covers CHECK and all other constraint kinds uniformly. -
0d52b68: Redact foreign-data-wrapper option values that are not on the allowlist of known-safe keys (libpq connection params, postgres_fdw behavior knobs, generic table-FDW shape, Supabase Wrappers non-credential keys). The policy applies to
CREATE / ALTER FOREIGN DATA WRAPPER,CREATE / ALTER SERVER,CREATE / ALTER USER MAPPING, andCREATE / ALTER FOREIGN TABLE— every value is replaced with `__OPTION___unless the key is recognised as safe. Previously credentials such aspassword,passfile,passcode,sslpassword,api_key,private_key,aws_secret_access_key, etc. were emitted in cleartext into plan SQL, catalog snapshots, declarative export, and fingerprints, ending up on disk and in CI logs (CLI-1467). Safe-listed options (host,port,user,dbname,sslmode,fetch_size,region,endpoint`, …) continue to roundtrip with their real values. The emitted DDL is not directly re-appliable for redacted options — operators must re-supply credentials out of band. -
62f39d4: fix(pg-delta): suppress GRANT/REVOKE on FOREIGN DATA WRAPPER in the supabase integration
GRANT/REVOKE ... ON FOREIGN DATA WRAPPERrequires superuser. On Supabase Cloud thepostgresrole has the elevated rights to apply these grants, but the local Docker image does not — so the previous diff output brokesupabase db resetwithpermission denied for foreign-data wrapper dblink_fdw. The existing system-role rule already covers wrappers owned bysupabase_admin, butpg_dumprewrites OWNER TO clauses to whoever the dump runs under, so after a restore the FDW ends up owned bypostgresand slips past the owner gate. The supabase integration filter now drops privilege-scope changes onforeign_data_wrapperregardless of owner, since the FDW ACL is never user-replayable in the local image.FOREIGN SERVERACL is intentionally left alone — server GRANT/REVOKE doesn't require superuser, and user-created servers (e.g. adblinkserver pointing to a peer DB) carry legitimate user ACL that should still roundtrip. -
62f39d4: fix(pg-delta): suppress CREATE/DROP/ALTER FOREIGN DATA WRAPPER for platform-managed Wasm wrappers in the supabase integration
The
supabaseintegration now skips any FDW whoseHANDLERorVALIDATORreferences a function in theextensionsschema. This covers the Wasm-based wrappers (clerk,clerk_oauth, etc.) that Supabase Cloud provisions assupabase_adminat project creation.CREATE FOREIGN DATA WRAPPERrequires superuser, and the local Docker image has no equivalent pre-step, so the previous diff output brokesupabase db reset. Owner-based filtering wasn't enough because the wrapper owner is often rewritten away fromsupabase_adminafter a dump/restore.
@supabase/pg-delta@1.0.0-alpha.24
Patch Changes
-
471f770: Fix drop-phase cycle breaking when publication table membership removal intersects with dropped foreign-key chains and a referenced constraint drop.
-
471f770: Fix
DropSequence ↔ DropTabledrop-phase cycle when an owning table is
promoted toDropTable + CreateTablebyexpandReplaceDependencies(for
example when a referenced enum has a label removed) and the same plan also
drops the SERIAL sequence because branch no longer carries the owned sequence.diffSequences.droppedshort-circuitsDropSequenceonly when the owning
table itself is absent from the branch catalog. When the table survives in
branch but is later replaced via expansion (table is inreplacedTableIds),
the explicitDROP SEQUENCEsurvives into the drop phase alongside the
expander'sDropTable, and the bidirectional pg_depend edges between the
sequence and its owning column close an unbreakable 2-cycle that none of the
existing dependency-filter / change-injection breakers match.normalizePostDiffChangesnow prunesDropSequence(S)whenever S isOWNED BYa column on a table inreplacedTableIds. TheDROP TABLEcascade
already drops the OWNED BY sequence at apply time, so the explicit
DROP SEQUENCEwas both redundant and the source of the cycle.
@supabase/pg-delta@1.0.0-alpha.23
Minor Changes
- 9a0831a: feat(pg-delta): add support for PostgreSQL SECURITY LABEL across all 17 supported object types (schemas, tables, columns, views, materialized views, sequences, functions, procedures, aggregates, composite/enum/range types, domains, event triggers, foreign tables, publications, subscriptions, roles). Includes round-trip fidelity, a new
scope: "security_label"in the filter DSL, and per-provider filtering via the newproviderextractor.
Patch Changes
- 9a0831a: Expose security-label providers to the filter DSL so provider-specific security label filters work as documented.
@supabase/pg-delta@1.0.0-alpha.22
Minor Changes
-
2d1991a: feat(pg-delta): retry catalog extractors when
pg_get_*def()returns NULLpg_get_indexdef,pg_get_constraintdef,pg_get_viewdef,pg_get_triggerdef,pg_get_ruledef, andpg_get_functiondefcan transiently return NULL when the underlying catalog row is dropped concurrently or the catalog state is in flux. Previously such rows were dropped silently after one attempt; now extraction retries the affected query a configurable number of times before falling back to filtering. In practice the second attempt no longer sees the dropped object (or successfully resolves the definition), so a real CREATE/DROP racing withcreatePlanis reliably preserved or excluded rather than half-captured.Configuration (precedence: option > env > default):
CreatePlanOptions.extractRetries?: number— public API option oncreatePlan.PGDELTA_EXTRACT_RETRIESenv var — same value, useful for CLI usage.- Default
1(i.e. the first attempt plus one retry, 2 attempts total).
After retries are exhausted, rows whose
pg_get_*def()is still NULL are filtered out and a warning is emitted viadebug('pg-delta:extract')(visible withDEBUG=pg-delta:extractorDEBUG=pg-delta:*). SettingextractRetries: 0disables retrying entirely and reproduces the previous "filter-on-first-attempt" behavior.
Patch Changes
-
9e3541d: fix(pg-delta): order dependency-breaking ALTERs before DROP for types, sequences, and policies (#230)
ALTER COLUMN ... DROP DEFAULT,ALTER COLUMN ... DROP IDENTITY, and
ALTER COLUMN ... TYPE <built-in>are now scheduled in the drop phase so
that the catalog edges inpg_dependorder them ahead of the matching
DROP TYPE/DROP SEQUENCE.ALTER COLUMN ... TYPEalso drops any
existing default before the rewrite (and re-emits aSET DEFAULTafter)
so the stale default expression cannot pin the old type. RLS policies
whoseUSING/WITH CHECKexpressions begin or stop referencing
different functions or relations are now emitted as drop+create, letting
the policy's drop run before the referenced object's drop and the
policy's recreate run after the new object's create. Plans that
previously aborted with PostgreSQL2BP01("cannot drop ... because
other objects depend on it") now apply cleanly. -
2d1991a: fix(pg-delta): skip rows when
pg_get_viewdef,pg_get_triggerdef,pg_get_ruledef, orpg_get_functiondefreturns NULL instead of crashing the relevantextract*with a ZodError. Same race conditions as the priorpg_get_indexdef(#223) andpg_get_constraintdeffixes — the underlying catalog row can vanish (concurrent DDL, transient catalog state, recovery edges). A single unreadable view, materialized view, trigger, rule, or function no longer aborts the whole catalog extraction andcreatePlancall. -
7c7d18a: fix(pg-delta): produce applyable migrations for
RENAMEoperations seen as drop+createpg-deltais a state-based diff and treats aRENAMEasDROP+CREATEbecause
the final catalogs are indistinguishable. Two scenarios in that drop+create
path failed at apply time on schemas that had been renamed in the target
(reported in #228):- A table with a
SERIALcolumn renamed in the target left the same-name
sequence (e.g.old_table_id_seq) "altered" in the diff (only its
OWNED BYref changed).DROP TABLEcascade-drops the sequence via
OWNED BY, after which the freshly created table's column default
nextval('old_table_id_seq'::regclass)referenced a non-existent relation
and the migration aborted.diffSequencesnow detects when the sequence's
main-side owning table is going away in the same plan and recreates the
sequence after the cascade, while suppressing an explicitDROP SEQUENCE
that would form an unbreakable cycle withDropTable. - A table renamed in the target with a dependent view (e.g.
CREATE VIEW user_count AS SELECT count(*) FROM userswith the table
renamed tomembers) failed withcannot drop table users because other objects depend on it.expandReplaceDependenciesnow seeds drop-only
schema objects (table, view, materialized view, type, domain) as expansion
roots so any surviving dependent inpg_dependgets promoted to
DROP+CREATE. The dependent's drop is sequenced before the parent drop,
and its create runs after the new replacement is in place.
- A table with a
-
3b9eb91: fix(pg-delta): preserve
REPLICA IDENTITY USING INDEXon tables instead of silently reverting toDEFAULTon declarative sync.The table extractor only stored
replica_identityas a single character ('d' | 'n' | 'f' | 'i') and discarded the index name when the mode was'i'. The diff path then explicitly skipped mode'i'("handled by index changes" — but no such handler existed), andAlterTableSetReplicaIdentity.serialize()fell back toREPLICA IDENTITY DEFAULTfor that mode. Compounding this,Index.is_replica_identityparticipated in equality and was marked non-alterable, so toggling the flag on the index triggered a spuriousDROP INDEX+CREATE INDEX— and Postgres reverts the table toREPLICA IDENTITY DEFAULTwhenever the configured replica-identity index is dropped.End result: a table configured with
ALTER TABLE foo REPLICA IDENTITY USING INDEX foo_idxwould extract asreplica_identity = 'i'but produce no setter on diff. The nextdeclarative syncwould generate a migration that dropped the user's index, reset the table toDEFAULT, and recreated the index — never converging (reported as supabase/cli#5141).The fix:
Table.replica_identity_indexis extracted viapg_index.indisreplidentand included indataFields, so the index name participates in equality.AlterTableSetReplicaIdentitynow serializesREPLICA IDENTITY USING INDEX <name>for mode'i'and declares the index as arequiresdependency so it is created first.- The table diff emits the change for all modes (including
'i') on bothCREATEandALTER, and re-emits when the configured index name changes while staying in'i'mode. Index.is_replica_identityis no longer indataFields/NON_ALTERABLE_FIELDS; the table side is the source of truth, set viaALTER TABLE. This stops the spuriousDROP INDEX+CREATE INDEXcycle.- A new
restoreReplicaIdentityAfterIndexReplacepass inpost-diff-normalization.tsre-emitsALTER TABLE ... REPLICA IDENTITY USING INDEX <name>after anyDropIndex(idx) + CreateIndex(idx)pair whereidxis the replica-identity index of a branch table. This covers the second flavor of the bug: when both main and branch already point at the same replica-identity index, but that index's definition changes (e.g. a column added to its key), the index is replaced, Postgres silently flipsrelreplidentto'd', and the table-level diff alone cannot see the cross-object interaction. The pass is idempotent — ifdiffTables()already emitted the same setter (because the table is also flipping mode or pointing to a different index), no duplicate is added.
The post-diff layer file
src/core/post-diff-cycle-breaking.tsis renamed topost-diff-normalization.tsandnormalizePostDiffCyclestonormalizePostDiffChanges— the file already contained dedup and replacement-superseded pruning that aren't strictly cycle-breaking, and actual cycle breaking moved to the lazy sort-phase dispatcher in a previous release. The rename brings the file in line with the "post-diff normalization" terminology already used in the package'sCLAUDE.mdrule of thumb. -
2d1991a: fix(pg-delta): skip table constraints where
pg_get_constraintdef()returns NULL instead of crashingextractTableswith a ZodError. Likepg_get_indexdef,pg_get_constraintdefcan return NULL under race conditions with concurrent DDL or transient catalog inconsistencies. Such constraints are now filtered out at extraction time so a single unreadable constraint no longer aborts the whole catalog extraction andcreatePlancall.
@supabase/pg-delta@1.0.0-alpha.21
Patch Changes
-
fa3f736: fix(pg-delta): emit USING and default-safe flow for ALTER COLUMN TYPE
-
363fef3: Fix ZodError when extracting tables with EXCLUDE constraints defined over expressions. PostgreSQL stores
attnum=0inpg_constraint.conkeyfor expression elements, which never matchespg_attribute, so the inner aggregate returned SQLNULLand trippedtablePropsSchemaatconstraints[*].key_columns. The extractor now coalesces the aggregate to an empty JSON array. -
cbe8946: Defer drop-phase cycle breaking from
normalizePostDiffCyclesto a lazy
dispatcher invoked bysortPhaseChangesonly when edge filtering can't
break a cycle. The happy path (no cycles, the vast majority of plans) no
longer walksiterCrossDropFkConstraintson every diff. The new
dispatcher generalizes the existing 2-cycle FK breaker to any
N≥2 strongly-connected component of dropped tables (for example
a→b→c→a) and breaks the
AlterPublicationDropTables ↔ AlterTableDropColumncycle that occurred
when a publication-listed column was dropped on a surviving table. The
breaker round-cap scales withphaseChanges.lengthso big diffs with
many independent unbreakable cycles in a single phase resolve cleanly
instead of throwing a spuriousCycleError.The sequence diff path now alters
data_typein place via
ALTER SEQUENCE ... AS <type>(valid PostgreSQL since PG10) instead of
emittingDROP SEQUENCE + CREATE SEQUENCE. This eliminates a
productionCycleErrorseen on alpha.16 (Sentry SUPABASE-API-7RS,
"DropSequence ↔ DropTable") triggered when a sequence whose
data_typechanges is referenced by aDEFAULT nextval(...)on a
surviving column. Altering in place also fixes a silent data-loss
regression where the recreated sequence would restart at1and
collide with existing row ids.
@supabase/pg-delta@1.0.0-alpha.20
Patch Changes
-
ac7b9b8: fix(pg-delta): skip
WITH SCHEMAwhen serializingpgsodiumandpg_tleunder the Supabase integrationBoth extensions create their install schema (
pgsodium,pgtle) themselves, and those schemas are filtered out of the declarative plan by the Supabase integration because they live inSUPABASE_SYSTEM_SCHEMAS. EmittingCREATE EXTENSION pgsodium WITH SCHEMA pgsodium(or the equivalent forpg_tle) therefore fails against a fresh database withschema "pgsodium" does not exist— the same bug shape PR #191 fixed forpgmq.Closes #222.
@supabase/pg-delta@1.0.0-alpha.19
Patch Changes
-
4867d88: Handle dependent index and view recreation when replacing a materialized view. Constraint-owned, primary, and partition-attached indexes are left to the owning constraint or parent-index DDL so table replacement does not emit a standalone
DROP INDEXon a PK-owned index. -
f00e9a4: fix(pg-delta): skip indexes where
pg_get_indexdef()returns NULL instead of crashingextractIndexeswith a ZodError. The three-argument form ofpg_get_indexdefcan return NULL under race conditions with concurrent DDL (e.g. the index being dropped mid-extraction) or when catalog metadata is transiently inconsistent. Such indexes are now filtered out with a debug log (DEBUG=pg-delta:extract:index) so a single unreadable index no longer aborts the whole catalog extraction andcreatePlancall. -
f33d579: fix(pg-delta): order RLS policies after referenced new objects
Policies whose
USING/WITH CHECKexpression references another new object could be emitted before the referenced object on a fresh database, causing plan/apply to fail.extractRlsPoliciesnow joinspg_dependto surface every relation (tables, partitioned tables, views, materialized views, foreign tables) and function the policy expression references. PostgreSQL already records those edges atCREATE POLICYtime viarecordDependencyOnExpr, so the catalog is authoritative and pg-delta's core diffing path does not reparse the expression text.CreateRlsPolicy.requiresdispatches per relation kind and emitsstableId.procedure(...)for functions, using the exact argument signature produced byformat_type(proargtypes)— matching the signature embedded in the procedure extractor's stable id.Sequences referenced via
nextval('seq'::regclass)remain a known gap (tracked as a skipped regression test) becausepg_dependonly records the edge forregclassliteral arguments.
@supabase/pg-delta@1.0.0-alpha.18
Patch Changes
-
feca870: fix(pg-delta): diff PostgreSQL 18 temporal constraints
-
b812a46: fix(pg-delta): emit DROP + CREATE for function signature changes (return type, parameter names, parameter defaults, modes) instead of unsupported
CREATE OR REPLACE FUNCTION -
feca870: fix(pg-delta): dedupe duplicate constraint ADDs on tables promoted to drop+create
When a table transitively depends on a replaced object (for example a
foreign key whose referenced primary key is being dropped and re-added to
flip toWITHOUT OVERLAPS/PERIOD),expandReplaceDependencies()
promotes the table to a fullDropTable + CreateTablepair and emits one
AlterTableAddConstraint(plus optionalVALIDATE CONSTRAINT/
COMMENT ON CONSTRAINT) per branch constraint. The original
diffTables()-emittedAlterTableAddConstrainttargeting the same
constraint on the same replaced table was previously left in the plan,
producing duplicateALTER TABLE ... ADD CONSTRAINTstatements and a
constraint "..." for relation "..." already existsapply failure.normalizePostDiffCycles()now dedupes same-table
AlterTableAddConstraint,AlterTableValidateConstraintand
CreateCommentOnConstraintchanges keyed by
(changeType, table.stableId, constraint.name)on replaced tables,
keeping only the last occurrence. BecauseexpandReplaceDependencies()
appends its additions after the originaldiffTables()output, the last
occurrence is always the expansion's emission — so correctness is
preserved while the earlier duplicate is removed. This fixes migrations
that combine a temporal-PK flip on one table with a temporal-FK flip on a
related table without regressing unrelated replace-expansion scenarios
(enum value removal, table replacement via other object replacements).
@supabase/pg-delta@1.0.0-alpha.17
Patch Changes
-
5cc2a21: fix(pg-delta): stop emitting spurious
CREATE OR REPLACE TRIGGERon logically-identical triggers whose underlying tables have different physical column layouts.The trigger diff was comparing
pg_trigger.tgattr(raw physical attnums) as part of its non-alterable fields. When the same logical trigger (e.g.BEFORE UPDATE OF col_a, col_b ...) existed on two tables with different physical column layouts — one built via a singleCREATE TABLE, the other grown viaALTER TABLE DROP/ADD COLUMN(which leaves "dead" attnums that are never renumbered) — the attnum vectors diverged while the trigger definition (rendered bypg_get_triggerdef()using column names) was byte-identical. The diff kept firing aReplaceTriggerevery round, and becauseCREATE OR REPLACE TRIGGERdoes not renumber the table's physical columns, the loop never converged.Triggers are now compared by
pg_get_triggerdef()output (column names) instead of rawtgattrattnums, matching the existingIndexpattern that handles the same class of bug forindkey.