@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
|
@@ -55,6 +55,86 @@ function fdwPrivilegeChange(fdw: { name: string; owner: string }): Change {
|
|
|
55
55
|
} as unknown as Change;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function serverChange(
|
|
59
|
+
operation: "create" | "alter" | "drop",
|
|
60
|
+
server: {
|
|
61
|
+
name: string;
|
|
62
|
+
owner: string;
|
|
63
|
+
foreign_data_wrapper: string;
|
|
64
|
+
wrapper_handler: string | null;
|
|
65
|
+
wrapper_validator: string | null;
|
|
66
|
+
},
|
|
67
|
+
): Change {
|
|
68
|
+
return {
|
|
69
|
+
objectType: "server",
|
|
70
|
+
operation,
|
|
71
|
+
scope: "object",
|
|
72
|
+
server: {
|
|
73
|
+
type: null,
|
|
74
|
+
version: null,
|
|
75
|
+
options: null,
|
|
76
|
+
comment: null,
|
|
77
|
+
privileges: [],
|
|
78
|
+
...server,
|
|
79
|
+
},
|
|
80
|
+
requires: [],
|
|
81
|
+
creates: [],
|
|
82
|
+
drops: [],
|
|
83
|
+
} as unknown as Change;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function foreignTableChange(
|
|
87
|
+
operation: "create" | "alter" | "drop",
|
|
88
|
+
foreignTable: {
|
|
89
|
+
schema: string;
|
|
90
|
+
name: string;
|
|
91
|
+
owner: string;
|
|
92
|
+
server: string;
|
|
93
|
+
wrapper_handler: string | null;
|
|
94
|
+
wrapper_validator: string | null;
|
|
95
|
+
},
|
|
96
|
+
): Change {
|
|
97
|
+
return {
|
|
98
|
+
objectType: "foreign_table",
|
|
99
|
+
operation,
|
|
100
|
+
scope: "object",
|
|
101
|
+
foreignTable: {
|
|
102
|
+
options: null,
|
|
103
|
+
comment: null,
|
|
104
|
+
columns: [],
|
|
105
|
+
privileges: [],
|
|
106
|
+
security_labels: [],
|
|
107
|
+
...foreignTable,
|
|
108
|
+
},
|
|
109
|
+
requires: [],
|
|
110
|
+
creates: [],
|
|
111
|
+
drops: [],
|
|
112
|
+
} as unknown as Change;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function userMappingChange(
|
|
116
|
+
operation: "create" | "alter" | "drop",
|
|
117
|
+
userMapping: {
|
|
118
|
+
user: string;
|
|
119
|
+
server: string;
|
|
120
|
+
wrapper_handler: string | null;
|
|
121
|
+
wrapper_validator: string | null;
|
|
122
|
+
},
|
|
123
|
+
): Change {
|
|
124
|
+
return {
|
|
125
|
+
objectType: "user_mapping",
|
|
126
|
+
operation,
|
|
127
|
+
scope: "object",
|
|
128
|
+
userMapping: {
|
|
129
|
+
options: null,
|
|
130
|
+
...userMapping,
|
|
131
|
+
},
|
|
132
|
+
requires: [],
|
|
133
|
+
creates: [],
|
|
134
|
+
drops: [],
|
|
135
|
+
} as unknown as Change;
|
|
136
|
+
}
|
|
137
|
+
|
|
58
138
|
function serverPrivilegeChange(server: {
|
|
59
139
|
name: string;
|
|
60
140
|
owner: string;
|
|
@@ -71,6 +151,33 @@ function serverPrivilegeChange(server: {
|
|
|
71
151
|
} as unknown as Change;
|
|
72
152
|
}
|
|
73
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Build a synthetic trigger change shaped like what `flattenChange` consumes.
|
|
156
|
+
* The flattener emits keys `trigger/schema`, `trigger/table_name`,
|
|
157
|
+
* `trigger/function_schema`, etc. by walking the nested `trigger` model.
|
|
158
|
+
*/
|
|
159
|
+
function triggerChange(
|
|
160
|
+
operation: "create" | "alter" | "drop",
|
|
161
|
+
trigger: {
|
|
162
|
+
schema: string;
|
|
163
|
+
name: string;
|
|
164
|
+
table_name: string;
|
|
165
|
+
function_schema: string;
|
|
166
|
+
function_name: string;
|
|
167
|
+
owner: string;
|
|
168
|
+
},
|
|
169
|
+
): Change {
|
|
170
|
+
return {
|
|
171
|
+
objectType: "trigger",
|
|
172
|
+
operation,
|
|
173
|
+
scope: "object",
|
|
174
|
+
trigger,
|
|
175
|
+
requires: [],
|
|
176
|
+
creates: [],
|
|
177
|
+
drops: [],
|
|
178
|
+
} as unknown as Change;
|
|
179
|
+
}
|
|
180
|
+
|
|
74
181
|
describe("supabase integration filter — foreign data wrappers", () => {
|
|
75
182
|
// Regression for CLI-1470. Wasm-based foreign data wrappers on Supabase
|
|
76
183
|
// (e.g. `clerk`, `clerk_oauth`) are provisioned at project creation by
|
|
@@ -128,6 +235,35 @@ describe("supabase integration filter — foreign data wrappers", () => {
|
|
|
128
235
|
expect(evaluatePattern(filter, change)).toBe(true);
|
|
129
236
|
});
|
|
130
237
|
|
|
238
|
+
// `postgres_fdw` (and other contrib FDWs) install their handler/validator
|
|
239
|
+
// into `extensions` on Supabase, but they ARE available in the local image,
|
|
240
|
+
// so a user-created `postgres_fdw` wrapper must roundtrip. Only the Wasm
|
|
241
|
+
// `wasm_fdw_handler` / `wasm_fdw_validator` functions identify the
|
|
242
|
+
// platform-managed wrappers that local Docker cannot provision.
|
|
243
|
+
test("preserves user FDW whose handler is extensions.postgres_fdw_handler", () => {
|
|
244
|
+
const change = fdwChange("create", {
|
|
245
|
+
name: "postgres_fdw",
|
|
246
|
+
owner: "postgres",
|
|
247
|
+
handler: "extensions.postgres_fdw_handler",
|
|
248
|
+
validator: "extensions.postgres_fdw_validator",
|
|
249
|
+
});
|
|
250
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// The Wasm discriminator must be an exact function-name match, not a
|
|
254
|
+
// prefix: a user function whose name merely starts with `wasm_fdw_handler`
|
|
255
|
+
// (e.g. `wasm_fdw_handler_custom`) is not the platform `wrappers` handler
|
|
256
|
+
// and must roundtrip.
|
|
257
|
+
test("preserves user FDW whose handler extends the wasm_fdw_handler prefix", () => {
|
|
258
|
+
const change = fdwChange("create", {
|
|
259
|
+
name: "custom_wasm",
|
|
260
|
+
owner: "postgres",
|
|
261
|
+
handler: "extensions.wasm_fdw_handler_custom",
|
|
262
|
+
validator: "extensions.wasm_fdw_validator_custom",
|
|
263
|
+
});
|
|
264
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
131
267
|
test("preserves user FDW with no handler/validator", () => {
|
|
132
268
|
const change = fdwChange("create", {
|
|
133
269
|
name: "user_fdw_bare",
|
|
@@ -196,3 +332,202 @@ describe("supabase integration filter — foreign data wrapper / server ACLs", (
|
|
|
196
332
|
expect(evaluatePattern(filter, change)).toBe(true);
|
|
197
333
|
});
|
|
198
334
|
});
|
|
335
|
+
|
|
336
|
+
describe("supabase integration filter — Wasm FDW dependents", () => {
|
|
337
|
+
const wasmWrapper = {
|
|
338
|
+
wrapper_handler: "extensions.wasm_fdw_handler",
|
|
339
|
+
wrapper_validator: "extensions.wasm_fdw_validator",
|
|
340
|
+
} as const;
|
|
341
|
+
|
|
342
|
+
const userWrapper = {
|
|
343
|
+
wrapper_handler: "public.postgres_fdw_handler",
|
|
344
|
+
wrapper_validator: "public.postgres_fdw_validator",
|
|
345
|
+
} as const;
|
|
346
|
+
|
|
347
|
+
// `postgres_fdw` installs its handler/validator into `extensions` on
|
|
348
|
+
// Supabase, but the contrib FDW IS available locally, so user-owned
|
|
349
|
+
// servers / foreign tables / user mappings built on it must roundtrip.
|
|
350
|
+
// Keying suppression on the bare `extensions.*` namespace would wrongly
|
|
351
|
+
// drop them; only the Wasm `wasm_fdw_*` functions mark platform wrappers.
|
|
352
|
+
const extensionsPgFdwWrapper = {
|
|
353
|
+
wrapper_handler: "extensions.postgres_fdw_handler",
|
|
354
|
+
wrapper_validator: "extensions.postgres_fdw_validator",
|
|
355
|
+
} as const;
|
|
356
|
+
|
|
357
|
+
test("suppresses CREATE SERVER bound to extensions.* Wasm FDW", () => {
|
|
358
|
+
const change = serverChange("create", {
|
|
359
|
+
name: "clerk_oauth_server",
|
|
360
|
+
owner: "postgres",
|
|
361
|
+
foreign_data_wrapper: "clerk_oauth",
|
|
362
|
+
...wasmWrapper,
|
|
363
|
+
});
|
|
364
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("suppresses DROP FOREIGN TABLE bound to extensions.* Wasm FDW", () => {
|
|
368
|
+
const change = foreignTableChange("drop", {
|
|
369
|
+
schema: "public",
|
|
370
|
+
name: "clerk_oauth",
|
|
371
|
+
owner: "postgres",
|
|
372
|
+
server: "clerk_oauth_server",
|
|
373
|
+
...wasmWrapper,
|
|
374
|
+
});
|
|
375
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("suppresses ALTER FOREIGN TABLE bound to extensions.* Wasm FDW", () => {
|
|
379
|
+
const change = foreignTableChange("alter", {
|
|
380
|
+
schema: "public",
|
|
381
|
+
name: "clerk_oauth",
|
|
382
|
+
owner: "postgres",
|
|
383
|
+
server: "clerk_oauth_server",
|
|
384
|
+
...wasmWrapper,
|
|
385
|
+
});
|
|
386
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("suppresses DROP USER MAPPING bound to extensions.* Wasm FDW", () => {
|
|
390
|
+
const change = userMappingChange("drop", {
|
|
391
|
+
user: "postgres",
|
|
392
|
+
server: "clerk_server",
|
|
393
|
+
...wasmWrapper,
|
|
394
|
+
});
|
|
395
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("suppresses CREATE USER MAPPING when only wrapper validator is in extensions", () => {
|
|
399
|
+
const change = userMappingChange("create", {
|
|
400
|
+
user: "postgres",
|
|
401
|
+
server: "clerk_server",
|
|
402
|
+
wrapper_handler: null,
|
|
403
|
+
wrapper_validator: "extensions.wasm_fdw_validator",
|
|
404
|
+
});
|
|
405
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("preserves CREATE SERVER bound to user postgres_fdw wrapper", () => {
|
|
409
|
+
const change = serverChange("create", {
|
|
410
|
+
name: "live_risk_server",
|
|
411
|
+
owner: "postgres",
|
|
412
|
+
foreign_data_wrapper: "postgres_fdw",
|
|
413
|
+
...userWrapper,
|
|
414
|
+
});
|
|
415
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("preserves server ACL when postgres_fdw handler lives in extensions", () => {
|
|
419
|
+
const change = serverPrivilegeChange({
|
|
420
|
+
name: "user_server",
|
|
421
|
+
owner: "postgres",
|
|
422
|
+
});
|
|
423
|
+
(change as unknown as { server: Record<string, unknown> }).server = {
|
|
424
|
+
name: "user_server",
|
|
425
|
+
owner: "postgres",
|
|
426
|
+
wrapper_handler: "extensions.postgres_fdw_handler",
|
|
427
|
+
wrapper_validator: "extensions.postgres_fdw_validator",
|
|
428
|
+
};
|
|
429
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("preserves CREATE FOREIGN TABLE on user postgres_fdw server", () => {
|
|
433
|
+
const change = foreignTableChange("create", {
|
|
434
|
+
schema: "live_risk",
|
|
435
|
+
name: "devices",
|
|
436
|
+
owner: "postgres",
|
|
437
|
+
server: "live_risk_server",
|
|
438
|
+
...userWrapper,
|
|
439
|
+
});
|
|
440
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("preserves CREATE SERVER when postgres_fdw handler lives in extensions", () => {
|
|
444
|
+
const change = serverChange("create", {
|
|
445
|
+
name: "user_pg_server",
|
|
446
|
+
owner: "postgres",
|
|
447
|
+
foreign_data_wrapper: "postgres_fdw",
|
|
448
|
+
...extensionsPgFdwWrapper,
|
|
449
|
+
});
|
|
450
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("preserves CREATE FOREIGN TABLE when postgres_fdw handler lives in extensions", () => {
|
|
454
|
+
const change = foreignTableChange("create", {
|
|
455
|
+
schema: "user_fdw_test",
|
|
456
|
+
name: "remote_row",
|
|
457
|
+
owner: "postgres",
|
|
458
|
+
server: "user_pg_server",
|
|
459
|
+
...extensionsPgFdwWrapper,
|
|
460
|
+
});
|
|
461
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("preserves CREATE USER MAPPING when postgres_fdw handler lives in extensions", () => {
|
|
465
|
+
const change = userMappingChange("create", {
|
|
466
|
+
user: "postgres",
|
|
467
|
+
server: "user_pg_server",
|
|
468
|
+
...extensionsPgFdwWrapper,
|
|
469
|
+
});
|
|
470
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Exact-match guard at the dependent level too: a server bound to a wrapper
|
|
474
|
+
// whose handler merely shares the `wasm_fdw_handler` prefix must roundtrip.
|
|
475
|
+
test("preserves CREATE SERVER when wrapper handler extends the wasm_fdw_handler prefix", () => {
|
|
476
|
+
const change = serverChange("create", {
|
|
477
|
+
name: "custom_wasm_server",
|
|
478
|
+
owner: "postgres",
|
|
479
|
+
foreign_data_wrapper: "custom_wasm",
|
|
480
|
+
wrapper_handler: "extensions.wasm_fdw_handler_custom",
|
|
481
|
+
wrapper_validator: "extensions.wasm_fdw_validator_custom",
|
|
482
|
+
});
|
|
483
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("supabase integration filter — pgmq queue triggers", () => {
|
|
488
|
+
// Regression for the pgmq-1.4.4 cloud projects. `pgmq.create('<name>')`
|
|
489
|
+
// materializes `pgmq.q_<name>` and `pgmq.a_<name>` at runtime — they are
|
|
490
|
+
// NOT created by `CREATE EXTENSION pgmq`. On a healthy install the trigger
|
|
491
|
+
// extractor's `extension_table_oids` join already drops these via the
|
|
492
|
+
// `pg_depend deptype='e'` row that newer pgmq versions record, but on
|
|
493
|
+
// pgmq 1.4.4 that row is never recorded, so user triggers on the queue
|
|
494
|
+
// tables leak into the diff and break `supabase db reset` with
|
|
495
|
+
// `relation "pgmq.q_<name>" does not exist`. The filter must drop them
|
|
496
|
+
// at the supabase-integration level too, regardless of pg_depend state.
|
|
497
|
+
|
|
498
|
+
test("suppresses CREATE trigger on pgmq.q_<name> calling a public function", () => {
|
|
499
|
+
const change = triggerChange("create", {
|
|
500
|
+
schema: "pgmq",
|
|
501
|
+
name: "after_insert_processed_milestones_queue",
|
|
502
|
+
table_name: "q_processed_milestones_queue",
|
|
503
|
+
function_schema: "public",
|
|
504
|
+
function_name: "move_data_from_queue",
|
|
505
|
+
owner: "postgres",
|
|
506
|
+
});
|
|
507
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("suppresses DROP trigger on pgmq.a_<name> calling a public function", () => {
|
|
511
|
+
const change = triggerChange("drop", {
|
|
512
|
+
schema: "pgmq",
|
|
513
|
+
name: "after_insert_archive",
|
|
514
|
+
table_name: "a_processed_milestones_queue",
|
|
515
|
+
function_schema: "public",
|
|
516
|
+
function_name: "archive_handler",
|
|
517
|
+
owner: "postgres",
|
|
518
|
+
});
|
|
519
|
+
expect(evaluatePattern(filter, change)).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("preserves CREATE trigger on auth.users calling a public function", () => {
|
|
523
|
+
const change = triggerChange("create", {
|
|
524
|
+
schema: "auth",
|
|
525
|
+
name: "on_auth_user_created",
|
|
526
|
+
table_name: "users",
|
|
527
|
+
function_schema: "public",
|
|
528
|
+
function_name: "handle_new_user",
|
|
529
|
+
owner: "supabase_auth_admin",
|
|
530
|
+
});
|
|
531
|
+
expect(evaluatePattern(filter, change)).toBe(true);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
@@ -125,6 +125,30 @@ export const supabase: IntegrationDSL = {
|
|
|
125
125
|
"trigger/function_schema": [...SUPABASE_SYSTEM_SCHEMAS],
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
|
+
// Defensive fallback for dynamically-created pgmq queue /
|
|
129
|
+
// archive tables. `pgmq.q_<name>` and `pgmq.a_<name>` are
|
|
130
|
+
// materialized by `select pgmq.create('<name>')`, NOT by
|
|
131
|
+
// `CREATE EXTENSION pgmq`, so emitting a user trigger against
|
|
132
|
+
// them fails locally with
|
|
133
|
+
// `relation "pgmq.q_<name>" does not exist`. On a healthy
|
|
134
|
+
// install the trigger extractor's `extension_table_oids` join
|
|
135
|
+
// (packages/pg-delta/src/core/objects/trigger/trigger.model.ts)
|
|
136
|
+
// already drops these via the `pg_depend deptype='e'` row pgmq
|
|
137
|
+
// records during `pgmq.create()`; this rule covers projects
|
|
138
|
+
// where that row is missing (older pgmq, manual table
|
|
139
|
+
// rewrites, `pg_dump`/restore that loses extension deps, ...).
|
|
140
|
+
// pgmq 1.4.4 — the version Supabase Cloud currently ships —
|
|
141
|
+
// does not record the dependency at all.
|
|
142
|
+
{
|
|
143
|
+
not: {
|
|
144
|
+
and: [
|
|
145
|
+
{ "trigger/schema": "pgmq" },
|
|
146
|
+
{
|
|
147
|
+
"trigger/table_name": { op: "regex", value: "^[qa]_" },
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
128
152
|
],
|
|
129
153
|
},
|
|
130
154
|
// Exclude system objects
|
|
@@ -185,15 +209,25 @@ export const supabase: IntegrationDSL = {
|
|
|
185
209
|
],
|
|
186
210
|
},
|
|
187
211
|
// Platform-managed foreign data wrappers — Wasm-based FDWs
|
|
188
|
-
// (e.g. `clerk`, `clerk_oauth`)
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
212
|
+
// (e.g. `clerk`, `clerk_oauth`) provisioned via the `wrappers`
|
|
213
|
+
// extension. Supabase Cloud creates these as
|
|
214
|
+
// `CREATE FOREIGN DATA WRAPPER clerk_oauth HANDLER
|
|
215
|
+
// extensions.wasm_fdw_handler VALIDATOR
|
|
216
|
+
// extensions.wasm_fdw_validator` at project creation; replaying
|
|
217
|
+
// the DDL against a local image fails because the local
|
|
218
|
+
// environment has no equivalent pre-step. We can't rely on the
|
|
219
|
+
// FDW owner alone — after a dump/restore the owner is often
|
|
220
|
+
// rewritten away from `supabase_admin` — so match on the shared
|
|
221
|
+
// Wasm handler/validator (`extensions.wasm_fdw_handler` /
|
|
222
|
+
// `extensions.wasm_fdw_validator`) instead.
|
|
223
|
+
//
|
|
224
|
+
// Matching the bare `extensions.*` namespace would be too broad:
|
|
225
|
+
// contrib FDWs like `postgres_fdw` also install their
|
|
226
|
+
// handler/validator into `extensions` on Supabase, and those ARE
|
|
227
|
+
// available in the local image, so a user-created `postgres_fdw`
|
|
228
|
+
// wrapper (and its servers/foreign tables/user mappings) must
|
|
229
|
+
// still roundtrip. Keying on the `wasm_fdw_*` function names
|
|
230
|
+
// targets only the platform Wasm wrappers.
|
|
197
231
|
{
|
|
198
232
|
and: [
|
|
199
233
|
{ objectType: "foreign_data_wrapper" },
|
|
@@ -202,13 +236,70 @@ export const supabase: IntegrationDSL = {
|
|
|
202
236
|
{
|
|
203
237
|
"foreign_data_wrapper/handler": {
|
|
204
238
|
op: "regex",
|
|
205
|
-
value: "^extensions\\.",
|
|
239
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
206
240
|
},
|
|
207
241
|
},
|
|
208
242
|
{
|
|
209
243
|
"foreign_data_wrapper/validator": {
|
|
210
244
|
op: "regex",
|
|
211
|
-
value: "^extensions\\.",
|
|
245
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
// Platform-managed Wasm FDW dependents (CLI-1470 follow-up).
|
|
253
|
+
// Suppressing the wrapper DDL alone leaves `CREATE SERVER` /
|
|
254
|
+
// `CREATE FOREIGN TABLE` / `CREATE USER MAPPING` that reference
|
|
255
|
+
// a wrapper local Docker never provisions (`clerk_oauth`, etc.).
|
|
256
|
+
// Match on the parent wrapper's Wasm handler/validator
|
|
257
|
+
// (`extensions.wasm_fdw_handler` / `extensions.wasm_fdw_validator`,
|
|
258
|
+
// joined at extract time) — the same discriminator used for the
|
|
259
|
+
// wrapper itself above. A bare `extensions.*` match would also
|
|
260
|
+
// drop user-created `postgres_fdw` servers/foreign tables/user
|
|
261
|
+
// mappings (whose handler installs into `extensions` but which
|
|
262
|
+
// the local image CAN provision), so keep it scoped to the Wasm
|
|
263
|
+
// function names. Server _privilege_ scope is excluded here —
|
|
264
|
+
// `GRANT/REVOKE ON SERVER` does not require superuser and remains
|
|
265
|
+
// user-declarative state (see CLI-1469 companion test).
|
|
266
|
+
{
|
|
267
|
+
and: [
|
|
268
|
+
{ objectType: "server" },
|
|
269
|
+
{ not: { scope: "privilege" } },
|
|
270
|
+
{
|
|
271
|
+
or: [
|
|
272
|
+
{
|
|
273
|
+
"{server,foreign_table,user_mapping}/wrapper_handler": {
|
|
274
|
+
op: "regex",
|
|
275
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"{server,foreign_table,user_mapping}/wrapper_validator": {
|
|
280
|
+
op: "regex",
|
|
281
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
and: [
|
|
290
|
+
{ objectType: ["foreign_table", "user_mapping"] },
|
|
291
|
+
{
|
|
292
|
+
or: [
|
|
293
|
+
{
|
|
294
|
+
"{server,foreign_table,user_mapping}/wrapper_handler": {
|
|
295
|
+
op: "regex",
|
|
296
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"{server,foreign_table,user_mapping}/wrapper_validator": {
|
|
301
|
+
op: "regex",
|
|
302
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
212
303
|
},
|
|
213
304
|
},
|
|
214
305
|
],
|
|
@@ -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 {
|
|
@@ -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 {
|
|
@@ -4,7 +4,7 @@ import type { ForeignDataWrapper } from "../foreign-data-wrapper.model.ts";
|
|
|
4
4
|
abstract class BaseForeignDataWrapperChange extends BaseChange {
|
|
5
5
|
abstract readonly foreignDataWrapper: ForeignDataWrapper;
|
|
6
6
|
abstract readonly scope: "object" | "comment" | "privilege";
|
|
7
|
-
readonly objectType
|
|
7
|
+
readonly objectType = "foreign_data_wrapper" as const;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export abstract class CreateForeignDataWrapperChange extends BaseForeignDataWrapperChange {
|
|
@@ -8,7 +8,7 @@ abstract class BaseForeignTableChange extends BaseChange {
|
|
|
8
8
|
| "comment"
|
|
9
9
|
| "privilege"
|
|
10
10
|
| "security_label";
|
|
11
|
-
readonly objectType
|
|
11
|
+
readonly objectType = "foreign_table" as const;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export abstract class CreateForeignTableChange extends BaseForeignTableChange {
|
|
@@ -36,6 +36,9 @@ const foreignTablePropsSchema = z.object({
|
|
|
36
36
|
columns: z.array(columnPropsSchema),
|
|
37
37
|
privileges: z.array(privilegePropsSchema),
|
|
38
38
|
security_labels: z.array(securityLabelPropsSchema).default([]).optional(),
|
|
39
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
40
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
41
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
39
42
|
});
|
|
40
43
|
|
|
41
44
|
type ForeignTablePrivilegeProps = PrivilegeProps;
|
|
@@ -51,6 +54,8 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
51
54
|
public readonly columns: ForeignTableProps["columns"];
|
|
52
55
|
public readonly privileges: ForeignTablePrivilegeProps[];
|
|
53
56
|
public readonly security_labels: SecurityLabelProps[];
|
|
57
|
+
public readonly wrapper_handler: ForeignTableProps["wrapper_handler"];
|
|
58
|
+
public readonly wrapper_validator: ForeignTableProps["wrapper_validator"];
|
|
54
59
|
|
|
55
60
|
constructor(props: ForeignTableProps) {
|
|
56
61
|
super();
|
|
@@ -67,6 +72,8 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
67
72
|
this.columns = props.columns;
|
|
68
73
|
this.privileges = props.privileges;
|
|
69
74
|
this.security_labels = props.security_labels ?? [];
|
|
75
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
76
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
get stableId(): `foreignTable:${string}` {
|
|
@@ -146,12 +153,22 @@ export async function extractForeignTables(
|
|
|
146
153
|
c.relowner::regrole::text as owner,
|
|
147
154
|
quote_ident(srv.srvname) as server,
|
|
148
155
|
coalesce(ft.ftoptions, array[]::text[]) as options,
|
|
149
|
-
c.oid as oid
|
|
156
|
+
c.oid as oid,
|
|
157
|
+
case
|
|
158
|
+
when fdw.fdwhandler = 0 then null
|
|
159
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
160
|
+
end as wrapper_handler,
|
|
161
|
+
case
|
|
162
|
+
when fdw.fdwvalidator = 0 then null
|
|
163
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
164
|
+
end as wrapper_validator
|
|
150
165
|
from
|
|
151
166
|
pg_class c
|
|
152
167
|
inner join pg_foreign_table ft on ft.ftrelid = c.oid
|
|
153
168
|
inner join pg_foreign_server srv on srv.oid = ft.ftserver
|
|
154
169
|
inner join pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
170
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
171
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
155
172
|
left outer join extension_oids e1 on c.oid = e1.objid
|
|
156
173
|
where
|
|
157
174
|
c.relkind = 'f'
|
|
@@ -165,6 +182,8 @@ export async function extractForeignTables(
|
|
|
165
182
|
ft.owner,
|
|
166
183
|
ft.server,
|
|
167
184
|
ft.options,
|
|
185
|
+
ft.wrapper_handler,
|
|
186
|
+
ft.wrapper_validator,
|
|
168
187
|
obj_description(ft.oid, 'pg_class') as comment,
|
|
169
188
|
coalesce(json_agg(
|
|
170
189
|
case when a.attname is not null then
|
|
@@ -250,7 +269,14 @@ export async function extractForeignTables(
|
|
|
250
269
|
left join pg_attrdef ad on a.attrelid = ad.adrelid and a.attnum = ad.adnum
|
|
251
270
|
left join pg_type ty on ty.oid = a.atttypid
|
|
252
271
|
group by
|
|
253
|
-
ft.oid,
|
|
272
|
+
ft.oid,
|
|
273
|
+
ft.schema,
|
|
274
|
+
ft.name,
|
|
275
|
+
ft.owner,
|
|
276
|
+
ft.server,
|
|
277
|
+
ft.options,
|
|
278
|
+
ft.wrapper_handler,
|
|
279
|
+
ft.wrapper_validator
|
|
254
280
|
order by
|
|
255
281
|
ft.schema, ft.name
|
|
256
282
|
`);
|
|
@@ -4,7 +4,7 @@ import type { Server } from "../server.model.ts";
|
|
|
4
4
|
abstract class BaseServerChange extends BaseChange {
|
|
5
5
|
abstract readonly server: Server;
|
|
6
6
|
abstract readonly scope: "object" | "comment" | "privilege";
|
|
7
|
-
readonly objectType
|
|
7
|
+
readonly objectType = "server" as const;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export abstract class CreateServerChange extends BaseServerChange {
|