@supabase/pg-delta 1.0.0-alpha.24 → 1.0.0-alpha.26
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.model.d.ts +2 -2
- package/dist/core/catalog.model.js +28 -21
- package/dist/core/expand-replace-dependencies.js +1 -7
- package/dist/core/integrations/supabase.js +84 -0
- package/dist/core/objects/aggregate/changes/aggregate.privilege.js +21 -9
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.js +4 -1
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.js +6 -3
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.d.ts +11 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +11 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.js +4 -1
- package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.js +6 -3
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +11 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +11 -0
- package/dist/core/objects/foreign-data-wrapper/sensitive-options.d.ts +32 -0
- package/dist/core/objects/foreign-data-wrapper/sensitive-options.js +129 -0
- package/dist/core/objects/foreign-data-wrapper/server/changes/server.alter.js +4 -1
- package/dist/core/objects/foreign-data-wrapper/server/changes/server.create.js +6 -3
- package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +10 -0
- package/dist/core/objects/foreign-data-wrapper/server/server.model.js +10 -0
- package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.js +4 -1
- package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.js +6 -3
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +10 -0
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +10 -0
- package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
- package/dist/core/objects/table/table.diff.js +53 -30
- package/dist/core/objects/table/table.model.js +7 -2
- package/dist/core/plan/hierarchy.js +4 -4
- package/dist/core/postgres-config.d.ts +7 -0
- package/dist/core/postgres-config.js +19 -5
- package/dist/core/sort/debug-visualization.js +1 -1
- package/dist/core/sort/topological-sort.js +2 -2
- package/package.json +34 -33
- package/src/core/catalog.model.ts +40 -23
- package/src/core/catalog.snapshot.test.ts +1 -0
- package/src/core/expand-replace-dependencies.test.ts +12 -0
- package/src/core/expand-replace-dependencies.ts +1 -12
- package/src/core/integrations/supabase.test.ts +198 -0
- package/src/core/integrations/supabase.ts +84 -0
- package/src/core/objects/aggregate/changes/aggregate.base.ts +1 -1
- package/src/core/objects/aggregate/changes/aggregate.privilege.test.ts +79 -0
- package/src/core/objects/aggregate/changes/aggregate.privilege.ts +22 -9
- package/src/core/objects/collation/changes/collation.base.ts +1 -1
- package/src/core/objects/domain/changes/domain.base.ts +1 -1
- package/src/core/objects/extension/changes/extension.base.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.test.ts +34 -4
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.ts +5 -1
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.base.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.test.ts +34 -0
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.ts +7 -5
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts +11 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.test.ts +25 -4
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.ts +5 -1
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.test.ts +54 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.ts +7 -5
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +11 -0
- package/src/core/objects/foreign-data-wrapper/sensitive-options.test.ts +98 -0
- package/src/core/objects/foreign-data-wrapper/sensitive-options.ts +133 -0
- package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.test.ts +39 -4
- package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.ts +5 -1
- package/src/core/objects/foreign-data-wrapper/server/changes/server.base.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/server/changes/server.create.test.ts +36 -0
- package/src/core/objects/foreign-data-wrapper/server/changes/server.create.ts +7 -5
- package/src/core/objects/foreign-data-wrapper/server/server.model.ts +10 -0
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.test.ts +39 -6
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.ts +5 -1
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.base.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.test.ts +38 -2
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.ts +7 -5
- package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +10 -0
- package/src/core/objects/index/changes/index.base.ts +1 -1
- package/src/core/objects/language/changes/language.base.ts +1 -1
- package/src/core/objects/materialized-view/changes/materialized-view.base.ts +1 -1
- package/src/core/objects/procedure/changes/procedure.base.ts +1 -1
- package/src/core/objects/rls-policy/changes/rls-policy.base.ts +1 -1
- package/src/core/objects/role/changes/role.base.ts +1 -1
- package/src/core/objects/schema/changes/schema.base.ts +1 -1
- package/src/core/objects/sequence/changes/sequence.base.ts +1 -1
- package/src/core/objects/table/changes/table.base.ts +1 -1
- package/src/core/objects/table/changes/table.comment.ts +2 -8
- package/src/core/objects/table/table.diff.test.ts +198 -5
- package/src/core/objects/table/table.diff.ts +63 -34
- package/src/core/objects/table/table.model.ts +7 -2
- package/src/core/objects/trigger/changes/trigger.alter.ts +1 -4
- package/src/core/objects/trigger/changes/trigger.base.ts +1 -1
- package/src/core/objects/type/composite-type/changes/composite-type.base.ts +1 -1
- package/src/core/objects/type/enum/changes/enum.base.ts +1 -1
- package/src/core/objects/type/range/changes/range.base.ts +1 -1
- package/src/core/objects/view/changes/view.base.ts +1 -1
- package/src/core/plan/hierarchy.ts +4 -4
- package/src/core/plan/sql-format/format-off.test.ts +4 -4
- package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +4 -4
- package/src/core/plan/sql-format/format-pretty-narrow.test.ts +5 -4
- package/src/core/plan/sql-format/format-pretty-preserve.test.ts +4 -4
- package/src/core/plan/sql-format/format-pretty-upper.test.ts +4 -4
- package/src/core/postgres-config.test.ts +39 -1
- package/src/core/postgres-config.ts +32 -16
- package/src/core/sort/debug-visualization.ts +1 -1
- package/src/core/sort/sort-changes.test.ts +1 -0
- package/src/core/sort/topological-sort.ts +2 -2
|
@@ -189,11 +189,13 @@ describe("expandReplaceDependencies", () => {
|
|
|
189
189
|
privileges: [],
|
|
190
190
|
});
|
|
191
191
|
const branchView = new View({
|
|
192
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
192
193
|
...mainView,
|
|
193
194
|
definition: " SELECT count(*) AS n FROM public.members;",
|
|
194
195
|
});
|
|
195
196
|
|
|
196
197
|
const mainCatalog = new Catalog({
|
|
198
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
197
199
|
...baseline,
|
|
198
200
|
tables: { [usersTable.stableId]: usersTable },
|
|
199
201
|
views: { [mainView.stableId]: mainView },
|
|
@@ -206,6 +208,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
206
208
|
],
|
|
207
209
|
});
|
|
208
210
|
const branchCatalog = new Catalog({
|
|
211
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
209
212
|
...baseline,
|
|
210
213
|
views: { [branchView.stableId]: branchView },
|
|
211
214
|
});
|
|
@@ -253,6 +256,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
253
256
|
owner: "postgres",
|
|
254
257
|
});
|
|
255
258
|
const branchSequence = new Sequence({
|
|
259
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
256
260
|
...mainSequence,
|
|
257
261
|
persistence: "p",
|
|
258
262
|
});
|
|
@@ -309,6 +313,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
309
313
|
{ [usersTable.stableId]: usersTable },
|
|
310
314
|
);
|
|
311
315
|
const mainCatalog = new Catalog({
|
|
316
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
312
317
|
...baseline,
|
|
313
318
|
sequences: { [mainSequence.stableId]: mainSequence },
|
|
314
319
|
tables: { [usersTable.stableId]: usersTable },
|
|
@@ -326,6 +331,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
326
331
|
],
|
|
327
332
|
});
|
|
328
333
|
const branchCatalog = new Catalog({
|
|
334
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
329
335
|
...baseline,
|
|
330
336
|
sequences: { [branchSequence.stableId]: branchSequence },
|
|
331
337
|
tables: { [usersTable.stableId]: usersTable },
|
|
@@ -369,6 +375,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
369
375
|
privileges: [],
|
|
370
376
|
});
|
|
371
377
|
const branchEnum = new Enum({
|
|
378
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
372
379
|
...mainEnum,
|
|
373
380
|
labels: [
|
|
374
381
|
{ sort_order: 1, label: "draft" },
|
|
@@ -430,6 +437,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
430
437
|
privileges: [],
|
|
431
438
|
});
|
|
432
439
|
const branchChildren = new Table({
|
|
440
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
433
441
|
...mainChildren,
|
|
434
442
|
columns: [
|
|
435
443
|
{ ...columnTemplate, name: "id", position: 1, not_null: true },
|
|
@@ -522,6 +530,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
522
530
|
];
|
|
523
531
|
|
|
524
532
|
const mainCatalog = new Catalog({
|
|
533
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
525
534
|
...baseline,
|
|
526
535
|
enums: { [mainEnum.stableId]: mainEnum },
|
|
527
536
|
tables: { [mainChildren.stableId]: mainChildren },
|
|
@@ -535,6 +544,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
535
544
|
],
|
|
536
545
|
});
|
|
537
546
|
const branchCatalog = new Catalog({
|
|
547
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
538
548
|
...baseline,
|
|
539
549
|
enums: { [branchEnum.stableId]: branchEnum },
|
|
540
550
|
tables: { [branchChildren.stableId]: branchChildren },
|
|
@@ -657,6 +667,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
657
667
|
];
|
|
658
668
|
|
|
659
669
|
const mainCatalog = new Catalog({
|
|
670
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
660
671
|
...baseline,
|
|
661
672
|
procedures: { [mainProcedure.stableId]: mainProcedure },
|
|
662
673
|
views: { [mainView.stableId]: mainView },
|
|
@@ -669,6 +680,7 @@ describe("expandReplaceDependencies", () => {
|
|
|
669
680
|
],
|
|
670
681
|
});
|
|
671
682
|
const branchCatalog = new Catalog({
|
|
683
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
672
684
|
...baseline,
|
|
673
685
|
procedures: { [branchProcedure.stableId]: branchProcedure },
|
|
674
686
|
views: { [branchView.stableId]: branchView },
|
|
@@ -8,10 +8,7 @@ import { CreateMaterializedView } from "./objects/materialized-view/changes/mate
|
|
|
8
8
|
import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.ts";
|
|
9
9
|
import { CreateProcedure } from "./objects/procedure/changes/procedure.create.ts";
|
|
10
10
|
import { DropProcedure } from "./objects/procedure/changes/procedure.drop.ts";
|
|
11
|
-
import {
|
|
12
|
-
AlterTableAddConstraint,
|
|
13
|
-
AlterTableValidateConstraint,
|
|
14
|
-
} from "./objects/table/changes/table.alter.ts";
|
|
11
|
+
import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.ts";
|
|
15
12
|
import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
|
|
16
13
|
import { CreateTable } from "./objects/table/changes/table.create.ts";
|
|
17
14
|
import { DropTable } from "./objects/table/changes/table.drop.ts";
|
|
@@ -455,14 +452,6 @@ function buildReplaceChanges(
|
|
|
455
452
|
constraint,
|
|
456
453
|
}),
|
|
457
454
|
];
|
|
458
|
-
if (!constraint.validated) {
|
|
459
|
-
items.push(
|
|
460
|
-
new AlterTableValidateConstraint({
|
|
461
|
-
table: resolved.branch,
|
|
462
|
-
constraint,
|
|
463
|
-
}),
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
455
|
if (
|
|
467
456
|
constraint.comment !== null &&
|
|
468
457
|
constraint.comment !== undefined
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Change } from "../change.types.ts";
|
|
3
|
+
import { evaluatePattern } from "./filter/dsl.ts";
|
|
4
|
+
import { supabase } from "./supabase.ts";
|
|
5
|
+
|
|
6
|
+
if (!supabase.filter) {
|
|
7
|
+
throw new Error("supabase integration is missing a filter");
|
|
8
|
+
}
|
|
9
|
+
const filter = supabase.filter;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a synthetic FDW change shaped like what `flattenChange` consumes.
|
|
13
|
+
* The change carries a `foreignDataWrapper` model whose `handler`/`validator`
|
|
14
|
+
* are schema-qualified function references (the form
|
|
15
|
+
* `extractForeignDataWrappers` produces).
|
|
16
|
+
*/
|
|
17
|
+
function fdwChange(
|
|
18
|
+
operation: "create" | "alter" | "drop",
|
|
19
|
+
fdw: {
|
|
20
|
+
name: string;
|
|
21
|
+
owner: string;
|
|
22
|
+
handler: string | null;
|
|
23
|
+
validator: string | null;
|
|
24
|
+
},
|
|
25
|
+
): Change {
|
|
26
|
+
return {
|
|
27
|
+
objectType: "foreign_data_wrapper",
|
|
28
|
+
operation,
|
|
29
|
+
scope: "object",
|
|
30
|
+
foreignDataWrapper: fdw,
|
|
31
|
+
requires: [],
|
|
32
|
+
creates: [],
|
|
33
|
+
drops: [],
|
|
34
|
+
} as unknown as Change;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Synthetic FDW privilege change. The three concrete privilege classes
|
|
39
|
+
* (`GrantForeignDataWrapperPrivileges`, `RevokeForeignDataWrapperPrivileges`,
|
|
40
|
+
* `RevokeGrantOptionForeignDataWrapperPrivileges`) all extend
|
|
41
|
+
* `AlterForeignDataWrapperChange`, so their `operation` is `"alter"` in
|
|
42
|
+
* production. The filter rule we exercise here keys off `scope` only,
|
|
43
|
+
* but pinning `operation: "alter"` keeps the synthetic shape honest.
|
|
44
|
+
*/
|
|
45
|
+
function fdwPrivilegeChange(fdw: { name: string; owner: string }): Change {
|
|
46
|
+
return {
|
|
47
|
+
objectType: "foreign_data_wrapper",
|
|
48
|
+
operation: "alter",
|
|
49
|
+
scope: "privilege",
|
|
50
|
+
foreignDataWrapper: { ...fdw, handler: null, validator: null },
|
|
51
|
+
grantee: "postgres",
|
|
52
|
+
requires: [],
|
|
53
|
+
creates: [],
|
|
54
|
+
drops: [],
|
|
55
|
+
} as unknown as Change;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function serverPrivilegeChange(server: {
|
|
59
|
+
name: string;
|
|
60
|
+
owner: string;
|
|
61
|
+
}): Change {
|
|
62
|
+
return {
|
|
63
|
+
objectType: "server",
|
|
64
|
+
operation: "alter",
|
|
65
|
+
scope: "privilege",
|
|
66
|
+
server,
|
|
67
|
+
grantee: "postgres",
|
|
68
|
+
requires: [],
|
|
69
|
+
creates: [],
|
|
70
|
+
drops: [],
|
|
71
|
+
} as unknown as Change;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("supabase integration filter — foreign data wrappers", () => {
|
|
75
|
+
// Regression for CLI-1470. Wasm-based foreign data wrappers on Supabase
|
|
76
|
+
// (e.g. `clerk`, `clerk_oauth`) are provisioned at project creation by
|
|
77
|
+
// `supabase_admin` and their handler/validator live in `extensions.*`.
|
|
78
|
+
// pg-delta must not emit `CREATE/DROP/ALTER FOREIGN DATA WRAPPER` for
|
|
79
|
+
// them, even when the FDW owner has been rewritten away from
|
|
80
|
+
// `supabase_admin` (e.g. after a dump/restore).
|
|
81
|
+
test("suppresses CREATE for FDW with handler in extensions schema", () => {
|
|
82
|
+
const change = fdwChange("create", {
|
|
83
|
+
name: "clerk",
|
|
84
|
+
owner: "postgres",
|
|
85
|
+
handler: "extensions.wasm_fdw_handler",
|
|
86
|
+
validator: "extensions.wasm_fdw_validator",
|
|
87
|
+
});
|
|
88
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("suppresses DROP for FDW with handler in extensions schema", () => {
|
|
92
|
+
const change = fdwChange("drop", {
|
|
93
|
+
name: "clerk_oauth",
|
|
94
|
+
owner: "postgres",
|
|
95
|
+
handler: "extensions.wasm_fdw_handler",
|
|
96
|
+
validator: "extensions.wasm_fdw_validator",
|
|
97
|
+
});
|
|
98
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("suppresses ALTER for FDW with handler in extensions schema", () => {
|
|
102
|
+
const change = fdwChange("alter", {
|
|
103
|
+
name: "clerk",
|
|
104
|
+
owner: "postgres",
|
|
105
|
+
handler: "extensions.wasm_fdw_handler",
|
|
106
|
+
validator: "extensions.wasm_fdw_validator",
|
|
107
|
+
});
|
|
108
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("suppresses FDW when only the validator lives in extensions", () => {
|
|
112
|
+
const change = fdwChange("create", {
|
|
113
|
+
name: "partial_wasm",
|
|
114
|
+
owner: "postgres",
|
|
115
|
+
handler: null,
|
|
116
|
+
validator: "extensions.wasm_fdw_validator",
|
|
117
|
+
});
|
|
118
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("preserves user FDW whose handler lives outside extensions", () => {
|
|
122
|
+
const change = fdwChange("create", {
|
|
123
|
+
name: "user_fdw",
|
|
124
|
+
owner: "postgres",
|
|
125
|
+
handler: "public.my_fdw_handler",
|
|
126
|
+
validator: "public.my_fdw_validator",
|
|
127
|
+
});
|
|
128
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("preserves user FDW with no handler/validator", () => {
|
|
132
|
+
const change = fdwChange("create", {
|
|
133
|
+
name: "user_fdw_bare",
|
|
134
|
+
owner: "postgres",
|
|
135
|
+
handler: null,
|
|
136
|
+
validator: null,
|
|
137
|
+
});
|
|
138
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("supabase integration filter — foreign data wrapper / server ACLs", () => {
|
|
143
|
+
// Regression for CLI-1469. `GRANT`/`REVOKE ... ON FOREIGN DATA WRAPPER`
|
|
144
|
+
// require superuser. On Supabase Cloud `postgres` has the elevated
|
|
145
|
+
// rights to make them work; the local Docker image does not, so
|
|
146
|
+
// `supabase db reset` aborts with `permission denied for foreign-data
|
|
147
|
+
// wrapper`. FDW ACL is platform-managed, not user-declarative state —
|
|
148
|
+
// suppress regardless of owner because `pg_dump` rewrites OWNER TO
|
|
149
|
+
// away from `supabase_admin`.
|
|
150
|
+
test("suppresses FDW ACL when owner=supabase_admin (existing */owner rule)", () => {
|
|
151
|
+
const change = fdwPrivilegeChange({
|
|
152
|
+
name: "dblink_fdw",
|
|
153
|
+
owner: "supabase_admin",
|
|
154
|
+
});
|
|
155
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("suppresses FDW ACL when owner=postgres (post-restore)", () => {
|
|
159
|
+
const change = fdwPrivilegeChange({
|
|
160
|
+
name: "dblink_fdw",
|
|
161
|
+
owner: "postgres",
|
|
162
|
+
});
|
|
163
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// FOREIGN SERVER ACL is owner-scoped, not blanket-suppressed:
|
|
167
|
+
// server GRANT/REVOKE does not require superuser, so a user-owned
|
|
168
|
+
// server's ACL must roundtrip. The pre-existing `*/owner` rule
|
|
169
|
+
// already drops platform-managed servers (owner ∈ system roles).
|
|
170
|
+
test("suppresses server ACL when owner=supabase_admin (existing */owner rule)", () => {
|
|
171
|
+
const change = serverPrivilegeChange({
|
|
172
|
+
name: "platform_server",
|
|
173
|
+
owner: "supabase_admin",
|
|
174
|
+
});
|
|
175
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("preserves server ACL when owner=postgres", () => {
|
|
179
|
+
const change = serverPrivilegeChange({
|
|
180
|
+
name: "user_dblink_server",
|
|
181
|
+
owner: "postgres",
|
|
182
|
+
});
|
|
183
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Non-privilege FDW changes whose handler/validator aren't in
|
|
187
|
+
// `extensions.*` should still pass through (a user FDW is plain DDL,
|
|
188
|
+
// not the platform-managed flavor).
|
|
189
|
+
test("preserves non-privilege FDW changes for user wrappers", () => {
|
|
190
|
+
const change = fdwChange("create", {
|
|
191
|
+
name: "user_fdw",
|
|
192
|
+
owner: "postgres",
|
|
193
|
+
handler: "public.my_fdw_handler",
|
|
194
|
+
validator: null,
|
|
195
|
+
});
|
|
196
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -99,6 +99,34 @@ export const supabase: IntegrationDSL = {
|
|
|
99
99
|
operation: "drop",
|
|
100
100
|
scope: "object",
|
|
101
101
|
},
|
|
102
|
+
// Include user-attached triggers on tables in Supabase-managed schemas.
|
|
103
|
+
//
|
|
104
|
+
// Triggers live in the schema of the table they fire on, so a user
|
|
105
|
+
// trigger on `auth.users` reports `trigger/schema = auth` and is
|
|
106
|
+
// otherwise indistinguishable from Supabase's own triggers via the
|
|
107
|
+
// schema-level deny list. Triggers also have no real owner — pg-delta
|
|
108
|
+
// surfaces the parent table's owner as `trigger/owner`, which for
|
|
109
|
+
// `auth.users` and `storage.objects` is always a Supabase system role,
|
|
110
|
+
// so the owner-level deny list catches them too.
|
|
111
|
+
//
|
|
112
|
+
// The trigger function, however, is genuinely user-owned: a customer
|
|
113
|
+
// who wants to run code on an auth event creates a function in
|
|
114
|
+
// `public` (or any non-managed schema) and points the trigger at it.
|
|
115
|
+
// Supabase's own auth/storage triggers either come from extensions
|
|
116
|
+
// (already filtered out at extract time via `pg_depend`) or call
|
|
117
|
+
// functions inside the same managed schema, so `function_schema`
|
|
118
|
+
// outside the managed list is a reliable user-defined marker.
|
|
119
|
+
{
|
|
120
|
+
and: [
|
|
121
|
+
{ objectType: "trigger" },
|
|
122
|
+
{ "trigger/schema": [...SUPABASE_SYSTEM_SCHEMAS] },
|
|
123
|
+
{
|
|
124
|
+
not: {
|
|
125
|
+
"trigger/function_schema": [...SUPABASE_SYSTEM_SCHEMAS],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
102
130
|
// Exclude system objects
|
|
103
131
|
{
|
|
104
132
|
not: {
|
|
@@ -131,6 +159,62 @@ export const supabase: IntegrationDSL = {
|
|
|
131
159
|
},
|
|
132
160
|
],
|
|
133
161
|
},
|
|
162
|
+
// Platform-managed foreign data wrapper ACL.
|
|
163
|
+
// `GRANT`/`REVOKE ... ON FOREIGN DATA WRAPPER` requires
|
|
164
|
+
// superuser. On Supabase Cloud `postgres` has the elevated
|
|
165
|
+
// rights to make this work, but the local Docker image does
|
|
166
|
+
// not, so `supabase db reset` aborts with
|
|
167
|
+
// `permission denied for foreign-data wrapper`. The
|
|
168
|
+
// `*/owner` rule above already covers wrappers owned by
|
|
169
|
+
// `supabase_admin`, but `pg_dump` rewrites OWNER TO clauses
|
|
170
|
+
// to whoever the dump runs under, so after a restore the
|
|
171
|
+
// FDW typically ends up owned by `postgres` and slips past
|
|
172
|
+
// the owner gate. A non-superuser `postgres` still can't
|
|
173
|
+
// grant on a FDW (this is true regardless of who owns the
|
|
174
|
+
// wrapper locally), so the ACL diff is not user-replayable.
|
|
175
|
+
// We don't apply the same blanket rule to `FOREIGN SERVER`:
|
|
176
|
+
// server GRANT/REVOKE doesn't require superuser, and
|
|
177
|
+
// user-created servers (e.g. a `dblink` server pointing to
|
|
178
|
+
// a peer DB) carry legitimate user ACL that should
|
|
179
|
+
// roundtrip — the existing `*/owner` rule already drops
|
|
180
|
+
// platform-managed servers.
|
|
181
|
+
{
|
|
182
|
+
and: [
|
|
183
|
+
{ objectType: "foreign_data_wrapper" },
|
|
184
|
+
{ scope: "privilege" },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
// Platform-managed foreign data wrappers — Wasm-based FDWs
|
|
188
|
+
// (e.g. `clerk`, `clerk_oauth`) whose handler/validator live in
|
|
189
|
+
// the `extensions` schema. `CREATE FOREIGN DATA WRAPPER`
|
|
190
|
+
// requires superuser, and Supabase Cloud provisions these via
|
|
191
|
+
// `supabase_admin` at project creation; replaying the DDL
|
|
192
|
+
// against a local image fails because the local environment
|
|
193
|
+
// has no equivalent pre-step. We can't rely on the FDW owner
|
|
194
|
+
// alone — after a dump/restore the owner is often rewritten
|
|
195
|
+
// away from `supabase_admin` — so match on the function
|
|
196
|
+
// reference instead.
|
|
197
|
+
{
|
|
198
|
+
and: [
|
|
199
|
+
{ objectType: "foreign_data_wrapper" },
|
|
200
|
+
{
|
|
201
|
+
or: [
|
|
202
|
+
{
|
|
203
|
+
"foreign_data_wrapper/handler": {
|
|
204
|
+
op: "regex",
|
|
205
|
+
value: "^extensions\\.",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"foreign_data_wrapper/validator": {
|
|
210
|
+
op: "regex",
|
|
211
|
+
value: "^extensions\\.",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
134
218
|
],
|
|
135
219
|
},
|
|
136
220
|
},
|
|
@@ -8,7 +8,7 @@ abstract class BaseAggregateChange extends BaseChange {
|
|
|
8
8
|
| "comment"
|
|
9
9
|
| "privilege"
|
|
10
10
|
| "security_label";
|
|
11
|
-
readonly objectType
|
|
11
|
+
readonly objectType = "aggregate" as const;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export abstract class CreateAggregateChange extends BaseAggregateChange {
|
|
@@ -92,6 +92,85 @@ describe("aggregate.privilege", () => {
|
|
|
92
92
|
);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
// Regression for CLI-1471: ordered-set / hypothetical-set / variadic
|
|
96
|
+
// aggregates have `identity_arguments` that include `ORDER BY` or
|
|
97
|
+
// `VARIADIC` keywords. Those keywords are rejected by
|
|
98
|
+
// `GRANT ... ON FUNCTION (...)` (only positional argument types are
|
|
99
|
+
// accepted there), so the serializer must drop back to the
|
|
100
|
+
// `proargtypes`-derived `argument_types` list.
|
|
101
|
+
test("grant on ordered-set aggregate emits proargtypes signature", async () => {
|
|
102
|
+
const aggregate = new Aggregate({
|
|
103
|
+
...base,
|
|
104
|
+
name: "os_last",
|
|
105
|
+
aggkind: "o",
|
|
106
|
+
identity_arguments: "anyelement ORDER BY anyelement",
|
|
107
|
+
argument_types: ["anyelement", "anyelement"],
|
|
108
|
+
return_type: "anyelement",
|
|
109
|
+
transition_function: "public.os_last_sfunc(anyelement,anyelement)",
|
|
110
|
+
state_data_type: "anyelement",
|
|
111
|
+
argument_count: 2,
|
|
112
|
+
});
|
|
113
|
+
const change = new GrantAggregatePrivileges({
|
|
114
|
+
aggregate,
|
|
115
|
+
grantee: "role_exec",
|
|
116
|
+
privileges: [{ privilege: "EXECUTE", grantable: false }],
|
|
117
|
+
version: 170000,
|
|
118
|
+
});
|
|
119
|
+
await assertValidSql(change.serialize());
|
|
120
|
+
expect(change.serialize()).toBe(
|
|
121
|
+
"GRANT ALL ON FUNCTION public.os_last(anyelement, anyelement) TO role_exec",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("revoke on hypothetical-set aggregate emits proargtypes signature", async () => {
|
|
126
|
+
const aggregate = new Aggregate({
|
|
127
|
+
...base,
|
|
128
|
+
name: "hyp_rank",
|
|
129
|
+
aggkind: "h",
|
|
130
|
+
identity_arguments: 'VARIADIC "any" ORDER BY VARIADIC "any"',
|
|
131
|
+
argument_types: ['"any"'],
|
|
132
|
+
return_type: "bigint",
|
|
133
|
+
transition_function:
|
|
134
|
+
'pg_catalog.ordered_set_transition_multi(internal,"any")',
|
|
135
|
+
state_data_type: "internal",
|
|
136
|
+
argument_count: 1,
|
|
137
|
+
});
|
|
138
|
+
const change = new RevokeAggregatePrivileges({
|
|
139
|
+
aggregate,
|
|
140
|
+
grantee: "role_old",
|
|
141
|
+
privileges: [{ privilege: "EXECUTE", grantable: false }],
|
|
142
|
+
version: 170000,
|
|
143
|
+
});
|
|
144
|
+
await assertValidSql(change.serialize());
|
|
145
|
+
expect(change.serialize()).toBe(
|
|
146
|
+
'REVOKE ALL ON FUNCTION public.hyp_rank("any") FROM role_old',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("revoke grant option on ordered-set aggregate emits proargtypes signature", async () => {
|
|
151
|
+
const aggregate = new Aggregate({
|
|
152
|
+
...base,
|
|
153
|
+
name: "os_last",
|
|
154
|
+
aggkind: "o",
|
|
155
|
+
identity_arguments: "anyelement ORDER BY anyelement",
|
|
156
|
+
argument_types: ["anyelement", "anyelement"],
|
|
157
|
+
return_type: "anyelement",
|
|
158
|
+
transition_function: "public.os_last_sfunc(anyelement,anyelement)",
|
|
159
|
+
state_data_type: "anyelement",
|
|
160
|
+
argument_count: 2,
|
|
161
|
+
});
|
|
162
|
+
const change = new RevokeGrantOptionAggregatePrivileges({
|
|
163
|
+
aggregate,
|
|
164
|
+
grantee: "role_with_option",
|
|
165
|
+
privilegeNames: ["EXECUTE"],
|
|
166
|
+
version: 170000,
|
|
167
|
+
});
|
|
168
|
+
await assertValidSql(change.serialize());
|
|
169
|
+
expect(change.serialize()).toBe(
|
|
170
|
+
"REVOKE GRANT OPTION FOR ALL ON FUNCTION public.os_last(anyelement, anyelement) FROM role_with_option",
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
95
174
|
test("revoke privileges and grant option", async () => {
|
|
96
175
|
const aggregate = new Aggregate(base);
|
|
97
176
|
const revoke = new RevokeAggregatePrivileges({
|
|
@@ -12,6 +12,25 @@ export type AggregatePrivilege =
|
|
|
12
12
|
| RevokeAggregatePrivileges
|
|
13
13
|
| RevokeGrantOptionAggregatePrivileges;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Build the signature `<schema>.<name>(<argtypes>)` for use inside
|
|
17
|
+
* `GRANT`/`REVOKE ... ON FUNCTION (...)`.
|
|
18
|
+
*
|
|
19
|
+
* The aggregate's `identityArguments` (from
|
|
20
|
+
* `pg_get_function_identity_arguments`) embeds `ORDER BY` for ordered-set
|
|
21
|
+
* and hypothetical-set aggregates (`aggkind` of `o`/`h`) and `VARIADIC`
|
|
22
|
+
* for variadic aggregates — both of which the GRANT parser rejects with
|
|
23
|
+
* a syntax error. PostgreSQL resolves the aggregate from the positional
|
|
24
|
+
* argument types alone, so use `argument_types` here regardless of
|
|
25
|
+
* `aggkind`. Other aggregate DDL (`ALTER AGGREGATE`, `COMMENT ON
|
|
26
|
+
* AGGREGATE`, `SECURITY LABEL ON AGGREGATE`, `DROP AGGREGATE`) accepts
|
|
27
|
+
* the identity form and keeps using it.
|
|
28
|
+
*/
|
|
29
|
+
function aggregateGrantSignature(aggregate: Aggregate): string {
|
|
30
|
+
const args = (aggregate.argument_types ?? []).join(", ");
|
|
31
|
+
return `${aggregate.schema}.${aggregate.name}(${args})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
export class GrantAggregatePrivileges extends AlterAggregateChange {
|
|
16
35
|
public readonly aggregate: Aggregate;
|
|
17
36
|
public readonly grantee: string;
|
|
@@ -52,9 +71,7 @@ export class GrantAggregatePrivileges extends AlterAggregateChange {
|
|
|
52
71
|
const kindPrefix = getObjectKindPrefix("FUNCTION");
|
|
53
72
|
const list = this.privileges.map((p) => p.privilege);
|
|
54
73
|
const privSql = formatObjectPrivilegeList("FUNCTION", list, this.version);
|
|
55
|
-
const
|
|
56
|
-
const signature = this.aggregate.identityArguments;
|
|
57
|
-
const qualified = `${aggregateName}(${signature})`;
|
|
74
|
+
const qualified = aggregateGrantSignature(this.aggregate);
|
|
58
75
|
return `GRANT ${privSql} ${kindPrefix} ${qualified} TO ${this.grantee}${withGrant}`;
|
|
59
76
|
}
|
|
60
77
|
}
|
|
@@ -97,9 +114,7 @@ export class RevokeAggregatePrivileges extends AlterAggregateChange {
|
|
|
97
114
|
const kindPrefix = getObjectKindPrefix("FUNCTION");
|
|
98
115
|
const list = this.privileges.map((p) => p.privilege);
|
|
99
116
|
const privSql = formatObjectPrivilegeList("FUNCTION", list, this.version);
|
|
100
|
-
const
|
|
101
|
-
const signature = this.aggregate.identityArguments;
|
|
102
|
-
const qualified = `${aggregateName}(${signature})`;
|
|
117
|
+
const qualified = aggregateGrantSignature(this.aggregate);
|
|
103
118
|
return `REVOKE ${privSql} ${kindPrefix} ${qualified} FROM ${this.grantee}`;
|
|
104
119
|
}
|
|
105
120
|
}
|
|
@@ -139,9 +154,7 @@ export class RevokeGrantOptionAggregatePrivileges extends AlterAggregateChange {
|
|
|
139
154
|
this.privilegeNames,
|
|
140
155
|
this.version,
|
|
141
156
|
);
|
|
142
|
-
const
|
|
143
|
-
const signature = this.aggregate.identityArguments;
|
|
144
|
-
const qualified = `${aggregateName}(${signature})`;
|
|
157
|
+
const qualified = aggregateGrantSignature(this.aggregate);
|
|
145
158
|
return `REVOKE GRANT OPTION FOR ${privSql} ${kindPrefix} ${qualified} FROM ${this.grantee}`;
|
|
146
159
|
}
|
|
147
160
|
}
|
|
@@ -4,7 +4,7 @@ import type { Collation } from "../collation.model.ts";
|
|
|
4
4
|
abstract class BaseCollationChange extends BaseChange {
|
|
5
5
|
abstract readonly collation: Collation;
|
|
6
6
|
abstract readonly scope: "object" | "comment";
|
|
7
|
-
readonly objectType
|
|
7
|
+
readonly objectType = "collation" as const;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export abstract class CreateCollationChange extends BaseCollationChange {
|
|
@@ -8,7 +8,7 @@ abstract class BaseDomainChange extends BaseChange {
|
|
|
8
8
|
| "comment"
|
|
9
9
|
| "privilege"
|
|
10
10
|
| "security_label";
|
|
11
|
-
readonly objectType
|
|
11
|
+
readonly objectType = "domain" as const;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export abstract class CreateDomainChange extends BaseDomainChange {
|
|
@@ -4,7 +4,7 @@ import type { Extension } from "../extension.model.ts";
|
|
|
4
4
|
abstract class BaseExtensionChange extends BaseChange {
|
|
5
5
|
abstract readonly extension: Extension;
|
|
6
6
|
abstract readonly scope: "object" | "comment";
|
|
7
|
-
readonly objectType
|
|
7
|
+
readonly objectType = "extension" as const;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export abstract class CreateExtensionChange extends BaseExtensionChange {
|
|
@@ -120,17 +120,47 @@ describe.concurrent("foreign-data-wrapper", () => {
|
|
|
120
120
|
const change = new AlterForeignDataWrapperSetOptions({
|
|
121
121
|
foreignDataWrapper: fdw,
|
|
122
122
|
options: [
|
|
123
|
-
{ action: "ADD", option: "
|
|
124
|
-
{ action: "SET", option: "
|
|
125
|
-
{ action: "DROP", option: "
|
|
123
|
+
{ action: "ADD", option: "use_remote_estimate", value: "true" },
|
|
124
|
+
{ action: "SET", option: "fetch_size", value: "200" },
|
|
125
|
+
{ action: "DROP", option: "fdw_tuple_cost" },
|
|
126
126
|
],
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
await assertValidSql(change.serialize());
|
|
130
130
|
|
|
131
131
|
expect(change.serialize()).toBe(
|
|
132
|
-
"ALTER FOREIGN DATA WRAPPER test_fdw OPTIONS (ADD
|
|
132
|
+
"ALTER FOREIGN DATA WRAPPER test_fdw OPTIONS (ADD use_remote_estimate 'true', SET fetch_size '200', DROP fdw_tuple_cost)",
|
|
133
133
|
);
|
|
134
134
|
});
|
|
135
|
+
|
|
136
|
+
test("redacts sensitive option values to prevent secret leakage (CLI-1467)", async () => {
|
|
137
|
+
const props: ForeignDataWrapperProps = {
|
|
138
|
+
name: "leaky_fdw",
|
|
139
|
+
owner: "postgres",
|
|
140
|
+
handler: null,
|
|
141
|
+
validator: null,
|
|
142
|
+
options: null,
|
|
143
|
+
comment: null,
|
|
144
|
+
privileges: [],
|
|
145
|
+
};
|
|
146
|
+
const fdw = new ForeignDataWrapper(props);
|
|
147
|
+
const change = new AlterForeignDataWrapperSetOptions({
|
|
148
|
+
foreignDataWrapper: fdw,
|
|
149
|
+
options: [
|
|
150
|
+
{ action: "ADD", option: "password", value: "shared-fdw-secret" },
|
|
151
|
+
{ action: "SET", option: "use_remote_estimate", value: "true" },
|
|
152
|
+
{ action: "ADD", option: "api_key", value: "leaked-api-key" },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await assertValidSql(change.serialize());
|
|
157
|
+
|
|
158
|
+
const sql = change.serialize();
|
|
159
|
+
expect(sql).not.toContain("shared-fdw-secret");
|
|
160
|
+
expect(sql).not.toContain("leaked-api-key");
|
|
161
|
+
expect(sql).toContain("SET use_remote_estimate 'true'");
|
|
162
|
+
expect(sql).toContain("ADD password '__OPTION_PASSWORD__'");
|
|
163
|
+
expect(sql).toContain("ADD api_key '__OPTION_API_KEY__'");
|
|
164
|
+
});
|
|
135
165
|
});
|
|
136
166
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SerializeOptions } from "../../../../integrations/serialize/serialize.types.ts";
|
|
2
2
|
import { quoteLiteral } from "../../../base.change.ts";
|
|
3
3
|
import { stableId } from "../../../utils.ts";
|
|
4
|
+
import { redactOptionValue } from "../../sensitive-options.ts";
|
|
4
5
|
import type { ForeignDataWrapper } from "../foreign-data-wrapper.model.ts";
|
|
5
6
|
import { AlterForeignDataWrapperChange } from "./foreign-data-wrapper.base.ts";
|
|
6
7
|
|
|
@@ -87,7 +88,10 @@ export class AlterForeignDataWrapperSetOptions extends AlterForeignDataWrapperCh
|
|
|
87
88
|
if (opt.action === "DROP") {
|
|
88
89
|
optionParts.push(`DROP ${opt.option}`);
|
|
89
90
|
} else {
|
|
90
|
-
const value =
|
|
91
|
+
const value =
|
|
92
|
+
opt.value !== undefined
|
|
93
|
+
? quoteLiteral(redactOptionValue(opt.option, opt.value))
|
|
94
|
+
: "''";
|
|
91
95
|
optionParts.push(`${opt.action} ${opt.option} ${value}`);
|
|
92
96
|
}
|
|
93
97
|
}
|