@supabase/pg-delta 1.0.0-alpha.24 → 1.0.0-alpha.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/core/catalog.model.d.ts +2 -2
  2. package/dist/core/catalog.model.js +28 -21
  3. package/dist/core/expand-replace-dependencies.js +1 -7
  4. package/dist/core/integrations/supabase.js +84 -0
  5. package/dist/core/objects/aggregate/changes/aggregate.privilege.js +21 -9
  6. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.js +4 -1
  7. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.js +6 -3
  8. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.d.ts +11 -0
  9. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +11 -0
  10. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.js +4 -1
  11. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.js +6 -3
  12. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +11 -0
  13. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +11 -0
  14. package/dist/core/objects/foreign-data-wrapper/sensitive-options.d.ts +32 -0
  15. package/dist/core/objects/foreign-data-wrapper/sensitive-options.js +129 -0
  16. package/dist/core/objects/foreign-data-wrapper/server/changes/server.alter.js +4 -1
  17. package/dist/core/objects/foreign-data-wrapper/server/changes/server.create.js +6 -3
  18. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +10 -0
  19. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +10 -0
  20. package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.js +4 -1
  21. package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.js +6 -3
  22. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +10 -0
  23. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +10 -0
  24. package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
  25. package/dist/core/objects/table/table.diff.js +53 -30
  26. package/dist/core/objects/table/table.model.js +7 -2
  27. package/dist/core/plan/hierarchy.js +4 -4
  28. package/dist/core/postgres-config.d.ts +7 -0
  29. package/dist/core/postgres-config.js +19 -5
  30. package/dist/core/sort/debug-visualization.js +1 -1
  31. package/dist/core/sort/topological-sort.js +2 -2
  32. package/package.json +34 -33
  33. package/src/core/catalog.model.ts +40 -23
  34. package/src/core/catalog.snapshot.test.ts +1 -0
  35. package/src/core/expand-replace-dependencies.test.ts +12 -0
  36. package/src/core/expand-replace-dependencies.ts +1 -12
  37. package/src/core/integrations/supabase.test.ts +198 -0
  38. package/src/core/integrations/supabase.ts +84 -0
  39. package/src/core/objects/aggregate/changes/aggregate.base.ts +1 -1
  40. package/src/core/objects/aggregate/changes/aggregate.privilege.test.ts +79 -0
  41. package/src/core/objects/aggregate/changes/aggregate.privilege.ts +22 -9
  42. package/src/core/objects/collation/changes/collation.base.ts +1 -1
  43. package/src/core/objects/domain/changes/domain.base.ts +1 -1
  44. package/src/core/objects/extension/changes/extension.base.ts +1 -1
  45. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.test.ts +34 -4
  46. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.ts +5 -1
  47. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.base.ts +1 -1
  48. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.test.ts +34 -0
  49. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.ts +7 -5
  50. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts +11 -0
  51. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.test.ts +25 -4
  52. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.ts +5 -1
  53. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +1 -1
  54. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.test.ts +54 -0
  55. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.ts +7 -5
  56. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +11 -0
  57. package/src/core/objects/foreign-data-wrapper/sensitive-options.test.ts +98 -0
  58. package/src/core/objects/foreign-data-wrapper/sensitive-options.ts +133 -0
  59. package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.test.ts +39 -4
  60. package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.ts +5 -1
  61. package/src/core/objects/foreign-data-wrapper/server/changes/server.base.ts +1 -1
  62. package/src/core/objects/foreign-data-wrapper/server/changes/server.create.test.ts +36 -0
  63. package/src/core/objects/foreign-data-wrapper/server/changes/server.create.ts +7 -5
  64. package/src/core/objects/foreign-data-wrapper/server/server.model.ts +10 -0
  65. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.test.ts +39 -6
  66. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.ts +5 -1
  67. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.base.ts +1 -1
  68. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.test.ts +38 -2
  69. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.ts +7 -5
  70. package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +10 -0
  71. package/src/core/objects/index/changes/index.base.ts +1 -1
  72. package/src/core/objects/language/changes/language.base.ts +1 -1
  73. package/src/core/objects/materialized-view/changes/materialized-view.base.ts +1 -1
  74. package/src/core/objects/procedure/changes/procedure.base.ts +1 -1
  75. package/src/core/objects/rls-policy/changes/rls-policy.base.ts +1 -1
  76. package/src/core/objects/role/changes/role.base.ts +1 -1
  77. package/src/core/objects/schema/changes/schema.base.ts +1 -1
  78. package/src/core/objects/sequence/changes/sequence.base.ts +1 -1
  79. package/src/core/objects/table/changes/table.base.ts +1 -1
  80. package/src/core/objects/table/changes/table.comment.ts +2 -8
  81. package/src/core/objects/table/table.diff.test.ts +198 -5
  82. package/src/core/objects/table/table.diff.ts +63 -34
  83. package/src/core/objects/table/table.model.ts +7 -2
  84. package/src/core/objects/trigger/changes/trigger.alter.ts +1 -4
  85. package/src/core/objects/trigger/changes/trigger.base.ts +1 -1
  86. package/src/core/objects/type/composite-type/changes/composite-type.base.ts +1 -1
  87. package/src/core/objects/type/enum/changes/enum.base.ts +1 -1
  88. package/src/core/objects/type/range/changes/range.base.ts +1 -1
  89. package/src/core/objects/view/changes/view.base.ts +1 -1
  90. package/src/core/plan/hierarchy.ts +4 -4
  91. package/src/core/plan/sql-format/format-off.test.ts +4 -4
  92. package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +4 -4
  93. package/src/core/plan/sql-format/format-pretty-narrow.test.ts +5 -4
  94. package/src/core/plan/sql-format/format-pretty-preserve.test.ts +4 -4
  95. package/src/core/plan/sql-format/format-pretty-upper.test.ts +4 -4
  96. package/src/core/postgres-config.test.ts +39 -1
  97. package/src/core/postgres-config.ts +32 -16
  98. package/src/core/sort/debug-visualization.ts +1 -1
  99. package/src/core/sort/sort-changes.test.ts +1 -0
  100. package/src/core/sort/topological-sort.ts +2 -2
