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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli/commands/catalog-export.js +22 -1
  2. package/dist/core/catalog.diff.js +22 -3
  3. package/dist/core/catalog.filter.d.ts +17 -0
  4. package/dist/core/catalog.filter.js +75 -0
  5. package/dist/core/catalog.model.js +7 -1
  6. package/dist/core/expand-replace-dependencies.d.ts +3 -1
  7. package/dist/core/expand-replace-dependencies.js +117 -7
  8. package/dist/core/integrations/supabase.js +102 -11
  9. package/dist/core/objects/base.change.d.ts +12 -0
  10. package/dist/core/objects/base.change.js +14 -0
  11. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +4 -0
  12. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +28 -2
  13. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +4 -0
  14. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +18 -1
  15. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +4 -0
  16. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +18 -1
  17. package/dist/core/objects/materialized-view/materialized-view.diff.d.ts +1 -0
  18. package/dist/core/objects/materialized-view/materialized-view.diff.js +59 -59
  19. package/dist/core/objects/table/changes/table.alter.d.ts +1 -0
  20. package/dist/core/objects/table/changes/table.alter.js +8 -0
  21. package/dist/core/objects/view/view.diff.d.ts +1 -0
  22. package/dist/core/objects/view/view.diff.js +35 -34
  23. package/dist/core/sort/graph-builder.js +6 -0
  24. package/package.json +1 -1
  25. package/src/cli/commands/catalog-export.ts +26 -1
  26. package/src/core/catalog.diff.test.ts +173 -0
  27. package/src/core/catalog.diff.ts +24 -3
  28. package/src/core/catalog.filter.ts +96 -0
  29. package/src/core/catalog.model.ts +10 -2
  30. package/src/core/expand-replace-dependencies.test.ts +282 -0
  31. package/src/core/expand-replace-dependencies.ts +165 -7
  32. package/src/core/integrations/supabase.test.ts +335 -0
  33. package/src/core/integrations/supabase.ts +102 -11
  34. package/src/core/objects/base.change.ts +15 -0
  35. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +28 -2
  36. package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -1
  37. package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +18 -1
  38. package/src/core/objects/materialized-view/materialized-view.diff.test.ts +3 -2
  39. package/src/core/objects/materialized-view/materialized-view.diff.ts +99 -92
  40. package/src/core/objects/table/changes/table.alter.ts +9 -0
  41. package/src/core/objects/view/view.diff.ts +67 -60
  42. package/src/core/sort/graph-builder.ts +6 -0
  43. package/src/core/sort/sort-changes.test.ts +73 -1
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { writeFile } from "node:fs/promises";
5
5
  import { buildCommand } from "@stricli/core";
6
+ import { filterCatalog } from "../../core/catalog.filter.js";
6
7
  import { extractCatalog } from "../../core/catalog.model.js";
7
8
  import { serializeCatalog, stringifyCatalogSnapshot, } from "../../core/catalog.snapshot.js";
8
9
  import { createManagedPool } from "../../core/postgres-config.js";
