Skip to content

Releases: supabase/pg-toolbelt

@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.

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

29 Apr 14:18
6702921

Choose a tag to compare

Pre-release

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=0 in pg_constraint.conkey for expression elements, which never matches pg_attribute, so the inner aggregate returned SQL NULL and tripped tablePropsSchema at constraints[*].key_columns. The extractor now coalesces the aggregate to an empty JSON array.

  • cbe8946: Defer drop-phase cycle breaking from normalizePostDiffCycles to a lazy
    dispatcher invoked by sortPhaseChanges only when edge filtering can't
    break a cycle. The happy path (no cycles, the vast majority of plans) no
    longer walks iterCrossDropFkConstraints on 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 ↔ AlterTableDropColumn cycle that occurred
    when a publication-listed column was dropped on a surviving table. The
    breaker round-cap scales with phaseChanges.length so big diffs with
    many independent unbreakable cycles in a single phase resolve cleanly
    instead of throwing a spurious CycleError.

    The sequence diff path now alters data_type in place via
    ALTER SEQUENCE ... AS <type> (valid PostgreSQL since PG10) instead of
    emitting DROP SEQUENCE + CREATE SEQUENCE. This eliminates a
    production CycleError seen on alpha.16 (Sentry SUPABASE-API-7RS,
    "DropSequence ↔ DropTable") triggered when a sequence whose
    data_type changes is referenced by a DEFAULT nextval(...) on a
    surviving column. Altering in place also fixes a silent data-loss
    regression where the recreated sequence would restart at 1 and
    collide with existing row ids.

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

24 Apr 08:33
8a31133

Choose a tag to compare

Pre-release

Patch Changes

  • ac7b9b8: fix(pg-delta): skip WITH SCHEMA when serializing pgsodium and pg_tle under the Supabase integration

    Both 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 in SUPABASE_SYSTEM_SCHEMAS. Emitting CREATE EXTENSION pgsodium WITH SCHEMA pgsodium (or the equivalent for pg_tle) therefore fails against a fresh database with schema "pgsodium" does not exist — the same bug shape PR #191 fixed for pgmq.

    Closes #222.

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

23 Apr 16:50
5772059

Choose a tag to compare

Pre-release

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 INDEX on a PK-owned index.

  • f00e9a4: fix(pg-delta): skip indexes where pg_get_indexdef() returns NULL instead of crashing extractIndexes with a ZodError. The three-argument form of pg_get_indexdef can 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 and createPlan call.

  • f33d579: fix(pg-delta): order RLS policies after referenced new objects

    Policies whose USING / WITH CHECK expression references another new object could be emitted before the referenced object on a fresh database, causing plan/apply to fail.

    extractRlsPolicies now joins pg_depend to surface every relation (tables, partitioned tables, views, materialized views, foreign tables) and function the policy expression references. PostgreSQL already records those edges at CREATE POLICY time via recordDependencyOnExpr, so the catalog is authoritative and pg-delta's core diffing path does not reparse the expression text. CreateRlsPolicy.requires dispatches per relation kind and emits stableId.procedure(...) for functions, using the exact argument signature produced by format_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) because pg_depend only records the edge for regclass literal arguments.

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

22 Apr 12:03
68ac846

Choose a tag to compare

Pre-release

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 to WITHOUT OVERLAPS / PERIOD), expandReplaceDependencies()
    promotes the table to a full DropTable + CreateTable pair and emits one
    AlterTableAddConstraint (plus optional VALIDATE CONSTRAINT /
    COMMENT ON CONSTRAINT) per branch constraint. The original
    diffTables()-emitted AlterTableAddConstraint targeting the same
    constraint on the same replaced table was previously left in the plan,
    producing duplicate ALTER TABLE ... ADD CONSTRAINT statements and a
    constraint "..." for relation "..." already exists apply failure.

    normalizePostDiffCycles() now dedupes same-table
    AlterTableAddConstraint, AlterTableValidateConstraint and
    CreateCommentOnConstraint changes keyed by
    (changeType, table.stableId, constraint.name) on replaced tables,
    keeping only the last occurrence. Because expandReplaceDependencies()
    appends its additions after the original diffTables() 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

21 Apr 13:43
42e8a40

Choose a tag to compare

Pre-release

Patch Changes

  • 5cc2a21: fix(pg-delta): stop emitting spurious CREATE OR REPLACE TRIGGER on 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 single CREATE TABLE, the other grown via ALTER TABLE DROP/ADD COLUMN (which leaves "dead" attnums that are never renumbered) — the attnum vectors diverged while the trigger definition (rendered by pg_get_triggerdef() using column names) was byte-identical. The diff kept firing a ReplaceTrigger every round, and because CREATE OR REPLACE TRIGGER does not renumber the table's physical columns, the loop never converged.

    Triggers are now compared by pg_get_triggerdef() output (column names) instead of raw tgattr attnums, matching the existing Index pattern that handles the same class of bug for indkey.