@supabase/pg-delta 1.0.0-alpha.25 → 1.0.0-alpha.27
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.filter.d.ts +17 -0
- package/dist/core/catalog.filter.js +75 -0
- package/dist/core/catalog.model.js +9 -1
- package/dist/core/expand-replace-dependencies.js +1 -7
- package/dist/core/integrations/supabase.js +102 -11
- 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/table/table.diff.js +53 -30
- 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/cli/commands/catalog-export.ts +26 -1
- package/src/core/catalog.filter.ts +96 -0
- package/src/core/catalog.model.ts +10 -1
- 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 +335 -0
- package/src/core/integrations/supabase.ts +102 -11
- package/src/core/objects/aggregate/changes/aggregate.base.ts +1 -1
- 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.base.ts +1 -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/foreign-table.model.ts +28 -2
- package/src/core/objects/foreign-data-wrapper/server/changes/server.base.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -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/user-mapping.model.ts +18 -1
- 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/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/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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { writeFile } from "node:fs/promises";
|
|
5
5
|
import { buildCommand } from "@stricli/core";
|
|
6
|
+
import { filterCatalog } from "../../core/catalog.filter.js";
|
|
6
7
|
import { extractCatalog } from "../../core/catalog.model.js";
|
|
7
8
|
import { serializeCatalog, stringifyCatalogSnapshot, } from "../../core/catalog.snapshot.js";
|
|
8
9
|
import { createManagedPool } from "../../core/postgres-config.js";
|
|
@@ -25,6 +26,19 @@ export const catalogExportCommand = buildCommand({
|
|
|
25
26
|
parse: String,
|
|
26
27
|
optional: true,
|
|
27
28
|
},
|
|
29
|
+
filter: {
|
|
30
|
+
kind: "parsed",
|
|
31
|
+
brief: 'Filter DSL as inline JSON to filter changes (e.g., \'{"*/schema": "app"}\').',
|
|
32
|
+
parse: (value) => {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(value);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(`Invalid filter JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
optional: true,
|
|
41
|
+
},
|
|
28
42
|
},
|
|
29
43
|
aliases: {
|
|
30
44
|
t: "target",
|
|
@@ -43,6 +57,10 @@ Use cases:
|
|
|
43
57
|
- Snapshot template1 for use as an empty-database baseline
|
|
44
58
|
- Snapshot a production database to generate revert migrations
|
|
45
59
|
- Snapshot any state for reproducible offline diffs
|
|
60
|
+
|
|
61
|
+
Pass --filter to scope the snapshot to a subset of the catalog (same
|
|
62
|
+
Filter DSL accepted by plan/sync). Useful when committing a baseline
|
|
63
|
+
snapshot to a repo and only one schema's drift is interesting.
|
|
46
64
|
`.trim(),
|
|
47
65
|
},
|
|
48
66
|
async func(flags) {
|
|
@@ -52,7 +70,10 @@ Use cases:
|
|
|
52
70
|
});
|
|
53
71
|
try {
|
|
54
72
|
const catalog = await extractCatalog(pool);
|
|
55
|
-
const
|
|
73
|
+
const scoped = flags.filter
|
|
74
|
+
? await filterCatalog(catalog, flags.filter)
|
|
75
|
+
: catalog;
|
|
76
|
+
const snapshot = serializeCatalog(scoped);
|
|
56
77
|
const json = stringifyCatalogSnapshot(snapshot);
|
|
57
78
|
await writeFile(flags.output, json, "utf-8");
|
|
58
79
|
this.process.stdout.write(`Catalog snapshot written to ${flags.output}\n`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune a catalog to the objects that match a Filter DSL expression.
|
|
3
|
+
*
|
|
4
|
+
* The Filter DSL is defined over Change objects, so the catalog is
|
|
5
|
+
* diffed against an empty baseline first to materialize one CREATE
|
|
6
|
+
* change per object. The filter then evaluates against the same shape
|
|
7
|
+
* it would at plan time, and the surviving stableIds drive the prune.
|
|
8
|
+
*
|
|
9
|
+
* Dependency cascade is not applied. A scoped snapshot is partial by
|
|
10
|
+
* design: out-of-scope owners, roles, and types must exist on the
|
|
11
|
+
* target DB at apply time. Cascading would expand the filter beyond
|
|
12
|
+
* what the caller asked for and, in practice, collapse schema-scoped
|
|
13
|
+
* exports whose kept objects reference cluster-scoped owners.
|
|
14
|
+
*/
|
|
15
|
+
import { Catalog } from "./catalog.model.ts";
|
|
16
|
+
import { type FilterDSL } from "./integrations/filter/dsl.ts";
|
|
17
|
+
export declare function filterCatalog(catalog: Catalog, filter: FilterDSL): Promise<Catalog>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune a catalog to the objects that match a Filter DSL expression.
|
|
3
|
+
*
|
|
4
|
+
* The Filter DSL is defined over Change objects, so the catalog is
|
|
5
|
+
* diffed against an empty baseline first to materialize one CREATE
|
|
6
|
+
* change per object. The filter then evaluates against the same shape
|
|
7
|
+
* it would at plan time, and the surviving stableIds drive the prune.
|
|
8
|
+
*
|
|
9
|
+
* Dependency cascade is not applied. A scoped snapshot is partial by
|
|
10
|
+
* design: out-of-scope owners, roles, and types must exist on the
|
|
11
|
+
* target DB at apply time. Cascading would expand the filter beyond
|
|
12
|
+
* what the caller asked for and, in practice, collapse schema-scoped
|
|
13
|
+
* exports whose kept objects reference cluster-scoped owners.
|
|
14
|
+
*/
|
|
15
|
+
import { diffCatalogs } from "./catalog.diff.js";
|
|
16
|
+
import { Catalog, createEmptyCatalog } from "./catalog.model.js";
|
|
17
|
+
import { compileFilterDSL } from "./integrations/filter/dsl.js";
|
|
18
|
+
export async function filterCatalog(catalog, filter) {
|
|
19
|
+
if (typeof filter === "object" &&
|
|
20
|
+
filter !== null &&
|
|
21
|
+
filter.cascade === true) {
|
|
22
|
+
throw new Error("Filter DSL `cascade: true` is not supported by catalog-export: " +
|
|
23
|
+
"scoped snapshots are intentionally partial. Out-of-scope owners, " +
|
|
24
|
+
"roles, and types must exist on the target DB at apply time.");
|
|
25
|
+
}
|
|
26
|
+
const empty = await createEmptyCatalog(catalog.version, catalog.currentUser);
|
|
27
|
+
const changes = diffCatalogs(empty, catalog);
|
|
28
|
+
const filterFn = compileFilterDSL(filter);
|
|
29
|
+
const keep = new Set();
|
|
30
|
+
for (const change of changes) {
|
|
31
|
+
if (!filterFn(change))
|
|
32
|
+
continue;
|
|
33
|
+
for (const id of change.creates ?? [])
|
|
34
|
+
keep.add(id);
|
|
35
|
+
}
|
|
36
|
+
return pruneCatalog(catalog, keep);
|
|
37
|
+
}
|
|
38
|
+
function filterRecord(record, keep) {
|
|
39
|
+
return Object.fromEntries(Object.entries(record).filter(([id]) => keep.has(id)));
|
|
40
|
+
}
|
|
41
|
+
function pruneCatalog(catalog, keep) {
|
|
42
|
+
const tables = filterRecord(catalog.tables, keep);
|
|
43
|
+
const materializedViews = filterRecord(catalog.materializedViews, keep);
|
|
44
|
+
return new Catalog({
|
|
45
|
+
aggregates: filterRecord(catalog.aggregates, keep),
|
|
46
|
+
collations: filterRecord(catalog.collations, keep),
|
|
47
|
+
compositeTypes: filterRecord(catalog.compositeTypes, keep),
|
|
48
|
+
domains: filterRecord(catalog.domains, keep),
|
|
49
|
+
enums: filterRecord(catalog.enums, keep),
|
|
50
|
+
extensions: filterRecord(catalog.extensions, keep),
|
|
51
|
+
procedures: filterRecord(catalog.procedures, keep),
|
|
52
|
+
indexes: filterRecord(catalog.indexes, keep),
|
|
53
|
+
materializedViews,
|
|
54
|
+
subscriptions: filterRecord(catalog.subscriptions, keep),
|
|
55
|
+
publications: filterRecord(catalog.publications, keep),
|
|
56
|
+
rlsPolicies: filterRecord(catalog.rlsPolicies, keep),
|
|
57
|
+
roles: filterRecord(catalog.roles, keep),
|
|
58
|
+
schemas: filterRecord(catalog.schemas, keep),
|
|
59
|
+
sequences: filterRecord(catalog.sequences, keep),
|
|
60
|
+
tables,
|
|
61
|
+
triggers: filterRecord(catalog.triggers, keep),
|
|
62
|
+
eventTriggers: filterRecord(catalog.eventTriggers, keep),
|
|
63
|
+
rules: filterRecord(catalog.rules, keep),
|
|
64
|
+
ranges: filterRecord(catalog.ranges, keep),
|
|
65
|
+
views: filterRecord(catalog.views, keep),
|
|
66
|
+
foreignDataWrappers: filterRecord(catalog.foreignDataWrappers, keep),
|
|
67
|
+
servers: filterRecord(catalog.servers, keep),
|
|
68
|
+
userMappings: filterRecord(catalog.userMappings, keep),
|
|
69
|
+
foreignTables: filterRecord(catalog.foreignTables, keep),
|
|
70
|
+
depends: catalog.depends.filter((d) => keep.has(d.dependent_stable_id) && keep.has(d.referenced_stable_id)),
|
|
71
|
+
indexableObjects: { ...tables, ...materializedViews },
|
|
72
|
+
version: catalog.version,
|
|
73
|
+
currentUser: catalog.currentUser,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -93,7 +93,7 @@ export class Catalog {
|
|
|
93
93
|
let _pg1516Baseline = null;
|
|
94
94
|
let _pg17Baseline = null;
|
|
95
95
|
async function loadBaselineJson() {
|
|
96
|
-
const mod = await import("./fixtures/empty-catalogs/postgres-15-16-baseline.json");
|
|
96
|
+
const mod = await import("./fixtures/empty-catalogs/postgres-15-16-baseline.json", { with: { type: "json" } });
|
|
97
97
|
return mod.default;
|
|
98
98
|
}
|
|
99
99
|
async function getPg1516Baseline() {
|
|
@@ -147,10 +147,12 @@ async function getPg17Baseline() {
|
|
|
147
147
|
export async function createEmptyCatalog(version, currentUser) {
|
|
148
148
|
if (version >= 170000) {
|
|
149
149
|
const baseline = await getPg17Baseline();
|
|
150
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
150
151
|
return new Catalog({ ...baseline, version, currentUser });
|
|
151
152
|
}
|
|
152
153
|
if (version >= 150000) {
|
|
153
154
|
const baseline = await getPg1516Baseline();
|
|
155
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
154
156
|
return new Catalog({ ...baseline, version, currentUser });
|
|
155
157
|
}
|
|
156
158
|
const publicSchema = new Schema({
|
|
@@ -284,6 +286,8 @@ function normalizeCatalog(catalog) {
|
|
|
284
286
|
options: redactSensitiveOptionPairs(server.options),
|
|
285
287
|
comment: server.comment,
|
|
286
288
|
privileges: server.privileges,
|
|
289
|
+
wrapper_handler: server.wrapper_handler,
|
|
290
|
+
wrapper_validator: server.wrapper_validator,
|
|
287
291
|
});
|
|
288
292
|
});
|
|
289
293
|
const userMappings = mapRecord(catalog.userMappings, (mapping) => {
|
|
@@ -291,6 +295,8 @@ function normalizeCatalog(catalog) {
|
|
|
291
295
|
user: mapping.user,
|
|
292
296
|
server: mapping.server,
|
|
293
297
|
options: redactSensitiveOptionPairs(mapping.options),
|
|
298
|
+
wrapper_handler: mapping.wrapper_handler,
|
|
299
|
+
wrapper_validator: mapping.wrapper_validator,
|
|
294
300
|
});
|
|
295
301
|
});
|
|
296
302
|
const foreignTables = mapRecord(catalog.foreignTables, (foreignTable) => new ForeignTable({
|
|
@@ -302,6 +308,8 @@ function normalizeCatalog(catalog) {
|
|
|
302
308
|
options: redactSensitiveOptionPairs(foreignTable.options),
|
|
303
309
|
comment: foreignTable.comment,
|
|
304
310
|
privileges: foreignTable.privileges,
|
|
311
|
+
wrapper_handler: foreignTable.wrapper_handler,
|
|
312
|
+
wrapper_validator: foreignTable.wrapper_validator,
|
|
305
313
|
}));
|
|
306
314
|
const subscriptions = mapRecord(catalog.subscriptions, (subscription) => {
|
|
307
315
|
return new Subscription({
|
|
@@ -6,7 +6,7 @@ import { CreateMaterializedView } from "./objects/materialized-view/changes/mate
|
|
|
6
6
|
import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.js";
|
|
7
7
|
import { CreateProcedure } from "./objects/procedure/changes/procedure.create.js";
|
|
8
8
|
import { DropProcedure } from "./objects/procedure/changes/procedure.drop.js";
|
|
9
|
-
import { AlterTableAddConstraint
|
|
9
|
+
import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.js";
|
|
10
10
|
import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.js";
|
|
11
11
|
import { CreateTable } from "./objects/table/changes/table.create.js";
|
|
12
12
|
import { DropTable } from "./objects/table/changes/table.drop.js";
|
|
@@ -312,12 +312,6 @@ function buildReplaceChanges(resolved, options) {
|
|
|
312
312
|
constraint,
|
|
313
313
|
}),
|
|
314
314
|
];
|
|
315
|
-
if (!constraint.validated) {
|
|
316
|
-
items.push(new AlterTableValidateConstraint({
|
|
317
|
-
table: resolved.branch,
|
|
318
|
-
constraint,
|
|
319
|
-
}));
|
|
320
|
-
}
|
|
321
315
|
if (constraint.comment !== null &&
|
|
322
316
|
constraint.comment !== undefined) {
|
|
323
317
|
items.push(new CreateCommentOnConstraint({
|
|
@@ -120,6 +120,30 @@ export const supabase = {
|
|
|
120
120
|
"trigger/function_schema": [...SUPABASE_SYSTEM_SCHEMAS],
|
|
121
121
|
},
|
|
122
122
|
},
|
|
123
|
+
// Defensive fallback for dynamically-created pgmq queue /
|
|
124
|
+
// archive tables. `pgmq.q_<name>` and `pgmq.a_<name>` are
|
|
125
|
+
// materialized by `select pgmq.create('<name>')`, NOT by
|
|
126
|
+
// `CREATE EXTENSION pgmq`, so emitting a user trigger against
|
|
127
|
+
// them fails locally with
|
|
128
|
+
// `relation "pgmq.q_<name>" does not exist`. On a healthy
|
|
129
|
+
// install the trigger extractor's `extension_table_oids` join
|
|
130
|
+
// (packages/pg-delta/src/core/objects/trigger/trigger.model.ts)
|
|
131
|
+
// already drops these via the `pg_depend deptype='e'` row pgmq
|
|
132
|
+
// records during `pgmq.create()`; this rule covers projects
|
|
133
|
+
// where that row is missing (older pgmq, manual table
|
|
134
|
+
// rewrites, `pg_dump`/restore that loses extension deps, ...).
|
|
135
|
+
// pgmq 1.4.4 — the version Supabase Cloud currently ships —
|
|
136
|
+
// does not record the dependency at all.
|
|
137
|
+
{
|
|
138
|
+
not: {
|
|
139
|
+
and: [
|
|
140
|
+
{ "trigger/schema": "pgmq" },
|
|
141
|
+
{
|
|
142
|
+
"trigger/table_name": { op: "regex", value: "^[qa]_" },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
123
147
|
],
|
|
124
148
|
},
|
|
125
149
|
// Exclude system objects
|
|
@@ -180,15 +204,25 @@ export const supabase = {
|
|
|
180
204
|
],
|
|
181
205
|
},
|
|
182
206
|
// Platform-managed foreign data wrappers — Wasm-based FDWs
|
|
183
|
-
// (e.g. `clerk`, `clerk_oauth`)
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
207
|
+
// (e.g. `clerk`, `clerk_oauth`) provisioned via the `wrappers`
|
|
208
|
+
// extension. Supabase Cloud creates these as
|
|
209
|
+
// `CREATE FOREIGN DATA WRAPPER clerk_oauth HANDLER
|
|
210
|
+
// extensions.wasm_fdw_handler VALIDATOR
|
|
211
|
+
// extensions.wasm_fdw_validator` at project creation; replaying
|
|
212
|
+
// the DDL against a local image fails because the local
|
|
213
|
+
// environment has no equivalent pre-step. We can't rely on the
|
|
214
|
+
// FDW owner alone — after a dump/restore the owner is often
|
|
215
|
+
// rewritten away from `supabase_admin` — so match on the shared
|
|
216
|
+
// Wasm handler/validator (`extensions.wasm_fdw_handler` /
|
|
217
|
+
// `extensions.wasm_fdw_validator`) instead.
|
|
218
|
+
//
|
|
219
|
+
// Matching the bare `extensions.*` namespace would be too broad:
|
|
220
|
+
// contrib FDWs like `postgres_fdw` also install their
|
|
221
|
+
// handler/validator into `extensions` on Supabase, and those ARE
|
|
222
|
+
// available in the local image, so a user-created `postgres_fdw`
|
|
223
|
+
// wrapper (and its servers/foreign tables/user mappings) must
|
|
224
|
+
// still roundtrip. Keying on the `wasm_fdw_*` function names
|
|
225
|
+
// targets only the platform Wasm wrappers.
|
|
192
226
|
{
|
|
193
227
|
and: [
|
|
194
228
|
{ objectType: "foreign_data_wrapper" },
|
|
@@ -197,13 +231,70 @@ export const supabase = {
|
|
|
197
231
|
{
|
|
198
232
|
"foreign_data_wrapper/handler": {
|
|
199
233
|
op: "regex",
|
|
200
|
-
value: "^extensions\\.",
|
|
234
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
201
235
|
},
|
|
202
236
|
},
|
|
203
237
|
{
|
|
204
238
|
"foreign_data_wrapper/validator": {
|
|
205
239
|
op: "regex",
|
|
206
|
-
value: "^extensions\\.",
|
|
240
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
// Platform-managed Wasm FDW dependents (CLI-1470 follow-up).
|
|
248
|
+
// Suppressing the wrapper DDL alone leaves `CREATE SERVER` /
|
|
249
|
+
// `CREATE FOREIGN TABLE` / `CREATE USER MAPPING` that reference
|
|
250
|
+
// a wrapper local Docker never provisions (`clerk_oauth`, etc.).
|
|
251
|
+
// Match on the parent wrapper's Wasm handler/validator
|
|
252
|
+
// (`extensions.wasm_fdw_handler` / `extensions.wasm_fdw_validator`,
|
|
253
|
+
// joined at extract time) — the same discriminator used for the
|
|
254
|
+
// wrapper itself above. A bare `extensions.*` match would also
|
|
255
|
+
// drop user-created `postgres_fdw` servers/foreign tables/user
|
|
256
|
+
// mappings (whose handler installs into `extensions` but which
|
|
257
|
+
// the local image CAN provision), so keep it scoped to the Wasm
|
|
258
|
+
// function names. Server _privilege_ scope is excluded here —
|
|
259
|
+
// `GRANT/REVOKE ON SERVER` does not require superuser and remains
|
|
260
|
+
// user-declarative state (see CLI-1469 companion test).
|
|
261
|
+
{
|
|
262
|
+
and: [
|
|
263
|
+
{ objectType: "server" },
|
|
264
|
+
{ not: { scope: "privilege" } },
|
|
265
|
+
{
|
|
266
|
+
or: [
|
|
267
|
+
{
|
|
268
|
+
"{server,foreign_table,user_mapping}/wrapper_handler": {
|
|
269
|
+
op: "regex",
|
|
270
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"{server,foreign_table,user_mapping}/wrapper_validator": {
|
|
275
|
+
op: "regex",
|
|
276
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
and: [
|
|
285
|
+
{ objectType: ["foreign_table", "user_mapping"] },
|
|
286
|
+
{
|
|
287
|
+
or: [
|
|
288
|
+
{
|
|
289
|
+
"{server,foreign_table,user_mapping}/wrapper_handler": {
|
|
290
|
+
op: "regex",
|
|
291
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"{server,foreign_table,user_mapping}/wrapper_validator": {
|
|
296
|
+
op: "regex",
|
|
297
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
207
298
|
},
|
|
208
299
|
},
|
|
209
300
|
],
|
|
@@ -52,6 +52,8 @@ declare const foreignTablePropsSchema: z.ZodObject<{
|
|
|
52
52
|
provider: z.ZodString;
|
|
53
53
|
label: z.ZodString;
|
|
54
54
|
}, z.z.core.$strip>>>>;
|
|
55
|
+
wrapper_handler: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
56
|
+
wrapper_validator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
55
57
|
}, z.z.core.$strip>;
|
|
56
58
|
type ForeignTablePrivilegeProps = PrivilegeProps;
|
|
57
59
|
export type ForeignTableProps = z.infer<typeof foreignTablePropsSchema>;
|
|
@@ -65,6 +67,8 @@ export declare class ForeignTable extends BasePgModel implements TableLikeObject
|
|
|
65
67
|
readonly columns: ForeignTableProps["columns"];
|
|
66
68
|
readonly privileges: ForeignTablePrivilegeProps[];
|
|
67
69
|
readonly security_labels: SecurityLabelProps[];
|
|
70
|
+
readonly wrapper_handler: ForeignTableProps["wrapper_handler"];
|
|
71
|
+
readonly wrapper_validator: ForeignTableProps["wrapper_validator"];
|
|
68
72
|
constructor(props: ForeignTableProps);
|
|
69
73
|
get stableId(): `foreignTable:${string}`;
|
|
70
74
|
get identityFields(): {
|
|
@@ -23,6 +23,9 @@ const foreignTablePropsSchema = z.object({
|
|
|
23
23
|
columns: z.array(columnPropsSchema),
|
|
24
24
|
privileges: z.array(privilegePropsSchema),
|
|
25
25
|
security_labels: z.array(securityLabelPropsSchema).default([]).optional(),
|
|
26
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
27
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
28
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
26
29
|
});
|
|
27
30
|
export class ForeignTable extends BasePgModel {
|
|
28
31
|
schema;
|
|
@@ -34,6 +37,8 @@ export class ForeignTable extends BasePgModel {
|
|
|
34
37
|
columns;
|
|
35
38
|
privileges;
|
|
36
39
|
security_labels;
|
|
40
|
+
wrapper_handler;
|
|
41
|
+
wrapper_validator;
|
|
37
42
|
constructor(props) {
|
|
38
43
|
super();
|
|
39
44
|
// Identity fields
|
|
@@ -47,6 +52,8 @@ export class ForeignTable extends BasePgModel {
|
|
|
47
52
|
this.columns = props.columns;
|
|
48
53
|
this.privileges = props.privileges;
|
|
49
54
|
this.security_labels = props.security_labels ?? [];
|
|
55
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
56
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
50
57
|
}
|
|
51
58
|
get stableId() {
|
|
52
59
|
return `foreignTable:${this.schema}.${this.name}`;
|
|
@@ -114,12 +121,22 @@ export async function extractForeignTables(pool) {
|
|
|
114
121
|
c.relowner::regrole::text as owner,
|
|
115
122
|
quote_ident(srv.srvname) as server,
|
|
116
123
|
coalesce(ft.ftoptions, array[]::text[]) as options,
|
|
117
|
-
c.oid as oid
|
|
124
|
+
c.oid as oid,
|
|
125
|
+
case
|
|
126
|
+
when fdw.fdwhandler = 0 then null
|
|
127
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
128
|
+
end as wrapper_handler,
|
|
129
|
+
case
|
|
130
|
+
when fdw.fdwvalidator = 0 then null
|
|
131
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
132
|
+
end as wrapper_validator
|
|
118
133
|
from
|
|
119
134
|
pg_class c
|
|
120
135
|
inner join pg_foreign_table ft on ft.ftrelid = c.oid
|
|
121
136
|
inner join pg_foreign_server srv on srv.oid = ft.ftserver
|
|
122
137
|
inner join pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
138
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
139
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
123
140
|
left outer join extension_oids e1 on c.oid = e1.objid
|
|
124
141
|
where
|
|
125
142
|
c.relkind = 'f'
|
|
@@ -133,6 +150,8 @@ export async function extractForeignTables(pool) {
|
|
|
133
150
|
ft.owner,
|
|
134
151
|
ft.server,
|
|
135
152
|
ft.options,
|
|
153
|
+
ft.wrapper_handler,
|
|
154
|
+
ft.wrapper_validator,
|
|
136
155
|
obj_description(ft.oid, 'pg_class') as comment,
|
|
137
156
|
coalesce(json_agg(
|
|
138
157
|
case when a.attname is not null then
|
|
@@ -218,7 +237,14 @@ export async function extractForeignTables(pool) {
|
|
|
218
237
|
left join pg_attrdef ad on a.attrelid = ad.adrelid and a.attnum = ad.adnum
|
|
219
238
|
left join pg_type ty on ty.oid = a.atttypid
|
|
220
239
|
group by
|
|
221
|
-
ft.oid,
|
|
240
|
+
ft.oid,
|
|
241
|
+
ft.schema,
|
|
242
|
+
ft.name,
|
|
243
|
+
ft.owner,
|
|
244
|
+
ft.server,
|
|
245
|
+
ft.options,
|
|
246
|
+
ft.wrapper_handler,
|
|
247
|
+
ft.wrapper_validator
|
|
222
248
|
order by
|
|
223
249
|
ft.schema, ft.name
|
|
224
250
|
`);
|
|
@@ -26,6 +26,8 @@ declare const serverPropsSchema: z.ZodObject<{
|
|
|
26
26
|
grantable: z.ZodBoolean;
|
|
27
27
|
columns: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
28
28
|
}, z.z.core.$strip>>;
|
|
29
|
+
wrapper_handler: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
30
|
+
wrapper_validator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
29
31
|
}, z.z.core.$strip>;
|
|
30
32
|
type ServerPrivilegeProps = PrivilegeProps;
|
|
31
33
|
export type ServerProps = z.infer<typeof serverPropsSchema>;
|
|
@@ -38,6 +40,8 @@ export declare class Server extends BasePgModel {
|
|
|
38
40
|
readonly options: ServerProps["options"];
|
|
39
41
|
readonly comment: ServerProps["comment"];
|
|
40
42
|
readonly privileges: ServerPrivilegeProps[];
|
|
43
|
+
readonly wrapper_handler: ServerProps["wrapper_handler"];
|
|
44
|
+
readonly wrapper_validator: ServerProps["wrapper_validator"];
|
|
41
45
|
constructor(props: ServerProps);
|
|
42
46
|
get stableId(): `server:${string}`;
|
|
43
47
|
get identityFields(): {
|
|
@@ -21,6 +21,9 @@ const serverPropsSchema = z.object({
|
|
|
21
21
|
options: z.array(z.string()).nullable(),
|
|
22
22
|
comment: z.string().nullable(),
|
|
23
23
|
privileges: z.array(privilegePropsSchema),
|
|
24
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
25
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
26
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
24
27
|
});
|
|
25
28
|
export class Server extends BasePgModel {
|
|
26
29
|
name;
|
|
@@ -31,6 +34,8 @@ export class Server extends BasePgModel {
|
|
|
31
34
|
options;
|
|
32
35
|
comment;
|
|
33
36
|
privileges;
|
|
37
|
+
wrapper_handler;
|
|
38
|
+
wrapper_validator;
|
|
34
39
|
constructor(props) {
|
|
35
40
|
super();
|
|
36
41
|
// Identity fields
|
|
@@ -43,6 +48,8 @@ export class Server extends BasePgModel {
|
|
|
43
48
|
this.options = props.options;
|
|
44
49
|
this.comment = props.comment;
|
|
45
50
|
this.privileges = props.privileges;
|
|
51
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
52
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
46
53
|
}
|
|
47
54
|
get stableId() {
|
|
48
55
|
return `server:${this.name}`;
|
|
@@ -96,10 +103,20 @@ export async function extractServers(pool) {
|
|
|
96
103
|
)
|
|
97
104
|
from lateral aclexplode(srv.srvacl) as x(grantor, grantee, privilege_type, is_grantable)
|
|
98
105
|
), '[]'
|
|
99
|
-
) as privileges
|
|
106
|
+
) as privileges,
|
|
107
|
+
case
|
|
108
|
+
when fdw.fdwhandler = 0 then null
|
|
109
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
110
|
+
end as wrapper_handler,
|
|
111
|
+
case
|
|
112
|
+
when fdw.fdwvalidator = 0 then null
|
|
113
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
114
|
+
end as wrapper_validator
|
|
100
115
|
from
|
|
101
116
|
pg_catalog.pg_foreign_server srv
|
|
102
117
|
inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
118
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
119
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
103
120
|
where
|
|
104
121
|
not fdw.fdwname like any(array['pg\\_%'])
|
|
105
122
|
order by
|
|
@@ -16,12 +16,16 @@ declare const userMappingPropsSchema: z.ZodObject<{
|
|
|
16
16
|
user: z.ZodString;
|
|
17
17
|
server: z.ZodString;
|
|
18
18
|
options: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
19
|
+
wrapper_handler: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
20
|
+
wrapper_validator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
21
|
}, z.z.core.$strip>;
|
|
20
22
|
export type UserMappingProps = z.infer<typeof userMappingPropsSchema>;
|
|
21
23
|
export declare class UserMapping extends BasePgModel {
|
|
22
24
|
readonly user: UserMappingProps["user"];
|
|
23
25
|
readonly server: UserMappingProps["server"];
|
|
24
26
|
readonly options: UserMappingProps["options"];
|
|
27
|
+
readonly wrapper_handler: UserMappingProps["wrapper_handler"];
|
|
28
|
+
readonly wrapper_validator: UserMappingProps["wrapper_validator"];
|
|
25
29
|
constructor(props: UserMappingProps);
|
|
26
30
|
get stableId(): `userMapping:${string}:${string}`;
|
|
27
31
|
get identityFields(): {
|
|
@@ -16,11 +16,16 @@ const userMappingPropsSchema = z.object({
|
|
|
16
16
|
user: z.string(),
|
|
17
17
|
server: z.string(),
|
|
18
18
|
options: z.array(z.string()).nullable(),
|
|
19
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
20
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
21
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
19
22
|
});
|
|
20
23
|
export class UserMapping extends BasePgModel {
|
|
21
24
|
user;
|
|
22
25
|
server;
|
|
23
26
|
options;
|
|
27
|
+
wrapper_handler;
|
|
28
|
+
wrapper_validator;
|
|
24
29
|
constructor(props) {
|
|
25
30
|
super();
|
|
26
31
|
// Identity fields
|
|
@@ -28,6 +33,8 @@ export class UserMapping extends BasePgModel {
|
|
|
28
33
|
this.server = props.server;
|
|
29
34
|
// Data fields
|
|
30
35
|
this.options = props.options;
|
|
36
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
37
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
31
38
|
}
|
|
32
39
|
get stableId() {
|
|
33
40
|
return `userMapping:${this.server}:${this.user}`;
|
|
@@ -62,11 +69,21 @@ export async function extractUserMappings(pool) {
|
|
|
62
69
|
else um.umuser::regrole::text
|
|
63
70
|
end as user,
|
|
64
71
|
quote_ident(srv.srvname) as server,
|
|
65
|
-
coalesce(um.umoptions, array[]::text[]) as options
|
|
72
|
+
coalesce(um.umoptions, array[]::text[]) as options,
|
|
73
|
+
case
|
|
74
|
+
when fdw.fdwhandler = 0 then null
|
|
75
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
76
|
+
end as wrapper_handler,
|
|
77
|
+
case
|
|
78
|
+
when fdw.fdwvalidator = 0 then null
|
|
79
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
80
|
+
end as wrapper_validator
|
|
66
81
|
from
|
|
67
82
|
pg_catalog.pg_user_mapping um
|
|
68
83
|
inner join pg_catalog.pg_foreign_server srv on srv.oid = um.umserver
|
|
69
84
|
inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
85
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
86
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
70
87
|
where
|
|
71
88
|
not fdw.fdwname like any(array['pg\\_%'])
|
|
72
89
|
order by
|