Skip to content

Releases: supabase/pg-toolbelt

@supabase/pg-topo@1.0.0-alpha.2

12 Jun 10:33
8810c1d

Choose a tag to compare

Pre-release

Patch Changes

  • a5a69fc: Track function dependencies in ALTER TABLE expression subcommands.
  • cf0df37: Resolve COMMENT ON RULE dependencies so comments are ordered after the rule they target. objectKindFromObjType now maps OBJECT_RULE, and rule comment refs use the same relation.objectName identity as triggers and policies. Plain views now also provide their implicit _RETURN rewrite rule, so COMMENT 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

12 Jun 12:03
d706336

Choose a tag to compare

Pre-release

Major Changes

  • c4b90f5: Replace the flat plan.statements list 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 explicit transactionMode and a boundary reason, so plans whose statements cannot share one transaction are represented and applied correctly:

    • ALTER TYPE ... ADD VALUE and 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 PUBLICATION with implicit refresh = true, DROP SUBSCRIPTION with an associated replication slot — are applied as standalone non-transactional units instead of failing inside BEGIN/COMMIT.
    • CREATE SUBSCRIPTION for a subscription whose replication slot already exists now emits create_slot = false (keeping connect = 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 on create_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);

    applyPlan result 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). applyPlan reports 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 with psql --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: 2 with units. Legacy v1 plan files (flat statements) 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 in error.cycle, instead of a plain Error that callers had to string-match. And pgdelta plan --output-dir <dir> writes one numbered, transaction-aware SQL file per migration unit.

@supabase/pg-delta@1.0.0-alpha.29

12 Jun 10:33
8810c1d

Choose a tag to compare

Pre-release

Patch Changes

  • 115dde8: Fix unhandled CycleError when 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 like supabase_realtime commonly 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

10 Jun 18:35
e57ea4d

Choose a tag to compare

Pre-release

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

05 Jun 14:03
31acf90

Choose a tag to compare

Pre-release

Minor Changes

  • b9b8b15: Add --filter option to the catalog-export CLI 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> and pgmq.a_<name> are materialized lazily by select pgmq.create('<name>'), not by CREATE EXTENSION pgmq. The trigger extractor already drops these via the pg_depend deptype='e' row that pgmq records, but real-world cloud projects can lose that row (older pgmq versions — pgmq 1.4.4 which Supabase Cloud currently ships never records it — manual pg_dump/restore that strips extension deps, etc.), so supabase db reset aborts at the trigger statement with relation "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_validator functions the wrappers extension ships — so db pull no longer emits CREATE SERVER clerk_oauth_server for platform Wasm FDWs that local Docker cannot provision.

    The discriminator is the Wasm handler/validator function names, not the bare extensions.* namespace: contrib FDWs like postgres_fdw install their handler/validator into extensions on Supabase too, but they ARE available in the local image, so user-created postgres_fdw wrappers (and their servers, foreign tables, and user mappings) must still roundtrip. Server privilege scope is likewise preserved — GRANT/REVOKE ON SERVER does not require superuser.

@supabase/pg-delta@1.0.0-alpha.26

04 Jun 11:21
c604489

Choose a tag to compare

Pre-release

