@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
@@ -86,14 +86,6 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
86
86
  constraint: c,
87
87
  }),
88
88
  );
89
- if (!c.validated) {
90
- changes.push(
91
- new AlterTableValidateConstraint({
92
- table: branchTable,
93
- constraint: c,
94
- }),
95
- );
96
- }
97
89
  // Add comment for newly created constraint
98
90
  if (c.comment !== null) {
99
91
  changes.push(
@@ -120,7 +112,7 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
120
112
  }
121
113
  }
122
114
 
123
- // Altered constraints -> drop + add
115
+ // Altered constraints -> drop + add (or VALIDATE-only shortcut)
124
116
  for (const [name, mainC] of mainByName) {
125
117
  const branchC = branchByName.get(name);
126
118
  if (!branchC) continue;
@@ -130,24 +122,68 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
130
122
  continue;
131
123
  }
132
124
 
125
+ // Cheap scalar `===` checks first; only fall through to JSON.stringify
126
+ // on the array fields when every scalar has already matched.
127
+ const fieldsEqualExceptValidated =
128
+ mainC.constraint_type === branchC.constraint_type &&
129
+ mainC.deferrable === branchC.deferrable &&
130
+ mainC.initially_deferred === branchC.initially_deferred &&
131
+ mainC.is_local === branchC.is_local &&
132
+ mainC.no_inherit === branchC.no_inherit &&
133
+ mainC.is_temporal === branchC.is_temporal &&
134
+ mainC.foreign_key_table === branchC.foreign_key_table &&
135
+ mainC.foreign_key_schema === branchC.foreign_key_schema &&
136
+ mainC.on_update === branchC.on_update &&
137
+ mainC.on_delete === branchC.on_delete &&
138
+ mainC.match_type === branchC.match_type &&
139
+ mainC.check_expression === branchC.check_expression &&
140
+ JSON.stringify(mainC.key_columns) ===
141
+ JSON.stringify(branchC.key_columns) &&
142
+ JSON.stringify(mainC.foreign_key_columns) ===
143
+ JSON.stringify(branchC.foreign_key_columns);
144
+
145
+ // Safe-migration shortcut: when the only difference is `validated`
146
+ // flipping from false to true, emit a single `ALTER TABLE ... VALIDATE
147
+ // CONSTRAINT` instead of drop+add. VALIDATE CONSTRAINT only takes
148
+ // SHARE UPDATE EXCLUSIVE (concurrent reads/writes proceed), whereas
149
+ // dropping and re-adding takes ACCESS EXCLUSIVE for the entire scan.
150
+ // Postgres has no reverse command, so `true -> false` must still go
151
+ // through drop+add below.
152
+ if (
153
+ fieldsEqualExceptValidated &&
154
+ mainC.validated === false &&
155
+ branchC.validated === true
156
+ ) {
157
+ changes.push(
158
+ new AlterTableValidateConstraint({
159
+ table: branchTable,
160
+ constraint: branchC,
161
+ }),
162
+ );
163
+ // VALIDATE preserves the constraint OID, so its comment is preserved
164
+ // too. Only emit a comment change if it actually differs.
165
+ if (mainC.comment !== branchC.comment) {
166
+ if (branchC.comment === null) {
167
+ changes.push(
168
+ new DropCommentOnConstraint({
169
+ table: mainTable,
170
+ constraint: mainC,
171
+ }),
172
+ );
173
+ } else {
174
+ changes.push(
175
+ new CreateCommentOnConstraint({
176
+ table: branchTable,
177
+ constraint: branchC,
178
+ }),
179
+ );
180
+ }
181
+ }
182
+ continue;
183
+ }
184
+
133
185
  const changed =
