@framers/sql-storage-adapter 0.6.15 → 0.7.0

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 (94) hide show
  1. package/README.md +174 -3
  2. package/dist/adapters/baseStorageAdapter.d.ts.map +1 -1
  3. package/dist/adapters/baseStorageAdapter.js +4 -2
  4. package/dist/adapters/baseStorageAdapter.js.map +1 -1
  5. package/dist/adapters/betterSqliteAdapter.d.ts +18 -2
  6. package/dist/adapters/betterSqliteAdapter.d.ts.map +1 -1
  7. package/dist/adapters/betterSqliteAdapter.js +23 -7
  8. package/dist/adapters/betterSqliteAdapter.js.map +1 -1
  9. package/dist/adapters/capacitorSqliteAdapter.d.ts +8 -0
  10. package/dist/adapters/capacitorSqliteAdapter.d.ts.map +1 -1
  11. package/dist/adapters/capacitorSqliteAdapter.js +11 -1
  12. package/dist/adapters/capacitorSqliteAdapter.js.map +1 -1
  13. package/dist/adapters/electron/electronMainAdapter.d.ts +10 -0
  14. package/dist/adapters/electron/electronMainAdapter.d.ts.map +1 -1
  15. package/dist/adapters/electron/electronMainAdapter.js +21 -12
  16. package/dist/adapters/electron/electronMainAdapter.js.map +1 -1
  17. package/dist/adapters/indexedDbAdapter.d.ts +202 -54
  18. package/dist/adapters/indexedDbAdapter.d.ts.map +1 -1
  19. package/dist/adapters/indexedDbAdapter.js +560 -123
  20. package/dist/adapters/indexedDbAdapter.js.map +1 -1
  21. package/dist/adapters/postgresAdapter.d.ts +159 -3
  22. package/dist/adapters/postgresAdapter.d.ts.map +1 -1
  23. package/dist/adapters/postgresAdapter.js +667 -140
  24. package/dist/adapters/postgresAdapter.js.map +1 -1
  25. package/dist/adapters/sqlJsAdapter.d.ts +21 -3
  26. package/dist/adapters/sqlJsAdapter.d.ts.map +1 -1
  27. package/dist/adapters/sqlJsAdapter.js +29 -26
  28. package/dist/adapters/sqlJsAdapter.js.map +1 -1
  29. package/dist/core/contracts/index.d.ts +9 -0
  30. package/dist/core/contracts/index.d.ts.map +1 -1
  31. package/dist/core/contracts/index.js.map +1 -1
  32. package/dist/core/database.d.ts +43 -8
  33. package/dist/core/database.d.ts.map +1 -1
  34. package/dist/core/database.js +40 -5
  35. package/dist/core/database.js.map +1 -1
  36. package/dist/core/resolver.d.ts +20 -1
  37. package/dist/core/resolver.d.ts.map +1 -1
  38. package/dist/core/resolver.js +33 -11
  39. package/dist/core/resolver.js.map +1 -1
  40. package/dist/features/kv/idb.d.ts +79 -0
  41. package/dist/features/kv/idb.d.ts.map +1 -0
  42. package/dist/features/kv/idb.js +14 -0
  43. package/dist/features/kv/idb.js.map +1 -0
  44. package/dist/features/kv/index.d.ts +14 -0
  45. package/dist/features/kv/index.d.ts.map +1 -0
  46. package/dist/features/kv/index.js +14 -0
  47. package/dist/features/kv/index.js.map +1 -0
  48. package/dist/features/kv/indexedDbKeyValueStore.d.ts +77 -0
  49. package/dist/features/kv/indexedDbKeyValueStore.d.ts.map +1 -0
  50. package/dist/features/kv/indexedDbKeyValueStore.js +127 -0
  51. package/dist/features/kv/indexedDbKeyValueStore.js.map +1 -0
  52. package/dist/features/kv/keyValueStore.d.ts +48 -0
  53. package/dist/features/kv/keyValueStore.d.ts.map +1 -0
  54. package/dist/features/kv/keyValueStore.js +54 -0
  55. package/dist/features/kv/keyValueStore.js.map +1 -0
  56. package/dist/features/kv/sqlKeyValueStore.d.ts +34 -0
  57. package/dist/features/kv/sqlKeyValueStore.d.ts.map +1 -0
  58. package/dist/features/kv/sqlKeyValueStore.js +91 -0
  59. package/dist/features/kv/sqlKeyValueStore.js.map +1 -0
  60. package/dist/features/kv/valueCodec.d.ts +35 -0
  61. package/dist/features/kv/valueCodec.d.ts.map +1 -0
  62. package/dist/features/kv/valueCodec.js +462 -0
  63. package/dist/features/kv/valueCodec.js.map +1 -0
  64. package/dist/features/migrations/dataExport.js +9 -6
  65. package/dist/features/migrations/dataExport.js.map +1 -1
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +5 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/shared/errors.d.ts +23 -0
  71. package/dist/shared/errors.d.ts.map +1 -0
  72. package/dist/shared/errors.js +44 -0
  73. package/dist/shared/errors.js.map +1 -0
  74. package/dist/shared/logger.d.ts +26 -0
  75. package/dist/shared/logger.d.ts.map +1 -0
  76. package/dist/shared/logger.js +26 -0
  77. package/dist/shared/logger.js.map +1 -0
  78. package/dist/shared/parameterUtils.d.ts +7 -0
  79. package/dist/shared/parameterUtils.d.ts.map +1 -1
  80. package/dist/shared/parameterUtils.js +25 -0
  81. package/dist/shared/parameterUtils.js.map +1 -1
  82. package/dist/shared/postgresTarget.d.ts +4 -3
  83. package/dist/shared/postgresTarget.d.ts.map +1 -1
  84. package/dist/shared/postgresTarget.js +6 -4
  85. package/dist/shared/postgresTarget.js.map +1 -1
  86. package/dist/shared/segment.d.ts +43 -0
  87. package/dist/shared/segment.d.ts.map +1 -0
  88. package/dist/shared/segment.js +77 -0
  89. package/dist/shared/segment.js.map +1 -0
  90. package/dist/shared/sqlPlaceholders.d.ts +40 -0
  91. package/dist/shared/sqlPlaceholders.d.ts.map +1 -0
  92. package/dist/shared/sqlPlaceholders.js +276 -0
  93. package/dist/shared/sqlPlaceholders.js.map +1 -0
  94. package/package.json +10 -2
