@framers/sql-storage-adapter 0.6.16 → 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 (72) hide show
  1. package/README.md +94 -3
  2. package/dist/adapters/betterSqliteAdapter.d.ts +18 -2
  3. package/dist/adapters/betterSqliteAdapter.d.ts.map +1 -1
  4. package/dist/adapters/betterSqliteAdapter.js +13 -5
  5. package/dist/adapters/betterSqliteAdapter.js.map +1 -1
  6. package/dist/adapters/capacitorSqliteAdapter.d.ts +8 -0
  7. package/dist/adapters/capacitorSqliteAdapter.d.ts.map +1 -1
  8. package/dist/adapters/capacitorSqliteAdapter.js +4 -1
  9. package/dist/adapters/capacitorSqliteAdapter.js.map +1 -1
  10. package/dist/adapters/electron/electronMainAdapter.d.ts +10 -0
  11. package/dist/adapters/electron/electronMainAdapter.d.ts.map +1 -1
  12. package/dist/adapters/electron/electronMainAdapter.js +21 -12
  13. package/dist/adapters/electron/electronMainAdapter.js.map +1 -1
  14. package/dist/adapters/indexedDbAdapter.d.ts +198 -54
  15. package/dist/adapters/indexedDbAdapter.d.ts.map +1 -1
  16. package/dist/adapters/indexedDbAdapter.js +549 -129
  17. package/dist/adapters/indexedDbAdapter.js.map +1 -1
  18. package/dist/adapters/postgresAdapter.d.ts +143 -4
  19. package/dist/adapters/postgresAdapter.d.ts.map +1 -1
  20. package/dist/adapters/postgresAdapter.js +612 -107
  21. package/dist/adapters/postgresAdapter.js.map +1 -1
  22. package/dist/adapters/sqlJsAdapter.d.ts +17 -3
  23. package/dist/adapters/sqlJsAdapter.d.ts.map +1 -1
  24. package/dist/adapters/sqlJsAdapter.js +15 -3
  25. package/dist/adapters/sqlJsAdapter.js.map +1 -1
  26. package/dist/core/database.d.ts +28 -8
  27. package/dist/core/database.d.ts.map +1 -1
  28. package/dist/core/database.js +34 -5
  29. package/dist/core/database.js.map +1 -1
  30. package/dist/core/resolver.d.ts +14 -1
  31. package/dist/core/resolver.d.ts.map +1 -1
  32. package/dist/core/resolver.js +28 -8
  33. package/dist/core/resolver.js.map +1 -1
  34. package/dist/features/kv/idb.d.ts +79 -0
  35. package/dist/features/kv/idb.d.ts.map +1 -0
  36. package/dist/features/kv/idb.js +14 -0
  37. package/dist/features/kv/idb.js.map +1 -0
  38. package/dist/features/kv/index.d.ts +14 -0
  39. package/dist/features/kv/index.d.ts.map +1 -0
  40. package/dist/features/kv/index.js +14 -0
  41. package/dist/features/kv/index.js.map +1 -0
  42. package/dist/features/kv/indexedDbKeyValueStore.d.ts +77 -0
  43. package/dist/features/kv/indexedDbKeyValueStore.d.ts.map +1 -0
  44. package/dist/features/kv/indexedDbKeyValueStore.js +127 -0
  45. package/dist/features/kv/indexedDbKeyValueStore.js.map +1 -0
  46. package/dist/features/kv/keyValueStore.d.ts +48 -0
  47. package/dist/features/kv/keyValueStore.d.ts.map +1 -0
  48. package/dist/features/kv/keyValueStore.js +54 -0
  49. package/dist/features/kv/keyValueStore.js.map +1 -0
  50. package/dist/features/kv/sqlKeyValueStore.d.ts +34 -0
  51. package/dist/features/kv/sqlKeyValueStore.d.ts.map +1 -0
  52. package/dist/features/kv/sqlKeyValueStore.js +91 -0
  53. package/dist/features/kv/sqlKeyValueStore.js.map +1 -0
  54. package/dist/features/kv/valueCodec.d.ts +35 -0
  55. package/dist/features/kv/valueCodec.d.ts.map +1 -0
  56. package/dist/features/kv/valueCodec.js +462 -0
  57. package/dist/features/kv/valueCodec.js.map +1 -0
  58. package/dist/features/migrations/dataExport.js +9 -6
  59. package/dist/features/migrations/dataExport.js.map +1 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +3 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/shared/postgresTarget.d.ts +4 -3
  65. package/dist/shared/postgresTarget.d.ts.map +1 -1
  66. package/dist/shared/postgresTarget.js +6 -4
  67. package/dist/shared/postgresTarget.js.map +1 -1
  68. package/dist/shared/segment.d.ts +43 -0
  69. package/dist/shared/segment.d.ts.map +1 -0
  70. package/dist/shared/segment.js +77 -0
  71. package/dist/shared/segment.js.map +1 -0
  72. package/package.json +10 -2