134
- mainC.constraint_type !== branchC.constraint_type ||
135
- mainC.deferrable !== branchC.deferrable ||
136
- mainC.initially_deferred !== branchC.initially_deferred ||
137
- mainC.validated !== branchC.validated ||
138
- mainC.is_local !== branchC.is_local ||
139
- mainC.no_inherit !== branchC.no_inherit ||
140
- mainC.is_temporal !== branchC.is_temporal ||
141
- JSON.stringify(mainC.key_columns) !==
142
- JSON.stringify(branchC.key_columns) ||
143
- JSON.stringify(mainC.foreign_key_columns) !==
144
- JSON.stringify(branchC.foreign_key_columns) ||
145
- mainC.foreign_key_table !== branchC.foreign_key_table ||
146
- mainC.foreign_key_schema !== branchC.foreign_key_schema ||
147
- mainC.on_update !== branchC.on_update ||
148
- mainC.on_delete !== branchC.on_delete ||
149
- mainC.match_type !== branchC.match_type ||
150
- mainC.check_expression !== branchC.check_expression;
186
+ mainC.validated !== branchC.validated || !fieldsEqualExceptValidated;
151
187
  if (changed) {
152
188
  changes.push(
153
189
  new AlterTableDropConstraint({
@@ -161,14 +197,6 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
161
197
  constraint: branchC,
162
198
  }),
163
199
  );
164
- if (!branchC.validated) {
165
- changes.push(
166
- new AlterTableValidateConstraint({
167
- table: branchTable,
168
- constraint: branchC,
169
- }),
170
- );
171
- }
172
200
  // Ensure constraint comment is applied after re-creation
173
201
  if (branchC.comment !== null) {
174
202
  changes.push(
@@ -265,6 +293,7 @@ export function diffTables(
265
293
  ...createAlterConstraintChange(
266
294
  // Create a dummy table with no constraints do diff constraints against
267
295
  new Table({
296
+ // oxlint-disable-next-line typescript/no-misused-spread
268
297
  ...branchTable,
269
298
  constraints: [],
270
299
  }),
@@ -341,8 +341,13 @@ select
341
341
  'no_inherit', c.connoinherit,
342
342
  'is_temporal', coalesce((to_jsonb(c)->>'conperiod')::boolean, false),
343
343
 
344
- -- NEW: propagated-to-partition tagging (PG15+)
345
- 'is_partition_clone', (c.conparentid <> 0::oid),
344
+ -- Inherited from a parent (partition or classical inheritance).
345
+ -- coninhcount > 0 is the canonical signal across every constraint
346
+ -- kind. We previously used conparentid <> 0, but PostgreSQL only
347
+ -- populates conparentid for PK / UNIQUE / FK on partitions; CHECK
348
+ -- constraints on partitions always have conparentid = 0 and were
349
+ -- being re-emitted on every child, failing apply with 42710.
350
+ 'is_partition_clone', (c.coninhcount > 0),
346
351
  'parent_constraint_schema', case when c.conparentid <> 0::oid then pc.connamespace::regnamespace::text end,
347
352
  'parent_constraint_name', case when c.conparentid <> 0::oid then quote_ident(pc.conname) end,
348
353
  'parent_table_schema', case when c.conparentid <> 0::oid then pc_rel.relnamespace::regnamespace::text end,
@@ -28,10 +28,7 @@ export class ReplaceTrigger extends AlterTriggerChange {
28
28
  public readonly indexableObject?: TableLikeObject;
29
29
  public readonly scope = "object" as const;
30
30
 
31
- constructor(props: {
32
- trigger: Trigger;
33
- indexableObject?: TableLikeObject;
34
- }) {
31
+ constructor(props: { trigger: Trigger; indexableObject?: TableLikeObject }) {
35
32
  super();
36
33
  this.trigger = props.trigger;
37
34
  this.indexableObject = props.indexableObject;
@@ -4,7 +4,7 @@ import type { Trigger } from "../trigger.model.ts";
4
4
  abstract class BaseTriggerChange extends BaseChange {
5
5
  abstract readonly trigger: Trigger;
6
6
  abstract readonly scope: "object" | "comment";
7
- readonly objectType: "trigger" = "trigger";
7
+ readonly objectType = "trigger" as const;
8
8
  }
9
9
 
10
10
  export abstract class CreateTriggerChange extends BaseTriggerChange {
@@ -8,7 +8,7 @@ abstract class BaseCompositeTypeChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "composite_type" = "composite_type";
11
+ readonly objectType = "composite_type" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateCompositeTypeChange extends BaseCompositeTypeChange {
@@ -8,7 +8,7 @@ abstract class BaseEnumChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "enum" = "enum";
11
+ readonly objectType = "enum" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateEnumChange extends BaseEnumChange {
@@ -8,7 +8,7 @@ abstract class BaseRangeChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "range" = "range";
11
+ readonly objectType = "range" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateRangeChange extends BaseRangeChange {
@@ -8,7 +8,7 @@ abstract class BaseViewChange extends BaseChange {
8
8
  | "comment"
9
9
  | "privilege"
10
10
  | "security_label";
11
- readonly objectType: "view" = "view";
11
+ readonly objectType = "view" as const;
12
12
  }
13
13
 
14
14
  export abstract class CreateViewChange extends BaseViewChange {
@@ -322,7 +322,7 @@ function addClusterChange(cluster: ClusterGroup, change: Change): void {
322
322
  break;
323
323
  default: {
324
324
  const _exhaustive: never = objectType;
325
- throw new Error(`Unhandled object type: ${_exhaustive}`);
325
+ throw new Error(`Unhandled object type: ${JSON.stringify(_exhaustive)}`);
326
326
  }
327
327
  }
328
328
  }
@@ -366,7 +366,7 @@ function addChildChange(schema: SchemaGroup, change: Change): void {
366
366
  break;
367
367
  default: {
368
368
  const _exhaustive: never = parentType;
369
- throw new Error(`Unhandled parent type: ${_exhaustive}`);
369
+ throw new Error(`Unhandled parent type: ${JSON.stringify(_exhaustive)}`);
370
370
  }
371
371
  }
372
372
 
@@ -416,7 +416,7 @@ function addChildChange(schema: SchemaGroup, change: Change): void {
416
416
  break;
417
417
  default: {
418
418
  const _exhaustive: never = objectType;
419
- throw new Error(`Unhandled object type: ${_exhaustive}`);
419
+ throw new Error(`Unhandled object type: ${JSON.stringify(_exhaustive)}`);
420
420
  }
421
421
  }
422
422
  }
@@ -568,7 +568,7 @@ function addSchemaLevelChange(
568
568
  break;
569
569
  default: {
570
570
  const _exhaustive: never = objectType;
571
- throw new Error(`Unhandled object type: ${_exhaustive}`);
571
+ throw new Error(`Unhandled object type: ${JSON.stringify(_exhaustive)}`);
572
572
  }
573
573
  }
574
574
  }
@@ -689,7 +689,7 @@ describe("sql formatting snapshots", () => {
689
689
  COMMENT ON SUBSCRIPTION sub_replica IS NULL;
690
690
 
691
691
  -- fdw.create
692
- CREATE FOREIGN DATA WRAPPER postgres_fdw HANDLER postgres_fdw_handler VALIDATOR postgres_fdw_validator OPTIONS (debug 'true');
692
+ CREATE FOREIGN DATA WRAPPER postgres_fdw HANDLER postgres_fdw_handler VALIDATOR postgres_fdw_validator OPTIONS (debug '__OPTION_DEBUG__');
693
693
 
694
694
  -- fdw.drop
695
695
  DROP FOREIGN DATA WRAPPER postgres_fdw;
@@ -698,7 +698,7 @@ describe("sql formatting snapshots", () => {
698
698
  ALTER FOREIGN DATA WRAPPER postgres_fdw OWNER TO new_owner;
699
699
 
700
700
  -- fdw.alter.set_options
701
- ALTER FOREIGN DATA WRAPPER postgres_fdw OPTIONS (SET debug 'false', ADD use_remote_estimate '');
701
+ ALTER FOREIGN DATA WRAPPER postgres_fdw OPTIONS (SET debug '__OPTION_DEBUG__', ADD use_remote_estimate '');
702
702
 
703
703
  -- fdw.comment
704
704
  COMMENT ON FOREIGN DATA WRAPPER postgres_fdw IS 'PostgreSQL foreign data wrapper';
@@ -794,13 +794,13 @@ describe("sql formatting snapshots", () => {
794
794
  REVOKE GRANT OPTION FOR ALL ON SERVER remote_server FROM app_user;
795
795
 
796
796
  -- user_mapping.create
797
- CREATE USER MAPPING FOR app_user SERVER remote_server OPTIONS (user 'remote_app', password 'secret123');
797
+ CREATE USER MAPPING FOR app_user SERVER remote_server OPTIONS (user 'remote_app', password '__OPTION_PASSWORD__');
798
798
 
799
799
  -- user_mapping.drop
800
800
  DROP USER MAPPING FOR app_user SERVER remote_server;
801
801
 
802
802
  -- user_mapping.alter.set_options
803
- ALTER USER MAPPING FOR app_user SERVER remote_server OPTIONS (SET password 'new_secret');"
803
+ ALTER USER MAPPING FOR app_user SERVER remote_server OPTIONS (SET password '__OPTION_PASSWORD__');"
804
804
  `);
805
805
  });
806
806
  });
@@ -912,7 +912,7 @@ describe("sql formatting snapshots", () => {
912
912
  create foreign data wrapper postgres_fdw
913
913
  handler postgres_fdw_handler
914
914
  validator postgres_fdw_validator
915
- options (debug 'true');
915
+ options (debug '__OPTION_DEBUG__');
916
916
 
917
917
  -- fdw.drop
918
918
  drop foreign data wrapper postgres_fdw;
@@ -924,7 +924,7 @@ describe("sql formatting snapshots", () => {
924
924
  -- fdw.alter.set_options
925
925
  alter foreign data wrapper postgres_fdw
926
926
  options (
927
- SET debug 'false'
927
+ SET debug '__OPTION_DEBUG__'
928
928
  , ADD use_remote_estimate ''
929
929
  );
930
930
 
@@ -1049,13 +1049,13 @@ describe("sql formatting snapshots", () => {
1049
1049
 
1050
1050
  -- user_mapping.create
1051
1051
  create user mapping for app_user server remote_server
1052
- options (user 'remote_app', password 'secret123');
1052
+ options (user 'remote_app', password '__OPTION_PASSWORD__');
1053
1053
 
1054
1054
  -- user_mapping.drop
1055
1055
  drop user mapping for app_user server remote_server;
1056
1056
 
1057
1057
  -- user_mapping.alter.set_options
1058
- alter user mapping for app_user server remote_server options (SET password 'new_secret');"
1058
+ alter user mapping for app_user server remote_server options (SET password '__OPTION_PASSWORD__');"
1059
1059
  `);
1060
1060
  });
1061
1061
  });
@@ -1094,7 +1094,7 @@ describe("sql formatting snapshots", () => {
1094
1094
  CREATE FOREIGN DATA WRAPPER postgres_fdw
1095
1095
  HANDLER postgres_fdw_handler
1096
1096
  VALIDATOR postgres_fdw_validator
1097
- OPTIONS (debug 'true');
1097
+ OPTIONS (debug '__OPTION_DEBUG__');
1098
1098
 
1099
1099
  -- fdw.drop
1100
1100
  DROP FOREIGN DATA WRAPPER postgres_fdw;
@@ -1106,7 +1106,7 @@ describe("sql formatting snapshots", () => {
1106
1106
  -- fdw.alter.set_options
1107
1107
  ALTER FOREIGN DATA WRAPPER postgres_fdw
1108
1108
  OPTIONS (
1109
- SET debug 'false',
1109
+ SET debug '__OPTION_DEBUG__',
1110
1110
  ADD use_remote_estimate ''
1111
1111
  );
1112
1112
 
@@ -1264,7 +1264,7 @@ describe("sql formatting snapshots", () => {
1264
1264
  remote_server
1265
1265
  OPTIONS
1266
1266
  (user 'remote_app', password
1267
- 'secret123');
1267
+ '__OPTION_PASSWORD__');
1268
1268
 
1269
1269
  -- user_mapping.drop
1270
1270
  DROP USER MAPPING FOR app_user SERVER
@@ -1273,7 +1273,8 @@ describe("sql formatting snapshots", () => {
1273
1273
  -- user_mapping.alter.set_options
1274
1274
  ALTER USER MAPPING FOR app_user SERVER
1275
1275
  remote_server
1276
- OPTIONS (SET password 'new_secret');"
1276
+ OPTIONS
1277
+ (SET password '__OPTION_PASSWORD__');"
1277
1278
  `);
1278
1279
  });
1279
1280
  });
@@ -908,7 +908,7 @@ describe("sql formatting snapshots", () => {
908
908
  CREATE FOREIGN DATA WRAPPER postgres_fdw
909
909
  HANDLER postgres_fdw_handler
910
910
  VALIDATOR postgres_fdw_validator
911
- OPTIONS (debug 'true');
911
+ OPTIONS (debug '__OPTION_DEBUG__');
912
912
 
913
913
  -- fdw.drop
914
914
  DROP FOREIGN DATA WRAPPER postgres_fdw;
@@ -920,7 +920,7 @@ describe("sql formatting snapshots", () => {
920
920
  -- fdw.alter.set_options
921
921
  ALTER FOREIGN DATA WRAPPER postgres_fdw
922
922
  OPTIONS (
923
- SET debug 'false',
923
+ SET debug '__OPTION_DEBUG__',
924
924
  ADD use_remote_estimate ''
925
925
  );
926
926
 
@@ -1045,13 +1045,13 @@ describe("sql formatting snapshots", () => {
1045
1045
 
1046
1046
  -- user_mapping.create
1047
1047
  CREATE USER MAPPING FOR app_user SERVER remote_server
1048
- OPTIONS (user 'remote_app', password 'secret123');
1048
+ OPTIONS (user 'remote_app', password '__OPTION_PASSWORD__');
1049
1049
 
1050
1050
  -- user_mapping.drop
1051
1051
  DROP USER MAPPING FOR app_user SERVER remote_server;
1052
1052
 
1053
1053
  -- user_mapping.alter.set_options
1054
- ALTER USER MAPPING FOR app_user SERVER remote_server OPTIONS (SET password 'new_secret');"
1054
+ ALTER USER MAPPING FOR app_user SERVER remote_server OPTIONS (SET password '__OPTION_PASSWORD__');"
1055
1055
  `);
1056
1056
  });
1057
1057
  });
@@ -899,7 +899,7 @@ describe("sql formatting snapshots", () => {
899
899
  CREATE FOREIGN DATA WRAPPER postgres_fdw
900
900
  HANDLER postgres_fdw_handler
901
901
  VALIDATOR postgres_fdw_validator
902
- OPTIONS (debug 'true');
902
+ OPTIONS (debug '__OPTION_DEBUG__');
903
903
 
904
904
  -- fdw.drop
905
905
  DROP FOREIGN DATA WRAPPER postgres_fdw;
@@ -911,7 +911,7 @@ describe("sql formatting snapshots", () => {
911
911
  -- fdw.alter.set_options
912
912
  ALTER FOREIGN DATA WRAPPER postgres_fdw
913
913
  OPTIONS (
914
- SET debug 'false',
914
+ SET debug '__OPTION_DEBUG__',
915
915
  ADD use_remote_estimate ''
916
916
  );
917
917
 
@@ -1036,13 +1036,13 @@ describe("sql formatting snapshots", () => {
1036
1036
 
1037
1037
  -- user_mapping.create
1038
1038
  CREATE USER MAPPING FOR app_user SERVER remote_server
1039
- OPTIONS (user 'remote_app', password 'secret123');
1039
+ OPTIONS (user 'remote_app', password '__OPTION_PASSWORD__');
1040
1040
 
1041
1041
  -- user_mapping.drop
1042
1042
  DROP USER MAPPING FOR app_user SERVER remote_server;
1043
1043
 
1044
1044
  -- user_mapping.alter.set_options
1045
- ALTER USER MAPPING FOR app_user SERVER remote_server OPTIONS (SET password 'new_secret');"
1045
+ ALTER USER MAPPING FOR app_user SERVER remote_server OPTIONS (SET password '__OPTION_PASSWORD__');"
1046
1046
  `);
1047
1047
  });
1048
1048
  });
@@ -1,6 +1,7 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, spyOn, test } from "bun:test";
2
2
  import {
3
3
  connectWithRetry,
4
+ connectWithTimeout,
4
5
  isRetryableConnectError,
5
6
  poolConfigFromUrl,
6
7
  } from "./postgres-config.ts";
@@ -241,6 +242,43 @@ describe("connectWithRetry", () => {
241
242
  });
242
243
  });
243
244
 
245
+ describe("connectWithTimeout", () => {
246
+ test("clears the timer when connect resolves before it fires", async () => {
247
+ const clearSpy = spyOn(globalThis, "clearTimeout");
248
+ try {
249
+ const sentinel = { client: true };
250
+ const result = await connectWithTimeout(
251
+ () => Promise.resolve(sentinel),
252
+ 60_000,
253
+ "source",
254
+ );
255
+ expect(result).toBe(sentinel);
256
+ expect(clearSpy).toHaveBeenCalled();
257
+ } finally {
258
+ clearSpy.mockRestore();
259
+ }
260
+ });
261
+
262
+ test("rejects with a timeout error when connect is too slow", async () => {
263
+ await expect(
264
+ connectWithTimeout(() => new Promise<never>(() => {}), 5, "target"),
265
+ ).rejects.toThrow(/timed out after 5ms/);
266
+ });
267
+
268
+ test("clears the timer even when connect rejects", async () => {
269
+ const clearSpy = spyOn(globalThis, "clearTimeout");
270
+ try {
271
+ const boom = new Error("connect ECONNREFUSED");
272
+ await expect(
273
+ connectWithTimeout(() => Promise.reject(boom), 60_000, "target"),
274
+ ).rejects.toBe(boom);
275
+ expect(clearSpy).toHaveBeenCalled();
276
+ } finally {
277
+ clearSpy.mockRestore();
278
+ }
279
+ });
280
+ });
281
+
244
282
  describe("poolConfigFromUrl", () => {
245
283
  describe("non-IPv6 URLs pass through as connectionString", () => {
246
284
  test("DNS hostname", () => {
@@ -209,6 +209,37 @@ export async function connectWithRetry<T>(opts: {
209
209
  throw lastError;
210
210
  }
211
211
 
212
+ /**
213
+ * Race `connect()` against a `timeoutMs` rejection and clear the timer when
214
+ * either side wins. If the timer is left running after a fast connect, the
215
+ * pending `setTimeout` keeps the event loop alive and the process hangs for
216
+ * the rest of `timeoutMs`.
217
+ */
218
+ export function connectWithTimeout<T>(
219
+ connect: () => Promise<T>,
220
+ timeoutMs: number,
221
+ label: "source" | "target",
222
+ ): Promise<T> {
223
+ let timer: ReturnType<typeof setTimeout>;
224
+ return Promise.race([
225
+ connect(),
226
+ new Promise<never>((_, reject) => {
227
+ timer = setTimeout(
228
+ () =>
229
+ reject(
230
+ new Error(
231
+ `Connection to ${label} database timed out after ${timeoutMs}ms. ` +
232
+ `The server may require SSL, use an invalid certificate, or be unreachable.`,
233
+ ),
234
+ ),
235
+ timeoutMs,
236
+ );
237
+ }),
238
+ ]).finally(() => {
239
+ clearTimeout(timer);
240
+ });
241
+ }
242
+
212
243
  /**
213
244
  * Options for creating a Pool with event listeners.
214
245
  */
@@ -412,22 +443,7 @@ export async function createManagedPool(
412
443
  const timeoutMs = DEFAULT_CONNECT_TIMEOUT_MS;
413
444
  try {
414
445
  const client = await connectWithRetry({
415
- connect: () =>
416
- Promise.race([
417
- pool.connect(),
418
- new Promise<never>((_, reject) =>
419
- setTimeout(
420
- () =>
421
- reject(
422
- new Error(
423
- `Connection to ${label} database timed out after ${timeoutMs}ms. ` +
424
- `The server may require SSL, use an invalid certificate, or be unreachable.`,
425
- ),
426
- ),
427
- timeoutMs,
428
- ),
429
- ),
430
- ]),
446
+ connect: () => connectWithTimeout(() => pool.connect(), timeoutMs, label),
431
447
  });
432
448
  client.release();
433
449
  } catch (err) {
@@ -233,7 +233,7 @@ export function printDebugGraph(
233
233
  "\n==== Mermaid (cycle detected) ====\n%s\n==== end ====",
234
234
  mermaidDiagram,
235
235
  );
236
- } catch (_error) {
236
+ } catch {
237
237
  // ignore debug printing errors
238
238
  }
239
239
  }
@@ -144,6 +144,7 @@ function table(
144
144
 
145
145
  async function catalogWithDepends(depends: PgDepend[]) {
146
146
  const base = await createEmptyCatalog(170000, "postgres");
147
+ // oxlint-disable-next-line typescript/no-misused-spread
147
148
  return new Catalog({ ...base, depends });
148
149
  }
149
150
 
@@ -14,7 +14,7 @@ export function performStableTopologicalSort(
14
14
  { length: nodeCount },
15
15
  () => new Set<number>(),
16
16
  );
17
- const inDegreeCounts = new Array<number>(nodeCount).fill(0);
17
+ const inDegreeCounts: number[] = Array.from({ length: nodeCount }, () => 0);
18
18
 
19
19
  for (const [sourceIndex, targetIndex] of edges) {
20
20
  if (!adjacencyList[sourceIndex].has(targetIndex)) {
@@ -73,7 +73,7 @@ export function findCycle(
73
73
  }
74
74
 
75
75
  // 0 = unvisited, 1 = visiting, 2 = completed
76
- const visitState = new Array<number>(nodeCount).fill(0);
76
+ const visitState: number[] = Array.from({ length: nodeCount }, () => 0);
77
77
  const pathStack: number[] = [];
78
78
  let cycleNodeIndexes: number[] | null = null;
79
79