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

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.
@@ -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,
@@ -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
+ }
@@ -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
  *
@@ -1,6 +1,7 @@
1
1
  import type { ObjectDiffContext } from "../diff-context.ts";
2
2
  import type { MaterializedViewChange } from "./changes/materialized-view.types.ts";
3
3
  import type { MaterializedView } from "./materialized-view.model.ts";
4
+ export declare function buildCreateMaterializedViewChanges(ctx: Pick<ObjectDiffContext, "version" | "currentUser" | "defaultPrivilegeState">, mv: MaterializedView): MaterializedViewChange[];
4
5
  /**
5
6
  * Diff two sets of materialized views from main and branch catalogs.
6
7
  *
@@ -8,6 +8,63 @@ import { CreateMaterializedView } from "./changes/materialized-view.create.js";
8
8
  import { DropMaterializedView } from "./changes/materialized-view.drop.js";
9
9
  import { GrantMaterializedViewPrivileges, RevokeGrantOptionMaterializedViewPrivileges, RevokeMaterializedViewPrivileges, } from "./changes/materialized-view.privilege.js";
10
10
  import { CreateSecurityLabelOnMaterializedView, DropSecurityLabelOnMaterializedView, } from "./changes/materialized-view.security-label.js";
11
+ export function buildCreateMaterializedViewChanges(ctx, mv) {
12
+ const changes = [
13
+ new CreateMaterializedView({
14
+ materializedView: mv,
15
+ }),
16
+ ];
17
+ // OWNER: If the materialized view should be owned by someone other than the current user,
18
+ // emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
19
+ if (mv.owner !== ctx.currentUser) {
20
+ changes.push(new AlterMaterializedViewChangeOwner({
21
+ materializedView: mv,
22
+ owner: mv.owner,
23
+ }));
24
+ }
25
+ // Materialized view comment on creation
26
+ if (mv.comment !== null) {
27
+ changes.push(new CreateCommentOnMaterializedView({
28
+ materializedView: mv,
29
+ }));
30
+ }
31
+ // Column comments on creation
32
+ for (const col of mv.columns) {
33
+ if (col.comment !== null) {
34
+ changes.push(new CreateCommentOnMaterializedViewColumn({
35
+ materializedView: mv,
36
+ column: col,
37
+ }));
38
+ }
39
+ }
40
+ // Security labels on the matview itself (columns of matviews are not
41
+ // supported targets of SECURITY LABEL, so we only label the relation).
42
+ for (const label of mv.security_labels) {
43
+ changes.push(new CreateSecurityLabelOnMaterializedView({
44
+ materializedView: mv,
45
+ securityLabel: label,
46
+ }));
47
+ }
48
+ // PRIVILEGES: For created objects, compare against default privileges state
49
+ // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
50
+ // so objects are created with the default privileges state in effect.
51
+ // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
52
+ // needed to reach the final desired state.
53
+ const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "materialized_view", mv.schema ?? "");
54
+ const creatorFilteredDefaults = mv.owner !== ctx.currentUser
55
+ ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
56
+ : effectiveDefaults;
57
+ const desiredPrivileges = mv.privileges;
58
+ // Filter out owner privileges - owner always has ALL privileges implicitly
59
+ // and shouldn't be compared. Use the materialized view owner as the reference.
60
+ const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, mv.owner);
61
+ changes.push(...emitColumnPrivilegeChanges(privilegeResults, mv, mv, "materializedView", {
62
+ Grant: GrantMaterializedViewPrivileges,
63
+ Revoke: RevokeMaterializedViewPrivileges,
64
+ RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
65
+ }, effectiveDefaults, ctx.version));
66
+ return changes;
67
+ }
11
68
  /**
12
69
  * Diff two sets of materialized views from main and branch catalogs.
13
70
  *
@@ -20,62 +77,7 @@ export function diffMaterializedViews(ctx, main, branch) {
20
77
  const { created, dropped, altered } = diffObjects(main, branch);
21
78
  const changes = [];
22
79
  for (const materializedViewId of created) {
23
- const mv = branch[materializedViewId];
24
- changes.push(new CreateMaterializedView({
25
- materializedView: mv,
26
- }));
27
- // OWNER: If the materialized view should be owned by someone other than the current user,
28
- // emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
29
- if (mv.owner !== ctx.currentUser) {
30
- changes.push(new AlterMaterializedViewChangeOwner({
31
- materializedView: mv,
32
- owner: mv.owner,
33
- }));
34
- }
35
- // Note: RLS (row_security, force_row_security) is a non-alterable property for materialized views.
36
- // If RLS needs to be enabled, the materialized view must be dropped and recreated, which is
37
- // handled in the "altered" section when non-alterable properties change.
38
- // Materialized view comment on creation
39
- if (mv.comment !== null) {
40
- changes.push(new CreateCommentOnMaterializedView({
41
- materializedView: mv,
42
- }));
43
- }
44
- // Column comments on creation
45
- for (const col of mv.columns) {
46
- if (col.comment !== null) {
47
- changes.push(new CreateCommentOnMaterializedViewColumn({
48
- materializedView: mv,
49
- column: col,
50
- }));
51
- }
52
- }
53
- // Security labels on the matview itself (columns of matviews are not
54
- // supported targets of SECURITY LABEL, so we only label the relation).
55
- for (const label of mv.security_labels) {
56
- changes.push(new CreateSecurityLabelOnMaterializedView({
57
- materializedView: mv,
58
- securityLabel: label,
59
- }));
60
- }
61
- // PRIVILEGES: For created objects, compare against default privileges state
62
- // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
63
- // so objects are created with the default privileges state in effect.
64
- // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
65
- // needed to reach the final desired state.
66
- const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "materialized_view", mv.schema ?? "");
67
- const creatorFilteredDefaults = mv.owner !== ctx.currentUser
68
- ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
69
- : effectiveDefaults;
70
- const desiredPrivileges = mv.privileges;
71
- // Filter out owner privileges - owner always has ALL privileges implicitly
72
- // and shouldn't be compared. Use the materialized view owner as the reference.
73
- const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, mv.owner);
74
- changes.push(...emitColumnPrivilegeChanges(privilegeResults, mv, mv, "materializedView", {
75
- Grant: GrantMaterializedViewPrivileges,
76
- Revoke: RevokeMaterializedViewPrivileges,
77
- RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
78
- }, effectiveDefaults, ctx.version));
80
+ changes.push(...buildCreateMaterializedViewChanges(ctx, branch[materializedViewId]));
79
81
  }
80
82
  for (const materializedViewId of dropped) {
81
83
  changes.push(new DropMaterializedView({ materializedView: main[materializedViewId] }));
@@ -101,9 +103,7 @@ export function diffMaterializedViews(ctx, main, branch) {
101
103
  const nonAlterablePropsChanged = hasNonAlterableChanges(mainMaterializedView, branchMaterializedView, NON_ALTERABLE_FIELDS, { options: deepEqual });
102
104
  if (nonAlterablePropsChanged) {
103
105
  // Replace the entire materialized view (drop + create)
104
- changes.push(new DropMaterializedView({ materializedView: mainMaterializedView }), new CreateMaterializedView({
105
- materializedView: branchMaterializedView,
106
- }));
106
+ changes.push(new DropMaterializedView({ materializedView: mainMaterializedView }), ...buildCreateMaterializedViewChanges(ctx, branchMaterializedView));
107
107
  }
108
108
  else {
109
109
  // Only alterable properties changed - check each one
@@ -285,6 +285,7 @@ export declare class AlterTableAlterColumnType extends AlterTableChange {
285
285
  previousColumn?: ColumnProps;
286
286
  });
287
287
  get requires(): `column:${string}.${string}.${string}`[];
288
+ get invalidates(): `column:${string}.${string}.${string}`[];
288
289
  serialize(_options?: SerializeOptions): string;
289
290
  }
290
291
  /**
@@ -452,6 +452,14 @@ export class AlterTableAlterColumnType extends AlterTableChange {
452
452
  stableId.column(this.table.schema, this.table.name, this.column.name),
453
453
  ];
454
454
  }
455
+ get invalidates() {
456
+ // ALTER COLUMN ... TYPE rewrites the column in place. The column keeps its
457
+ // identity, but anything bound to its old type (views, rules, etc.) must be
458
+ // dropped before the rewrite and rebuilt after, so report it as invalidated.
459
+ return [
460
+ stableId.column(this.table.schema, this.table.name, this.column.name),
461
+ ];
462
+ }
455
463
  serialize(_options) {
456
464
  // previousColumn is optional so direct serializer tests/fixtures can keep
457
465
  // emitting canonical ALTER TYPE SQL without forcing a USING expression.
@@ -1,6 +1,7 @@
1
1
  import type { ObjectDiffContext } from "../diff-context.ts";
2
2
  import type { ViewChange } from "./changes/view.types.ts";
3
3
  import type { View } from "./view.model.ts";
4
+ export declare function buildCreateViewChanges(ctx: Pick<ObjectDiffContext, "version" | "currentUser" | "defaultPrivilegeState">, view: View): ViewChange[];
4
5
  /**
5
6
  * Diff two sets of views from main and branch catalogs.
6
7
  *
@@ -9,6 +9,39 @@ import { CreateView } from "./changes/view.create.js";
9
9
  import { DropView } from "./changes/view.drop.js";
10
10
  import { GrantViewPrivileges, RevokeGrantOptionViewPrivileges, RevokeViewPrivileges, } from "./changes/view.privilege.js";
11
11
  import { CreateSecurityLabelOnView, DropSecurityLabelOnView, } from "./changes/view.security-label.js";
12
+ export function buildCreateViewChanges(ctx, view) {
13
+ const changes = [new CreateView({ view })];
14
+ // OWNER: If the view should be owned by someone other than the current user,
15
+ // emit ALTER VIEW ... OWNER TO after creation
16
+ if (view.owner !== ctx.currentUser) {
17
+ changes.push(new AlterViewChangeOwner({ view, owner: view.owner }));
18
+ }
19
+ if (view.comment !== null) {
20
+ changes.push(new CreateCommentOnView({ view }));
21
+ }
22
+ for (const label of view.security_labels) {
23
+ changes.push(new CreateSecurityLabelOnView({ view, securityLabel: label }));
24
+ }
25
+ // PRIVILEGES: For created objects, compare against default privileges state
26
+ // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
27
+ // so objects are created with the default privileges state in effect.
28
+ // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
29
+ // needed to reach the final desired state.
30
+ const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "view", view.schema ?? "");
31
+ const creatorFilteredDefaults = view.owner !== ctx.currentUser
32
+ ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
33
+ : effectiveDefaults;
34
+ const desiredPrivileges = view.privileges;
35
+ // Filter out owner privileges - owner always has ALL privileges implicitly
36
+ // and shouldn't be compared. Use the view owner as the reference.
37
+ const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, view.owner);
38
+ changes.push(...emitColumnPrivilegeChanges(privilegeResults, view, view, "view", {
39
+ Grant: GrantViewPrivileges,
40
+ Revoke: RevokeViewPrivileges,
41
+ RevokeGrantOption: RevokeGrantOptionViewPrivileges,
42
+ }, effectiveDefaults, ctx.version));
43
+ return changes;
44
+ }
12
45
  /**
13
46
  * Diff two sets of views from main and branch catalogs.
14
47
  *
@@ -20,40 +53,8 @@ import { CreateSecurityLabelOnView, DropSecurityLabelOnView, } from "./changes/v
20
53
  export function diffViews(ctx, main, branch) {
21
54
  const { created, dropped, altered } = diffObjects(main, branch);
22
55
  const changes = [];
23
- const appendCreateViewChanges = (view) => {
24
- changes.push(new CreateView({ view }));
25
- // OWNER: If the view should be owned by someone other than the current user,
26
- // emit ALTER VIEW ... OWNER TO after creation
27
- if (view.owner !== ctx.currentUser) {
28
- changes.push(new AlterViewChangeOwner({ view, owner: view.owner }));
29
- }
30
- if (view.comment !== null) {
31
- changes.push(new CreateCommentOnView({ view }));
32
- }
33
- for (const label of view.security_labels) {
34
- changes.push(new CreateSecurityLabelOnView({ view, securityLabel: label }));
35
- }
36
- // PRIVILEGES: For created objects, compare against default privileges state
37
- // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
38
- // so objects are created with the default privileges state in effect.
39
- // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
40
- // needed to reach the final desired state.
41
- const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "view", view.schema ?? "");
42
- const creatorFilteredDefaults = view.owner !== ctx.currentUser
43
- ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
44
- : effectiveDefaults;
45
- const desiredPrivileges = view.privileges;
46
- // Filter out owner privileges - owner always has ALL privileges implicitly
47
- // and shouldn't be compared. Use the view owner as the reference.
48
- const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, view.owner);
49
- changes.push(...emitColumnPrivilegeChanges(privilegeResults, view, view, "view", {
50
- Grant: GrantViewPrivileges,
51
- Revoke: RevokeViewPrivileges,
52
- RevokeGrantOption: RevokeGrantOptionViewPrivileges,
53
- }, effectiveDefaults, ctx.version));
54
- };
55
56
  for (const viewId of created) {
56
- appendCreateViewChanges(branch[viewId]);
57
+ changes.push(...buildCreateViewChanges(ctx, branch[viewId]));
57
58
  }
58
59
  for (const viewId of dropped) {
59
60
  changes.push(new DropView({ view: main[viewId] }));
@@ -83,7 +84,7 @@ export function diffViews(ctx, main, branch) {
83
84
  // NON_ALTERABLE_FIELDS - a position change always implies a definition change.
84
85
  if (!deepEqual(normalizeColumns(mainView.columns), normalizeColumns(branchView.columns))) {
85
86
  changes.push(new DropView({ view: mainView }));
86
- appendCreateViewChanges(branchView);
87
+ changes.push(...buildCreateViewChanges(ctx, branchView));
87
88
  }
88
89
  else if (nonAlterablePropsChanged) {
89
90
  // Replace the entire view using CREATE OR REPLACE to avoid drop when possible
@@ -351,9 +351,14 @@ function tryBreakPublicationFkConstraintDropCycle(cycleNodeIndexes, phaseChanges
351
351
  if (!publicationTableIds.has(terminalConstraintDrop.table.stableId)) {
352
352
  return null;
353
353
  }
354
- for (const dropTable of dropTables) {
355
- if (!publicationTableIds.has(dropTable.table.stableId))
356
- return null;
354
+ // At least one dropped table must be a publication member — that's the
355
+ // publication → DropTable edge that pulls the publication change into the
356
+ // cycle (the back-edge is the terminal constraint's table, checked above).
357
+ // Don't require ALL of them: publications like supabase_realtime commonly
358
+ // contain only a subset of tables, so intermediate FK-chain tables may not
359
+ // be members (Sentry SUPABASE-API-7RS / CLI-1605).
360
+ if (!dropTables.some((dropTable) => publicationTableIds.has(dropTable.table.stableId))) {
361
+ return null;
357
362
  }
358
363
  const cycleDropTableIds = new Set(dropTables.map((change) => change.table.stableId));
359
364
  let hasFkToTerminalConstraint = false;
@@ -119,6 +119,12 @@ export function buildGraphData(phaseChanges, options) {
119
119
  for (const droppedId of changeItem.drops ?? []) {
120
120
  createdIds.add(droppedId);
121
121
  }
122
+ // In-place mutations keep the object identity but invalidate
123
+ // dependents, so for drop-phase ordering they behave like producers of
124
+ // the invalidated ids without changing Change.drops.
125
+ for (const invalidatedId of changeItem.invalidates) {
126
+ createdIds.add(invalidatedId);
127
+ }
122
128
  }
123
129
  return createdIds;
124
130
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/pg-delta",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.29",
4
4
  "description": "PostgreSQL migrations made easy",
5
5
  "keywords": [
6
6
  "diff",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@stricli/core": "^1.2.4",
82
- "@supabase/pg-topo": "^1.0.0-alpha.1",
82
+ "@supabase/pg-topo": "^1.0.0-alpha.2",
83
83
  "@ts-safeql/sql-tag": "^0.2.0",
84
84
  "chalk": "^5.6.2",
85
85
  "debug": "^4.3.7",