@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.
- package/dist/core/catalog.diff.js +22 -3
- package/dist/core/expand-replace-dependencies.d.ts +3 -1
- package/dist/core/expand-replace-dependencies.js +117 -7
- package/dist/core/objects/base.change.d.ts +12 -0
- package/dist/core/objects/base.change.js +14 -0
- package/dist/core/objects/materialized-view/materialized-view.diff.d.ts +1 -0
- package/dist/core/objects/materialized-view/materialized-view.diff.js +59 -59
- package/dist/core/objects/table/changes/table.alter.d.ts +1 -0
- package/dist/core/objects/table/changes/table.alter.js +8 -0
- package/dist/core/objects/view/view.diff.d.ts +1 -0
- package/dist/core/objects/view/view.diff.js +35 -34
- package/dist/core/sort/cycle-breakers.js +8 -3
- package/dist/core/sort/graph-builder.js +6 -0
- package/package.json +2 -2
- package/src/core/catalog.diff.test.ts +173 -0
- package/src/core/catalog.diff.ts +24 -3
- package/src/core/expand-replace-dependencies.test.ts +282 -0
- package/src/core/expand-replace-dependencies.ts +165 -7
- package/src/core/objects/base.change.ts +15 -0
- package/src/core/objects/materialized-view/materialized-view.diff.test.ts +3 -2
- package/src/core/objects/materialized-view/materialized-view.diff.ts +99 -92
- package/src/core/objects/table/changes/table.alter.ts +9 -0
- package/src/core/objects/view/view.diff.ts +67 -60
- package/src/core/sort/cycle-breakers.test.ts +126 -0
- package/src/core/sort/cycle-breakers.ts +12 -2
- package/src/core/sort/graph-builder.ts +6 -0
- package/src/core/sort/sort-changes.test.ts +73 -1
|
@@ -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
|
|
142
|
-
// Avoid emitting redundant
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
|
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
|
-
?
|
|
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
|
-
|
|
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 }),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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.
|
|
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.
|
|
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",
|