@supabase/pg-delta 1.0.0-alpha.14 → 1.0.0-alpha.16

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.
@@ -0,0 +1,317 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Catalog, createEmptyCatalog } from "./catalog.model.ts";
3
+ import type { Change } from "./change.types.ts";
4
+ import {
5
+ AlterTableChangeOwner,
6
+ AlterTableDropColumn,
7
+ AlterTableDropConstraint,
8
+ AlterTableEnableRowLevelSecurity,
9
+ AlterTableSetReplicaIdentity,
10
+ } from "./objects/table/changes/table.alter.ts";
11
+ import { CreateTable } from "./objects/table/changes/table.create.ts";
12
+ import { DropTable } from "./objects/table/changes/table.drop.ts";
13
+ import { GrantTablePrivileges } from "./objects/table/changes/table.privilege.ts";
14
+ import { Table } from "./objects/table/table.model.ts";
15
+ import { stableId } from "./objects/utils.ts";
16
+ import { normalizePostDiffCycles } from "./post-diff-cycle-breaking.ts";
17
+
18
+ const baseTableProps = {
19
+ schema: "public",
20
+ persistence: "p" as const,
21
+ row_security: false,
22
+ force_row_security: false,
23
+ has_indexes: false,
24
+ has_rules: false,
25
+ has_triggers: false,
26
+ has_subclasses: false,
27
+ is_populated: true,
28
+ replica_identity: "d" as const,
29
+ is_partition: false,
30
+ options: null,
31
+ partition_bound: null,
32
+ partition_by: null,
33
+ owner: "postgres",
34
+ comment: null,
35
+ parent_schema: null,
36
+ parent_name: null,
37
+ privileges: [],
38
+ };
39
+
40
+ function integerColumn(name: string, position: number) {
41
+ return {
42
+ name,
43
+ position,
44
+ data_type: "integer" as const,
45
+ data_type_str: "integer",
46
+ is_custom_type: false as const,
47
+ custom_type_type: null,
48
+ custom_type_category: null,
49
+ custom_type_schema: null,
50
+ custom_type_name: null,
51
+ not_null: false,
52
+ is_identity: false,
53
+ is_identity_always: false,
54
+ is_generated: false,
55
+ collation: null,
56
+ default: null,
57
+ comment: null,
58
+ };
59
+ }
60
+
61
+ describe("normalizePostDiffCycles", () => {
62
+ test("injects explicit FK drops for mutually dependent dropped tables", async () => {
63
+ const baseline = await createEmptyCatalog(170000, "postgres");
64
+ const tableA = new Table({
65
+ ...baseTableProps,
66
+ name: "a",
67
+ columns: [
68
+ { ...integerColumn("id", 1), not_null: true },
69
+ integerColumn("b_id", 2),
70
+ ],
71
+ constraints: [
72
+ {
73
+ name: "a_b_fkey",
74
+ constraint_type: "f",
75
+ deferrable: false,
76
+ initially_deferred: false,
77
+ validated: true,
78
+ is_local: true,
79
+ no_inherit: false,
80
+ is_partition_clone: false,
81
+ parent_constraint_schema: null,
82
+ parent_constraint_name: null,
83
+ parent_table_schema: null,
84
+ parent_table_name: null,
85
+ key_columns: ["b_id"],
86
+ foreign_key_columns: ["id"],
87
+ foreign_key_table: "b",
88
+ foreign_key_schema: "public",
89
+ foreign_key_table_is_partition: false,
90
+ foreign_key_parent_schema: null,
91
+ foreign_key_parent_table: null,
92
+ foreign_key_effective_schema: "public",
93
+ foreign_key_effective_table: "b",
94
+ on_update: "a",
95
+ on_delete: "a",
96
+ match_type: "s",
97
+ check_expression: null,
98
+ owner: "postgres",
99
+ definition: "FOREIGN KEY (b_id) REFERENCES public.b(id)",
100
+ comment: null,
101
+ },
102
+ ],
103
+ });
104
+ const tableB = new Table({
105
+ ...baseTableProps,
106
+ name: "b",
107
+ columns: [
108
+ { ...integerColumn("id", 1), not_null: true },
109
+ integerColumn("a_id", 2),
110
+ ],
111
+ constraints: [
112
+ {
113
+ name: "b_a_fkey",
114
+ constraint_type: "f",
115
+ deferrable: false,
116
+ initially_deferred: false,
117
+ validated: true,
118
+ is_local: true,
119
+ no_inherit: false,
120
+ is_partition_clone: false,
121
+ parent_constraint_schema: null,
122
+ parent_constraint_name: null,
123
+ parent_table_schema: null,
124
+ parent_table_name: null,
125
+ key_columns: ["a_id"],
126
+ foreign_key_columns: ["id"],
127
+ foreign_key_table: "a",
128
+ foreign_key_schema: "public",
129
+ foreign_key_table_is_partition: false,
130
+ foreign_key_parent_schema: null,
131
+ foreign_key_parent_table: null,
132
+ foreign_key_effective_schema: "public",
133
+ foreign_key_effective_table: "a",
134
+ on_update: "a",
135
+ on_delete: "a",
136
+ match_type: "s",
137
+ check_expression: null,
138
+ owner: "postgres",
139
+ definition: "FOREIGN KEY (a_id) REFERENCES public.a(id)",
140
+ comment: null,
141
+ },
142
+ ],
143
+ });
144
+ const mainCatalog = new Catalog({
145
+ ...baseline,
146
+ tables: {
147
+ [tableA.stableId]: tableA,
148
+ [tableB.stableId]: tableB,
149
+ },
150
+ });
151
+ const changes: Change[] = [
152
+ new DropTable({ table: tableA }),
153
+ new DropTable({ table: tableB }),
154
+ ];
155
+
156
+ const normalized = normalizePostDiffCycles({
157
+ changes,
158
+ mainCatalog,
159
+ });
160
+
161
+ const explicitConstraintDrops = normalized.filter(
162
+ (change) => change instanceof AlterTableDropConstraint,
163
+ );
164
+ expect(explicitConstraintDrops).toHaveLength(2);
165
+
166
+ const normalizedDropTableA = normalized.find(
167
+ (change) =>
168
+ change instanceof DropTable &&
169
+ change.table.stableId === tableA.stableId,
170
+ );
171
+ const normalizedDropTableB = normalized.find(
172
+ (change) =>
173
+ change instanceof DropTable &&
174
+ change.table.stableId === tableB.stableId,
175
+ );
176
+ if (!(normalizedDropTableA instanceof DropTable)) {
177
+ throw new Error("expected normalized DropTable(public.a)");
178
+ }
179
+ if (!(normalizedDropTableB instanceof DropTable)) {
180
+ throw new Error("expected normalized DropTable(public.b)");
181
+ }
182
+
183
+ expect(
184
+ normalizedDropTableA.externallyDroppedConstraints.has("a_b_fkey"),
185
+ ).toBe(true);
186
+ expect(
187
+ normalizedDropTableB.externallyDroppedConstraints.has("b_a_fkey"),
188
+ ).toBe(true);
189
+ expect(
190
+ normalizedDropTableA.requires.includes(
191
+ stableId.constraint("public", "a", "a_b_fkey"),
192
+ ),
193
+ ).toBe(false);
194
+ expect(
195
+ normalizedDropTableB.requires.includes(
196
+ stableId.constraint("public", "b", "b_a_fkey"),
197
+ ),
198
+ ).toBe(false);
199
+ });
200
+
201
+ test("prunes same-table drop-column and drop-constraint ALTERs for replaced tables only", async () => {
202
+ const baseline = await createEmptyCatalog(170000, "postgres");
203
+ const mainChildren = new Table({
204
+ ...baseTableProps,
205
+ name: "children",
206
+ columns: [
207
+ { ...integerColumn("id", 1), not_null: true },
208
+ integerColumn("parent_ref", 2),
209
+ integerColumn("status", 3),
210
+ ],
211
+ });
212
+ const branchChildren = new Table({
213
+ ...baseTableProps,
214
+ name: "children",
215
+ columns: [
216
+ { ...integerColumn("id", 1), not_null: true },
217
+ integerColumn("status", 2),
218
+ ],
219
+ });
220
+
221
+ const droppedColumn = mainChildren.columns.find(
222
+ (column) => column.name === "parent_ref",
223
+ );
224
+ if (!droppedColumn) throw new Error("test setup: parent_ref missing");
225
+
226
+ const preExistingDropColumn = new AlterTableDropColumn({
227
+ table: mainChildren,
228
+ column: droppedColumn,
229
+ });
230
+ const preExistingDropConstraint = new AlterTableDropConstraint({
231
+ table: mainChildren,
232
+ constraint: {
233
+ name: "children_parent_ref_fkey",
234
+ constraint_type: "f",
235
+ deferrable: false,
236
+ initially_deferred: false,
237
+ validated: true,
238
+ is_local: true,
239
+ no_inherit: false,
240
+ is_partition_clone: false,
241
+ parent_constraint_schema: null,
242
+ parent_constraint_name: null,
243
+ parent_table_schema: null,
244
+ parent_table_name: null,
245
+ key_columns: ["parent_ref"],
246
+ foreign_key_columns: ["id"],
247
+ foreign_key_table: "parents",
248
+ foreign_key_schema: "public",
249
+ foreign_key_table_is_partition: false,
250
+ foreign_key_parent_schema: null,
251
+ foreign_key_parent_table: null,
252
+ foreign_key_effective_schema: "public",
253
+ foreign_key_effective_table: "parents",
254
+ on_update: "a",
255
+ on_delete: "a",
256
+ match_type: "s",
257
+ check_expression: null,
258
+ owner: "postgres",
259
+ definition: "FOREIGN KEY (parent_ref) REFERENCES public.parents(id)",
260
+ comment: null,
261
+ },
262
+ });
263
+ const preExistingChangeOwner = new AlterTableChangeOwner({
264
+ table: branchChildren,
265
+ owner: "new_owner",
266
+ });
267
+ const preExistingEnableRls = new AlterTableEnableRowLevelSecurity({
268
+ table: branchChildren,
269
+ });
270
+ const preExistingReplicaIdentity = new AlterTableSetReplicaIdentity({
271
+ table: branchChildren,
272
+ mode: "f",
273
+ });
274
+ const preExistingGrant = new GrantTablePrivileges({
275
+ table: branchChildren,
276
+ grantee: "reader",
277
+ privileges: [{ privilege: "SELECT", grantable: false }],
278
+ });
279
+ const changes: Change[] = [
280
+ new DropTable({ table: mainChildren }),
281
+ new CreateTable({ table: branchChildren }),
282
+ preExistingDropColumn,
283
+ preExistingDropConstraint,
284
+ preExistingChangeOwner,
285
+ preExistingEnableRls,
286
+ preExistingReplicaIdentity,
287
+ preExistingGrant,
288
+ ];
289
+ const mainCatalog = new Catalog({
290
+ ...baseline,
291
+ tables: { [mainChildren.stableId]: mainChildren },
292
+ });
293
+
294
+ const normalized = normalizePostDiffCycles({
295
+ changes,
296
+ mainCatalog,
297
+ replacedTableIds: new Set([mainChildren.stableId]),
298
+ });
299
+
300
+ expect(normalized.some((change) => change instanceof DropTable)).toBe(true);
301
+ expect(normalized.some((change) => change instanceof CreateTable)).toBe(
302
+ true,
303
+ );
304
+ expect(normalized).not.toContain(preExistingDropColumn);
305
+ expect(normalized).not.toContain(preExistingDropConstraint);
306
+ expect(
307
+ normalized.some((change) => change instanceof AlterTableDropColumn),
308
+ ).toBe(false);
309
+ expect(
310
+ normalized.some((change) => change instanceof AlterTableDropConstraint),
311
+ ).toBe(false);
312
+ expect(normalized).toContain(preExistingChangeOwner);
313
+ expect(normalized).toContain(preExistingEnableRls);
314
+ expect(normalized).toContain(preExistingReplicaIdentity);
315
+ expect(normalized).toContain(preExistingGrant);
316
+ });
317
+ });
@@ -0,0 +1,236 @@
1
+ import type { Catalog } from "./catalog.model.ts";
2
+ import type { Change } from "./change.types.ts";
3
+ import {
4
+ AlterTableDropColumn,
5
+ AlterTableDropConstraint,
6
+ } from "./objects/table/changes/table.alter.ts";
7
+ import { DropTable } from "./objects/table/changes/table.drop.ts";
8
+ import { stableId } from "./objects/utils.ts";
9
+
10
+ function constraintStableId(
11
+ table: { schema: string; name: string },
12
+ constraintName: string,
13
+ ) {
14
+ return stableId.constraint(table.schema, table.name, constraintName);
15
+ }
16
+
17
+ /**
18
+ * Yield FK constraints on `table` whose referenced table is also dropped in the
19
+ * final plan. Self-references are left alone because the sort phase already
20
+ * handles the resulting self-loop correctly.
21
+ */
22
+ function* iterCrossDropFkConstraints(
23
+ table: Catalog["tables"][string],
24
+ droppedSet: ReadonlySet<string>,
25
+ ) {
26
+ for (const constraint of table.constraints) {
27
+ if (constraint.constraint_type !== "f") continue;
28
+ if (constraint.is_partition_clone) continue;
29
+ if (!constraint.foreign_key_schema || !constraint.foreign_key_table) {
30
+ continue;
31
+ }
32
+ const referencedId = stableId.table(
33
+ constraint.foreign_key_schema,
34
+ constraint.foreign_key_table,
35
+ );
36
+ if (referencedId === table.stableId) continue;
37
+ if (!droppedSet.has(referencedId)) continue;
38
+ yield { constraint, referencedId };
39
+ }
40
+ }
41
+
42
+ function isSupersededByTableReplacement(
43
+ change: Change,
44
+ replacedTableIds: ReadonlySet<string>,
45
+ ): boolean {
46
+ if (
47
+ !(change instanceof AlterTableDropColumn) &&
48
+ !(change instanceof AlterTableDropConstraint)
49
+ ) {
50
+ return false;
51
+ }
52
+ return replacedTableIds.has(change.table.stableId);
53
+ }
54
+
55
+ function collectExplicitConstraintDropIds(changes: Change[]) {
56
+ const explicitConstraintDropIds = new Set<string>();
57
+
58
+ for (const change of changes) {
59
+ if (!(change instanceof AlterTableDropConstraint)) continue;
60
+ explicitConstraintDropIds.add(
61
+ constraintStableId(change.table, change.constraint.name),
62
+ );
63
+ }
64
+
65
+ return explicitConstraintDropIds;
66
+ }
67
+
68
+ function hasSameEntries(
69
+ left: ReadonlySet<string>,
70
+ right: ReadonlySet<string>,
71
+ ): boolean {
72
+ if (left.size !== right.size) return false;
73
+ for (const value of left) {
74
+ if (!right.has(value)) return false;
75
+ }
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Normalize change-list cycles that only become apparent after all object
81
+ * diffs have been collected.
82
+ *
83
+ * This pass intentionally handles whole-plan interactions only:
84
+ * - If replace expansion added `DropTable(T)+CreateTable(T)`, targeted
85
+ * `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)` changes are
86
+ * redundant and create an unbreakable drop-phase cycle, so we elide them.
87
+ * - If two dropped tables reference each other via FK, we insert dedicated
88
+ * `AlterTableDropConstraint` changes and teach the paired `DropTable`
89
+ * changes not to claim those FK stable IDs.
90
+ *
91
+ * Object-local PostgreSQL semantics (for example owned-sequence cascades) stay
92
+ * in the corresponding `diff*` function instead of this pass.
93
+ */
94
+ export function normalizePostDiffCycles({
95
+ changes,
96
+ mainCatalog,
97
+ replacedTableIds = new Set<string>(),
98
+ }: {
99
+ changes: Change[];
100
+ mainCatalog: Catalog;
101
+ replacedTableIds?: ReadonlySet<string>;
102
+ }): Change[] {
103
+ const structurallyNormalizedChanges =
104
+ replacedTableIds.size === 0
105
+ ? changes
106
+ : changes.filter(
107
+ (change) => !isSupersededByTableReplacement(change, replacedTableIds),
108
+ );
109
+
110
+ const dropTableChanges = structurallyNormalizedChanges.filter(
111
+ (change): change is DropTable => change instanceof DropTable,
112
+ );
113
+
114
+ if (dropTableChanges.length < 2) {
115
+ return structurallyNormalizedChanges;
116
+ }
117
+
118
+ const droppedSet = new Set(
119
+ dropTableChanges.map((change) => change.table.stableId),
120
+ );
121
+ const droppedFkTargets = new Map<string, Set<string>>();
122
+
123
+ for (const dropTableChange of dropTableChanges) {
124
+ const mainTable =
125
+ mainCatalog.tables[dropTableChange.table.stableId] ??
126
+ dropTableChange.table;
127
+ const targets = new Set<string>();
128
+
129
+ for (const { referencedId } of iterCrossDropFkConstraints(
130
+ mainTable,
131
+ droppedSet,
132
+ )) {
133
+ targets.add(referencedId);
134
+ }
135
+
136
+ droppedFkTargets.set(mainTable.stableId, targets);
137
+ }
138
+
139
+ const explicitConstraintDropIds = collectExplicitConstraintDropIds(
140
+ structurallyNormalizedChanges,
141
+ );
142
+ const injectedConstraintDropsByTableId = new Map<
143
+ string,
144
+ AlterTableDropConstraint[]
145
+ >();
146
+ const externallyDroppedConstraintsByTableId = new Map<
147
+ string,
148
+ ReadonlySet<string>
149
+ >();
150
+ let didMutate = structurallyNormalizedChanges !== changes;
151
+
152
+ for (const dropTableChange of dropTableChanges) {
153
+ const mainTable =
154
+ mainCatalog.tables[dropTableChange.table.stableId] ??
155
+ dropTableChange.table;
156
+ const externallyDroppedConstraints = new Set(
157
+ dropTableChange.externallyDroppedConstraints,
158
+ );
159
+
160
+ for (const { constraint, referencedId } of iterCrossDropFkConstraints(
161
+ mainTable,
162
+ droppedSet,
163
+ )) {
164
+ const isMutual =
165
+ droppedFkTargets.get(referencedId)?.has(mainTable.stableId) === true;
166
+ if (!isMutual) continue;
167
+
168
+ const droppedConstraintStableId = constraintStableId(
169
+ mainTable,
170
+ constraint.name,
171
+ );
172
+ externallyDroppedConstraints.add(constraint.name);
173
+
174
+ if (!explicitConstraintDropIds.has(droppedConstraintStableId)) {
175
+ const injectedDrop = new AlterTableDropConstraint({
176
+ table: mainTable,
177
+ constraint,
178
+ });
179
+ const existingDrops =
180
+ injectedConstraintDropsByTableId.get(mainTable.stableId) ?? [];
181
+ existingDrops.push(injectedDrop);
182
+ injectedConstraintDropsByTableId.set(mainTable.stableId, existingDrops);
183
+ explicitConstraintDropIds.add(droppedConstraintStableId);
184
+ didMutate = true;
185
+ }
186
+ }
187
+
188
+ if (
189
+ !hasSameEntries(
190
+ dropTableChange.externallyDroppedConstraints,
191
+ externallyDroppedConstraints,
192
+ )
193
+ ) {
194
+ externallyDroppedConstraintsByTableId.set(
195
+ mainTable.stableId,
196
+ externallyDroppedConstraints,
197
+ );
198
+ didMutate = true;
199
+ }
200
+ }
201
+
202
+ if (!didMutate) {
203
+ return changes;
204
+ }
205
+
206
+ const normalizedChanges: Change[] = [];
207
+
208
+ for (const change of structurallyNormalizedChanges) {
209
+ if (!(change instanceof DropTable)) {
210
+ normalizedChanges.push(change);
211
+ continue;
212
+ }
213
+
214
+ const injectedConstraintDrops =
215
+ injectedConstraintDropsByTableId.get(change.table.stableId) ?? [];
216
+ if (injectedConstraintDrops.length > 0) {
217
+ normalizedChanges.push(...injectedConstraintDrops);
218
+ }
219
+
220
+ const externallyDroppedConstraints =
221
+ externallyDroppedConstraintsByTableId.get(change.table.stableId);
222
+ if (!externallyDroppedConstraints) {
223
+ normalizedChanges.push(change);
224
+ continue;
225
+ }
226
+
227
+ normalizedChanges.push(
228
+ new DropTable({
229
+ table: change.table,
230
+ externallyDroppedConstraints,
231
+ }),
232
+ );
233
+ }
234
+
235
+ return normalizedChanges;
236
+ }
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  connectWithRetry,
4
4
  isRetryableConnectError,
5
+ poolConfigFromUrl,
5
6
  } from "./postgres-config.ts";
