@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.
- package/dist/cli/commands/catalog-export.js +22 -1
- package/dist/core/catalog.diff.js +22 -3
- package/dist/core/catalog.filter.d.ts +17 -0
- package/dist/core/catalog.filter.js +75 -0
- package/dist/core/catalog.model.js +7 -1
- package/dist/core/expand-replace-dependencies.d.ts +3 -1
- package/dist/core/expand-replace-dependencies.js +117 -7
- package/dist/core/integrations/supabase.js +102 -11
- package/dist/core/objects/base.change.d.ts +12 -0
- package/dist/core/objects/base.change.js +14 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +4 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +28 -2
- package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +4 -0
- package/dist/core/objects/foreign-data-wrapper/server/server.model.js +18 -1
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +4 -0
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +18 -1
- 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/graph-builder.js +6 -0
- package/package.json +1 -1
- package/src/cli/commands/catalog-export.ts +26 -1
- package/src/core/catalog.diff.test.ts +173 -0
- package/src/core/catalog.diff.ts +24 -3
- package/src/core/catalog.filter.ts +96 -0
- package/src/core/catalog.model.ts +10 -2
- package/src/core/expand-replace-dependencies.test.ts +282 -0
- package/src/core/expand-replace-dependencies.ts +165 -7
- package/src/core/integrations/supabase.test.ts +335 -0
- package/src/core/integrations/supabase.ts +102 -11
- package/src/core/objects/base.change.ts +15 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +28 -2
- package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -1
- package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +18 -1
- 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/graph-builder.ts +6 -0
- package/src/core/sort/sort-changes.test.ts +73 -1
|
@@ -1,13 +1,18 @@
|
|
|
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
|
import { CreateDomain } from "./objects/domain/changes/domain.create.ts";
|
|
4
5
|
import { DropDomain } from "./objects/domain/changes/domain.drop.ts";
|
|
5
6
|
import { CreateIndex } from "./objects/index/changes/index.create.ts";
|
|
6
7
|
import { DropIndex } from "./objects/index/changes/index.drop.ts";
|
|
7
8
|
import { CreateMaterializedView } from "./objects/materialized-view/changes/materialized-view.create.ts";
|
|
8
9
|
import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.ts";
|
|
10
|
+
import { buildCreateMaterializedViewChanges } from "./objects/materialized-view/materialized-view.diff.ts";
|
|
9
11
|
import { CreateProcedure } from "./objects/procedure/changes/procedure.create.ts";
|
|
10
12
|
import { DropProcedure } from "./objects/procedure/changes/procedure.drop.ts";
|
|
13
|
+
import { CreateCommentOnRlsPolicy } from "./objects/rls-policy/changes/rls-policy.comment.ts";
|
|
14
|
+
import { CreateRlsPolicy } from "./objects/rls-policy/changes/rls-policy.create.ts";
|
|
15
|
+
import { DropRlsPolicy } from "./objects/rls-policy/changes/rls-policy.drop.ts";
|
|
11
16
|
import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.ts";
|
|
12
17
|
import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
|
|
13
18
|
import { CreateTable } from "./objects/table/changes/table.create.ts";
|
|
@@ -21,6 +26,7 @@ import { DropRange } from "./objects/type/range/changes/range.drop.ts";
|
|
|
21
26
|
import { stableId } from "./objects/utils.ts";
|
|
22
27
|
import { CreateView } from "./objects/view/changes/view.create.ts";
|
|
23
28
|
import { DropView } from "./objects/view/changes/view.drop.ts";
|
|
29
|
+
import { buildCreateViewChanges } from "./objects/view/view.diff.ts";
|
|
24
30
|
|
|
25
31
|
type ResolvedObject =
|
|
26
32
|
| {
|
|
@@ -49,6 +55,11 @@ type ResolvedObject =
|
|
|
49
55
|
main: Catalog["procedures"][string];
|
|
50
56
|
branch: Catalog["procedures"][string];
|
|
51
57
|
}
|
|
58
|
+
| {
|
|
59
|
+
kind: "rls_policy";
|
|
60
|
+
main: Catalog["rlsPolicies"][string];
|
|
61
|
+
branch: Catalog["rlsPolicies"][string];
|
|
62
|
+
}
|
|
52
63
|
| {
|
|
53
64
|
kind: "enum";
|
|
54
65
|
main: Catalog["enums"][string];
|
|
@@ -87,10 +98,15 @@ export function expandReplaceDependencies({
|
|
|
87
98
|
changes,
|
|
88
99
|
mainCatalog,
|
|
89
100
|
branchCatalog,
|
|
101
|
+
diffContext,
|
|
90
102
|
}: {
|
|
91
103
|
changes: Change[];
|
|
92
104
|
mainCatalog: Catalog;
|
|
93
105
|
branchCatalog: Catalog;
|
|
106
|
+
diffContext?: Pick<
|
|
107
|
+
ObjectDiffContext,
|
|
108
|
+
"version" | "currentUser" | "defaultPrivilegeState"
|
|
109
|
+
>;
|
|
94
110
|
}): ExpandReplaceDependenciesResult {
|
|
95
111
|
const createdIds = new Set<string>();
|
|
96
112
|
const droppedIds = new Set<string>();
|
|
@@ -107,6 +123,16 @@ export function expandReplaceDependencies({
|
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
125
|
|
|
126
|
+
const promotedRlsPolicyIds = new Set<string>();
|
|
127
|
+
const additions: Change[] = collectInvalidatedRlsPolicyReplacements({
|
|
128
|
+
changes,
|
|
129
|
+
mainCatalog,
|
|
130
|
+
branchCatalog,
|
|
131
|
+
createdIds,
|
|
132
|
+
droppedIds,
|
|
133
|
+
promotedRlsPolicyIds,
|
|
134
|
+
});
|
|
135
|
+
|
|
110
136
|
// Procedure stableIds are signature-qualified
|
|
111
137
|
// (`procedure:schema.name(argtypes)`), so a function whose parameter types
|
|
112
138
|
// change has different ids in `createdIds` and `droppedIds` and would not
|
|
@@ -150,7 +176,7 @@ export function expandReplaceDependencies({
|
|
|
150
176
|
}
|
|
151
177
|
}
|
|
152
178
|
|
|
153
|
-
if (replaceRoots.size === 0) {
|
|
179
|
+
if (replaceRoots.size === 0 && additions.length === 0) {
|
|
154
180
|
return {
|
|
155
181
|
changes,
|
|
156
182
|
replacedTableIds: new Set<string>(),
|
|
@@ -168,7 +194,6 @@ export function expandReplaceDependencies({
|
|
|
168
194
|
list.add(dep.dependent_stable_id);
|
|
169
195
|
}
|
|
170
196
|
|
|
171
|
-
const additions: Change[] = [];
|
|
172
197
|
const visitedTargets = new Set<string>();
|
|
173
198
|
const visitedRefs = new Set<string>(replaceRoots);
|
|
174
199
|
const queue: string[] = [...replaceRoots];
|
|
@@ -236,10 +261,14 @@ export function expandReplaceDependencies({
|
|
|
236
261
|
const replacementChanges = buildReplaceChanges(resolved, {
|
|
237
262
|
addDrop,
|
|
238
263
|
addCreate,
|
|
264
|
+
diffContext,
|
|
239
265
|
});
|
|
240
266
|
if (!replacementChanges) continue;
|
|
241
267
|
|
|
242
268
|
additions.push(...replacementChanges);
|
|
269
|
+
if (resolved.kind === "rls_policy") {
|
|
270
|
+
promotedRlsPolicyIds.add(targetId);
|
|
271
|
+
}
|
|
243
272
|
|
|
244
273
|
// If we added a DropTable(T) for an existing table, mark T so any
|
|
245
274
|
// pre-existing object-scope AlterTable*(T) changes get dropped below —
|
|
@@ -264,11 +293,90 @@ export function expandReplaceDependencies({
|
|
|
264
293
|
}
|
|
265
294
|
|
|
266
295
|
return {
|
|
267
|
-
changes: [
|
|
296
|
+
changes: [
|
|
297
|
+
...removeSupersededRlsPolicyAlters(changes, promotedRlsPolicyIds),
|
|
298
|
+
...additions,
|
|
299
|
+
],
|
|
268
300
|
replacedTableIds: tablesReplacedByExpansion,
|
|
269
301
|
};
|
|
270
302
|
}
|
|
271
303
|
|
|
304
|
+
function collectInvalidatedRlsPolicyReplacements({
|
|
305
|
+
changes,
|
|
306
|
+
mainCatalog,
|
|
307
|
+
branchCatalog,
|
|
308
|
+
createdIds,
|
|
309
|
+
droppedIds,
|
|
310
|
+
promotedRlsPolicyIds,
|
|
311
|
+
}: {
|
|
312
|
+
changes: Change[];
|
|
313
|
+
mainCatalog: Catalog;
|
|
314
|
+
branchCatalog: Catalog;
|
|
315
|
+
createdIds: Set<string>;
|
|
316
|
+
droppedIds: Set<string>;
|
|
317
|
+
promotedRlsPolicyIds: Set<string>;
|
|
318
|
+
}): Change[] {
|
|
319
|
+
// In-place rewrites report stable ids through `invalidates`: the referenced
|
|
320
|
+
// object keeps its identity, but dependents bound to the old definition must
|
|
321
|
+
// be torn down first. RLS policy expressions are tracked in pg_depend, so use
|
|
322
|
+
// those catalog edges to promote only policies that depend on an invalidated
|
|
323
|
+
// id, without coupling this expansion pass to a concrete table-change class.
|
|
324
|
+
const invalidatedIds = new Set<string>();
|
|
325
|
+
for (const change of changes) {
|
|
326
|
+
for (const invalidatedId of change.invalidates) {
|
|
327
|
+
invalidatedIds.add(invalidatedId);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (invalidatedIds.size === 0) return [];
|
|
331
|
+
|
|
332
|
+
const replacements: Change[] = [];
|
|
333
|
+
for (const dep of mainCatalog.depends) {
|
|
334
|
+
if (!invalidatedIds.has(dep.referenced_stable_id)) continue;
|
|
335
|
+
|
|
336
|
+
const targetId = normalizeDependentId(dep.dependent_stable_id);
|
|
337
|
+
if (!targetId?.startsWith("rlsPolicy:")) continue;
|
|
338
|
+
if (promotedRlsPolicyIds.has(targetId)) continue;
|
|
339
|
+
if (createdIds.has(targetId) && droppedIds.has(targetId)) continue;
|
|
340
|
+
|
|
341
|
+
const resolved = resolveObjectForStableId(
|
|
342
|
+
targetId,
|
|
343
|
+
mainCatalog,
|
|
344
|
+
branchCatalog,
|
|
345
|
+
);
|
|
346
|
+
if (!resolved || resolved.kind !== "rls_policy") continue;
|
|
347
|
+
|
|
348
|
+
const addDrop = !droppedIds.has(targetId);
|
|
349
|
+
const addCreate = !createdIds.has(targetId);
|
|
350
|
+
const replacementChanges = buildReplaceChanges(resolved, {
|
|
351
|
+
addDrop,
|
|
352
|
+
addCreate,
|
|
353
|
+
});
|
|
354
|
+
if (!replacementChanges) continue;
|
|
355
|
+
|
|
356
|
+
replacements.push(...replacementChanges);
|
|
357
|
+
promotedRlsPolicyIds.add(targetId);
|
|
358
|
+
for (const change of replacementChanges) {
|
|
359
|
+
for (const id of change.creates ?? []) createdIds.add(id);
|
|
360
|
+
for (const id of change.drops ?? []) droppedIds.add(id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return replacements;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function removeSupersededRlsPolicyAlters(
|
|
368
|
+
changes: Change[],
|
|
369
|
+
promotedRlsPolicyIds: ReadonlySet<string>,
|
|
370
|
+
): Change[] {
|
|
371
|
+
if (promotedRlsPolicyIds.size === 0) return changes;
|
|
372
|
+
return changes.filter((change) => {
|
|
373
|
+
if (change.objectType !== "rls_policy" || change.operation !== "alter") {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
return !promotedRlsPolicyIds.has(change.policy.stableId);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
272
380
|
function isOwnedSequenceColumnDependency(
|
|
273
381
|
referencedId: string,
|
|
274
382
|
dependentId: string,
|
|
@@ -395,6 +503,12 @@ function resolveObjectForStableId(
|
|
|
395
503
|
return main && branch ? { kind: "procedure", main, branch } : null;
|
|
396
504
|
}
|
|
397
505
|
|
|
506
|
+
if (stableId.startsWith("rlsPolicy:")) {
|
|
507
|
+
const main = mainCatalog.rlsPolicies[stableId];
|
|
508
|
+
const branch = branchCatalog.rlsPolicies[stableId];
|
|
509
|
+
return main && branch ? { kind: "rls_policy", main, branch } : null;
|
|
510
|
+
}
|
|
511
|
+
|
|
398
512
|
if (stableId.startsWith("domain:")) {
|
|
399
513
|
const main = mainCatalog.domains[stableId];
|
|
400
514
|
const branch = branchCatalog.domains[stableId];
|
|
@@ -430,9 +544,16 @@ function resolveObjectForStableId(
|
|
|
430
544
|
|
|
431
545
|
function buildReplaceChanges(
|
|
432
546
|
resolved: ResolvedObject,
|
|
433
|
-
options: {
|
|
547
|
+
options: {
|
|
548
|
+
addDrop: boolean;
|
|
549
|
+
addCreate: boolean;
|
|
550
|
+
diffContext?: Pick<
|
|
551
|
+
ObjectDiffContext,
|
|
552
|
+
"version" | "currentUser" | "defaultPrivilegeState"
|
|
553
|
+
>;
|
|
554
|
+
},
|
|
434
555
|
): Change[] | null {
|
|
435
|
-
const { addDrop, addCreate } = options;
|
|
556
|
+
const { addDrop, addCreate, diffContext } = options;
|
|
436
557
|
|
|
437
558
|
if (!addDrop && !addCreate) return null;
|
|
438
559
|
|
|
@@ -471,7 +592,9 @@ function buildReplaceChanges(
|
|
|
471
592
|
case "view":
|
|
472
593
|
return [
|
|
473
594
|
...(addDrop ? [new DropView({ view: resolved.main })] : []),
|
|
474
|
-
...(addCreate
|
|
595
|
+
...(addCreate
|
|
596
|
+
? buildCreateViewReplacementChanges(resolved.branch, diffContext)
|
|
597
|
+
: []),
|
|
475
598
|
];
|
|
476
599
|
case "materialized_view":
|
|
477
600
|
return [
|
|
@@ -479,7 +602,13 @@ function buildReplaceChanges(
|
|
|
479
602
|
? [new DropMaterializedView({ materializedView: resolved.main })]
|
|
480
603
|
: []),
|
|
481
604
|
...(addCreate
|
|
482
|
-
?
|
|
605
|
+
? diffContext
|
|
606
|
+
? buildCreateMaterializedViewChanges(diffContext, resolved.branch)
|
|
607
|
+
: [
|
|
608
|
+
new CreateMaterializedView({
|
|
609
|
+
materializedView: resolved.branch,
|
|
610
|
+
}),
|
|
611
|
+
]
|
|
483
612
|
: []),
|
|
484
613
|
];
|
|
485
614
|
case "index":
|
|
@@ -519,6 +648,18 @@ function buildReplaceChanges(
|
|
|
519
648
|
? [new CreateProcedure({ procedure: resolved.branch })]
|
|
520
649
|
: []),
|
|
521
650
|
];
|
|
651
|
+
case "rls_policy":
|
|
652
|
+
return [
|
|
653
|
+
...(addDrop ? [new DropRlsPolicy({ policy: resolved.main })] : []),
|
|
654
|
+
...(addCreate
|
|
655
|
+
? [
|
|
656
|
+
new CreateRlsPolicy({ policy: resolved.branch }),
|
|
657
|
+
...(resolved.branch.comment !== null
|
|
658
|
+
? [new CreateCommentOnRlsPolicy({ policy: resolved.branch })]
|
|
659
|
+
: []),
|
|
660
|
+
]
|
|
661
|
+
: []),
|
|
662
|
+
];
|
|
522
663
|
case "enum":
|
|
523
664
|
return [
|
|
524
665
|
...(addDrop ? [new DropEnum({ enum: resolved.main })] : []),
|
|
@@ -547,3 +688,20 @@ function buildReplaceChanges(
|
|
|
547
688
|
return null;
|
|
548
689
|
}
|
|
549
690
|
}
|
|
691
|
+
|
|
692
|
+
function buildCreateViewReplacementChanges(
|
|
693
|
+
view: Catalog["views"][string],
|
|
694
|
+
diffContext:
|
|
695
|
+
| Pick<
|
|
696
|
+
ObjectDiffContext,
|
|
697
|
+
"version" | "currentUser" | "defaultPrivilegeState"
|
|
698
|
+
>
|
|
699
|
+
| undefined,
|
|
700
|
+
): Change[] {
|
|
701
|
+
// Dependency-closure replacements synthesize a create without going through
|
|
702
|
+
// `diffViews`, so replay the same owner/comment/security-label/ACL metadata
|
|
703
|
+
// that a normal non-alterable view replacement would emit.
|
|
704
|
+
return diffContext
|
|
705
|
+
? buildCreateViewChanges(diffContext, view)
|
|
706
|
+
: [new CreateView({ view })];
|
|
707
|
+
}
|
|
@@ -55,6 +55,86 @@ function fdwPrivilegeChange(fdw: { name: string; owner: string }): Change {
|
|
|
55
55
|
} as unknown as Change;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function serverChange(
|
|
59
|
+
operation: "create" | "alter" | "drop",
|
|
60
|
+
server: {
|
|
61
|
+
name: string;
|
|
62
|
+
owner: string;
|
|
63
|
+
foreign_data_wrapper: string;
|
|
64
|
+
wrapper_handler: string | null;
|
|
65
|
+
wrapper_validator: string | null;
|
|
66
|
+
},
|
|
67
|
+
): Change {
|
|
68
|
+
return {
|
|
69
|
+
objectType: "server",
|
|
70
|
+
operation,
|
|
71
|
+
scope: "object",
|
|
72
|
+
server: {
|
|
73
|
+
type: null,
|
|
74
|
+
version: null,
|
|
75
|
+
options: null,
|
|
76
|
+
comment: null,
|
|
77
|
+
privileges: [],
|
|
78
|
+
...server,
|
|
79
|
+
},
|
|
80
|
+
requires: [],
|
|
81
|
+
creates: [],
|
|
82
|
+
drops: [],
|
|
83
|
+
} as unknown as Change;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function foreignTableChange(
|
|
87
|
+
operation: "create" | "alter" | "drop",
|
|
88
|
+
foreignTable: {
|
|
89
|
+
schema: string;
|
|
90
|
+
name: string;
|
|
91
|
+
owner: string;
|
|
92
|
+
server: string;
|
|
93
|
+
wrapper_handler: string | null;
|
|
94
|
+
wrapper_validator: string | null;
|
|
95
|
+
},
|
|
96
|
+
): Change {
|
|
97
|
+
return {
|
|
98
|
+
objectType: "foreign_table",
|
|
99
|
+
operation,
|
|
100
|
+
scope: "object",
|
|
101
|
+
foreignTable: {
|
|
102
|
+
options: null,
|
|
103
|
+
comment: null,
|
|
104
|
+
columns: [],
|
|
105
|
+
privileges: [],
|
|
106
|
+
security_labels: [],
|
|
107
|
+
...foreignTable,
|
|
108
|
+
},
|
|
109
|
+
requires: [],
|
|
110
|
+
creates: [],
|
|
111
|
+
drops: [],
|
|
112
|
+
} as unknown as Change;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function userMappingChange(
|
|
116
|
+
operation: "create" | "alter" | "drop",
|
|
117
|
+
userMapping: {
|
|
118
|
+
user: string;
|
|
119
|
+
server: string;
|
|
120
|
+
wrapper_handler: string | null;
|
|
121
|
+
wrapper_validator: string | null;
|
|
122
|
+
},
|
|
123
|
+
): Change {
|
|
124
|
+
return {
|
|
125
|
+
objectType: "user_mapping",
|
|
126
|
+
operation,
|
|
127
|
+
scope: "object",
|
|
128
|
+
userMapping: {
|
|
129
|
+
options: null,
|
|
130
|
+
...userMapping,
|
|
131
|
+
},
|
|
132
|
+
requires: [],
|
|
133
|
+
creates: [],
|
|
134
|
+
drops: [],
|
|
135
|
+
} as unknown as Change;
|
|
136
|
+
}
|
|
137
|
+
|
|
58
138
|
function serverPrivilegeChange(server: {
|
|
59
139
|
name: string;
|
|
60
140
|
owner: string;
|
|
@@ -71,6 +151,33 @@ function serverPrivilegeChange(server: {
|
|
|
71
151
|
} as unknown as Change;
|
|
72
152
|
}
|
|
73
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Build a synthetic trigger change shaped like what `flattenChange` consumes.
|
|
156
|
+
* The flattener emits keys `trigger/schema`, `trigger/table_name`,
|
|
157
|
+
* `trigger/function_schema`, etc. by walking the nested `trigger` model.
|
|
158
|
+
*/
|
|
159
|
+
function triggerChange(
|
|
160
|
+
operation: "create" | "alter" | "drop",
|
|
161
|
+
trigger: {
|
|
162
|
+
schema: string;
|
|
163
|
+
name: string;
|
|
164
|
+
table_name: string;
|
|
165
|
+
function_schema: string;
|
|
166
|
+
function_name: string;
|
|
167
|
+
owner: string;
|
|
168
|
+
},
|
|
169
|
+
): Change {
|
|
170
|
+
return {
|
|
171
|
+
objectType: "trigger",
|
|
172
|
+
operation,
|
|
173
|
+
scope: "object",
|
|
174
|
+
trigger,
|
|
175
|
+
requires: [],
|
|
176
|
+
creates: [],
|
|
177
|
+
drops: [],
|
|
178
|
+
} as unknown as Change;
|
|
179
|
+
}
|
|
180
|
+
|
|
74
181
|
describe("supabase integration filter — foreign data wrappers", () => {
|
|
75
182
|
// Regression for CLI-1470. Wasm-based foreign data wrappers on Supabase
|
|
76
183
|
// (e.g. `clerk`, `clerk_oauth`) are provisioned at project creation by
|
|
@@ -128,6 +235,35 @@ describe("supabase integration filter — foreign data wrappers", () => {
|
|
|
128
235
|
expect(evaluatePattern(filter, change)).toBe(true);
|
|
129
236
|
});
|
|
130
237
|
|
|
238
|
+
// `postgres_fdw` (and other contrib FDWs) install their handler/validator
|
|
239
|
+
// into `extensions` on Supabase, but they ARE available in the local image,
|
|
240
|
+
// so a user-created `postgres_fdw` wrapper must roundtrip. Only the Wasm
|
|
241
|
+
// `wasm_fdw_handler` / `wasm_fdw_validator` functions identify the
|
|
242
|
+
// platform-managed wrappers that local Docker cannot provision.
|
|
243
|
+
test("preserves user FDW whose handler is extensions.postgres_fdw_handler", () => {
|
|
244
|
+
const change = fdwChange("create", {
|
|
245
|
+
name: "postgres_fdw",
|
|
246
|
+
owner: "postgres",
|
|
247
|
+
handler: "extensions.postgres_fdw_handler",
|
|
248
|
+
validator: "extensions.postgres_fdw_validator",
|
|
249
|
+
});
|
|
250
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// The Wasm discriminator must be an exact function-name match, not a
|
|
254
|
+
// prefix: a user function whose name merely starts with `wasm_fdw_handler`
|
|
255
|
+
// (e.g. `wasm_fdw_handler_custom`) is not the platform `wrappers` handler
|
|
256
|
+
// and must roundtrip.
|
|
257
|
+
test("preserves user FDW whose handler extends the wasm_fdw_handler prefix", () => {
|
|
258
|
+
const change = fdwChange("create", {
|
|
259
|
+
name: "custom_wasm",
|
|
260
|
+
owner: "postgres",
|
|
261
|
+
handler: "extensions.wasm_fdw_handler_custom",
|
|
262
|
+
validator: "extensions.wasm_fdw_validator_custom",
|
|
263
|
+
});
|
|
264
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
131
267
|
test("preserves user FDW with no handler/validator", () => {
|
|
132
268
|
const change = fdwChange("create", {
|
|
133
269
|
name: "user_fdw_bare",
|
|
@@ -196,3 +332,202 @@ describe("supabase integration filter — foreign data wrapper / server ACLs", (
|
|
|
196
332
|
expect(evaluatePattern(filter, change)).toBe(true);
|
|
197
333
|
});
|
|
198
334
|
});
|
|
335
|
+
|
|
336
|
+
describe("supabase integration filter — Wasm FDW dependents", () => {
|
|
337
|
+
const wasmWrapper = {
|
|
338
|
+
wrapper_handler: "extensions.wasm_fdw_handler",
|
|
339
|
+
wrapper_validator: "extensions.wasm_fdw_validator",
|
|
340
|
+
} as const;
|
|
341
|
+
|
|
342
|
+
const userWrapper = {
|
|
343
|
+
wrapper_handler: "public.postgres_fdw_handler",
|
|
344
|
+
wrapper_validator: "public.postgres_fdw_validator",
|
|
345
|
+
} as const;
|
|
346
|
+
|
|
347
|
+
// `postgres_fdw` installs its handler/validator into `extensions` on
|
|
348
|
+
// Supabase, but the contrib FDW IS available locally, so user-owned
|
|
349
|
+
// servers / foreign tables / user mappings built on it must roundtrip.
|
|
350
|
+
// Keying suppression on the bare `extensions.*` namespace would wrongly
|
|
351
|
+
// drop them; only the Wasm `wasm_fdw_*` functions mark platform wrappers.
|
|
352
|
+
const extensionsPgFdwWrapper = {
|
|
353
|
+
wrapper_handler: "extensions.postgres_fdw_handler",
|
|
354
|
+
wrapper_validator: "extensions.postgres_fdw_validator",
|
|
355
|
+
} as const;
|
|
356
|
+
|
|
357
|
+
test("suppresses CREATE SERVER bound to extensions.* Wasm FDW", () => {
|
|
358
|
+
const change = serverChange("create", {
|
|
359
|
+
name: "clerk_oauth_server",
|
|
360
|
+
owner: "postgres",
|
|
361
|
+
foreign_data_wrapper: "clerk_oauth",
|
|
362
|
+
...wasmWrapper,
|
|
363
|
+
});
|
|
364
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("suppresses DROP FOREIGN TABLE bound to extensions.* Wasm FDW", () => {
|
|
368
|
+
const change = foreignTableChange("drop", {
|
|
369
|
+
schema: "public",
|
|
370
|
+
name: "clerk_oauth",
|
|
371
|
+
owner: "postgres",
|
|
372
|
+
server: "clerk_oauth_server",
|
|
373
|
+
...wasmWrapper,
|
|
374
|
+
});
|
|
375
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("suppresses ALTER FOREIGN TABLE bound to extensions.* Wasm FDW", () => {
|
|
379
|
+
const change = foreignTableChange("alter", {
|
|
380
|
+
schema: "public",
|
|
381
|
+
name: "clerk_oauth",
|
|
382
|
+
owner: "postgres",
|
|
383
|
+
server: "clerk_oauth_server",
|
|
384
|
+
...wasmWrapper,
|
|
385
|
+
});
|
|
386
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("suppresses DROP USER MAPPING bound to extensions.* Wasm FDW", () => {
|
|
390
|
+
const change = userMappingChange("drop", {
|
|
391
|
+
user: "postgres",
|
|
392
|
+
server: "clerk_server",
|
|
393
|
+
...wasmWrapper,
|
|
394
|
+
});
|
|
395
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("suppresses CREATE USER MAPPING when only wrapper validator is in extensions", () => {
|
|
399
|
+
const change = userMappingChange("create", {
|
|
400
|
+
user: "postgres",
|
|
401
|
+
server: "clerk_server",
|
|
402
|
+
wrapper_handler: null,
|
|
403
|
+
wrapper_validator: "extensions.wasm_fdw_validator",
|
|
404
|
+
});
|
|
405
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("preserves CREATE SERVER bound to user postgres_fdw wrapper", () => {
|
|
409
|
+
const change = serverChange("create", {
|
|
410
|
+
name: "live_risk_server",
|
|
411
|
+
owner: "postgres",
|
|
412
|
+
foreign_data_wrapper: "postgres_fdw",
|
|
413
|
+
...userWrapper,
|
|
414
|
+
});
|
|
415
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("preserves server ACL when postgres_fdw handler lives in extensions", () => {
|
|
419
|
+
const change = serverPrivilegeChange({
|
|
420
|
+
name: "user_server",
|
|
421
|
+
owner: "postgres",
|
|
422
|
+
});
|
|
423
|
+
(change as unknown as { server: Record<string, unknown> }).server = {
|
|
424
|
+
name: "user_server",
|
|
425
|
+
owner: "postgres",
|
|
426
|
+
wrapper_handler: "extensions.postgres_fdw_handler",
|
|
427
|
+
wrapper_validator: "extensions.postgres_fdw_validator",
|
|
428
|
+
};
|
|
429
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("preserves CREATE FOREIGN TABLE on user postgres_fdw server", () => {
|
|
433
|
+
const change = foreignTableChange("create", {
|
|
434
|
+
schema: "live_risk",
|
|
435
|
+
name: "devices",
|
|
436
|
+
owner: "postgres",
|
|
437
|
+
server: "live_risk_server",
|
|
438
|
+
...userWrapper,
|
|
439
|
+
});
|
|
440
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("preserves CREATE SERVER when postgres_fdw handler lives in extensions", () => {
|
|
444
|
+
const change = serverChange("create", {
|
|
445
|
+
name: "user_pg_server",
|
|
446
|
+
owner: "postgres",
|
|
447
|
+
foreign_data_wrapper: "postgres_fdw",
|
|
448
|
+
...extensionsPgFdwWrapper,
|
|
449
|
+
});
|
|
450
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("preserves CREATE FOREIGN TABLE when postgres_fdw handler lives in extensions", () => {
|
|
454
|
+
const change = foreignTableChange("create", {
|
|
455
|
+
schema: "user_fdw_test",
|
|
456
|
+
name: "remote_row",
|
|
457
|
+
owner: "postgres",
|
|
458
|
+
server: "user_pg_server",
|
|
459
|
+
...extensionsPgFdwWrapper,
|
|
460
|
+
});
|
|
461
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("preserves CREATE USER MAPPING when postgres_fdw handler lives in extensions", () => {
|
|
465
|
+
const change = userMappingChange("create", {
|
|
466
|
+
user: "postgres",
|
|
467
|
+
server: "user_pg_server",
|
|
468
|
+
...extensionsPgFdwWrapper,
|
|
469
|
+
});
|
|
470
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Exact-match guard at the dependent level too: a server bound to a wrapper
|
|
474
|
+
// whose handler merely shares the `wasm_fdw_handler` prefix must roundtrip.
|
|
475
|
+
test("preserves CREATE SERVER when wrapper handler extends the wasm_fdw_handler prefix", () => {
|
|
476
|
+
const change = serverChange("create", {
|
|
477
|
+
name: "custom_wasm_server",
|
|
478
|
+
owner: "postgres",
|
|
479
|
+
foreign_data_wrapper: "custom_wasm",
|
|
480
|
+
wrapper_handler: "extensions.wasm_fdw_handler_custom",
|
|
481
|
+
wrapper_validator: "extensions.wasm_fdw_validator_custom",
|
|
482
|
+
});
|
|
483
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("supabase integration filter — pgmq queue triggers", () => {
|
|
488
|
+
// Regression for the pgmq-1.4.4 cloud projects. `pgmq.create('<name>')`
|
|
489
|
+
// materializes `pgmq.q_<name>` and `pgmq.a_<name>` at runtime — they are
|
|
490
|
+
// NOT created by `CREATE EXTENSION pgmq`. On a healthy install the trigger
|
|
491
|
+
// extractor's `extension_table_oids` join already drops these via the
|
|
492
|
+
// `pg_depend deptype='e'` row that newer pgmq versions record, but on
|
|
493
|
+
// pgmq 1.4.4 that row is never recorded, so user triggers on the queue
|
|
494
|
+
// tables leak into the diff and break `supabase db reset` with
|
|
495
|
+
// `relation "pgmq.q_<name>" does not exist`. The filter must drop them
|
|
496
|
+
// at the supabase-integration level too, regardless of pg_depend state.
|
|
497
|
+
|
|
498
|
+
test("suppresses CREATE trigger on pgmq.q_<name> calling a public function", () => {
|
|
499
|
+
const change = triggerChange("create", {
|
|
500
|
+
schema: "pgmq",
|
|
501
|
+
name: "after_insert_processed_milestones_queue",
|
|
502
|
+
table_name: "q_processed_milestones_queue",
|
|
503
|
+
function_schema: "public",
|
|
504
|
+
function_name: "move_data_from_queue",
|
|
505
|
+
owner: "postgres",
|
|
506
|
+
});
|
|
507
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("suppresses DROP trigger on pgmq.a_<name> calling a public function", () => {
|
|
511
|
+
const change = triggerChange("drop", {
|
|
512
|
+
schema: "pgmq",
|
|
513
|
+
name: "after_insert_archive",
|
|
514
|
+
table_name: "a_processed_milestones_queue",
|
|
515
|
+
function_schema: "public",
|
|
516
|
+
function_name: "archive_handler",
|
|
517
|
+
owner: "postgres",
|
|
518
|
+
});
|
|
519
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("preserves CREATE trigger on auth.users calling a public function", () => {
|
|
523
|
+
const change = triggerChange("create", {
|
|
524
|
+
schema: "auth",
|
|
525
|
+
name: "on_auth_user_created",
|
|
526
|
+
table_name: "users",
|
|
527
|
+
function_schema: "public",
|
|
528
|
+
function_name: "handle_new_user",
|
|
529
|
+
owner: "supabase_auth_admin",
|
|
530
|
+
});
|
|
531
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
532
|
+
});
|
|
533
|
+
});
|