@@ -25,6 +26,19 @@ export const catalogExportCommand = buildCommand({
25
26
  parse: String,
26
27
  optional: true,
27
28
  },
29
+ filter: {
30
+ kind: "parsed",
31
+ brief: 'Filter DSL as inline JSON to filter changes (e.g., \'{"*/schema": "app"}\').',
32
+ parse: (value) => {
33
+ try {
34
+ return JSON.parse(value);
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Invalid filter JSON: ${error instanceof Error ? error.message : String(error)}`);
38
+ }
39
+ },
40
+ optional: true,
41
+ },
28
42
  },
29
43
  aliases: {
30
44
  t: "target",
@@ -43,6 +57,10 @@ Use cases:
43
57
  - Snapshot template1 for use as an empty-database baseline
44
58
  - Snapshot a production database to generate revert migrations
45
59
  - Snapshot any state for reproducible offline diffs
60
+
61
+ Pass --filter to scope the snapshot to a subset of the catalog (same
62
+ Filter DSL accepted by plan/sync). Useful when committing a baseline
63
+ snapshot to a repo and only one schema's drift is interesting.
46
64
  `.trim(),
47
65
  },
48
66
  async func(flags) {
@@ -52,7 +70,10 @@ Use cases:
52
70
  });
53
71
  try {
54
72
  const catalog = await extractCatalog(pool);
55
- const snapshot = serializeCatalog(catalog);
73
+ const scoped = flags.filter
74
+ ? await filterCatalog(catalog, flags.filter)
75
+ : catalog;
76
+ const snapshot = serializeCatalog(scoped);
56
77
  const json = stringifyCatalogSnapshot(snapshot);
57
78
  await writeFile(flags.output, json, "utf-8");
58
79
  this.process.stdout.write(`Catalog snapshot written to ${flags.output}\n`);
@@ -138,19 +138,37 @@ export function diffCatalogs(main, branch, options) {
138
138
  changes.push(...diffServers(diffContext, main.servers, branch.servers));
139
139
  changes.push(...diffUserMappings(main.userMappings, branch.userMappings));
140
140
  changes.push(...diffForeignTables(diffContext, main.foreignTables, branch.foreignTables));
141
- // Filter privilege REVOKEs for objects that are being dropped
142
- // Avoid emitting redundant REVOKE statements for targets that will no longer exist.
141
+ // Filter privilege changes for objects that are only being dropped.
142
+ // Avoid emitting redundant ACL statements for targets that will no longer exist.
143
143
  const droppedObjectStableIds = new Set();
144
+ const createdStableIds = new Set();
144
145
  for (const change of changes) {
145
146
  if (change.operation === "drop" && change.scope === "object") {
146
147
  for (const dep of change.requires) {
147
148
  droppedObjectStableIds.add(dep);
148
149
  }
149
150
  }
151
+ if (change.operation === "create" && change.scope === "object") {
152
+ for (const dep of change.creates) {
153
+ createdStableIds.add(dep);
154
+ }
155
+ }
150
156
  }
157
+ // A pure DROP does not need ACL cleanup: the target object is going away.
158
+ // A replacement is different: it has both DROP and CREATE for the same stable
159
+ // id, and its privilege ALTERs describe the ACL state of the newly created
160
+ // object. Keep all of them, including REVOKE/REVOKE GRANT OPTION generated to
161
+ // subtract privileges inherited from ALTER DEFAULT PRIVILEGES at create time.
162
+ const replacementStableIds = new Set([...droppedObjectStableIds].filter((id) => createdStableIds.has(id)));
151
163
  let filteredChanges = changes.filter((change) => {
152
164
  if (change.operation === "alter" && change.scope === "privilege") {
153
- return !droppedObjectStableIds.has(getPrivilegeTargetStableId(change));
165
+ const targetStableId = getPrivilegeTargetStableId(change);
166
+ // Checking only privilege creates would keep replacement GRANTs but drop
167
+ // replacement REVOKEs, so preserve by replacement target stable id instead.
168
+ if (replacementStableIds.has(targetStableId)) {
169
+ return true;
170
+ }
171
+ return !droppedObjectStableIds.has(targetStableId);
154
172
  }
155
173
  return true;
156
174
  });
@@ -158,6 +176,7 @@ export function diffCatalogs(main, branch, options) {
158
176
  changes: filteredChanges,
159
177
  mainCatalog: main,
160
178
  branchCatalog: branch,
179
+ diffContext,
161
180
  });
162
181
  filteredChanges = normalizePostDiffChanges({
163
182
  changes: expandedDependencies.changes,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Prune a catalog to the objects that match a Filter DSL expression.
3
+ *
4
+ * The Filter DSL is defined over Change objects, so the catalog is
5
+ * diffed against an empty baseline first to materialize one CREATE
6
+ * change per object. The filter then evaluates against the same shape
7
+ * it would at plan time, and the surviving stableIds drive the prune.
8
+ *
9
+ * Dependency cascade is not applied. A scoped snapshot is partial by
10
+ * design: out-of-scope owners, roles, and types must exist on the
11
+ * target DB at apply time. Cascading would expand the filter beyond
12
+ * what the caller asked for and, in practice, collapse schema-scoped
13
+ * exports whose kept objects reference cluster-scoped owners.
14
+ */
15
+ import { Catalog } from "./catalog.model.ts";
16
+ import { type FilterDSL } from "./integrations/filter/dsl.ts";
17
+ export declare function filterCatalog(catalog: Catalog, filter: FilterDSL): Promise<Catalog>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Prune a catalog to the objects that match a Filter DSL expression.
3
+ *
4
+ * The Filter DSL is defined over Change objects, so the catalog is
5
+ * diffed against an empty baseline first to materialize one CREATE
6
+ * change per object. The filter then evaluates against the same shape
7
+ * it would at plan time, and the surviving stableIds drive the prune.
8
+ *
9
+ * Dependency cascade is not applied. A scoped snapshot is partial by
10
+ * design: out-of-scope owners, roles, and types must exist on the
11
+ * target DB at apply time. Cascading would expand the filter beyond
12
+ * what the caller asked for and, in practice, collapse schema-scoped
13
+ * exports whose kept objects reference cluster-scoped owners.
14
+ */
15
+ import { diffCatalogs } from "./catalog.diff.js";
16
+ import { Catalog, createEmptyCatalog } from "./catalog.model.js";
17
+ import { compileFilterDSL } from "./integrations/filter/dsl.js";
18
+ export async function filterCatalog(catalog, filter) {
19
+ if (typeof filter === "object" &&
20
+ filter !== null &&
21
+ filter.cascade === true) {
22
+ throw new Error("Filter DSL `cascade: true` is not supported by catalog-export: " +
23
+ "scoped snapshots are intentionally partial. Out-of-scope owners, " +
24
+ "roles, and types must exist on the target DB at apply time.");
25
+ }
26
+ const empty = await createEmptyCatalog(catalog.version, catalog.currentUser);
27
+ const changes = diffCatalogs(empty, catalog);
28
+ const filterFn = compileFilterDSL(filter);
29
+ const keep = new Set();
30
+ for (const change of changes) {
31
+ if (!filterFn(change))
32
+ continue;
33
+ for (const id of change.creates ?? [])
34
+ keep.add(id);
35
+ }
36
+ return pruneCatalog(catalog, keep);
37
+ }
38
+ function filterRecord(record, keep) {
39
+ return Object.fromEntries(Object.entries(record).filter(([id]) => keep.has(id)));
40
+ }
41
+ function pruneCatalog(catalog, keep) {
42
+ const tables = filterRecord(catalog.tables, keep);
43
+ const materializedViews = filterRecord(catalog.materializedViews, keep);
44
+ return new Catalog({
45
+ aggregates: filterRecord(catalog.aggregates, keep),
46
+ collations: filterRecord(catalog.collations, keep),
47
+ compositeTypes: filterRecord(catalog.compositeTypes, keep),
48
+ domains: filterRecord(catalog.domains, keep),
49
+ enums: filterRecord(catalog.enums, keep),
50
+ extensions: filterRecord(catalog.extensions, keep),
51
+ procedures: filterRecord(catalog.procedures, keep),
52
+ indexes: filterRecord(catalog.indexes, keep),
53
+ materializedViews,
54
+ subscriptions: filterRecord(catalog.subscriptions, keep),
55
+ publications: filterRecord(catalog.publications, keep),
56
+ rlsPolicies: filterRecord(catalog.rlsPolicies, keep),
57
+ roles: filterRecord(catalog.roles, keep),
58
+ schemas: filterRecord(catalog.schemas, keep),
59
+ sequences: filterRecord(catalog.sequences, keep),
60
+ tables,
61
+ triggers: filterRecord(catalog.triggers, keep),
62
+ eventTriggers: filterRecord(catalog.eventTriggers, keep),
63
+ rules: filterRecord(catalog.rules, keep),
64
+ ranges: filterRecord(catalog.ranges, keep),
65
+ views: filterRecord(catalog.views, keep),
66
+ foreignDataWrappers: filterRecord(catalog.foreignDataWrappers, keep),
67
+ servers: filterRecord(catalog.servers, keep),
68
+ userMappings: filterRecord(catalog.userMappings, keep),
69
+ foreignTables: filterRecord(catalog.foreignTables, keep),
70
+ depends: catalog.depends.filter((d) => keep.has(d.dependent_stable_id) && keep.has(d.referenced_stable_id)),
71
+ indexableObjects: { ...tables, ...materializedViews },
72
+ version: catalog.version,
73
+ currentUser: catalog.currentUser,
74
+ });
75
+ }
@@ -93,7 +93,7 @@ export class Catalog {
93
93
  let _pg1516Baseline = null;
94
94
  let _pg17Baseline = null;
95
95
  async function loadBaselineJson() {
96
- const mod = await import("./fixtures/empty-catalogs/postgres-15-16-baseline.json");
96
+ const mod = await import("./fixtures/empty-catalogs/postgres-15-16-baseline.json", { with: { type: "json" } });
97
97
  return mod.default;
98
98
  }
99
99
  async function getPg1516Baseline() {
@@ -286,6 +286,8 @@ function normalizeCatalog(catalog) {
286
286
  options: redactSensitiveOptionPairs(server.options),
287
287
  comment: server.comment,
288
288
  privileges: server.privileges,
289
+ wrapper_handler: server.wrapper_handler,
290
+ wrapper_validator: server.wrapper_validator,
289
291
  });
290
292
  });
291
293
  const userMappings = mapRecord(catalog.userMappings, (mapping) => {
@@ -293,6 +295,8 @@ function normalizeCatalog(catalog) {
293
295
  user: mapping.user,
294
296
  server: mapping.server,
295
297
  options: redactSensitiveOptionPairs(mapping.options),
298
+ wrapper_handler: mapping.wrapper_handler,
299
+ wrapper_validator: mapping.wrapper_validator,
296
300
  });
297
301
  });
298
302
  const foreignTables = mapRecord(catalog.foreignTables, (foreignTable) => new ForeignTable({
@@ -304,6 +308,8 @@ function normalizeCatalog(catalog) {
304
308
  options: redactSensitiveOptionPairs(foreignTable.options),
305
309
  comment: foreignTable.comment,
306
310
  privileges: foreignTable.privileges,
311
+ wrapper_handler: foreignTable.wrapper_handler,
312
+ wrapper_validator: foreignTable.wrapper_validator,
307
313
  }));
308
314
  const subscriptions = mapRecord(catalog.subscriptions, (subscription) => {
309
315
  return new Subscription({
@@ -1,5 +1,6 @@
1
1
  import type { Catalog } from "./catalog.model.ts";
2
2
  import type { Change } from "./change.types.ts";
3
+ import type { ObjectDiffContext } from "./objects/diff-context.ts";
3
4
  /**
4
5
  * For objects we are replacing (drop + create), ensure that any dependents are also
5
6
  * replaced so that destructive drops succeed. Uses dependency edges from pg_depend
@@ -12,9 +13,10 @@ interface ExpandReplaceDependenciesResult {
12
13
  changes: Change[];
13
14
  replacedTableIds: ReadonlySet<string>;
14
15
  }
15
- export declare function expandReplaceDependencies({ changes, mainCatalog, branchCatalog, }: {
16
+ export declare function expandReplaceDependencies({ changes, mainCatalog, branchCatalog, diffContext, }: {
16
17
  changes: Change[];
17
18
  mainCatalog: Catalog;
18
19
  branchCatalog: Catalog;
20
+ diffContext?: Pick<ObjectDiffContext, "version" | "currentUser" | "defaultPrivilegeState">;
19
21
  }): ExpandReplaceDependenciesResult;
20
22
  export {};
@@ -4,8 +4,12 @@ import { CreateIndex } from "./objects/index/changes/index.create.js";
4
4
  import { DropIndex } from "./objects/index/changes/index.drop.js";
5
5
  import { CreateMaterializedView } from "./objects/materialized-view/changes/materialized-view.create.js";
6
6
  import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.js";
7
+ import { buildCreateMaterializedViewChanges } from "./objects/materialized-view/materialized-view.diff.js";
7
8
  import { CreateProcedure } from "./objects/procedure/changes/procedure.create.js";
8
9
  import { DropProcedure } from "./objects/procedure/changes/procedure.drop.js";
10
+ import { CreateCommentOnRlsPolicy } from "./objects/rls-policy/changes/rls-policy.comment.js";
11
+ import { CreateRlsPolicy } from "./objects/rls-policy/changes/rls-policy.create.js";
12
+ import { DropRlsPolicy } from "./objects/rls-policy/changes/rls-policy.drop.js";
9
13
  import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.js";
10
14
  import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.js";
11
15
  import { CreateTable } from "./objects/table/changes/table.create.js";
@@ -19,7 +23,8 @@ import { DropRange } from "./objects/type/range/changes/range.drop.js";
19
23
  import { stableId } from "./objects/utils.js";
20
24
  import { CreateView } from "./objects/view/changes/view.create.js";
21
25
  import { DropView } from "./objects/view/changes/view.drop.js";
22
- export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog, }) {
26
+ import { buildCreateViewChanges } from "./objects/view/view.diff.js";
27
+ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog, diffContext, }) {
23
28
  const createdIds = new Set();
24
29
  const droppedIds = new Set();
25
30
  for (const change of changes) {
@@ -34,6 +39,15 @@ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog,
34
39
  replaceRoots.add(id);
35
40
  }
36
41
  }
42
+ const promotedRlsPolicyIds = new Set();
43
+ const additions = collectInvalidatedRlsPolicyReplacements({
44
+ changes,
45
+ mainCatalog,
46
+ branchCatalog,
47
+ createdIds,
48
+ droppedIds,
49
+ promotedRlsPolicyIds,
50
+ });
37
51
  // Procedure stableIds are signature-qualified
38
52
  // (`procedure:schema.name(argtypes)`), so a function whose parameter types
39
53
  // change has different ids in `createdIds` and `droppedIds` and would not
@@ -76,7 +90,7 @@ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog,
76
90
  replaceRoots.add(id);
77
91
  }
78
92
  }
79
- if (replaceRoots.size === 0) {
93
+ if (replaceRoots.size === 0 && additions.length === 0) {
80
94
  return {
81
95
  changes,
82
96
  replacedTableIds: new Set(),
@@ -92,7 +106,6 @@ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog,
92
106
  }
93
107
  list.add(dep.dependent_stable_id);
94
108
  }
95
- const additions = [];
96
109
  const visitedTargets = new Set();
97
110
  const visitedRefs = new Set(replaceRoots);
98
111
  const queue = [...replaceRoots];
@@ -144,10 +157,14 @@ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog,
144
157
  const replacementChanges = buildReplaceChanges(resolved, {
145
158
  addDrop,
146
159
  addCreate,
160
+ diffContext,
147
161
  });
148
162
  if (!replacementChanges)
149
163
  continue;
150
164
  additions.push(...replacementChanges);
165
+ if (resolved.kind === "rls_policy") {
166
+ promotedRlsPolicyIds.add(targetId);
167
+ }
151
168
  // If we added a DropTable(T) for an existing table, mark T so any
152
169
  // pre-existing object-scope AlterTable*(T) changes get dropped below —
153
170
  // the DropTable+CreateTable pair supersedes all structural alterations.
@@ -170,10 +187,70 @@ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog,
170
187
  };
171
188
  }
172
189
  return {
173
- changes: [...changes, ...additions],
190
+ changes: [
191
+ ...removeSupersededRlsPolicyAlters(changes, promotedRlsPolicyIds),
192
+ ...additions,
193
+ ],
174
194
  replacedTableIds: tablesReplacedByExpansion,
175
195
  };
176
196
  }
197
+ function collectInvalidatedRlsPolicyReplacements({ changes, mainCatalog, branchCatalog, createdIds, droppedIds, promotedRlsPolicyIds, }) {
198
+ // In-place rewrites report stable ids through `invalidates`: the referenced
199
+ // object keeps its identity, but dependents bound to the old definition must
200
+ // be torn down first. RLS policy expressions are tracked in pg_depend, so use
201
+ // those catalog edges to promote only policies that depend on an invalidated
202
+ // id, without coupling this expansion pass to a concrete table-change class.
203
+ const invalidatedIds = new Set();
204
+ for (const change of changes) {
205
+ for (const invalidatedId of change.invalidates) {
206
+ invalidatedIds.add(invalidatedId);
207
+ }
208
+ }
209
+ if (invalidatedIds.size === 0)
210
+ return [];
211
+ const replacements = [];
212
+ for (const dep of mainCatalog.depends) {
213
+ if (!invalidatedIds.has(dep.referenced_stable_id))
214
+ continue;
215
+ const targetId = normalizeDependentId(dep.dependent_stable_id);
216
+ if (!targetId?.startsWith("rlsPolicy:"))
217
+ continue;
218
+ if (promotedRlsPolicyIds.has(targetId))
219
+ continue;
220
+ if (createdIds.has(targetId) && droppedIds.has(targetId))
221
+ continue;
222
+ const resolved = resolveObjectForStableId(targetId, mainCatalog, branchCatalog);
223
+ if (!resolved || resolved.kind !== "rls_policy")
224
+ continue;
225
+ const addDrop = !droppedIds.has(targetId);
226
+ const addCreate = !createdIds.has(targetId);
227
+ const replacementChanges = buildReplaceChanges(resolved, {
228
+ addDrop,
229
+ addCreate,
230
+ });
231
+ if (!replacementChanges)
232
+ continue;
233
+ replacements.push(...replacementChanges);
234
+ promotedRlsPolicyIds.add(targetId);
235
+ for (const change of replacementChanges) {
236
+ for (const id of change.creates ?? [])
237
+ createdIds.add(id);
238
+ for (const id of change.drops ?? [])
239
+ droppedIds.add(id);
240
+ }
241
+ }
242
+ return replacements;
243
+ }
244
+ function removeSupersededRlsPolicyAlters(changes, promotedRlsPolicyIds) {
245
+ if (promotedRlsPolicyIds.size === 0)
246
+ return changes;
247
+ return changes.filter((change) => {
248
+ if (change.objectType !== "rls_policy" || change.operation !== "alter") {
249
+ return true;
250
+ }
251
+ return !promotedRlsPolicyIds.has(change.policy.stableId);
252
+ });
253
+ }
177
254
  function isOwnedSequenceColumnDependency(referencedId, dependentId, mainCatalog, branchCatalog) {
178
255
  // When a sequence replace root is still OWNED BY the same column, the
179
256
  // sequence->column pg_depend edge is bookkeeping for ownership, not a signal
@@ -264,6 +341,11 @@ function resolveObjectForStableId(stableId, mainCatalog, branchCatalog) {
264
341
  const branch = branchCatalog.procedures[stableId];
265
342
  return main && branch ? { kind: "procedure", main, branch } : null;
266
343
  }
344
+ if (stableId.startsWith("rlsPolicy:")) {
345
+ const main = mainCatalog.rlsPolicies[stableId];
346
+ const branch = branchCatalog.rlsPolicies[stableId];
347
+ return main && branch ? { kind: "rls_policy", main, branch } : null;
348
+ }
267
349
  if (stableId.startsWith("domain:")) {
268
350
  const main = mainCatalog.domains[stableId];
269
351
  const branch = branchCatalog.domains[stableId];
@@ -293,7 +375,7 @@ function resolveObjectForStableId(stableId, mainCatalog, branchCatalog) {
293
375
  return null;
294
376
  }
295
377
  function buildReplaceChanges(resolved, options) {
296
- const { addDrop, addCreate } = options;
378
+ const { addDrop, addCreate, diffContext } = options;
297
379
  if (!addDrop && !addCreate)
298
380
  return null;
299
381
  switch (resolved.kind) {
@@ -327,7 +409,9 @@ function buildReplaceChanges(resolved, options) {
327
409
  case "view":
328
410
  return [
329
411
  ...(addDrop ? [new DropView({ view: resolved.main })] : []),
330
- ...(addCreate ? [new CreateView({ view: resolved.branch })] : []),
412
+ ...(addCreate
413
+ ? buildCreateViewReplacementChanges(resolved.branch, diffContext)
414
+ : []),
331
415
  ];
332
416
  case "materialized_view":
333
417
  return [
@@ -335,7 +419,13 @@ function buildReplaceChanges(resolved, options) {
335
419
  ? [new DropMaterializedView({ materializedView: resolved.main })]
336
420
  : []),
337
421
  ...(addCreate
338
- ? [new CreateMaterializedView({ materializedView: resolved.branch })]
422
+ ? diffContext
423
+ ? buildCreateMaterializedViewChanges(diffContext, resolved.branch)
424
+ : [
425
+ new CreateMaterializedView({
426
+ materializedView: resolved.branch,
427
+ }),
428
+ ]
339
429
  : []),
340
430
  ];
341
431
  case "index":
@@ -373,6 +463,18 @@ function buildReplaceChanges(resolved, options) {
373
463
  ? [new CreateProcedure({ procedure: resolved.branch })]
374
464
  : []),
375
465
  ];
466
+ case "rls_policy":
467
+ return [
468
+ ...(addDrop ? [new DropRlsPolicy({ policy: resolved.main })] : []),
469
+ ...(addCreate
470
+ ? [
471
+ new CreateRlsPolicy({ policy: resolved.branch }),
472
+ ...(resolved.branch.comment !== null
473
+ ? [new CreateCommentOnRlsPolicy({ policy: resolved.branch })]
474
+ : []),
475
+ ]
476
+ : []),
477
+ ];
376
478
  case "enum":
377
479
  return [
378
480
  ...(addDrop ? [new DropEnum({ enum: resolved.main })] : []),
@@ -401,3 +503,11 @@ function buildReplaceChanges(resolved, options) {
401
503
  return null;
402
504
  }
403
505
  }
506
+ function buildCreateViewReplacementChanges(view, diffContext) {
507
+ // Dependency-closure replacements synthesize a create without going through
508
+ // `diffViews`, so replay the same owner/comment/security-label/ACL metadata
509
+ // that a normal non-alterable view replacement would emit.
510
+ return diffContext
511
+ ? buildCreateViewChanges(diffContext, view)
512
+ : [new CreateView({ view })];
513
+ }
@@ -120,6 +120,30 @@ export const supabase = {
120
120
  "trigger/function_schema": [...SUPABASE_SYSTEM_SCHEMAS],
121
121
  },
122
122
  },
123
+ // Defensive fallback for dynamically-created pgmq queue /
124
+ // archive tables. `pgmq.q_<name>` and `pgmq.a_<name>` are
125
+ // materialized by `select pgmq.create('<name>')`, NOT by
126
+ // `CREATE EXTENSION pgmq`, so emitting a user trigger against
127
+ // them fails locally with
128
+ // `relation "pgmq.q_<name>" does not exist`. On a healthy
129
+ // install the trigger extractor's `extension_table_oids` join
130
+ // (packages/pg-delta/src/core/objects/trigger/trigger.model.ts)
131
+ // already drops these via the `pg_depend deptype='e'` row pgmq
132
+ // records during `pgmq.create()`; this rule covers projects
133
+ // where that row is missing (older pgmq, manual table
134
+ // rewrites, `pg_dump`/restore that loses extension deps, ...).
135
+ // pgmq 1.4.4 — the version Supabase Cloud currently ships —
136
+ // does not record the dependency at all.
137
+ {
138
+ not: {
139
+ and: [
140
+ { "trigger/schema": "pgmq" },
141
+ {
142
+ "trigger/table_name": { op: "regex", value: "^[qa]_" },
143
+ },
144
+ ],
145
+ },
146
+ },
123
147
  ],
124
148
  },
125
149
  // Exclude system objects
@@ -180,15 +204,25 @@ export const supabase = {
180
204
  ],
181
205
  },
182
206
  // Platform-managed foreign data wrappers — Wasm-based FDWs
183
- // (e.g. `clerk`, `clerk_oauth`) whose handler/validator live in
184
- // the `extensions` schema. `CREATE FOREIGN DATA WRAPPER`
185
- // requires superuser, and Supabase Cloud provisions these via
186
- // `supabase_admin` at project creation; replaying the DDL
187
- // against a local image fails because the local environment
188
- // has no equivalent pre-step. We can't rely on the FDW owner
189
- // alone after a dump/restore the owner is often rewritten
190
- // away from `supabase_admin`so match on the function
191
- // reference instead.
207
+ // (e.g. `clerk`, `clerk_oauth`) provisioned via the `wrappers`
208
+ // extension. Supabase Cloud creates these as
209
+ // `CREATE FOREIGN DATA WRAPPER clerk_oauth HANDLER
210
+ // extensions.wasm_fdw_handler VALIDATOR
211
+ // extensions.wasm_fdw_validator` at project creation; replaying
212
+ // the DDL against a local image fails because the local
213
+ // environment has no equivalent pre-step. We can't rely on the
214
+ // FDW owner aloneafter a dump/restore the owner is often
215
+ // rewritten away from `supabase_admin` — so match on the shared
216
+ // Wasm handler/validator (`extensions.wasm_fdw_handler` /
217
+ // `extensions.wasm_fdw_validator`) instead.
218
+ //
219
+ // Matching the bare `extensions.*` namespace would be too broad:
220
+ // contrib FDWs like `postgres_fdw` also install their
221
+ // handler/validator into `extensions` on Supabase, and those ARE
222
+ // available in the local image, so a user-created `postgres_fdw`
223
+ // wrapper (and its servers/foreign tables/user mappings) must
224
+ // still roundtrip. Keying on the `wasm_fdw_*` function names
225
+ // targets only the platform Wasm wrappers.
192
226
  {
193
227
  and: [
194
228
  { objectType: "foreign_data_wrapper" },
@@ -197,13 +231,70 @@ export const supabase = {
197
231
  {
198
232
  "foreign_data_wrapper/handler": {
199
233
  op: "regex",
200
- value: "^extensions\\.",
234
+ value: "^extensions\\.wasm_fdw_handler$",
201
235
  },
202
236
  },
203
237
  {
204
238
  "foreign_data_wrapper/validator": {
205
239
  op: "regex",
206
- value: "^extensions\\.",
240
+ value: "^extensions\\.wasm_fdw_validator$",
241
+ },
242
+ },
243
+ ],
244
+ },
245
+ ],
246
+ },
247
+ // Platform-managed Wasm FDW dependents (CLI-1470 follow-up).
248
+ // Suppressing the wrapper DDL alone leaves `CREATE SERVER` /
249
+ // `CREATE FOREIGN TABLE` / `CREATE USER MAPPING` that reference
250
+ // a wrapper local Docker never provisions (`clerk_oauth`, etc.).
251
+ // Match on the parent wrapper's Wasm handler/validator
252
+ // (`extensions.wasm_fdw_handler` / `extensions.wasm_fdw_validator`,
253
+ // joined at extract time) — the same discriminator used for the
254
+ // wrapper itself above. A bare `extensions.*` match would also
255
+ // drop user-created `postgres_fdw` servers/foreign tables/user
256
+ // mappings (whose handler installs into `extensions` but which
257
+ // the local image CAN provision), so keep it scoped to the Wasm
258
+ // function names. Server _privilege_ scope is excluded here —
259
+ // `GRANT/REVOKE ON SERVER` does not require superuser and remains
260
+ // user-declarative state (see CLI-1469 companion test).
261
+ {
262
+ and: [
263
+ { objectType: "server" },
264
+ { not: { scope: "privilege" } },
265
+ {
266
+ or: [
267
+ {
268
+ "{server,foreign_table,user_mapping}/wrapper_handler": {
269
+ op: "regex",
270
+ value: "^extensions\\.wasm_fdw_handler$",
271
+ },
272
+ },
273
+ {
274
+ "{server,foreign_table,user_mapping}/wrapper_validator": {
275
+ op: "regex",
276
+ value: "^extensions\\.wasm_fdw_validator$",
277
+ },
278
+ },
279
+ ],
280
+ },
281
+ ],
282
+ },
283
+ {
284
+ and: [
285
+ { objectType: ["foreign_table", "user_mapping"] },
286
+ {
287
+ or: [
288
+ {
289
+ "{server,foreign_table,user_mapping}/wrapper_handler": {
290
+ op: "regex",
291
+ value: "^extensions\\.wasm_fdw_handler$",
292
+ },
293
+ },
294
+ {
295
+ "{server,foreign_table,user_mapping}/wrapper_validator": {
296
+ op: "regex",
297
+ value: "^extensions\\.wasm_fdw_validator$",
207
298
  },
208
299
  },
209
300
  ],
@@ -39,6 +39,18 @@ export declare abstract class BaseChange {
39
39
  * Defaults to an empty array. Override in subclasses that remove objects.
40
40
  */
41
41
  get drops(): string[];
42
+ /**
43
+ * Stable identifiers this change invalidates in place.
44
+ *
45
+ * Unlike `drops`, the object keeps its identity. This is an ordering-only
46
+ * signal for mutations that rewrite an existing object in a way that requires
47
+ * dependents bound to the old definition to be dropped before the mutation
48
+ * and rebuilt afterward.
49
+ *
50
+ * Defaults to an empty array. Override in subclasses that invalidate
51
+ * dependents without dropping the object.
52
+ */
53
+ get invalidates(): string[];
42
54
  /**
43
55
  * Stable identifiers this change requires to exist beforehand.
44
56
  *
@@ -31,6 +31,20 @@ export class BaseChange {
31
31
  get drops() {
32
32
  return [];
33
33
  }
34
+ /**
35
+ * Stable identifiers this change invalidates in place.
36
+ *
37
+ * Unlike `drops`, the object keeps its identity. This is an ordering-only
38
+ * signal for mutations that rewrite an existing object in a way that requires
39
+ * dependents bound to the old definition to be dropped before the mutation
40
+ * and rebuilt afterward.
41
+ *
42
+ * Defaults to an empty array. Override in subclasses that invalidate
43
+ * dependents without dropping the object.
44
+ */
45
+ get invalidates() {
46
+ return [];
47
+ }
34
48
  /**
35
49
  * Stable identifiers this change requires to exist beforehand.
36
50
  *