Patch Changes

  • 82d4700: feat(pg-delta): emit VALIDATE CONSTRAINT shortcut when only validated flips from false to true

    When the only difference between main and branch for an existing table constraint is convalidated flipping from false to true (i.e. the user wants to validate a previously NOT VALID constraint), pg-delta now emits a single ALTER TABLE ... VALIDATE CONSTRAINT ... instead of dropping and re-adding the constraint.

    VALIDATE CONSTRAINT only takes SHARE UPDATE EXCLUSIVE on the table (concurrent reads and writes continue while the row scan runs), whereas drop+add takes ACCESS EXCLUSIVE for the duration of the scan. This matches the standard "ADD CONSTRAINT ... NOT VALID; later VALIDATE CONSTRAINT" two-phase safe-migration pattern.

    The reverse direction (validatedNOT 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 a validated flip 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

    createManagedPool raced pool.connect() against a setTimeout rejection but never cleared the timer. When the connect won (the normal, fast case), the pending setTimeout kept the event loop alive, so the process hung for the rest of PGDELTA_CONNECT_TIMEOUT_MS even 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 a connectWithTimeout helper 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

20 May 13:52
0bd7dc2

Choose a tag to compare

Pre-release

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 from supabase integration 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 /
    RevokeGrantOptionAggregatePrivileges previously serialized the
    aggregate signature using pg_get_function_identity_arguments, which
    embeds ORDER BY for ordered-set / hypothetical-set aggregates
    (aggkind of o / h) and VARIADIC for variadic aggregates. The
    PostgreSQL GRANT ... ON FUNCTION parser rejects both keywords inside
    the argument list, so the generated GRANT/REVOKE failed with a
    syntax error for any aggregate that wasn't a plain aggkind = 'n'.
    The serializer now uses the proargtypes-derived argument_types
    list, matching the signature shape PostgreSQL expects for GRANT/REVOKE.

  • ae4c499: fix(pg-delta): skip redundant ALTER TABLE … ADD CONSTRAINT for CHECK constraints inherited by partition children

    Previously the inheritance signal used pg_constraint.conparentid <> 0, but PostgreSQL only populates conparentid for PK / UNIQUE / FK constraints on partitions — CHECK constraints on partitions always have conparentid = 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 uses coninhcount > 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, and CREATE / 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 WRAPPER requires superuser. On Supabase Cloud the postgres role has the elevated rights to apply these grants, but the local Docker image does not — so the previous diff output broke supabase db reset with permission denied for foreign-data wrapper dblink_fdw. The existing system-role rule already covers wrappers owned by supabase_admin, but pg_dump rewrites OWNER TO clauses to whoever the dump runs under, so after a restore the FDW ends up owned by postgres and slips past the owner gate. The supabase integration filter now drops privilege-scope changes on foreign_data_wrapper regardless of owner, since the FDW ACL is never user-replayable in the local image. FOREIGN SERVER ACL is intentionally left alone — server GRANT/REVOKE doesn't require superuser, and user-created servers (e.g. a dblink server 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 supabase integration now skips any FDW whose HANDLER or VALIDATOR references a function in the extensions schema. This covers the Wasm-based wrappers (clerk, clerk_oauth, etc.) that Supabase Cloud provisions as supabase_admin at project creation. CREATE FOREIGN DATA WRAPPER requires superuser, and the local Docker image has no equivalent pre-step, so the previous diff output broke supabase db reset. Owner-based filtering wasn't enough because the wrapper owner is often rewritten away from supabase_admin after a dump/restore.

@supabase/pg-delta@1.0.0-alpha.24

05 May 13:52
102ef99

Choose a tag to compare

Pre-release

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 ↔ DropTable drop-phase cycle when an owning table is
    promoted to DropTable + CreateTable by expandReplaceDependencies (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.dropped short-circuits DropSequence only 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 in replacedTableIds),
    the explicit DROP SEQUENCE survives into the drop phase alongside the
    expander's DropTable, 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.

    normalizePostDiffChanges now prunes DropSequence(S) whenever S is OWNED BY a column on a table in replacedTableIds. The DROP TABLE cascade
    already drops the OWNED BY sequence at apply time, so the explicit
    DROP SEQUENCE was both redundant and the source of the cycle.

@supabase/pg-delta@1.0.0-alpha.23

05 May 07:33
beb72cf

Choose a tag to compare

Pre-release

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 new provider extractor.

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

30 Apr 15:24
c7e97f8

Choose a tag to compare

Pre-release

Minor Changes

  • 2d1991a: feat(pg-delta): retry catalog extractors when pg_get_*def() returns NULL

    pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef, pg_get_triggerdef, pg_get_ruledef, and pg_get_functiondef can 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 with createPlan is reliably preserved or excluded rather than half-captured.

    Configuration (precedence: option > env > default):

    • CreatePlanOptions.extractRetries?: number — public API option on createPlan.
    • PGDELTA_EXTRACT_RETRIES env 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 via debug('pg-delta:extract') (visible with DEBUG=pg-delta:extract or DEBUG=pg-delta:*). Setting extractRetries: 0 disables 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 in pg_depend order them ahead of the matching
    DROP TYPE / DROP SEQUENCE. ALTER COLUMN ... TYPE also drops any
    existing default before the rewrite (and re-emits a SET DEFAULT after)
    so the stale default expression cannot pin the old type. RLS policies
    whose USING / WITH CHECK expressions 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 PostgreSQL 2BP01 ("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, or pg_get_functiondef returns NULL instead of crashing the relevant extract* with a ZodError. Same race conditions as the prior pg_get_indexdef (#223) and pg_get_constraintdef fixes — 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 and createPlan call.

  • 7c7d18a: fix(pg-delta): produce applyable migrations for RENAME operations seen as drop+create

    pg-delta is a state-based diff and treats a RENAME as DROP+CREATE because
    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 SERIAL column renamed in the target left the same-name
      sequence (e.g. old_table_id_seq) "altered" in the diff (only its
      OWNED BY ref changed). DROP TABLE cascade-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. diffSequences now 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 explicit DROP SEQUENCE
      that would form an unbreakable cycle with DropTable.
    • A table renamed in the target with a dependent view (e.g.
      CREATE VIEW user_count AS SELECT count(*) FROM users with the table
      renamed to members) failed with cannot drop table users because other objects depend on it. expandReplaceDependencies now seeds drop-only
      schema objects (table, view, materialized view, type, domain) as expansion
      roots so any surviving dependent in pg_depend gets 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.
  • 3b9eb91: fix(pg-delta): preserve REPLICA IDENTITY USING INDEX on tables instead of silently reverting to DEFAULT on declarative sync.

    The table extractor only stored replica_identity as 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), and AlterTableSetReplicaIdentity.serialize() fell back to REPLICA IDENTITY DEFAULT for that mode. Compounding this, Index.is_replica_identity participated in equality and was marked non-alterable, so toggling the flag on the index triggered a spurious DROP INDEX + CREATE INDEX — and Postgres reverts the table to REPLICA IDENTITY DEFAULT whenever the configured replica-identity index is dropped.

    End result: a table configured with ALTER TABLE foo REPLICA IDENTITY USING INDEX foo_idx would extract as replica_identity = 'i' but produce no setter on diff. The next declarative sync would generate a migration that dropped the user's index, reset the table to DEFAULT, and recreated the index — never converging (reported as supabase/cli#5141).

    The fix:

    • Table.replica_identity_index is extracted via pg_index.indisreplident and included in dataFields, so the index name participates in equality.
    • AlterTableSetReplicaIdentity now serializes REPLICA IDENTITY USING INDEX <name> for mode 'i' and declares the index as a requires dependency so it is created first.
    • The table diff emits the change for all modes (including 'i') on both CREATE and ALTER, and re-emits when the configured index name changes while staying in 'i' mode.
    • Index.is_replica_identity is no longer in dataFields / NON_ALTERABLE_FIELDS; the table side is the source of truth, set via ALTER TABLE. This stops the spurious DROP INDEX + CREATE INDEX cycle.
    • A new restoreReplicaIdentityAfterIndexReplace pass in post-diff-normalization.ts re-emits ALTER TABLE ... REPLICA IDENTITY USING INDEX <name> after any DropIndex(idx) + CreateIndex(idx) pair where idx is 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 flips relreplident to 'd', and the table-level diff alone cannot see the cross-object interaction. The pass is idempotent — if diffTables() 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.ts is renamed to post-diff-normalization.ts and normalizePostDiffCycles to normalizePostDiffChanges — 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's CLAUDE.md rule of thumb.

  • 2d1991a: fix(pg-delta): skip table constraints where pg_get_constraintdef() returns NULL instead of crashing extractTables with a ZodError. Like pg_get_indexdef, pg_get_constraintdef can 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 and createPlan call.