@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.
Files changed (62) hide show
  1. package/dist/cli/commands/catalog-export.js +22 -1
  2. package/dist/core/catalog.filter.d.ts +17 -0
  3. package/dist/core/catalog.filter.js +75 -0
  4. package/dist/core/catalog.model.js +9 -1
  5. package/dist/core/expand-replace-dependencies.js +1 -7
  6. package/dist/core/integrations/supabase.js +102 -11
  7. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +4 -0
  8. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +28 -2
  9. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +4 -0
  10. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +18 -1
  11. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +4 -0
  12. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +18 -1
  13. package/dist/core/objects/table/table.diff.js +53 -30
  14. package/dist/core/plan/hierarchy.js +4 -4
  15. package/dist/core/postgres-config.d.ts +7 -0
  16. package/dist/core/postgres-config.js +19 -5
  17. package/dist/core/sort/debug-visualization.js +1 -1
  18. package/dist/core/sort/topological-sort.js +2 -2
  19. package/package.json +34 -33
  20. package/src/cli/commands/catalog-export.ts +26 -1
  21. package/src/core/catalog.filter.ts +96 -0
  22. package/src/core/catalog.model.ts +10 -1
  23. package/src/core/catalog.snapshot.test.ts +1 -0
  24. package/src/core/expand-replace-dependencies.test.ts +12 -0
  25. package/src/core/expand-replace-dependencies.ts +1 -12
  26. package/src/core/integrations/supabase.test.ts +335 -0
  27. package/src/core/integrations/supabase.ts +102 -11
  28. package/src/core/objects/aggregate/changes/aggregate.base.ts +1 -1
  29. package/src/core/objects/collation/changes/collation.base.ts +1 -1
  30. package/src/core/objects/domain/changes/domain.base.ts +1 -1
  31. package/src/core/objects/extension/changes/extension.base.ts +1 -1
  32. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.base.ts +1 -1
  33. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +1 -1
  34. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +28 -2
  35. package/src/core/objects/foreign-data-wrapper/server/changes/server.base.ts +1 -1
  36. package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -1
  37. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.base.ts +1 -1
  38. package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +18 -1
  39. package/src/core/objects/index/changes/index.base.ts +1 -1
  40. package/src/core/objects/language/changes/language.base.ts +1 -1
  41. package/src/core/objects/materialized-view/changes/materialized-view.base.ts +1 -1
  42. package/src/core/objects/procedure/changes/procedure.base.ts +1 -1
  43. package/src/core/objects/rls-policy/changes/rls-policy.base.ts +1 -1
  44. package/src/core/objects/role/changes/role.base.ts +1 -1
  45. package/src/core/objects/schema/changes/schema.base.ts +1 -1
  46. package/src/core/objects/sequence/changes/sequence.base.ts +1 -1
  47. package/src/core/objects/table/changes/table.base.ts +1 -1
  48. package/src/core/objects/table/changes/table.comment.ts +2 -8
  49. package/src/core/objects/table/table.diff.test.ts +198 -5
  50. package/src/core/objects/table/table.diff.ts +63 -34
  51. package/src/core/objects/trigger/changes/trigger.alter.ts +1 -4
  52. package/src/core/objects/trigger/changes/trigger.base.ts +1 -1
  53. package/src/core/objects/type/composite-type/changes/composite-type.base.ts +1 -1
  54. package/src/core/objects/type/enum/changes/enum.base.ts +1 -1
  55. package/src/core/objects/type/range/changes/range.base.ts +1 -1
  56. package/src/core/objects/view/changes/view.base.ts +1 -1
  57. package/src/core/plan/hierarchy.ts +4 -4
  58. package/src/core/postgres-config.test.ts +39 -1
  59. package/src/core/postgres-config.ts +32 -16
  60. package/src/core/sort/debug-visualization.ts +1 -1
  61. package/src/core/sort/sort-changes.test.ts +1 -0
  62. 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 snapshot = serializeCatalog(catalog);
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, AlterTableValidateConstraint, } from "./objects/table/changes/table.alter.js";
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`) whose handler/validator live in
184
- // the `extensions` schema. `CREATE FOREIGN DATA WRAPPER`
185
- // requires superuser, and Supabase Cloud provisions these via
186
- // `supabase_admin` at project creation; replaying the DDL
187
- // against a local image fails because the local environment
188
- // has no equivalent pre-step. We can't rely on the FDW owner
189
- // alone after a dump/restore the owner is often rewritten
190
- // away from `supabase_admin`so match on the function
191
- // reference instead.
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 aloneafter 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, ft.schema, ft.name, ft.owner, ft.server, ft.options
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