@@ -58,5 +58,15 @@ export declare class Server extends BasePgModel {
58
58
  }[];
59
59
  };
60
60
  }
61
+ /**
62
+ * Extract `pg_foreign_server` rows into `Server` models.
63
+ *
64
+ * The returned models carry option values **verbatim** from
65
+ * `pg_foreign_server.srvoptions`, which means cleartext secrets like
66
+ * `password` are present in memory. Always route through
67
+ * `extractCatalog` (which calls `normalizeCatalog`) before emitting
68
+ * options to any output channel — see CLI-1467 and
69
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
70
+ */
61
71
  export declare function extractServers(pool: Pool): Promise<Server[]>;
62
72
  export {};
@@ -64,6 +64,16 @@ export class Server extends BasePgModel {
64
64
  };
65
65
  }
66
66
  }
67
+ /**
68
+ * Extract `pg_foreign_server` rows into `Server` models.
69
+ *
70
+ * The returned models carry option values **verbatim** from
71
+ * `pg_foreign_server.srvoptions`, which means cleartext secrets like
72
+ * `password` are present in memory. Always route through
73
+ * `extractCatalog` (which calls `normalizeCatalog`) before emitting
74
+ * options to any output channel — see CLI-1467 and
75
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
76
+ */
67
77
  export async function extractServers(pool) {
68
78
  const { rows: serverRows } = await pool.query(sql `
69
79
  select
@@ -1,4 +1,5 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
+ import { redactOptionValue } from "../../sensitive-options.js";
2
3
  import { AlterUserMappingChange } from "./user-mapping.base.js";
3
4
  /**
4
5
  * ALTER USER MAPPING ... OPTIONS ( ADD | SET | DROP ... )
@@ -22,7 +23,9 @@ export class AlterUserMappingSetOptions extends AlterUserMappingChange {
22
23
  optionParts.push(`DROP ${opt.option}`);
23
24
  }
24
25
  else {
25
- const value = opt.value !== undefined ? quoteLiteral(opt.value) : "''";
26
+ const value = opt.value !== undefined
27
+ ? quoteLiteral(redactOptionValue(opt.option, opt.value))
28
+ : "''";
26
29
  optionParts.push(`${opt.action} ${opt.option} ${value}`);
27
30
  }
28
31
  }
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { CreateUserMappingChange } from "./user-mapping.base.js";
4
5
  /**
5
6
  * Create a user mapping.
@@ -39,9 +40,11 @@ export class CreateUserMapping extends CreateUserMappingChange {
39
40
  if (this.userMapping.options && this.userMapping.options.length > 0) {
40
41
  const optionPairs = [];
41
42
  for (let i = 0; i < this.userMapping.options.length; i += 2) {
42
- if (i + 1 < this.userMapping.options.length) {
43
- optionPairs.push(`${this.userMapping.options[i]} ${quoteLiteral(this.userMapping.options[i + 1])}`);
44
- }
43
+ const key = this.userMapping.options[i];
44
+ const value = this.userMapping.options[i + 1];
45
+ if (key === undefined || value === undefined)
46
+ continue;
47
+ optionPairs.push(`${key} ${quoteLiteral(redactOptionValue(key, value))}`);
45
48
  }
46
49
  if (optionPairs.length > 0) {
47
50
  parts.push(`OPTIONS (${optionPairs.join(", ")})`);
@@ -32,5 +32,15 @@ export declare class UserMapping extends BasePgModel {
32
32
  options: string[] | null;
33
33
  };
34
34
  }
35
+ /**
36
+ * Extract `pg_user_mapping` rows into `UserMapping` models.
37
+ *
38
+ * The returned models carry option values **verbatim** from
39
+ * `pg_user_mapping.umoptions`, which means cleartext secrets like
40
+ * `password` are present in memory. Always route through
41
+ * `extractCatalog` (which calls `normalizeCatalog`) before emitting
42
+ * options to any output channel — see CLI-1467 and
43
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
44
+ */
35
45
  export declare function extractUserMappings(pool: Pool): Promise<UserMapping[]>;
36
46
  export {};
@@ -44,6 +44,16 @@ export class UserMapping extends BasePgModel {
44
44
  };
45
45
  }
46
46
  }
47
+ /**
48
+ * Extract `pg_user_mapping` rows into `UserMapping` models.
49
+ *
50
+ * The returned models carry option values **verbatim** from
51
+ * `pg_user_mapping.umoptions`, which means cleartext secrets like
52
+ * `password` are present in memory. Always route through
53
+ * `extractCatalog` (which calls `normalizeCatalog`) before emitting
54
+ * options to any output channel — see CLI-1467 and
55
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
56
+ */
47
57
  export async function extractUserMappings(pool) {
48
58
  const { rows: mappingRows } = await pool.query(sql `
49
59
  select
@@ -3,9 +3,9 @@ import z from "zod";
3
3
  import { BasePgModel } from "../base.model.ts";
4
4
  declare const rlsPolicyReferencedRelationSchema: z.ZodObject<{
5
5
  kind: z.ZodEnum<{
6
+ table: "table";
6
7
  foreign_table: "foreign_table";
7
8
  materialized_view: "materialized_view";
8
- table: "table";
9
9
  view: "view";
10
10
  }>;
11
11
  schema: z.ZodString;
@@ -37,9 +37,9 @@ declare const rlsPolicyPropsSchema: z.ZodObject<{
37
37
  comment: z.ZodNullable<z.ZodString>;
38
38
  referenced_relations: z.ZodArray<z.ZodObject<{
39
39
  kind: z.ZodEnum<{
40
+ table: "table";
40
41
  foreign_table: "foreign_table";
41
42
  materialized_view: "materialized_view";
42
- table: "table";
43
43
  view: "view";
44
44
  }>;
45
45
  schema: z.ZodString;
@@ -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));
@@ -292,8 +292,13 @@ select
292
292
  'no_inherit', c.connoinherit,
293
293
  'is_temporal', coalesce((to_jsonb(c)->>'conperiod')::boolean, false),
294
294
 
295
- -- NEW: propagated-to-partition tagging (PG15+)
296
- 'is_partition_clone', (c.conparentid <> 0::oid),
295
+ -- Inherited from a parent (partition or classical inheritance).
296
+ -- coninhcount > 0 is the canonical signal across every constraint
297
+ -- kind. We previously used conparentid <> 0, but PostgreSQL only
298
+ -- populates conparentid for PK / UNIQUE / FK on partitions; CHECK
299
+ -- constraints on partitions always have conparentid = 0 and were
300
+ -- being re-emitted on every child, failing apply with 42710.
301
+ 'is_partition_clone', (c.coninhcount > 0),
297
302
  'parent_constraint_schema', case when c.conparentid <> 0::oid then pc.connamespace::regnamespace::text end,
298
303
  'parent_constraint_name', case when c.conparentid <> 0::oid then quote_ident(pc.conname) end,
299
304
  'parent_table_schema', case when c.conparentid <> 0::oid then pc_rel.relnamespace::regnamespace::text end,
@@ -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.24",
3
+ "version": "1.0.0-alpha.26",
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
  }
@@ -22,12 +22,13 @@ import {
22
22
  } from "./objects/extension/extension.model.ts";
23
23
  import {
24
24
  extractForeignDataWrappers,
25
- type ForeignDataWrapper,
25
+ ForeignDataWrapper,
26
26
  } from "./objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts";
27
27
  import {
28
28
  extractForeignTables,
29
- type ForeignTable,
29
+ ForeignTable,
30
30
  } from "./objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts";
31
+ import { redactSensitiveOptionPairs } from "./objects/foreign-data-wrapper/sensitive-options.ts";
31
32
  import {
32
33
  extractServers,
33
34
  Server,
@@ -181,9 +182,8 @@ let _pg1516Baseline: Catalog | null = null;
181
182
  let _pg17Baseline: Catalog | null = null;
182
183
 
183
184
  async function loadBaselineJson(): Promise<Record<string, unknown>> {
184
- const mod = await import(
185
- "./fixtures/empty-catalogs/postgres-15-16-baseline.json"
186
- );
185
+ const mod =
186
+ await import("./fixtures/empty-catalogs/postgres-15-16-baseline.json");
187
187
  return mod.default as Record<string, unknown>;
188
188
  }
189
189
 
@@ -255,10 +255,12 @@ export async function createEmptyCatalog(
255
255
  ): Promise<Catalog> {
256
256
  if (version >= 170000) {
257
257
  const baseline = await getPg17Baseline();
258
+ // oxlint-disable-next-line typescript/no-misused-spread
258
259
  return new Catalog({ ...baseline, version, currentUser });
259
260
  }
260
261
  if (version >= 150000) {
261
262
  const baseline = await getPg1516Baseline();
263
+ // oxlint-disable-next-line typescript/no-misused-spread
262
264
  return new Catalog({ ...baseline, version, currentUser });
263
265
  }
264
266
 
@@ -421,29 +423,56 @@ function listToRecord<T extends BasePgModel>(list: T[]) {
421
423
  }
422
424
 
423
425
  function normalizeCatalog(catalog: Catalog): Catalog {
426
+ const foreignDataWrappers = mapRecord(
427
+ catalog.foreignDataWrappers,
428
+ (fdw) =>
429
+ new ForeignDataWrapper({
430
+ name: fdw.name,
431
+ owner: fdw.owner,
432
+ handler: fdw.handler,
433
+ validator: fdw.validator,
434
+ options: redactSensitiveOptionPairs(fdw.options),
435
+ comment: fdw.comment,
436
+ privileges: fdw.privileges,
437
+ }),
438
+ );
439
+
424
440
  const servers = mapRecord(catalog.servers, (server) => {
425
- const maskedOptions = maskOptions(server.options);
426
441
  return new Server({
427
442
  name: server.name,
428
443
  owner: server.owner,
429
444
  foreign_data_wrapper: server.foreign_data_wrapper,
430
445
  type: server.type,
431
446
  version: server.version,
432
- options: maskedOptions,
447
+ options: redactSensitiveOptionPairs(server.options),
433
448
  comment: server.comment,
434
449
  privileges: server.privileges,
435
450
  });
436
451
  });
437
452
 
438
453
  const userMappings = mapRecord(catalog.userMappings, (mapping) => {
439
- const maskedOptions = maskOptions(mapping.options);
440
454
  return new UserMapping({
441
455
  user: mapping.user,
442
456
  server: mapping.server,
443
- options: maskedOptions,
457
+ options: redactSensitiveOptionPairs(mapping.options),
444
458
  });
445
459
  });
446
460
 
461
+ const foreignTables = mapRecord(
462
+ catalog.foreignTables,
463
+ (foreignTable) =>
464
+ new ForeignTable({
465
+ schema: foreignTable.schema,
466
+ name: foreignTable.name,
467
+ owner: foreignTable.owner,
468
+ server: foreignTable.server,
469
+ columns: foreignTable.columns,
470
+ options: redactSensitiveOptionPairs(foreignTable.options),
471
+ comment: foreignTable.comment,
472
+ privileges: foreignTable.privileges,
473
+ }),
474
+ );
475
+
447
476
  const subscriptions = mapRecord(catalog.subscriptions, (subscription) => {
448
477
  return new Subscription({
449
478
  name: subscription.name,
@@ -490,10 +519,10 @@ function normalizeCatalog(catalog: Catalog): Catalog {
490
519
  rules: catalog.rules,
491
520
  ranges: catalog.ranges,
492
521
  views: catalog.views,
493
- foreignDataWrappers: catalog.foreignDataWrappers,
522
+ foreignDataWrappers,
494
523
  servers,
495
524
  userMappings,
496
- foreignTables: catalog.foreignTables,
525
+ foreignTables,
497
526
  depends: catalog.depends,
498
527
  indexableObjects: catalog.indexableObjects,
499
528
  version: catalog.version,
@@ -501,18 +530,6 @@ function normalizeCatalog(catalog: Catalog): Catalog {
501
530
  });
502
531
  }
503
532
 
504
- function maskOptions(options: string[] | null): string[] | null {
505
- if (!options || options.length === 0) return options;
506
- const masked: string[] = [];
507
- for (let i = 0; i < options.length; i += 2) {
508
- const key = options[i];
509
- const value = options[i + 1];
510
- if (key === undefined || value === undefined) continue;
511
- masked.push(key, `__OPTION_${key.toUpperCase()}__`);
512
- }
513
- return masked.length > 0 ? masked : null;
514
- }
515
-
516
533
  function mapRecord<TValue, TResult>(
517
534
  record: Record<string, TValue>,
518
535
  mapper: (value: TValue) => TResult,
@@ -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);