@@ -2,8 +2,70 @@
2
2
  // this file without crashing. A runtime error will be thrown inside `open()` if
3
3
  // execution actually occurs in a browser.
4
4
  const __isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
5
- import { normaliseParameters } from '../shared/parameterUtils.js';
5
+ import { toPostgresStatement } from '../shared/sqlPlaceholders.js';
6
6
  import { splitSqlStatements } from '../shared/splitSqlStatements.js';
7
+ import { validateSegment } from '../shared/segment.js';
8
+ import { getStorageLogger } from '../shared/logger.js';
9
+ /** The type-parser overrides `parsers` asks for, by OID, after checking each value. */
10
+ const typeParserOverrides = (parsers) => {
11
+ const overrides = new Map();
12
+ if (!parsers)
13
+ return overrides;
14
+ const choice = (field, allowed) => {
15
+ const value = parsers[field];
16
+ if (value === undefined)
17
+ return undefined;
18
+ if (!allowed.includes(value)) {
19
+ throw new Error(`typeParsers.${field} must be ${allowed.map((option) => `'${option}'`).join(' or ')}; got ${JSON.stringify(value)}.`);
20
+ }
21
+ return value;
22
+ };
23
+ const int8 = choice('int8', ['string', 'number', 'bigint']);
24
+ if (int8 === 'string')
25
+ overrides.set(20, (text) => text);
26
+ if (int8 === 'bigint')
27
+ overrides.set(20, (text) => BigInt(text));
28
+ if (int8 === 'number') {
29
+ overrides.set(20, (text) => {
30
+ const value = Number(text);
31
+ if (!Number.isSafeInteger(value)) {
32
+ throw new RangeError(`int8 value ${text} is outside Number's safe integer range; read it with typeParsers.int8 'bigint' or 'string'.`);
33
+ }
34
+ return value;
35
+ });
36
+ }
37
+ const numeric = choice('numeric', ['string', 'number']);
38
+ if (numeric === 'string')
39
+ overrides.set(1700, (text) => text);
40
+ if (numeric === 'number')
41
+ overrides.set(1700, (text) => Number(text));
42
+ if (choice('date', ['date', 'string']) === 'string')
43
+ overrides.set(1082, (text) => text);
44
+ if (choice('json', ['parse', 'string']) === 'string') {
45
+ overrides.set(114, (text) => text);
46
+ overrides.set(3802, (text) => text);
47
+ }
48
+ return overrides;
49
+ };
50
+ /** The options that configure a pool when one is made, which a pool of the caller's already has. */
51
+ const POOL_SETTINGS = [
52
+ 'connectionString',
53
+ 'host',
54
+ 'port',
55
+ 'database',
56
+ 'user',
57
+ 'password',
58
+ 'ssl',
59
+ 'max',
60
+ 'min',
61
+ 'idleTimeoutMillis',
62
+ 'connectionTimeoutMillis',
63
+ 'application_name',
64
+ 'statement_timeout',
65
+ 'query_timeout',
66
+ 'allowExitOnIdle',
67
+ 'typeParsers',
68
+ ];
7
69
  const ISOLATION_LEVELS = new Map([
8
70
  ['read uncommitted', 'READ UNCOMMITTED'],
9
71
  ['read committed', 'READ COMMITTED'],
@@ -34,64 +96,409 @@ const transactionStatus = (client) => {
34
96
  const read = client.getTransactionStatus;
35
97
  return typeof read === 'function' ? read.call(client) : null;
36
98
  };
37
- const isPositional = (statement) => statement.includes('?');
38
- const buildNamedStatement = (statement, named) => {
39
- const order = [];
40
- const text = statement.replace(/@([A-Za-z0-9_]+)/g, (_, name) => {
41
- order.push(name);
42
- return `$${order.length}`;
43
- });
44
- const values = order.map((key) => named[key]);
45
- return { text, values };
46
- };
47
- const buildPositionalStatement = (statement, positional) => {
48
- let index = 0;
49
- const text = statement.replace(/\?/g, () => {
50
- index += 1;
51
- return `$${index}`;
52
- });
53
- return { text, values: positional };
54
- };
55
- const prepareStatement = (statement, parameters) => {
56
- const { named, positional } = normaliseParameters(parameters);
57
- if (named) {
58
- return buildNamedStatement(statement, named);
59
- }
60
- if (positional) {
61
- return buildPositionalStatement(statement, positional);
62
- }
63
- if (isPositional(statement)) {
64
- return buildPositionalStatement(statement, []);
65
- }
66
- return { text: statement, values: [] };
67
- };
68
- /** Execute a mutation through a specific pool or transaction client. */
69
- const runWithExecutor = async (executor, statement, parameters) => {
99
+ /** The statement and values node-postgres receives: see toPostgresStatement. */
100
+ const prepareStatement = toPostgresStatement;
101
+ /** Execute a mutation through a pool or a transaction client. */
102
+ const runWith = async (query, statement, parameters) => {
70
103
  const { text, values } = prepareStatement(statement, parameters);
71
- const result = await executor.query(text, values);
104
+ const result = await query(text, values);
72
105
  const firstRow = result.rows?.[0];
73
106
  const lastInsertRowid = firstRow && typeof firstRow === 'object' && 'id' in firstRow
74
107
  ? firstRow.id
75
108
  : null;
76
109
  return { changes: result.rowCount ?? 0, lastInsertRowid };
77
110
  };
78
- /** Read one row through a specific pool or transaction client. */
79
- const getWithExecutor = async (executor, statement, parameters) => {
111
+ /** Read one row through a pool or a transaction client. */
112
+ const getWith = async (query, statement, parameters) => {
80
113
  const { text, values } = prepareStatement(statement, parameters);
81
- const result = await executor.query(text, values);
114
+ const result = await query(text, values);
82
115
  return result.rows?.[0] ?? null;
83
116
  };
84
- /** Read every matching row through a specific pool or transaction client. */
85
- const allWithExecutor = async (executor, statement, parameters) => {
117
+ /** Read every matching row through a pool or a transaction client. */
118
+ const allWith = async (query, statement, parameters) => {
86
119
  const { text, values } = prepareStatement(statement, parameters);
87
- const result = await executor.query(text, values);
120
+ const result = await query(text, values);
88
121
  return result.rows ?? [];
89
122
  };
90
- /** Execute a statement script through a specific pool or transaction client. */
91
- const execWithExecutor = async (executor, script) => {
123
+ /** Execute a statement script through a pool or a transaction client. */
124
+ const execWith = async (query, script) => {
92
125
  const statements = splitSqlStatements(script);
93
126
  for (const text of statements) {
94
- await executor.query(text);
127
+ await query(text, []);
128
+ }
129
+ };
130
+ /**
131
+ * The first `limit` words of a statement, past its comments: enough to tell
132
+ * what the statement is. String literals and dollar-quoted strings count as
133
+ * one word each and never read as a keyword.
134
+ */
135
+ const leadingWords = (statement, limit) => {
136
+ const words = [];
137
+ let i = 0;
138
+ const length = statement.length;
139
+ while (i < length && words.length < limit) {
140
+ const char = statement[i];
141
+ if (/\s/.test(char)) {
142
+ i += 1;
143
+ }
144
+ else if (char === '-' && statement[i + 1] === '-') {
145
+ const end = statement.indexOf('\n', i);
146
+ i = end === -1 ? length : end + 1;
147
+ }
148
+ else if (char === '/' && statement[i + 1] === '*') {
149
+ // PostgreSQL's block comments nest.
150
+ let depth = 1;
151
+ i += 2;
152
+ while (i < length && depth > 0) {
153
+ if (statement[i] === '/' && statement[i + 1] === '*') {
154
+ depth += 1;
155
+ i += 2;
156
+ }
157
+ else if (statement[i] === '*' && statement[i + 1] === '/') {
158
+ depth -= 1;
159
+ i += 2;
160
+ }
161
+ else {
162
+ i += 1;
163
+ }
164
+ }
165
+ }
166
+ else if (char === "'") {
167
+ i += 1;
168
+ while (i < length) {
169
+ if (statement[i] === "'" && statement[i + 1] === "'")
170
+ i += 2;
171
+ else if (statement[i] === "'")
172
+ break;
173
+ else
174
+ i += 1;
175
+ }
176
+ i += 1;
177
+ words.push({ keyword: "'", identifier: '' });
178
+ }
179
+ else if (char === '"') {
180
+ let identifier = '';
181
+ i += 1;
182
+ while (i < length) {
183
+ if (statement[i] === '"' && statement[i + 1] === '"') {
184
+ identifier += '"';
185
+ i += 2;
186
+ }
187
+ else if (statement[i] === '"') {
188
+ break;
189
+ }
190
+ else {
191
+ identifier += statement[i];
192
+ i += 1;
193
+ }
194
+ }
195
+ i += 1;
196
+ words.push({ keyword: '"', identifier });
197
+ }
198
+ else if (char === '$') {
199
+ const tag = /^\$[A-Za-z_]*\$/.exec(statement.slice(i));
200
+ if (tag) {
201
+ const end = statement.indexOf(tag[0], i + tag[0].length);
202
+ i = end === -1 ? length : end + tag[0].length;
203
+ words.push({ keyword: '$', identifier: '' });
204
+ }
205
+ else {
206
+ i += 1;
207
+ words.push({ keyword: '$', identifier: '' });
208
+ }
209
+ }
210
+ else if (/[A-Za-z_]/.test(char)) {
211
+ const word = /^[A-Za-z_][A-Za-z0-9_$]*/.exec(statement.slice(i))[0];
212
+ i += word.length;
213
+ words.push({ keyword: word.toUpperCase(), identifier: word.toLowerCase() });
214
+ }
215
+ else {
216
+ i += 1;
217
+ words.push({ keyword: char, identifier: '' });
218
+ }
219
+ }
220
+ return words;
221
+ };
222
+ /**
223
+ * The extension a `CREATE EXTENSION` statement creates when the statement
224
+ * names no schema (`SCHEMA` or `WITH SCHEMA`), or null for any other
225
+ * statement.
226
+ */
227
+ const unqualifiedCreateExtension = (statement) => {
228
+ const words = leadingWords(statement, 16);
229
+ if (words[0]?.keyword !== 'CREATE' || words[1]?.keyword !== 'EXTENSION')
230
+ return null;
231
+ let at = 2;
232
+ if (words[2]?.keyword === 'IF' && words[3]?.keyword === 'NOT' && words[4]?.keyword === 'EXISTS')
233
+ at = 5;
234
+ const name = words[at];
235
+ if (!name || (name.keyword !== '"' && !/^[A-Z_]/.test(name.keyword)))
236
+ return null;
237
+ if (words.slice(at + 1).some((word) => word.keyword === 'SCHEMA'))
238
+ return null;
239
+ return name.identifier;
240
+ };
241
+ /** An unknown thrown value as an Error. */
242
+ const asError = (value) => (value instanceof Error ? value : new Error(String(value)));
243
+ /**
244
+ * Puts a failed ROLLBACK's error on the transaction's error, as a
245
+ * non-enumerable `rollbackError` property, when that error can take one.
246
+ */
247
+ const keepRollbackError = (error, rollbackError) => {
248
+ if (typeof error !== 'object' || error === null || !Object.isExtensible(error))
249
+ return;
250
+ try {
251
+ Object.defineProperty(error, 'rollbackError', {
252
+ value: rollbackError,
253
+ enumerable: false,
254
+ configurable: true,
255
+ writable: true,
256
+ });
257
+ }
258
+ catch {
259
+ // A non-configurable rollbackError is already there: keep it.
260
+ }
261
+ };
262
+ /**
263
+ * Listens for a connection error on a checked-out client until `stop()`. pg
264
+ * emits a client's connection error (the server ending the session, a broken
265
+ * socket) on the client when no query is running, and pg-pool listens only on
266
+ * idle clients, so without this an unheard error event would end the process.
267
+ */
268
+ const watchClient = (client) => {
269
+ let failure;
270
+ // A pg-compatible pool whose clients are not event emitters emits nothing to hear.
271
+ if (typeof client.on !== 'function' || typeof client.removeListener !== 'function') {
272
+ return { failure: () => undefined, stop: () => undefined };
273
+ }
274
+ const onError = (error) => {
275
+ if (!failure)
276
+ failure = error;
277
+ };
278
+ client.on('error', onError);
279
+ return {
280
+ failure: () => failure,
281
+ stop: () => {
282
+ client.removeListener?.('error', onError);
283
+ },
284
+ };
285
+ };
286
+ /** Where an installed extension's objects are, and whether ALTER EXTENSION ... SET SCHEMA can move them. */
287
+ const EXTENSION_PLACE = 'SELECT n.nspname AS schema, e.extrelocatable AS relocatable FROM pg_extension e ' +
288
+ 'JOIN pg_namespace n ON n.oid = e.extnamespace WHERE e.extname = $1';
289
+ /**
290
+ * Runs a statement through a segment's connections with care for the one
291
+ * statement whose effect depends on where it runs: an unqualified
292
+ * `CREATE EXTENSION` installs into the current schema, which on a segment is
293
+ * the segment, where no other segment and not the host find the extension's
294
+ * types. Such a statement runs with `search_path` set to `public` for itself
295
+ * alone, as it would without a segment (an extension whose control file names
296
+ * a schema still goes there). Afterwards the adapter checks where the
297
+ * extension is, since `IF NOT EXISTS` also succeeds for one installed in a
298
+ * schema the segment cannot see.
299
+ */
300
+ class SegmentStatements {
301
+ constructor(segment) {
302
+ this.segment = segment;
303
+ /** Extensions already reported as out of the segment's reach. */
304
+ this.reported = new Set();
305
+ }
306
+ /**
307
+ * Runs `text` on the pool. The connection that runs a `CREATE EXTENSION`
308
+ * goes back to the pool unless its BEGIN or ROLLBACK failed or it broke
309
+ * while held, in which case it is closed.
310
+ */
311
+ async onPool(pool, text, values) {
312
+ const extension = unqualifiedCreateExtension(text);
313
+ if (extension === null) {
314
+ return pool.query(text, values);
315
+ }
316
+ const client = await pool.connect();
317
+ const watch = watchClient(client);
318
+ let brokenBy;
319
+ try {
320
+ return await this.inOwnTransaction(client, text, values, extension, (error) => {
321
+ if (!brokenBy)
322
+ brokenBy = error;
323
+ });
324
+ }
325
+ finally {
326
+ watch.stop();
327
+ client.release(brokenBy ?? watch.failure());
328
+ }
329
+ }
330
+ /** Runs `text` on a transaction's client, inside the caller's transaction. */
331
+ async inTransaction(client, text, values) {
332
+ const extension = unqualifiedCreateExtension(text);
333
+ if (extension === null) {
334
+ return client.query(text, values);
335
+ }
336
+ return this.withinTransaction(client, text, values, extension);
337
+ }
338
+ /**
339
+ * Runs `text` on the connection a `withConnection()` callback holds: inside
340
+ * the transaction the callback began, as `inTransaction` does, or outside
341
+ * one in a transaction of its own, as on the pool. On a pg that does not
342
+ * report a connection's transaction status (before 8.21), the session's
343
+ * search_path is set to `public` around the statement and set back after it.
344
+ * `onBroken` hears when the connection cannot be trusted any more, so
345
+ * `withConnection()` closes it instead of returning it to the pool.
346
+ */
347
+ async onConnection(client, text, values, onBroken) {
348
+ const extension = unqualifiedCreateExtension(text);
349
+ if (extension === null) {
350
+ return client.query(text, values);
351
+ }
352
+ switch (transactionStatus(client)) {
353
+ case 'T':
354
+ return this.withinTransaction(client, text, values, extension);
355
+ case 'I':
356
+ return this.inOwnTransaction(client, text, values, extension, onBroken);
357
+ case 'E':
358
+ // The transaction has failed: the statement fails with its reason.
359
+ return client.query(text, values);
360
+ default:
361
+ return this.aroundSession(client, text, values, extension, onBroken);
362
+ }
363
+ }
364
+ /**
365
+ * In a transaction of its own on `client`, whose COMMIT or ROLLBACK ends the
366
+ * setting. A BEGIN or ROLLBACK that fails leaves the connection in a state
367
+ * nobody knows, so `onBroken` hears of it.
368
+ */
369
+ async inOwnTransaction(client, text, values, extension, onBroken) {
370
+ try {
371
+ await client.query('BEGIN');
372
+ }
373
+ catch (error) {
374
+ onBroken(asError(error));
375
+ throw error;
376
+ }
377
+ let result;
378
+ try {
379
+ // set_config(..., true) is SET LOCAL: the COMMIT or ROLLBACK below ends it,
380
+ // so the connection goes back to the pool on the segment's search_path.
381
+ await client.query("SELECT set_config('search_path', 'public', true)");
382
+ result = await client.query(text, values);
383
+ await client.query('COMMIT');
384
+ }
385
+ catch (error) {
386
+ try {
387
+ await client.query('ROLLBACK');
388
+ }
389
+ catch (rollbackError) {
390
+ onBroken(asError(rollbackError));
391
+ keepRollbackError(error, asError(rollbackError));
392
+ }
393
+ throw error;
394
+ }
395
+ await this.checkReach((t, v) => client.query(t, v), extension);
396
+ return result;
397
+ }
398
+ /** Inside the transaction open on `client`, setting the search_path for the statement and back. */
399
+ async withinTransaction(client, text, values, extension) {
400
+ const saved = await client.query("SELECT current_setting('search_path') AS path");
401
+ const path = saved.rows[0]?.path;
402
+ await client.query("SELECT set_config('search_path', 'public', true)");
403
+ // A failure aborts the caller's transaction, whose rollback (or a
404
+ // rollback to a savepoint before this) undoes the setting.
405
+ const result = await client.query(text, values);
406
+ await client.query('SELECT set_config($1, $2, true)', ['search_path', typeof path === 'string' ? path : `${this.segment}, public`]);
407
+ await this.checkReach((t, v) => client.query(t, v), extension);
408
+ return result;
409
+ }
410
+ /**
411
+ * With the session's search_path set to `public` for the statement, and set
412
+ * back after it. When the statement fails inside a transaction, setting it
413
+ * back fails too, and that transaction's rollback restores it. Without the
414
+ * transaction status this cannot tell that case from a connection whose
415
+ * search_path stays on `public`, so a failed set-back tells `onBroken`, and
416
+ * the connection is closed rather than returned to the pool outside the
417
+ * segment.
418
+ */
419
+ async aroundSession(client, text, values, extension, onBroken) {
420
+ const saved = await client.query("SELECT current_setting('search_path') AS path");
421
+ const savedPath = saved.rows[0]?.path;
422
+ const path = typeof savedPath === 'string' ? savedPath : `${this.segment}, public`;
423
+ await client.query("SELECT set_config('search_path', 'public', false)");
424
+ const setBack = async () => {
425
+ try {
426
+ await client.query('SELECT set_config($1, $2, false)', ['search_path', path]);
427
+ }
428
+ catch (error) {
429
+ onBroken(asError(error));
430
+ throw error;
431
+ }
432
+ };
433
+ let result;
434
+ try {
435
+ result = await client.query(text, values);
436
+ }
437
+ catch (error) {
438
+ await setBack().catch(() => undefined);
439
+ throw error;
440
+ }
441
+ await setBack();
442
+ await this.checkReach((t, v) => client.query(t, v), extension);
443
+ return result;
444
+ }
445
+ async checkReach(query, extension) {
446
+ if (this.reported.has(extension))
447
+ return;
448
+ const found = await query(EXTENSION_PLACE, [extension]);
449
+ const place = found.rows[0];
450
+ if (!place || place.relocatable !== true)
451
+ return;
452
+ if (place.schema === 'public' || place.schema === this.segment)
453
+ return;
454
+ this.reported.add(extension);
455
+ getStorageLogger().warn({ adapter: 'postgres', segment: this.segment, extension, schema: String(place.schema) }, `PostgreSQL extension "${extension}" is installed in schema "${String(place.schema)}", which segment ` +
456
+ `"${this.segment}" does not reach: its search_path is ${this.segment}, public, so the extension's types and functions ` +
457
+ `do not resolve here. ALTER EXTENSION "${extension}" SET SCHEMA public moves it where every segment finds it.`);
458
+ }
459
+ }
460
+ /** A pool on which `end()` has been called. */
461
+ const hasEnded = (pool) => pool.ending === true || pool.ended === true;
462
+ /** The listener the adapter puts on a pool it owns when no `onError` was given. */
463
+ const warnOfIdleError = (error) => {
464
+ const message = error instanceof Error ? error.message : String(error);
465
+ getStorageLogger().warn({ adapter: 'postgres', error: message }, `PostgreSQL: an idle connection of the pool failed; the pool opens another when one is needed. ${message}`);
466
+ };
467
+ /** The value of an environment variable, where there is an environment. */
468
+ const readEnv = (key) => typeof process !== 'undefined' && process.env ? process.env[key] : undefined;
469
+ /**
470
+ * Refuses a connection string that sets its own startup options when a
471
+ * segment needs that parameter: pg copies a connection string's parameters
472
+ * over the explicit configuration, so its `options` would replace the
473
+ * segment's search_path.
474
+ */
475
+ const refuseOwnStartupOptions = (connectionString) => {
476
+ if (connectionString && /[?&]options=/i.test(connectionString)) {
477
+ throw new Error('PostgresAdapter: a segment cannot be used with a connection string that sets its own startup options (options=...): ' +
478
+ "pg sends those in place of the segment's search_path. Move them out of the connection string, into PGOPTIONS.");
479
+ }
480
+ };
481
+ /**
482
+ * Creates a segment's schema when it is missing. The check comes first, so a
483
+ * role without CREATE on the database can open a segment that exists: CREATE
484
+ * SCHEMA IF NOT EXISTS checks that privilege before it looks for the schema.
485
+ */
486
+ const createSchema = async (client, segment) => {
487
+ const found = await client.query('SELECT 1 FROM pg_namespace WHERE nspname = $1', [segment]);
488
+ if (found.rows.length > 0) {
489
+ return;
490
+ }
491
+ try {
492
+ // validateSegment admits lower-case letters, digits and underscores alone.
493
+ await client.query(`CREATE SCHEMA IF NOT EXISTS "${segment}"`);
494
+ }
495
+ catch (error) {
496
+ // Two processes that open one new segment at once can both find it
497
+ // missing; the slower CREATE then fails on pg_namespace's unique index.
498
+ const code = error?.code;
499
+ if (code !== '23505' && code !== '42P06') {
500
+ throw error;
501
+ }
95
502
  }
96
503
  };
97
504
  /**
@@ -103,12 +510,14 @@ const execWithExecutor = async (executor, script) => {
103
510
  * adapter's pool, or the held connection the transaction ran on.
104
511
  */
105
512
  class PostgresTransactionAdapter {
106
- constructor(client, capabilities, afterEnd) {
107
- this.client = client;
513
+ constructor(client, capabilities, segment, afterEnd) {
108
514
  this.capabilities = capabilities;
109
515
  this.afterEnd = afterEnd;
110
516
  this.kind = 'postgres';
111
517
  this.ended = false;
518
+ this.query = segment
519
+ ? (text, values) => segment.inTransaction(client, text, values)
520
+ : (text, values) => client.query(text, values);
112
521
  }
113
522
  async open() {
114
523
  // The owning PostgresAdapter opens the pool before creating this view.
@@ -116,22 +525,22 @@ class PostgresTransactionAdapter {
116
525
  async run(statement, parameters) {
117
526
  if (this.ended)
118
527
  return this.afterEnd.run(statement, parameters);
119
- return runWithExecutor(this.client, statement, parameters);
528
+ return runWith(this.query, statement, parameters);
120
529
  }
121
530
  async get(statement, parameters) {
122
531
  if (this.ended)
123
532
  return this.afterEnd.get(statement, parameters);
124
- return getWithExecutor(this.client, statement, parameters);
533
+ return getWith(this.query, statement, parameters);
125
534
  }
126
535
  async all(statement, parameters) {
127
536
  if (this.ended)
128
537
  return this.afterEnd.all(statement, parameters);
129
- return allWithExecutor(this.client, statement, parameters);
538
+ return allWith(this.query, statement, parameters);
130
539
  }
131
540
  async exec(script) {
132
541
  if (this.ended)
133
542
  return this.afterEnd.exec(script);
134
- await execWithExecutor(this.client, script);
543
+ await execWith(this.query, script);
135
544
  }
136
545
  async transaction(fn) {
137
546
  // After the transaction has ended, a new one begins where afterEnd runs it.
@@ -149,51 +558,6 @@ class PostgresTransactionAdapter {
149
558
  this.ended = true;
150
559
  }
151
560
  }
152
- /** An unknown thrown value as an Error. */
153
- const asError = (value) => (value instanceof Error ? value : new Error(String(value)));
154
- /**
155
- * Puts a failed ROLLBACK's error on the transaction's error, as a
156
- * non-enumerable `rollbackError` property, when that error can take one.
157
- */
158
- const keepRollbackError = (error, rollbackError) => {
159
- if (typeof error !== 'object' || error === null || !Object.isExtensible(error))
160
- return;
161
- try {
162
- Object.defineProperty(error, 'rollbackError', {
163
- value: rollbackError,
164
- enumerable: false,
165
- configurable: true,
166
- writable: true,
167
- });
168
- }
169
- catch {
170
- // A non-configurable rollbackError is already there: keep it.
171
- }
172
- };
173
- /**
174
- * Listens for a connection error on a checked-out client until `stop()`. pg
175
- * emits a client's connection error (the server ending the session, a broken
176
- * socket) on the client when no query is running, and pg-pool listens only on
177
- * idle clients, so without this an unheard error event would end the process.
178
- */
179
- const watchClient = (client) => {
180
- let failure;
181
- // A pg-compatible pool whose clients are not event emitters emits nothing to hear.
182
- if (typeof client.on !== 'function' || typeof client.removeListener !== 'function') {
183
- return { failure: () => undefined, stop: () => undefined };
184
- }
185
- const onError = (error) => {
186
- if (!failure)
187
- failure = error;
188
- };
189
- client.on('error', onError);
190
- return {
191
- failure: () => failure,
192
- stop: () => {
193
- client.removeListener('error', onError);
194
- },
195
- };
196
- };
197
561
  /**
198
562
  * Runs `fn` in a transaction that `begin` opens on `client`: COMMIT when `fn`
199
563
  * resolves, ROLLBACK when `fn` or COMMIT throws, and that error is rethrown.
@@ -202,7 +566,7 @@ const watchClient = (client) => {
202
566
  * pool. When ROLLBACK fails, the original error is still the one thrown, with
203
567
  * the ROLLBACK error on it as `rollbackError`.
204
568
  */
205
- const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroken) => {
569
+ const runTransaction = async (client, begin, fn, capabilities, segment, afterEnd, onBroken) => {
206
570
  try {
207
571
  await client.query(begin);
208
572
  }
@@ -210,7 +574,7 @@ const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroke
210
574
  onBroken(asError(error));
211
575
  throw error;
212
576
  }
213
- const handle = new PostgresTransactionAdapter(client, capabilities, afterEnd);
577
+ const handle = new PostgresTransactionAdapter(client, capabilities, segment, afterEnd);
214
578
  try {
215
579
  const result = await fn(handle);
216
580
  await client.query('COMMIT');
@@ -224,7 +588,7 @@ const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroke
224
588
  const failure = asError(rollbackError);
225
589
  onBroken(failure);
226
590
  keepRollbackError(error, failure);
227
- console.warn(`[StorageAdapter] PostgreSQL ROLLBACK failed after a transaction error, so the connection was closed; the transaction's own error is rethrown. ${failure.message}`);
591
+ getStorageLogger().warn({ adapter: 'postgres', error: failure.message }, `PostgreSQL ROLLBACK failed after a transaction error, so the connection was closed; the transaction's own error is rethrown. ${failure.message}`);
228
592
  }
229
593
  throw error;
230
594
  }
@@ -238,27 +602,37 @@ const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroke
238
602
  * closed.
239
603
  */
240
604
  class PostgresConnectionAdapter {
241
- constructor(client, capabilities) {
605
+ constructor(client, capabilities, segment) {
242
606
  this.client = client;
243
607
  this.capabilities = capabilities;
608
+ this.segment = segment;
244
609
  this.kind = 'postgres';
245
610
  this.ended = false;
246
611
  this.transactionOpen = false;
612
+ /** Keeps the first reason the connection cannot be trusted any more. */
613
+ this.onBroken = (error) => {
614
+ if (!this.brokenBy)
615
+ this.brokenBy = error;
616
+ };
617
+ this.query = (text, values) => {
618
+ const live = this.live();
619
+ return segment ? segment.onConnection(live, text, values, this.onBroken) : live.query(text, values);
620
+ };
247
621
  }
248
622
  async open() {
249
623
  // withConnection() checks the connection out before the callback runs.
250
624
  }
251
625
  async run(statement, parameters) {
252
- return runWithExecutor(this.live(), statement, parameters);
626
+ return runWith(this.query, statement, parameters);
253
627
  }
254
628
  async get(statement, parameters) {
255
- return getWithExecutor(this.live(), statement, parameters);
629
+ return getWith(this.query, statement, parameters);
256
630
  }
257
631
  async all(statement, parameters) {
258
- return allWithExecutor(this.live(), statement, parameters);
632
+ return allWith(this.query, statement, parameters);
259
633
  }
260
634
  async exec(script) {
261
- await execWithExecutor(this.live(), script);
635
+ await execWith(this.query, script);
262
636
  }
263
637
  async transaction(fn, options) {
264
638
  const begin = beginStatement(options);
@@ -268,10 +642,7 @@ class PostgresConnectionAdapter {
268
642
  }
269
643
  this.transactionOpen = true;
270
644
  try {
271
- return await runTransaction(client, begin, fn, this.capabilities, this, (error) => {
272
- if (!this.brokenBy)
273
- this.brokenBy = error;
274
- });
645
+ return await runTransaction(client, begin, fn, this.capabilities, this.segment, this, this.onBroken);
275
646
  }
276
647
  finally {
277
648
  this.transactionOpen = false;
@@ -385,6 +756,18 @@ class PostgresConnectionAdapter {
385
756
  * });
386
757
  * ```
387
758
  *
759
+ * ### A pool the host already holds
760
+ * ```typescript
761
+ * const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
762
+ * // The adapter runs on the host's pool; close() leaves it open.
763
+ * const db = new PostgresAdapter({ pool, onError: (error) => console.warn('idle connection failed', error) });
764
+ * ```
765
+ *
766
+ * ### A segment: the adapter's tables in a schema of their own
767
+ * ```typescript
768
+ * const agentos = createPostgresAdapter({ connectionString: process.env.DATABASE_URL, segment: 'agentos' });
769
+ * ```
770
+ *
388
771
  * ## Connection failures
389
772
  * - The pool drops a connection that fails, and the next query opens a new one.
390
773
  * - The adapter never falls back to SQLite. `resolveStorageAdapter()` without
@@ -418,16 +801,60 @@ export class PostgresAdapter {
418
801
  'arrays', // Native array data types
419
802
  'prepared' // Prepared statements for security/performance
420
803
  ]);
804
+ /**
805
+ * The pool statements run on, or null while the adapter is closed. Hosts
806
+ * written against 0.6 hand the adapter a Pool by setting this field before
807
+ * `open()`; the adapter then runs on that Pool and ends it on `close()`, as
808
+ * 0.6 did. Keep its name, and keep it a plain field that starts as null.
809
+ */
421
810
  this.pool = null;
422
811
  this.pgModule = null;
812
+ /** The error listener the adapter has on a pool, and that pool. */
813
+ this.listening = null;
814
+ this.opening = null;
815
+ this.closing = null;
816
+ /** The runner of statements outside a transaction. */
817
+ this.query = (text, values) => {
818
+ const pool = this.requirePool();
819
+ return this.segmentStatements ? this.segmentStatements.onPool(pool, text, values) : pool.query(text, values);
820
+ };
423
821
  // Support both string and object initialization
424
- if (typeof options === 'string') {
425
- this.options = { connectionString: options };
822
+ this.options = typeof options === 'string' ? { connectionString: options } : { ...options };
823
+ const segment = this.options.segment === undefined ? undefined : validateSegment(this.options.segment);
824
+ this.segmentStatements = segment === undefined ? null : new SegmentStatements(segment);
825
+ const given = this.options.pool;
826
+ if (given) {
827
+ if (segment !== undefined) {
828
+ throw new Error("PostgresAdapter: a segment cannot run on a pool its caller made: the segment's search_path is set when each " +
829
+ 'connection of the pool starts, and the adapter does not start the connections of a pool it did not make. ' +
830
+ 'Give the connection settings and the segment instead of the pool.');
831
+ }
832
+ const settings = POOL_SETTINGS.filter((key) => this.options[key] !== undefined);
833
+ if (settings.length > 0) {
834
+ throw new Error(`PostgresAdapter: ${settings.join(', ')} cannot be given with pool: they configure a pool when one is made, ` +
835
+ 'and the pool given was made by its caller. Set them on that pool' +
836
+ (settings.includes('typeParsers') ? " (typeParsers through the pool's own types option)." : '.'));
837
+ }
838
+ this.givenPool = given;
839
+ this.ownsPool = this.options.ownsPool === true;
840
+ this.pool = given;
841
+ this.listenOn(given);
426
842
  }
427
843
  else {
428
- this.options = options;
844
+ this.givenPool = null;
845
+ this.ownsPool = true;
846
+ if (segment !== undefined) {
847
+ refuseOwnStartupOptions(this.options.connectionString);
848
+ }
429
849
  }
430
850
  }
851
+ /**
852
+ * The pool the adapter runs on: the pool it was given, or the pool it made
853
+ * while it is open. Null while the adapter is closed.
854
+ */
855
+ getPool() {
856
+ return this.pool;
857
+ }
431
858
  /**
432
859
  * Lazy loader for pg module to keep the dependency optional and prevent bundling in browser builds.
433
860
  */
@@ -447,21 +874,69 @@ export class PostgresAdapter {
447
874
  throw new Error(`[StorageAdapter] Failed to load pg module. Install it with: npm install pg. Error: ${error}`);
448
875
  }
449
876
  }
450
- async open(openOptions) {
877
+ /**
878
+ * Opens the adapter: makes its pool and checks that a connection can be
879
+ * made (and with a segment, creates the segment's schema when it is
880
+ * missing), or takes up the pool it was given. A failure ends the pool it
881
+ * made, so a later `open()` starts again. Two calls at once open once.
882
+ */
883
+ open(openOptions) {
451
884
  if (this.pool) {
452
- return;
885
+ return Promise.resolve();
453
886
  }
887
+ if (!this.opening) {
888
+ this.opening = this.openNow(openOptions).finally(() => {
889
+ this.opening = null;
890
+ });
891
+ }
892
+ return this.opening;
893
+ }
894
+ async openNow(openOptions) {
454
895
  if (__isBrowser) {
455
896
  throw new Error('[StorageAdapter] PostgreSQL adapter cannot be opened in a browser environment.');
456
897
  }
898
+ if (this.givenPool) {
899
+ if (hasEnded(this.givenPool)) {
900
+ throw new Error('PostgresAdapter: the pool it was given has ended, so the adapter cannot open on it again.');
901
+ }
902
+ this.pool = this.givenPool;
903
+ this.listenOn(this.givenPool);
904
+ return;
905
+ }
906
+ const connectionString = openOptions?.connectionString ?? this.options.connectionString;
907
+ if (this.segmentStatements) {
908
+ refuseOwnStartupOptions(connectionString);
909
+ }
457
910
  // Load pg module dynamically
458
911
  const pg = await this.loadPgModule();
459
- const { Pool } = pg;
460
- // Build pool configuration
912
+ const pool = new pg.Pool(this.poolConfig(pg, connectionString));
913
+ this.listenOn(pool);
914
+ try {
915
+ // Test connection
916
+ const client = await pool.connect();
917
+ try {
918
+ if (this.segmentStatements) {
919
+ await createSchema(client, this.segmentStatements.segment);
920
+ }
921
+ }
922
+ finally {
923
+ client.release();
924
+ }
925
+ }
926
+ catch (error) {
927
+ // The listener stays on the pool through its end; the adapter forgets it.
928
+ this.listening = null;
929
+ await pool.end().catch(() => undefined);
930
+ throw error;
931
+ }
932
+ this.pool = pool;
933
+ }
934
+ /** The configuration of the pool the adapter makes. */
935
+ poolConfig(pg, connectionString) {
461
936
  const poolConfig = {};
462
937
  // Use connection string if provided
463
- if (this.options.connectionString || openOptions?.connectionString) {
464
- poolConfig.connectionString = openOptions?.connectionString ?? this.options.connectionString;
938
+ if (connectionString) {
939
+ poolConfig.connectionString = connectionString;
465
940
  }
466
941
  else {
467
942
  // Build from individual options
@@ -494,30 +969,49 @@ export class PostgresAdapter {
494
969
  if (this.options.allowExitOnIdle !== undefined) {
495
970
  poolConfig.allowExitOnIdle = this.options.allowExitOnIdle;
496
971
  }
972
+ const overrides = typeParserOverrides(this.options.typeParsers);
973
+ if (overrides.size > 0) {
974
+ const defaults = pg.types;
975
+ poolConfig.types = {
976
+ getTypeParser: (oid, format) => (format === undefined || format === 'text') && overrides.has(oid)
977
+ ? overrides.get(oid)
978
+ : defaults.getTypeParser(oid, format),
979
+ };
980
+ }
497
981
  // SSL configuration
498
982
  if (this.options.ssl !== undefined) {
499
983
  poolConfig.ssl = this.options.ssl;
500
984
  }
501
- this.pool = new Pool(poolConfig);
502
- // Test connection
503
- const client = await this.pool.connect();
504
- client.release();
985
+ if (this.segmentStatements) {
986
+ // Every connection the pool opens, a replacement included, starts on the
987
+ // segment. A startup setting is the connection's default, so RESET and
988
+ // DISCARD ALL return to it. pg reads PGOPTIONS only for a connection
989
+ // without options of its own, so its options are kept here.
990
+ const searchPath = `-c search_path=${this.segmentStatements.segment},public`;
991
+ const inherited = readEnv('PGOPTIONS')?.trim();
992
+ poolConfig.options = inherited ? `${inherited} ${searchPath}` : searchPath;
993
+ }
994
+ return poolConfig;
995
+ }
996
+ /** Puts the adapter's error listener on `pool`: `onError`, or the warning on a pool it owns. */
997
+ listenOn(pool) {
998
+ const listener = this.options.onError ?? (this.ownsPool ? warnOfIdleError : null);
999
+ if (!listener || typeof pool.on !== 'function' || this.listening?.pool === pool)
1000
+ return;
1001
+ pool.on('error', listener);
1002
+ this.listening = { pool, listener };
505
1003
  }
506
1004
  async run(statement, parameters) {
507
- const executor = await this.getExecutor();
508
- return runWithExecutor(executor, statement, parameters);
1005
+ return runWith(this.query, statement, parameters);
509
1006
  }
510
1007
  async get(statement, parameters) {
511
- const executor = await this.getExecutor();
512
- return getWithExecutor(executor, statement, parameters);
1008
+ return getWith(this.query, statement, parameters);
513
1009
  }
514
1010
  async all(statement, parameters) {
515
- const executor = await this.getExecutor();
516
- return allWithExecutor(executor, statement, parameters);
1011
+ return allWith(this.query, statement, parameters);
517
1012
  }
518
1013
  async exec(script) {
519
- const executor = await this.getExecutor();
520
- await execWithExecutor(executor, script);
1014
+ await execWith(this.query, script);
521
1015
  }
522
1016
  /**
523
1017
  * Runs `fn` in a transaction on one pooled connection: COMMIT when it
@@ -536,7 +1030,7 @@ export class PostgresAdapter {
536
1030
  const watch = watchClient(client);
537
1031
  let brokenBy;
538
1032
  try {
539
- return await runTransaction(client, begin, fn, this.capabilities, this, (error) => {
1033
+ return await runTransaction(client, begin, fn, this.capabilities, this.segmentStatements, this, (error) => {
540
1034
  if (!brokenBy)
541
1035
  brokenBy = error;
542
1036
  });
@@ -561,12 +1055,14 @@ export class PostgresAdapter {
561
1055
  * connection is closed, which rolls the transaction back, and the call
562
1056
  * rejects (pg 8.21 and later report the transaction status). The connection
563
1057
  * refuses statements once `fn` has ended. A connection that broke while `fn`
564
- * held it (the server ended the session) is closed, not returned.
1058
+ * held it (the server ended the session) is closed, not returned. On a
1059
+ * segmented adapter the connection starts in the segment, as every
1060
+ * connection of its pool does.
565
1061
  */
566
1062
  async withConnection(fn) {
567
1063
  const client = await this.requirePool().connect();
568
1064
  const watch = watchClient(client);
569
- const connection = new PostgresConnectionAdapter(client, this.capabilities);
1065
+ const connection = new PostgresConnectionAdapter(client, this.capabilities, this.segmentStatements);
570
1066
  let releaseError;
571
1067
  try {
572
1068
  const result = await fn(connection);
@@ -591,21 +1087,52 @@ export class PostgresAdapter {
591
1087
  client.release(releaseError ?? connection.brokenBy ?? watch.failure());
592
1088
  }
593
1089
  }
594
- /** The pool's counts, or null when the adapter is not open. */
1090
+ /**
1091
+ * The pool's counts, or null when the adapter is not open. A pool of the
1092
+ * caller's that does not count its connections, as pg's Pool does, reads 0.
1093
+ */
595
1094
  getPoolStats() {
596
- if (!this.pool) {
1095
+ const pool = this.pool;
1096
+ if (!pool) {
597
1097
  return null;
598
1098
  }
599
- return { total: this.pool.totalCount, idle: this.pool.idleCount, waiting: this.pool.waitingCount };
1099
+ return { total: pool.totalCount ?? 0, idle: pool.idleCount ?? 0, waiting: pool.waitingCount ?? 0 };
600
1100
  }
601
- async close() {
602
- if (this.pool) {
603
- await this.pool.end();
604
- this.pool = null;
1101
+ /**
1102
+ * Closes the adapter. A pool the adapter made or owns is ended, once; a pool
1103
+ * of its caller's that it does not own is left open, with the adapter's
1104
+ * listener taken off it. A call made while a close is running waits for it.
1105
+ */
1106
+ close() {
1107
+ if (!this.closing) {
1108
+ this.closing = this.closeNow().finally(() => {
1109
+ this.closing = null;
1110
+ });
605
1111
  }
1112
+ return this.closing;
606
1113
  }
607
- async getExecutor() {
608
- return this.requirePool();
1114
+ async closeNow() {
1115
+ if (this.opening) {
1116
+ await this.opening.catch(() => undefined);
1117
+ }
1118
+ const pool = this.pool;
1119
+ this.pool = null;
1120
+ if (!pool) {
1121
+ return;
1122
+ }
1123
+ const listening = this.listening?.pool === pool ? this.listening : null;
1124
+ this.listening = null;
1125
+ if (this.ownsPool) {
1126
+ // The listener stays on: a connection the pool lets go can still report
1127
+ // its end after end() has resolved.
1128
+ if (!hasEnded(pool)) {
1129
+ await pool.end();
1130
+ }
1131
+ return;
1132
+ }
1133
+ if (listening && typeof pool.removeListener === 'function') {
1134
+ pool.removeListener('error', listening.listener);
1135
+ }
609
1136
  }
610
1137
  requirePool() {
611
1138
  if (!this.pool) {