6
7
 
7
8
  function makeError(message: string, code?: string): Error {
@@ -239,3 +240,97 @@ describe("connectWithRetry", () => {
239
240
  expect(attempts).toBe(1);
240
241
  });
241
242
  });
243
+
244
+ describe("poolConfigFromUrl", () => {
245
+ describe("non-IPv6 URLs pass through as connectionString", () => {
246
+ test("DNS hostname", () => {
247
+ const url = "postgresql://user:pass@db.example.com:5432/mydb";
248
+ expect(poolConfigFromUrl(url)).toEqual({ connectionString: url });
249
+ });
250
+
251
+ test("IPv4 host", () => {
252
+ const url = "postgresql://user:pass@127.0.0.1:5432/mydb";
253
+ expect(poolConfigFromUrl(url)).toEqual({ connectionString: url });
254
+ });
255
+
256
+ test("DNS hostname with query params", () => {
257
+ const url =
258
+ "postgresql://user:pass@db.example.com:5432/mydb?application_name=test";
259
+ expect(poolConfigFromUrl(url)).toEqual({ connectionString: url });
260
+ });
261
+ });
262
+
263
+ describe("bracketed IPv6 URLs expand to explicit fields with no brackets", () => {
264
+ test("full 8-group IPv6 — host has no brackets and no connectionString", () => {
265
+ const url =
266
+ "postgresql://user:pass@[2600:1f16:1cd0:3340:f92e:f4cb:7a52:10a1]:5432/mydb";
267
+ const config = poolConfigFromUrl(url);
268
+ expect(config.connectionString).toBeUndefined();
269
+ expect(config.host).toBe("2600:1f16:1cd0:3340:f92e:f4cb:7a52:10a1");
270
+ expect(config.port).toBe(5432);
271
+ expect(config.user).toBe("user");
272
+ expect(config.password).toBe("pass");
273
+ expect(config.database).toBe("mydb");
274
+ });
275
+
276
+ test("compressed ::1 form", () => {
277
+ const url = "postgresql://user:pass@[::1]:5432/mydb";
278
+ const config = poolConfigFromUrl(url);
279
+ expect(config.host).toBe("::1");
280
+ expect(config.port).toBe(5432);
281
+ });
282
+
283
+ test("host bracket strip survives percent-decoded username/password", () => {
284
+ const url = "postgresql://user:p%40ss%2Fword@[::1]:5432/mydb";
285
+ const config = poolConfigFromUrl(url);
286
+ expect(config.host).toBe("::1");
287
+ expect(config.user).toBe("user");
288
+ expect(config.password).toBe("p@ss/word");
289
+ });
290
+
291
+ test("works without port", () => {
292
+ const url = "postgresql://user:pass@[::1]/mydb";
293
+ const config = poolConfigFromUrl(url);
294
+ expect(config.host).toBe("::1");
295
+ expect(config.port).toBeUndefined();
296
+ });
297
+
298
+ test("works without database (pathname='/')", () => {
299
+ const url = "postgresql://user:pass@[::1]:5432/";
300
+ const config = poolConfigFromUrl(url);
301
+ expect(config.host).toBe("::1");
302
+ expect(config.database).toBeUndefined();
303
+ });
304
+
305
+ test("works without userinfo", () => {
306
+ const url = "postgresql://[::1]:5432/mydb";
307
+ const config = poolConfigFromUrl(url);
308
+ expect(config.host).toBe("::1");
309
+ expect(config.user).toBeUndefined();
310
+ expect(config.password).toBeUndefined();
311
+ });
312
+
313
+ test("query params are forwarded as top-level config keys", () => {
314
+ const url =
315
+ "postgresql://user:pass@[::1]:5432/mydb?application_name=pgdelta&connect_timeout=5";
316
+ const config = poolConfigFromUrl(url) as unknown as Record<
317
+ string,
318
+ unknown
319
+ >;
320
+ expect(config.application_name).toBe("pgdelta");
321
+ expect(config.connect_timeout).toBe("5");
322
+ expect(config.host).toBe("::1");
323
+ });
324
+
325
+ test("IPv4-mapped IPv6 is stripped of brackets (WHATWG canonicalisation is fine)", () => {
326
+ // WHATWG URL canonicalises `::ffff:192.0.2.1` to `::ffff:c000:201`;
327
+ // either form resolves to the same IPv6 address, and the point of this
328
+ // test is purely that no brackets escape to pg.
329
+ const url = "postgresql://user:pass@[::ffff:192.0.2.1]:5432/mydb";
330
+ const config = poolConfigFromUrl(url);
331
+ expect(config.host).not.toContain("[");
332
+ expect(config.host).not.toContain("]");
333
+ expect(config.host).toBe("::ffff:c000:201");
334
+ });
335
+ });
336
+ });