@@ -4,6 +4,7 @@
4
4
  const __isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
5
5
  import { toPostgresStatement } from '../shared/sqlPlaceholders.js';
6
6
  import { splitSqlStatements } from '../shared/splitSqlStatements.js';
7
+ import { validateSegment } from '../shared/segment.js';
7
8
  import { getStorageLogger } from '../shared/logger.js';
8
9
  /** The type-parser overrides `parsers` asks for, by OID, after checking each value. */
9
10
  const typeParserOverrides = (parsers) => {
@@ -46,6 +47,25 @@ const typeParserOverrides = (parsers) => {
46
47
  }
47
48
  return overrides;
48
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
+ ];
49
69
  const ISOLATION_LEVELS = new Map([
50
70
  ['read uncommitted', 'READ UNCOMMITTED'],
51
71
  ['read committed', 'READ COMMITTED'],
@@ -78,33 +98,407 @@ const transactionStatus = (client) => {
78
98
  };
79
99
  /** The statement and values node-postgres receives: see toPostgresStatement. */
80
100
  const prepareStatement = toPostgresStatement;
81
- /** Execute a mutation through a specific pool or transaction client. */
82
- const runWithExecutor = async (executor, statement, parameters) => {
101
+ /** Execute a mutation through a pool or a transaction client. */
102
+ const runWith = async (query, statement, parameters) => {
83
103
  const { text, values } = prepareStatement(statement, parameters);
84
- const result = await executor.query(text, values);
104
+ const result = await query(text, values);
85
105
  const firstRow = result.rows?.[0];
86
106
  const lastInsertRowid = firstRow && typeof firstRow === 'object' && 'id' in firstRow
87
107
  ? firstRow.id
88
108
  : null;
89
109
  return { changes: result.rowCount ?? 0, lastInsertRowid };
90
110
  };
91
- /** Read one row through a specific pool or transaction client. */
92
- const getWithExecutor = async (executor, statement, parameters) => {
111
+ /** Read one row through a pool or a transaction client. */
112
+ const getWith = async (query, statement, parameters) => {
93
113
  const { text, values } = prepareStatement(statement, parameters);
94
- const result = await executor.query(text, values);
114
+ const result = await query(text, values);
95
115
  return result.rows?.[0] ?? null;
96
116
  };
97
- /** Read every matching row through a specific pool or transaction client. */
98
- 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) => {
99
119
  const { text, values } = prepareStatement(statement, parameters);
100
- const result = await executor.query(text, values);
120
+ const result = await query(text, values);
101
121
  return result.rows ?? [];
102
122
  };
103
- /** Execute a statement script through a specific pool or transaction client. */
104
- const execWithExecutor = async (executor, script) => {
123
+ /** Execute a statement script through a pool or a transaction client. */
124
+ const execWith = async (query, script) => {
105
125
  const statements = splitSqlStatements(script);
106
126
  for (const text of statements) {
107
- 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
+ }
108
502
  }
109
503
  };
110
504
  /**
@@ -116,12 +510,14 @@ const execWithExecutor = async (executor, script) => {
116
510
  * adapter's pool, or the held connection the transaction ran on.
117
511
  */
118
512
  class PostgresTransactionAdapter {
119
- constructor(client, capabilities, afterEnd) {
120
- this.client = client;
513
+ constructor(client, capabilities, segment, afterEnd) {
121
514
  this.capabilities = capabilities;
122
515
  this.afterEnd = afterEnd;
123
516
  this.kind = 'postgres';
124
517
  this.ended = false;
518
+ this.query = segment
519
+ ? (text, values) => segment.inTransaction(client, text, values)
520
+ : (text, values) => client.query(text, values);
125
521
  }
126
522
  async open() {
127
523
  // The owning PostgresAdapter opens the pool before creating this view.
@@ -129,22 +525,22 @@ class PostgresTransactionAdapter {
129
525
  async run(statement, parameters) {
130
526
  if (this.ended)
131
527
  return this.afterEnd.run(statement, parameters);
132
- return runWithExecutor(this.client, statement, parameters);
528
+ return runWith(this.query, statement, parameters);
133
529
  }
134
530
  async get(statement, parameters) {
135
531
  if (this.ended)
136
532
  return this.afterEnd.get(statement, parameters);
137
- return getWithExecutor(this.client, statement, parameters);
533
+ return getWith(this.query, statement, parameters);
138
534
  }
139
535
  async all(statement, parameters) {
140
536
  if (this.ended)
141
537
  return this.afterEnd.all(statement, parameters);
142
- return allWithExecutor(this.client, statement, parameters);
538
+ return allWith(this.query, statement, parameters);
143
539
  }
144
540
  async exec(script) {
145
541
  if (this.ended)
146
542
  return this.afterEnd.exec(script);
147
- await execWithExecutor(this.client, script);
543
+ await execWith(this.query, script);
148
544
  }
149
545
  async transaction(fn) {
150
546
  // After the transaction has ended, a new one begins where afterEnd runs it.
@@ -162,51 +558,6 @@ class PostgresTransactionAdapter {
162
558
  this.ended = true;
163
559
  }
164
560
  }
165
- /** An unknown thrown value as an Error. */
166
- const asError = (value) => (value instanceof Error ? value : new Error(String(value)));
167
- /**
168
- * Puts a failed ROLLBACK's error on the transaction's error, as a
169
- * non-enumerable `rollbackError` property, when that error can take one.
170
- */
171
- const keepRollbackError = (error, rollbackError) => {
172
- if (typeof error !== 'object' || error === null || !Object.isExtensible(error))
173
- return;
174
- try {
175
- Object.defineProperty(error, 'rollbackError', {
176
- value: rollbackError,
177
- enumerable: false,
178
- configurable: true,
179
- writable: true,
180
- });
181
- }
182
- catch {
183
- // A non-configurable rollbackError is already there: keep it.
184
- }
185
- };
186
- /**
187
- * Listens for a connection error on a checked-out client until `stop()`. pg
188
- * emits a client's connection error (the server ending the session, a broken
189
- * socket) on the client when no query is running, and pg-pool listens only on
190
- * idle clients, so without this an unheard error event would end the process.
191
- */
192
- const watchClient = (client) => {
193
- let failure;
194
- // A pg-compatible pool whose clients are not event emitters emits nothing to hear.
195
- if (typeof client.on !== 'function' || typeof client.removeListener !== 'function') {
196
- return { failure: () => undefined, stop: () => undefined };
197
- }
198
- const onError = (error) => {
199
- if (!failure)
200
- failure = error;
201
- };
202
- client.on('error', onError);
203
- return {
204
- failure: () => failure,
205
- stop: () => {
206
- client.removeListener('error', onError);
207
- },
208
- };
209
- };
210
561
  /**
211
562
  * Runs `fn` in a transaction that `begin` opens on `client`: COMMIT when `fn`
212
563
  * resolves, ROLLBACK when `fn` or COMMIT throws, and that error is rethrown.
@@ -215,7 +566,7 @@ const watchClient = (client) => {
215
566
  * pool. When ROLLBACK fails, the original error is still the one thrown, with
216
567
  * the ROLLBACK error on it as `rollbackError`.
217
568
  */
218
- const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroken) => {
569
+ const runTransaction = async (client, begin, fn, capabilities, segment, afterEnd, onBroken) => {
219
570
  try {
220
571
  await client.query(begin);
221
572
  }
@@ -223,7 +574,7 @@ const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroke
223
574
  onBroken(asError(error));
224
575
  throw error;
225
576
  }
226
- const handle = new PostgresTransactionAdapter(client, capabilities, afterEnd);
577
+ const handle = new PostgresTransactionAdapter(client, capabilities, segment, afterEnd);
227
578
  try {
228
579
  const result = await fn(handle);
229
580
  await client.query('COMMIT');
@@ -251,27 +602,37 @@ const runTransaction = async (client, begin, fn, capabilities, afterEnd, onBroke
251
602
  * closed.
252
603
  */
253
604
  class PostgresConnectionAdapter {
254
- constructor(client, capabilities) {
605
+ constructor(client, capabilities, segment) {
255
606
  this.client = client;
256
607
  this.capabilities = capabilities;
608
+ this.segment = segment;
257
609
  this.kind = 'postgres';
258
610
  this.ended = false;
259
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
+ };
260
621
  }
261
622
  async open() {
262
623
  // withConnection() checks the connection out before the callback runs.
263
624
  }
264
625
  async run(statement, parameters) {
265
- return runWithExecutor(this.live(), statement, parameters);
626
+ return runWith(this.query, statement, parameters);
266
627
  }
267
628
  async get(statement, parameters) {
268
- return getWithExecutor(this.live(), statement, parameters);
629
+ return getWith(this.query, statement, parameters);
269
630
  }
270
631
  async all(statement, parameters) {
271
- return allWithExecutor(this.live(), statement, parameters);
632
+ return allWith(this.query, statement, parameters);
272
633
  }
273
634
  async exec(script) {
274
- await execWithExecutor(this.live(), script);
635
+ await execWith(this.query, script);
275
636
  }
276
637
  async transaction(fn, options) {
277
638
  const begin = beginStatement(options);
@@ -281,10 +642,7 @@ class PostgresConnectionAdapter {
281
642
  }
282
643
  this.transactionOpen = true;
283
644
  try {
284
- return await runTransaction(client, begin, fn, this.capabilities, this, (error) => {
285
- if (!this.brokenBy)
286
- this.brokenBy = error;
287
- });
645
+ return await runTransaction(client, begin, fn, this.capabilities, this.segment, this, this.onBroken);
288
646
  }
289
647
  finally {
290
648
  this.transactionOpen = false;
@@ -398,6 +756,18 @@ class PostgresConnectionAdapter {
398
756
  * });
399
757
  * ```
400
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
+ *
401
771
  * ## Connection failures
402
772
  * - The pool drops a connection that fails, and the next query opens a new one.
403
773
  * - The adapter never falls back to SQLite. `resolveStorageAdapter()` without
@@ -431,16 +801,60 @@ export class PostgresAdapter {
431
801
  'arrays', // Native array data types
432
802
  'prepared' // Prepared statements for security/performance
433
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
+ */
434
810
  this.pool = null;
435
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
+ };
436
821
  // Support both string and object initialization
437
- if (typeof options === 'string') {
438
- 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);
439
842
  }
440
843
  else {
441
- this.options = options;
844
+ this.givenPool = null;
845
+ this.ownsPool = true;
846
+ if (segment !== undefined) {
847
+ refuseOwnStartupOptions(this.options.connectionString);
848
+ }
442
849
  }
443
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
+ }
444
858
  /**
445
859
  * Lazy loader for pg module to keep the dependency optional and prevent bundling in browser builds.
446
860
  */
@@ -460,21 +874,69 @@ export class PostgresAdapter {
460
874
  throw new Error(`[StorageAdapter] Failed to load pg module. Install it with: npm install pg. Error: ${error}`);
461
875
  }
462
876
  }
463
- 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) {
464
884
  if (this.pool) {
465
- return;
885
+ return Promise.resolve();
886
+ }
887
+ if (!this.opening) {
888
+ this.opening = this.openNow(openOptions).finally(() => {
889
+ this.opening = null;
890
+ });
466
891
  }
892
+ return this.opening;
893
+ }
894
+ async openNow(openOptions) {
467
895
  if (__isBrowser) {
468
896
  throw new Error('[StorageAdapter] PostgreSQL adapter cannot be opened in a browser environment.');
469
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
+ }
470
910
  // Load pg module dynamically
471
911
  const pg = await this.loadPgModule();
472
- const { Pool } = pg;
473
- // 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) {
474
936
  const poolConfig = {};
475
937
  // Use connection string if provided
476
- if (this.options.connectionString || openOptions?.connectionString) {
477
- poolConfig.connectionString = openOptions?.connectionString ?? this.options.connectionString;
938
+ if (connectionString) {
939
+ poolConfig.connectionString = connectionString;
478
940
  }
479
941
  else {
480
942
  // Build from individual options
@@ -520,26 +982,36 @@ export class PostgresAdapter {
520
982
  if (this.options.ssl !== undefined) {
521
983
  poolConfig.ssl = this.options.ssl;
522
984
  }
523
- this.pool = new Pool(poolConfig);
524
- // Test connection
525
- const client = await this.pool.connect();
526
- 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 };
527
1003
  }
528
1004
  async run(statement, parameters) {
529
- const executor = await this.getExecutor();
530
- return runWithExecutor(executor, statement, parameters);
1005
+ return runWith(this.query, statement, parameters);
531
1006
  }
532
1007
  async get(statement, parameters) {
533
- const executor = await this.getExecutor();
534
- return getWithExecutor(executor, statement, parameters);
1008
+ return getWith(this.query, statement, parameters);
535
1009
  }
536
1010
  async all(statement, parameters) {
537
- const executor = await this.getExecutor();
538
- return allWithExecutor(executor, statement, parameters);
1011
+ return allWith(this.query, statement, parameters);
539
1012
  }
540
1013
  async exec(script) {
541
- const executor = await this.getExecutor();
542
- await execWithExecutor(executor, script);
1014
+ await execWith(this.query, script);
543
1015
  }
544
1016
  /**
545
1017
  * Runs `fn` in a transaction on one pooled connection: COMMIT when it
@@ -558,7 +1030,7 @@ export class PostgresAdapter {
558
1030
  const watch = watchClient(client);
559
1031
  let brokenBy;
560
1032
  try {
561
- return await runTransaction(client, begin, fn, this.capabilities, this, (error) => {
1033
+ return await runTransaction(client, begin, fn, this.capabilities, this.segmentStatements, this, (error) => {
562
1034
  if (!brokenBy)
563
1035
  brokenBy = error;
564
1036
  });
@@ -583,12 +1055,14 @@ export class PostgresAdapter {
583
1055
  * connection is closed, which rolls the transaction back, and the call
584
1056
  * rejects (pg 8.21 and later report the transaction status). The connection
585
1057
  * refuses statements once `fn` has ended. A connection that broke while `fn`
586
- * 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.
587
1061
  */
588
1062
  async withConnection(fn) {
589
1063
  const client = await this.requirePool().connect();
590
1064
  const watch = watchClient(client);
591
- const connection = new PostgresConnectionAdapter(client, this.capabilities);
1065
+ const connection = new PostgresConnectionAdapter(client, this.capabilities, this.segmentStatements);
592
1066
  let releaseError;
593
1067
  try {
594
1068
  const result = await fn(connection);
@@ -613,21 +1087,52 @@ export class PostgresAdapter {
613
1087
  client.release(releaseError ?? connection.brokenBy ?? watch.failure());
614
1088
  }
615
1089
  }
616
- /** 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
+ */
617
1094
  getPoolStats() {
618
- if (!this.pool) {
1095
+ const pool = this.pool;
1096
+ if (!pool) {
619
1097
  return null;
620
1098
  }
621
- 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 };
622
1100
  }
623
- async close() {
624
- if (this.pool) {
625
- await this.pool.end();
626
- 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
+ });
627
1111
  }
1112
+ return this.closing;
628
1113
  }
629
- async getExecutor() {
630
- 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
+ }
631
1136
  }
632
1137
  requirePool() {
633
1138
  if (!this.pool) {