@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
@@ -28,12 +28,6 @@ function createAlterConstraintChange(mainTable, branchTable) {
28
28
  table: branchTable,
29
29
  constraint: c,
30
30
  }));
31
- if (!c.validated) {
32
- changes.push(new AlterTableValidateConstraint({
33
- table: branchTable,
34
- constraint: c,
35
- }));
36
- }
37
31
  // Add comment for newly created constraint
38
32
  if (c.comment !== null) {
39
33
  changes.push(new CreateCommentOnConstraint({
@@ -53,7 +47,7 @@ function createAlterConstraintChange(mainTable, branchTable) {
53
47
  changes.push(new AlterTableDropConstraint({ table: mainTable, constraint: c }));
54
48
  }
55
49
  }
56
- // Altered constraints -> drop + add
50
+ // Altered constraints -> drop + add (or VALIDATE-only shortcut)
57
51
  for (const [name, mainC] of mainByName) {
58
52
  const branchC = branchByName.get(name);
59
53
  if (!branchC)
@@ -62,23 +56,57 @@ function createAlterConstraintChange(mainTable, branchTable) {
62
56
  if (mainC.is_partition_clone || branchC.is_partition_clone) {
63
57
  continue;
64
58
  }
65
- const changed = mainC.constraint_type !== branchC.constraint_type ||
66
- mainC.deferrable !== branchC.deferrable ||
67
- mainC.initially_deferred !== branchC.initially_deferred ||
68
- mainC.validated !== branchC.validated ||
69
- mainC.is_local !== branchC.is_local ||
70
- mainC.no_inherit !== branchC.no_inherit ||
71
- mainC.is_temporal !== branchC.is_temporal ||
72
- JSON.stringify(mainC.key_columns) !==
73
- JSON.stringify(branchC.key_columns) ||
74
- JSON.stringify(mainC.foreign_key_columns) !==
75
- JSON.stringify(branchC.foreign_key_columns) ||
76
- mainC.foreign_key_table !== branchC.foreign_key_table ||
77
- mainC.foreign_key_schema !== branchC.foreign_key_schema ||
78
- mainC.on_update !== branchC.on_update ||
79
- mainC.on_delete !== branchC.on_delete ||
80
- mainC.match_type !== branchC.match_type ||
81
- mainC.check_expression !== branchC.check_expression;
59
+ // Cheap scalar `===` checks first; only fall through to JSON.stringify
60
+ // on the array fields when every scalar has already matched.
61
+ const fieldsEqualExceptValidated = mainC.constraint_type === branchC.constraint_type &&
62
+ mainC.deferrable === branchC.deferrable &&
63
+ mainC.initially_deferred === branchC.initially_deferred &&
64
+ mainC.is_local === branchC.is_local &&
65
+ mainC.no_inherit === branchC.no_inherit &&
66
+ mainC.is_temporal === branchC.is_temporal &&
67
+ mainC.foreign_key_table === branchC.foreign_key_table &&
68
+ mainC.foreign_key_schema === branchC.foreign_key_schema &&
69
+ mainC.on_update === branchC.on_update &&
70
+ mainC.on_delete === branchC.on_delete &&
71
+ mainC.match_type === branchC.match_type &&
72
+ mainC.check_expression === branchC.check_expression &&
73
+ JSON.stringify(mainC.key_columns) ===
74
+ JSON.stringify(branchC.key_columns) &&
75
+ JSON.stringify(mainC.foreign_key_columns) ===
76
+ JSON.stringify(branchC.foreign_key_columns);
77
+ // Safe-migration shortcut: when the only difference is `validated`
78
+ // flipping from false to true, emit a single `ALTER TABLE ... VALIDATE
79
+ // CONSTRAINT` instead of drop+add. VALIDATE CONSTRAINT only takes
80
+ // SHARE UPDATE EXCLUSIVE (concurrent reads/writes proceed), whereas
81
+ // dropping and re-adding takes ACCESS EXCLUSIVE for the entire scan.
82
+ // Postgres has no reverse command, so `true -> false` must still go
83
+ // through drop+add below.
84
+ if (fieldsEqualExceptValidated &&
85
+ mainC.validated === false &&
86
+ branchC.validated === true) {
87
+ changes.push(new AlterTableValidateConstraint({
88
+ table: branchTable,
89
+ constraint: branchC,
90
+ }));
91
+ // VALIDATE preserves the constraint OID, so its comment is preserved
92
+ // too. Only emit a comment change if it actually differs.
93
+ if (mainC.comment !== branchC.comment) {
94
+ if (branchC.comment === null) {
95
+ changes.push(new DropCommentOnConstraint({
96
+ table: mainTable,
97
+ constraint: mainC,
98
+ }));
99
+ }
100
+ else {
101
+ changes.push(new CreateCommentOnConstraint({
102
+ table: branchTable,
103
+ constraint: branchC,
104
+ }));
105
+ }
106
+ }
107
+ continue;
108
+ }
109
+ const changed = mainC.validated !== branchC.validated || !fieldsEqualExceptValidated;
82
110
  if (changed) {
83
111
  changes.push(new AlterTableDropConstraint({
84
112
  table: mainTable,
@@ -88,12 +116,6 @@ function createAlterConstraintChange(mainTable, branchTable) {
88
116
  table: branchTable,
89
117
  constraint: branchC,
90
118
  }));
91
- if (!branchC.validated) {
92
- changes.push(new AlterTableValidateConstraint({
93
- table: branchTable,
94
- constraint: branchC,
95
- }));
96
- }
97
119
  // Ensure constraint comment is applied after re-creation
98
120
  if (branchC.comment !== null) {
99
121
  changes.push(new CreateCommentOnConstraint({
@@ -163,6 +185,7 @@ export function diffTables(ctx, main, branch) {
163
185
  changes.push(...createAlterConstraintChange(
164
186
  // Create a dummy table with no constraints do diff constraints against
165
187
  new Table({
188
+ // oxlint-disable-next-line typescript/no-misused-spread
166
189
  ...branchTable,
167
190
  constraints: [],
168
191
  }), branchTable));
@@ -262,7 +262,7 @@ function addClusterChange(cluster, change) {
262
262
  break;
263
263
  default: {
264
264
  const _exhaustive = objectType;
265
- throw new Error(`Unhandled object type: ${_exhaustive}`);
265
+ throw new Error(`Unhandled object type: ${JSON.stringify(_exhaustive)}`);
266
266
  }
267
267
  }
268
268
  }
@@ -303,7 +303,7 @@ function addChildChange(schema, change) {
303
303
  break;
304
304
  default: {
305
305
  const _exhaustive = parentType;
306
- throw new Error(`Unhandled parent type: ${_exhaustive}`);
306
+ throw new Error(`Unhandled parent type: ${JSON.stringify(_exhaustive)}`);
307
307
  }
308
308
  }
309
309
  const objectType = change.objectType;
@@ -351,7 +351,7 @@ function addChildChange(schema, change) {
351
351
  break;
352
352
  default: {
353
353
  const _exhaustive = objectType;
354
- throw new Error(`Unhandled object type: ${_exhaustive}`);
354
+ throw new Error(`Unhandled object type: ${JSON.stringify(_exhaustive)}`);
355
355
  }
356
356
  }
357
357
  }
@@ -482,7 +482,7 @@ function addSchemaLevelChange(schema, change, enrichment) {
482
482
  break;
483
483
  default: {
484
484
  const _exhaustive = objectType;
485
- throw new Error(`Unhandled object type: ${_exhaustive}`);
485
+ throw new Error(`Unhandled object type: ${JSON.stringify(_exhaustive)}`);
486
486
  }
487
487
  }
488
488
  }
@@ -30,6 +30,13 @@ export declare function connectWithRetry<T>(opts: {
30
30
  maxBackoffMs?: number;
31
31
  sleep?: (ms: number) => Promise<void>;
32
32
  }): Promise<T>;
33
+ /**
34
+ * Race `connect()` against a `timeoutMs` rejection and clear the timer when
35
+ * either side wins. If the timer is left running after a fast connect, the
36
+ * pending `setTimeout` keeps the event loop alive and the process hangs for
37
+ * the rest of `timeoutMs`.
38
+ */
39
+ export declare function connectWithTimeout<T>(connect: () => Promise<T>, timeoutMs: number, label: "source" | "target"): Promise<T>;
33
40
  /**
34
41
  * Options for creating a Pool with event listeners.
35
42
  */
@@ -180,6 +180,24 @@ export async function connectWithRetry(opts) {
180
180
  // Unreachable: loop either returns or throws.
181
181
  throw lastError;
182
182
  }
183
+ /**
184
+ * Race `connect()` against a `timeoutMs` rejection and clear the timer when
185
+ * either side wins. If the timer is left running after a fast connect, the
186
+ * pending `setTimeout` keeps the event loop alive and the process hangs for
187
+ * the rest of `timeoutMs`.
188
+ */
189
+ export function connectWithTimeout(connect, timeoutMs, label) {
190
+ let timer;
191
+ return Promise.race([
192
+ connect(),
193
+ new Promise((_, reject) => {
194
+ timer = setTimeout(() => reject(new Error(`Connection to ${label} database timed out after ${timeoutMs}ms. ` +
195
+ `The server may require SSL, use an invalid certificate, or be unreachable.`)), timeoutMs);
196
+ }),
197
+ ]).finally(() => {
198
+ clearTimeout(timer);
199
+ });
200
+ }
183
201
  /**
184
202
  * Create a Pool with custom type handlers and optional event listeners.
185
203
  *
@@ -347,11 +365,7 @@ export async function createManagedPool(url, options) {
347
365
  const timeoutMs = DEFAULT_CONNECT_TIMEOUT_MS;
348
366
  try {
349
367
  const client = await connectWithRetry({
350
- connect: () => Promise.race([
351
- pool.connect(),
352
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Connection to ${label} database timed out after ${timeoutMs}ms. ` +
353
- `The server may require SSL, use an invalid certificate, or be unreachable.`)), timeoutMs)),
354
- ]),
368
+ connect: () => connectWithTimeout(() => pool.connect(), timeoutMs, label),
355
369
  });
356
370
  client.release();
357
371
  }
@@ -139,7 +139,7 @@ export function printDebugGraph(phaseChanges, graphData, edges, dependencyRows,
139
139
  const mermaidDiagram = generateMermaidDiagram(phaseChanges, graphData, edges, requirementSets, dependenciesByReferencedId);
140
140
  debugGraph("\n==== Mermaid (cycle detected) ====\n%s\n==== end ====", mermaidDiagram);
141
141
  }
142
- catch (_error) {
142
+ catch {
143
143
  // ignore debug printing errors
144
144
  }
145
145
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export function performStableTopologicalSort(nodeCount, edges) {
7
7
  const adjacencyList = Array.from({ length: nodeCount }, () => new Set());
8
- const inDegreeCounts = new Array(nodeCount).fill(0);
8
+ const inDegreeCounts = Array.from({ length: nodeCount }, () => 0);
9
9
  for (const [sourceIndex, targetIndex] of edges) {
10
10
  if (!adjacencyList[sourceIndex].has(targetIndex)) {
11
11
  adjacencyList[sourceIndex].add(targetIndex);
@@ -51,7 +51,7 @@ export function findCycle(nodeCount, edges) {
51
51
  adjacencyList[sourceIndex].push(targetIndex);
52
52
  }
53
53
  // 0 = unvisited, 1 = visiting, 2 = completed
54
- const visitState = new Array(nodeCount).fill(0);
54
+ const visitState = Array.from({ length: nodeCount }, () => 0);
55
55
  const pathStack = [];
56
56
  let cycleNodeIndexes = null;
57
57
  const depthFirstSearch = (nodeIndex) => {
package/package.json CHANGED
@@ -1,7 +1,33 @@
1
1
  {
2
2
  "name": "@supabase/pg-delta",
3
- "version": "1.0.0-alpha.25",
3
+ "version": "1.0.0-alpha.27",
4
4
  "description": "PostgreSQL migrations made easy",
5
+ "keywords": [
6
+ "diff",
7
+ "migrations",
8
+ "pg",
9
+ "pg-delta",
10
+ "pgdelta",
11
+ "postgres"
12
+ ],
13
+ "homepage": "https://github.com/supabase/pg-toolbelt",
14
+ "bugs": "https://github.com/supabase/pg-toolbelt/issues",
15
+ "license": "MIT",
16
+ "author": "Supabase",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/supabase/pg-toolbelt.git",
20
+ "directory": "packages/pg-delta"
21
+ },
22
+ "bin": {
23
+ "pgdelta": "./dist/cli/bin/cli.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
5
31
  "type": "module",
6
32
  "sideEffects": false,
7
33
  "main": "./dist/index.js",
@@ -36,40 +62,12 @@
36
62
  "default": "./dist/core/catalog-export/index.js"
37
63
  }
38
64
  },
39
- "bin": {
40
- "pgdelta": "./dist/cli/bin/cli.js"
41
- },
42
- "files": [
43
- "dist",
44
- "src",
45
- "README.md",
46
- "LICENSE"
47
- ],
48
- "keywords": [
49
- "pg",
50
- "postgres",
51
- "migrations",
52
- "diff",
53
- "pg-delta",
54
- "pgdelta"
55
- ],
56
- "author": "Supabase",
57
- "license": "MIT",
58
- "homepage": "https://github.com/supabase/pg-toolbelt",
59
- "repository": {
60
- "type": "git",
61
- "url": "https://github.com/supabase/pg-toolbelt.git",
62
- "directory": "packages/pg-delta"
63
- },
64
- "bugs": "https://github.com/supabase/pg-toolbelt/issues",
65
- "engines": {
66
- "node": ">=20.0.0"
67
- },
68
65
  "scripts": {
69
66
  "build": "tsc --project tsconfig.build.json",
70
67
  "check-types": "tsc --noEmit",
71
68
  "docs": "typedoc",
72
- "format-and-lint": "biome check . --error-on-warnings",
69
+ "format-and-lint": "oxfmt --check . && oxlint --deny-warnings",
70
+ "format-and-lint:fix": "oxfmt . && oxlint --fix",
73
71
  "knip": "knip",
74
72
  "pgdelta": "bun src/cli/bin/cli.ts",
75
73
  "sync-base-images": "bun scripts/sync-supabase-base-images.ts",
@@ -77,12 +75,12 @@
77
75
  "test:unit": "bun run test src/",
78
76
  "test:integration": "bun run test tests/",
79
77
  "update-empty-baseline": "bun scripts/update-empty-catalog-baseline.ts",
80
- "version": "changeset version && bun install --no-frozen-lockfile && bun run format-and-lint --write"
78
+ "version": "changeset version && bun install --no-frozen-lockfile && bun run format-and-lint:fix"
81
79
  },
82
80
  "dependencies": {
83
81
  "@stricli/core": "^1.2.4",
84
- "@ts-safeql/sql-tag": "^0.2.0",
85
82
  "@supabase/pg-topo": "^1.0.0-alpha.1",
83
+ "@ts-safeql/sql-tag": "^0.2.0",
86
84
  "chalk": "^5.6.2",
87
85
  "debug": "^4.3.7",
88
86
  "pg": "^8.17.2",
@@ -103,5 +101,8 @@
103
101
  "testcontainers": "^11.10.0",
104
102
  "typedoc": "^0.28.17",
105
103
  "typescript": "^5.9.3"
104
+ },
105
+ "engines": {
106
+ "node": ">=20.0.0"
106
107
  }
107
108
  }
@@ -4,11 +4,13 @@
4
4
 
5
5
  import { writeFile } from "node:fs/promises";
6
6
  import { buildCommand, type CommandContext } from "@stricli/core";
7
+ import { filterCatalog } from "../../core/catalog.filter.ts";
7
8
  import { extractCatalog } from "../../core/catalog.model.ts";
8
9
  import {
9
10
  serializeCatalog,
10
11
  stringifyCatalogSnapshot,
11
12
  } from "../../core/catalog.snapshot.ts";
13
+ import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
12
14
  import { createManagedPool } from "../../core/postgres-config.ts";
13
15
 
14
16
  export const catalogExportCommand = buildCommand({
@@ -30,6 +32,21 @@ export const catalogExportCommand = buildCommand({
30
32
  parse: String,
31
33
  optional: true,
32
34
  },
35
+ filter: {
36
+ kind: "parsed",
37
+ brief:
38
+ 'Filter DSL as inline JSON to filter changes (e.g., \'{"*/schema": "app"}\').',
39
+ parse: (value: string): FilterDSL => {
40
+ try {
41
+ return JSON.parse(value) as FilterDSL;
42
+ } catch (error) {
43
+ throw new Error(
44
+ `Invalid filter JSON: ${error instanceof Error ? error.message : String(error)}`,
45
+ );
46
+ }
47
+ },
48
+ optional: true,
49
+ },
33
50
  },
34
51
  aliases: {
35
52
  t: "target",
@@ -48,6 +65,10 @@ Use cases:
48
65
  - Snapshot template1 for use as an empty-database baseline
49
66
  - Snapshot a production database to generate revert migrations
50
67
  - Snapshot any state for reproducible offline diffs
68
+
69
+ Pass --filter to scope the snapshot to a subset of the catalog (same
70
+ Filter DSL accepted by plan/sync). Useful when committing a baseline
71
+ snapshot to a repo and only one schema's drift is interesting.
51
72
  `.trim(),
52
73
  },
53
74
  async func(
@@ -56,6 +77,7 @@ Use cases:
56
77
  target: string;
57
78
  output: string;
58
79
  role?: string;
80
+ filter?: FilterDSL;
59
81
  },
60
82
  ) {
61
83
  const { pool, close } = await createManagedPool(flags.target, {
@@ -65,7 +87,10 @@ Use cases:
65
87
 
66
88
  try {
67
89
  const catalog = await extractCatalog(pool);
68
- const snapshot = serializeCatalog(catalog);
90
+ const scoped = flags.filter
91
+ ? await filterCatalog(catalog, flags.filter)
92
+ : catalog;
93
+ const snapshot = serializeCatalog(scoped);
69
94
  const json = stringifyCatalogSnapshot(snapshot);
70
95
  await writeFile(flags.output, json, "utf-8");
71
96
  this.process.stdout.write(
@@ -0,0 +1,96 @@
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
+
16
+ import { diffCatalogs } from "./catalog.diff.ts";
17
+ import { Catalog, createEmptyCatalog } from "./catalog.model.ts";
18
+ import { compileFilterDSL, type FilterDSL } from "./integrations/filter/dsl.ts";
19
+
20
+ export async function filterCatalog(
21
+ catalog: Catalog,
22
+ filter: FilterDSL,
23
+ ): Promise<Catalog> {
24
+ if (
25
+ typeof filter === "object" &&
26
+ filter !== null &&
27
+ (filter as Record<string, unknown>).cascade === true
28
+ ) {
29
+ throw new Error(
30
+ "Filter DSL `cascade: true` is not supported by catalog-export: " +
31
+ "scoped snapshots are intentionally partial. Out-of-scope owners, " +
32
+ "roles, and types must exist on the target DB at apply time.",
33
+ );
34
+ }
35
+
36
+ const empty = await createEmptyCatalog(catalog.version, catalog.currentUser);
37
+ const changes = diffCatalogs(empty, catalog);
38
+ const filterFn = compileFilterDSL(filter);
39
+
40
+ const keep = new Set<string>();
41
+ for (const change of changes) {
42
+ if (!filterFn(change)) continue;
43
+ for (const id of change.creates ?? []) keep.add(id);
44
+ }
45
+
46
+ return pruneCatalog(catalog, keep);
47
+ }
48
+
49
+ function filterRecord<T>(
50
+ record: Record<string, T>,
51
+ keep: ReadonlySet<string>,
52
+ ): Record<string, T> {
53
+ return Object.fromEntries(
54
+ Object.entries(record).filter(([id]) => keep.has(id)),
55
+ );
56
+ }
57
+
58
+ function pruneCatalog(catalog: Catalog, keep: ReadonlySet<string>): Catalog {
59
+ const tables = filterRecord(catalog.tables, keep);
60
+ const materializedViews = filterRecord(catalog.materializedViews, keep);
61
+
62
+ return new Catalog({
63
+ aggregates: filterRecord(catalog.aggregates, keep),
64
+ collations: filterRecord(catalog.collations, keep),
65
+ compositeTypes: filterRecord(catalog.compositeTypes, keep),
66
+ domains: filterRecord(catalog.domains, keep),
67
+ enums: filterRecord(catalog.enums, keep),
68
+ extensions: filterRecord(catalog.extensions, keep),
69
+ procedures: filterRecord(catalog.procedures, keep),
70
+ indexes: filterRecord(catalog.indexes, keep),
71
+ materializedViews,
72
+ subscriptions: filterRecord(catalog.subscriptions, keep),
73
+ publications: filterRecord(catalog.publications, keep),
74
+ rlsPolicies: filterRecord(catalog.rlsPolicies, keep),
75
+ roles: filterRecord(catalog.roles, keep),
76
+ schemas: filterRecord(catalog.schemas, keep),
77
+ sequences: filterRecord(catalog.sequences, keep),
78
+ tables,
79
+ triggers: filterRecord(catalog.triggers, keep),
80
+ eventTriggers: filterRecord(catalog.eventTriggers, keep),
81
+ rules: filterRecord(catalog.rules, keep),
82
+ ranges: filterRecord(catalog.ranges, keep),
83
+ views: filterRecord(catalog.views, keep),
84
+ foreignDataWrappers: filterRecord(catalog.foreignDataWrappers, keep),
85
+ servers: filterRecord(catalog.servers, keep),
86
+ userMappings: filterRecord(catalog.userMappings, keep),
87
+ foreignTables: filterRecord(catalog.foreignTables, keep),
88
+ depends: catalog.depends.filter(
89
+ (d) =>
90
+ keep.has(d.dependent_stable_id) && keep.has(d.referenced_stable_id),
91
+ ),
92
+ indexableObjects: { ...tables, ...materializedViews },
93
+ version: catalog.version,
94
+ currentUser: catalog.currentUser,
95
+ });
96
+ }
@@ -183,7 +183,8 @@ let _pg17Baseline: Catalog | null = null;
183
183
 
184
184
  async function loadBaselineJson(): Promise<Record<string, unknown>> {
185
185
  const mod = await import(
186
- "./fixtures/empty-catalogs/postgres-15-16-baseline.json"
186
+ "./fixtures/empty-catalogs/postgres-15-16-baseline.json",
187
+ { with: { type: "json" } }
187
188
  );
188
189
  return mod.default as Record<string, unknown>;
189
190
  }
@@ -256,10 +257,12 @@ export async function createEmptyCatalog(
256
257
  ): Promise<Catalog> {
257
258
  if (version >= 170000) {
258
259
  const baseline = await getPg17Baseline();
260
+ // oxlint-disable-next-line typescript/no-misused-spread
259
261
  return new Catalog({ ...baseline, version, currentUser });
260
262
  }
261
263
  if (version >= 150000) {
262
264
  const baseline = await getPg1516Baseline();
265
+ // oxlint-disable-next-line typescript/no-misused-spread
263
266
  return new Catalog({ ...baseline, version, currentUser });
264
267
  }
265
268
 
@@ -446,6 +449,8 @@ function normalizeCatalog(catalog: Catalog): Catalog {
446
449
  options: redactSensitiveOptionPairs(server.options),
447
450
  comment: server.comment,
448
451
  privileges: server.privileges,
452
+ wrapper_handler: server.wrapper_handler,
453
+ wrapper_validator: server.wrapper_validator,
449
454
  });
450
455
  });
451
456
 
@@ -454,6 +459,8 @@ function normalizeCatalog(catalog: Catalog): Catalog {
454
459
  user: mapping.user,
455
460
  server: mapping.server,
456
461
  options: redactSensitiveOptionPairs(mapping.options),
462
+ wrapper_handler: mapping.wrapper_handler,
463
+ wrapper_validator: mapping.wrapper_validator,
457
464
  });
458
465
  });
459
466
 
@@ -469,6 +476,8 @@ function normalizeCatalog(catalog: Catalog): Catalog {
469
476
  options: redactSensitiveOptionPairs(foreignTable.options),
470
477
  comment: foreignTable.comment,
471
478
  privileges: foreignTable.privileges,
479
+ wrapper_handler: foreignTable.wrapper_handler,
480
+ wrapper_validator: foreignTable.wrapper_validator,
472
481
  }),
473
482
  );
474
483
 
@@ -294,6 +294,7 @@ describe("catalog snapshot serde", () => {
294
294
 
295
295
  const sourceCatalog = await createEmptyCatalog(160000, "postgres");
296
296
  const targetCatalog = await createEmptyCatalog(160000, "postgres");
297
+ // oxlint-disable-next-line typescript/no-misused-spread
297
298
  const source = { ...sourceCatalog };
298
299
 
299
300
  expect(source instanceof Catalog).toBe(false);
@@ -189,11 +189,13 @@ describe("expandReplaceDependencies", () => {
189
189
  privileges: [],
190
190
  });
191
191
  const branchView = new View({
192
+ // oxlint-disable-next-line typescript/no-misused-spread
192
193
  ...mainView,
193
194
  definition: " SELECT count(*) AS n FROM public.members;",
194
195
  });
195
196
 
196
197
  const mainCatalog = new Catalog({
198
+ // oxlint-disable-next-line typescript/no-misused-spread
197
199
  ...baseline,
198
200
  tables: { [usersTable.stableId]: usersTable },
199
201
  views: { [mainView.stableId]: mainView },
@@ -206,6 +208,7 @@ describe("expandReplaceDependencies", () => {
206
208
  ],
207
209
  });
208
210
  const branchCatalog = new Catalog({
211
+ // oxlint-disable-next-line typescript/no-misused-spread
209
212
  ...baseline,
210
213
  views: { [branchView.stableId]: branchView },
211
214
  });
@@ -253,6 +256,7 @@ describe("expandReplaceDependencies", () => {
253
256
  owner: "postgres",
254
257
  });
255
258
  const branchSequence = new Sequence({
259
+ // oxlint-disable-next-line typescript/no-misused-spread
256
260
  ...mainSequence,
257
261
  persistence: "p",
258
262
  });
@@ -309,6 +313,7 @@ describe("expandReplaceDependencies", () => {
309
313
  { [usersTable.stableId]: usersTable },
310
314
  );
311
315
  const mainCatalog = new Catalog({
316
+ // oxlint-disable-next-line typescript/no-misused-spread
312
317
  ...baseline,
313
318
  sequences: { [mainSequence.stableId]: mainSequence },
314
319
  tables: { [usersTable.stableId]: usersTable },
@@ -326,6 +331,7 @@ describe("expandReplaceDependencies", () => {
326
331
  ],
327
332
  });
328
333
  const branchCatalog = new Catalog({
334
+ // oxlint-disable-next-line typescript/no-misused-spread
329
335
  ...baseline,
330
336
  sequences: { [branchSequence.stableId]: branchSequence },
331
337
  tables: { [usersTable.stableId]: usersTable },
@@ -369,6 +375,7 @@ describe("expandReplaceDependencies", () => {
369
375
  privileges: [],
370
376
  });
371
377
  const branchEnum = new Enum({
378
+ // oxlint-disable-next-line typescript/no-misused-spread
372
379
  ...mainEnum,
373
380
  labels: [
374
381
  { sort_order: 1, label: "draft" },
@@ -430,6 +437,7 @@ describe("expandReplaceDependencies", () => {
430
437
  privileges: [],
431
438
  });
432
439
  const branchChildren = new Table({
440
+ // oxlint-disable-next-line typescript/no-misused-spread
433
441
  ...mainChildren,
434
442
  columns: [
435
443
  { ...columnTemplate, name: "id", position: 1, not_null: true },
@@ -522,6 +530,7 @@ describe("expandReplaceDependencies", () => {
522
530
  ];
523
531
 
524
532
  const mainCatalog = new Catalog({
533
+ // oxlint-disable-next-line typescript/no-misused-spread
525
534
  ...baseline,
526
535
  enums: { [mainEnum.stableId]: mainEnum },
527
536
  tables: { [mainChildren.stableId]: mainChildren },
@@ -535,6 +544,7 @@ describe("expandReplaceDependencies", () => {
535
544
  ],
536
545
  });
537
546
  const branchCatalog = new Catalog({
547
+ // oxlint-disable-next-line typescript/no-misused-spread
538
548
  ...baseline,
539
549
  enums: { [branchEnum.stableId]: branchEnum },
540
550
  tables: { [branchChildren.stableId]: branchChildren },
@@ -657,6 +667,7 @@ describe("expandReplaceDependencies", () => {
657
667
  ];
658
668
 
659
669
  const mainCatalog = new Catalog({
670
+ // oxlint-disable-next-line typescript/no-misused-spread
660
671
  ...baseline,
661
672
  procedures: { [mainProcedure.stableId]: mainProcedure },
662
673
  views: { [mainView.stableId]: mainView },
@@ -669,6 +680,7 @@ describe("expandReplaceDependencies", () => {
669
680
  ],
670
681
  });
671
682
  const branchCatalog = new Catalog({
683
+ // oxlint-disable-next-line typescript/no-misused-spread
672
684
  ...baseline,
673
685
  procedures: { [branchProcedure.stableId]: branchProcedure },
674
686
  views: { [branchView.stableId]: branchView },
@@ -8,10 +8,7 @@ import { CreateMaterializedView } from "./objects/materialized-view/changes/mate
8
8
  import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.ts";
9
9
  import { CreateProcedure } from "./objects/procedure/changes/procedure.create.ts";
10
10
  import { DropProcedure } from "./objects/procedure/changes/procedure.drop.ts";
11
- import {
12
- AlterTableAddConstraint,
13
- AlterTableValidateConstraint,
14
- } from "./objects/table/changes/table.alter.ts";
11
+ import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.ts";
15
12
  import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
16
13
  import { CreateTable } from "./objects/table/changes/table.create.ts";
17
14
  import { DropTable } from "./objects/table/changes/table.drop.ts";
@@ -455,14 +452,6 @@ function buildReplaceChanges(
455
452
  constraint,
456
453
  }),
457
454
  ];
458
- if (!constraint.validated) {
459
- items.push(
460
- new AlterTableValidateConstraint({
461
- table: resolved.branch,
462
- constraint,
463
- }),
464
- );
465
- }
466
455
  if (
467
456
  constraint.comment !== null &&
468
457
  constraint.comment !== undefined