Releases: supabase/pg-toolbelt
@supabase/pg-topo@1.0.0-alpha.2
Patch Changes
- a5a69fc: Track function dependencies in ALTER TABLE expression subcommands.
- cf0df37: Resolve
COMMENT ON RULEdependencies so comments are ordered after the rule they target.objectKindFromObjTypenow mapsOBJECT_RULE, and rule comment refs use the samerelation.objectNameidentity as triggers and policies. Plain views now also provide their implicit_RETURNrewrite rule, soCOMMENT ON RULE "_RETURN" ON <view>resolves to the view instead of reporting an unresolved dependency. - 436b3d1: Support ordering CREATE RULE statements with predicate and action dependencies.
@supabase/pg-delta@1.0.0-alpha.30
Major Changes
-
c4b90f5: Replace the flat
plan.statementslist with execution-aware migration units.A plan is now an ordered list of
MigrationUnits (plan.units) plus session-level statements (plan.sessionStatements). Each unit carries an explicittransactionModeand a boundaryreason, so plans whose statements cannot share one transaction are represented and applied correctly:ALTER TYPE ... ADD VALUEand any later statement now run in separate transactions, fixing PostgreSQL error 55P04 ("unsafe use of new value of enum type") when a migration adds an enum value and uses it (#262).- Statements PostgreSQL rejects inside a transaction block —
ALTER SUBSCRIPTION ... SET PUBLICATIONwith implicitrefresh = true,DROP SUBSCRIPTIONwith an associated replication slot — are applied as standalone non-transactional units instead of failing insideBEGIN/COMMIT. CREATE SUBSCRIPTIONfor a subscription whose replication slot already exists now emitscreate_slot = false(keepingconnect = true), so the existing slot is reused instead of failing with "replication slot already exists"; that form is transactional (PostgreSQL's transaction-block gate is oncreate_slot = true).
Execution semantics are declared on the change classes (
nonTransactional,commitBoundary), never inferred from rendered SQL.Migrating from
plan.statements:// before const script = plan.statements.join(";\n"); // after — transaction-aware script (BEGIN/COMMIT per unit, unit headers) const script = renderPlanSql(plan); // or one numbered file per unit (also: pgdelta plan --output-dir <dir>) const files = renderPlanFiles(plan); // or the raw ordered statements (session statements included) when // transaction context does not matter const statements = flattenPlanStatements(plan);
applyPlanresult changes:// before | { status: "applied"; statements: number; warnings?: string[] } | { status: "failed"; error: unknown; script: string } // after | { status: "applied"; statements: number; units: number; warnings?: string[] } | { status: "failed"; error: unknown; script: string; failedUnitIndex?: number; completedUnits: number }
Behavioral consequences:
- Multi-unit plans are not atomic as a whole: earlier units commit before later units run, and a later failure does not roll back already-committed units (an added enum value cannot be dropped).
applyPlanreports the failing unit and how many units committed. - Non-transactional units run without any transaction wrapper. Rendered scripts must be executed by a statement-splitting runner such as
psql -f(not as a single multi-statement query string, and not withpsql --single-transaction): PostgreSQL runs multi-command strings in an implicit transaction block, which would fail any non-transactional unit. - Single-unit plans (the common case) still apply as one transaction.
Plan JSON: new plans are written as
version: 2withunits. Legacy v1 plan files (flatstatements) are still read and normalized into a single transactional unit — faithful to how v1 executed them — but v2 plan files are not readable by older pg-delta versions.New: unorderable dependency cycles now throw a typed
UnorderableCycleError(exported) carrying the offending changes inerror.cycle, instead of a plainErrorthat callers had to string-match. Andpgdelta plan --output-dir <dir>writes one numbered, transaction-aware SQL file per migration unit.
@supabase/pg-delta@1.0.0-alpha.29
Patch Changes
- 115dde8: Fix unhandled
CycleErrorwhen dropping a FK chain of tables alongside a referenced unique constraint while only some of the involved tables are publication members. The publication FK-chain cycle breaker required every dropped table in the cycle to be a member of the publication, but publications likesupabase_realtimecommonly contain only a subset of tables; the guard now only requires the publication edge that actually participates in the cycle. - Updated dependencies [a5a69fc]
- Updated dependencies [cf0df37]
- Updated dependencies [436b3d1]
- @supabase/pg-topo@1.0.0-alpha.2
@supabase/pg-delta@1.0.0-alpha.28
Patch Changes
- 9f01826: Order dependent view drops before column type rewrites, and preserve view or materialized-view metadata, including ACL adjustments, when those dependents are dropped and recreated during replacement.
- f95e0a8: Recreate RLS policies that depend on replaced functions.
- e396579: Recreate RLS policies that depend on rewritten columns.
@supabase/pg-delta@1.0.0-alpha.27
Minor Changes
- b9b8b15: Add
--filteroption to thecatalog-exportCLI command to scope the exported catalog to matching schemas/objects.
Patch Changes
-
71cce8a: fix(pg-delta): suppress user triggers on pgmq queue/archive tables in supabase integration
Follow-up to the Wasm FDW dependents fix.
pgmq.q_<name>andpgmq.a_<name>are materialized lazily byselect pgmq.create('<name>'), not byCREATE EXTENSION pgmq. The trigger extractor already drops these via thepg_depend deptype='e'row that pgmq records, but real-world cloud projects can lose that row (older pgmq versions — pgmq1.4.4which Supabase Cloud currently ships never records it — manualpg_dump/restore that strips extension deps, etc.), sosupabase db resetaborts at the trigger statement withrelation "pgmq.q_<name>" does not exist. Add a defensive name-match fallback in the supabase integration filter so the trigger is dropped even when the principled signal is missing. -
71cce8a: fix(pg-delta): suppress Wasm FDW servers, foreign tables, and user mappings in supabase integration
Follow-up to CLI-1470. Also suppress SERVER (object/comment/security-label scopes), FOREIGN TABLE, and USER MAPPING changes whose parent wrapper is a Supabase Wasm FDW — identified by the
extensions.wasm_fdw_handler/extensions.wasm_fdw_validatorfunctions thewrappersextension ships — sodb pullno longer emitsCREATE SERVER clerk_oauth_serverfor platform Wasm FDWs that local Docker cannot provision.The discriminator is the Wasm handler/validator function names, not the bare
extensions.*namespace: contrib FDWs likepostgres_fdwinstall their handler/validator intoextensionson Supabase too, but they ARE available in the local image, so user-createdpostgres_fdwwrappers (and their servers, foreign tables, and user mappings) must still roundtrip. Server privilege scope is likewise preserved —GRANT/REVOKE ON SERVERdoes not require superuser.
@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.