@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.
- package/README.md +174 -3
- package/dist/adapters/baseStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/baseStorageAdapter.js +4 -2
- package/dist/adapters/baseStorageAdapter.js.map +1 -1
- package/dist/adapters/betterSqliteAdapter.d.ts +18 -2
- package/dist/adapters/betterSqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/betterSqliteAdapter.js +23 -7
- package/dist/adapters/betterSqliteAdapter.js.map +1 -1
- package/dist/adapters/capacitorSqliteAdapter.d.ts +8 -0
- package/dist/adapters/capacitorSqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitorSqliteAdapter.js +11 -1
- package/dist/adapters/capacitorSqliteAdapter.js.map +1 -1
- package/dist/adapters/electron/electronMainAdapter.d.ts +10 -0
- package/dist/adapters/electron/electronMainAdapter.d.ts.map +1 -1
- package/dist/adapters/electron/electronMainAdapter.js +21 -12
- package/dist/adapters/electron/electronMainAdapter.js.map +1 -1
- package/dist/adapters/indexedDbAdapter.d.ts +202 -54
- package/dist/adapters/indexedDbAdapter.d.ts.map +1 -1
- package/dist/adapters/indexedDbAdapter.js +560 -123
- package/dist/adapters/indexedDbAdapter.js.map +1 -1
- package/dist/adapters/postgresAdapter.d.ts +159 -3
- package/dist/adapters/postgresAdapter.d.ts.map +1 -1
- package/dist/adapters/postgresAdapter.js +667 -140
- package/dist/adapters/postgresAdapter.js.map +1 -1
- package/dist/adapters/sqlJsAdapter.d.ts +21 -3
- package/dist/adapters/sqlJsAdapter.d.ts.map +1 -1
- package/dist/adapters/sqlJsAdapter.js +29 -26
- package/dist/adapters/sqlJsAdapter.js.map +1 -1
- package/dist/core/contracts/index.d.ts +9 -0
- package/dist/core/contracts/index.d.ts.map +1 -1
- package/dist/core/contracts/index.js.map +1 -1
- package/dist/core/database.d.ts +43 -8
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +40 -5
- package/dist/core/database.js.map +1 -1
- package/dist/core/resolver.d.ts +20 -1
- package/dist/core/resolver.d.ts.map +1 -1
- package/dist/core/resolver.js +33 -11
- package/dist/core/resolver.js.map +1 -1
- package/dist/features/kv/idb.d.ts +79 -0
- package/dist/features/kv/idb.d.ts.map +1 -0
- package/dist/features/kv/idb.js +14 -0
- package/dist/features/kv/idb.js.map +1 -0
- package/dist/features/kv/index.d.ts +14 -0
- package/dist/features/kv/index.d.ts.map +1 -0
- package/dist/features/kv/index.js +14 -0
- package/dist/features/kv/index.js.map +1 -0
- package/dist/features/kv/indexedDbKeyValueStore.d.ts +77 -0
- package/dist/features/kv/indexedDbKeyValueStore.d.ts.map +1 -0
- package/dist/features/kv/indexedDbKeyValueStore.js +127 -0
- package/dist/features/kv/indexedDbKeyValueStore.js.map +1 -0
- package/dist/features/kv/keyValueStore.d.ts +48 -0
- package/dist/features/kv/keyValueStore.d.ts.map +1 -0
- package/dist/features/kv/keyValueStore.js +54 -0
- package/dist/features/kv/keyValueStore.js.map +1 -0
- package/dist/features/kv/sqlKeyValueStore.d.ts +34 -0
- package/dist/features/kv/sqlKeyValueStore.d.ts.map +1 -0
- package/dist/features/kv/sqlKeyValueStore.js +91 -0
- package/dist/features/kv/sqlKeyValueStore.js.map +1 -0
- package/dist/features/kv/valueCodec.d.ts +35 -0
- package/dist/features/kv/valueCodec.d.ts.map +1 -0
- package/dist/features/kv/valueCodec.js +462 -0
- package/dist/features/kv/valueCodec.js.map +1 -0
- package/dist/features/migrations/dataExport.js +9 -6
- package/dist/features/migrations/dataExport.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/shared/errors.d.ts +23 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +44 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/logger.d.ts +26 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +26 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/parameterUtils.d.ts +7 -0
- package/dist/shared/parameterUtils.d.ts.map +1 -1
- package/dist/shared/parameterUtils.js +25 -0
- package/dist/shared/parameterUtils.js.map +1 -1
- package/dist/shared/postgresTarget.d.ts +4 -3
- package/dist/shared/postgresTarget.d.ts.map +1 -1
- package/dist/shared/postgresTarget.js +6 -4
- package/dist/shared/postgresTarget.js.map +1 -1
- package/dist/shared/segment.d.ts +43 -0
- package/dist/shared/segment.d.ts.map +1 -0
- package/dist/shared/segment.js +77 -0
- package/dist/shared/segment.js.map +1 -0
- package/dist/shared/sqlPlaceholders.d.ts +40 -0
- package/dist/shared/sqlPlaceholders.d.ts.map +1 -0
- package/dist/shared/sqlPlaceholders.js +276 -0
- package/dist/shared/sqlPlaceholders.js.map +1 -0
- 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 {
|
|
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
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
79
|
-
const
|
|
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
|
|
114
|
+
const result = await query(text, values);
|
|
82
115
|
return result.rows?.[0] ?? null;
|
|
83
116
|
};
|
|
84
|
-
/** Read every matching row through a
|
|
85
|
-
const
|
|
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
|
|
120
|
+
const result = await query(text, values);
|
|
88
121
|
return result.rows ?? [];
|
|
89
122
|
};
|
|
90
|
-
/** Execute a statement script through a
|
|
91
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
626
|
+
return runWith(this.query, statement, parameters);
|
|
253
627
|
}
|
|
254
628
|
async get(statement, parameters) {
|
|
255
|
-
return
|
|
629
|
+
return getWith(this.query, statement, parameters);
|
|
256
630
|
}
|
|
257
631
|
async all(statement, parameters) {
|
|
258
|
-
return
|
|
632
|
+
return allWith(this.query, statement, parameters);
|
|
259
633
|
}
|
|
260
634
|
async exec(script) {
|
|
261
|
-
await
|
|
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,
|
|
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
|
-
|
|
425
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
460
|
-
|
|
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 (
|
|
464
|
-
poolConfig.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.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
508
|
-
return runWithExecutor(executor, statement, parameters);
|
|
1005
|
+
return runWith(this.query, statement, parameters);
|
|
509
1006
|
}
|
|
510
1007
|
async get(statement, parameters) {
|
|
511
|
-
|
|
512
|
-
return getWithExecutor(executor, statement, parameters);
|
|
1008
|
+
return getWith(this.query, statement, parameters);
|
|
513
1009
|
}
|
|
514
1010
|
async all(statement, parameters) {
|
|
515
|
-
|
|
516
|
-
return allWithExecutor(executor, statement, parameters);
|
|
1011
|
+
return allWith(this.query, statement, parameters);
|
|
517
1012
|
}
|
|
518
1013
|
async exec(script) {
|
|
519
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1095
|
+
const pool = this.pool;
|
|
1096
|
+
if (!pool) {
|
|
597
1097
|
return null;
|
|
598
1098
|
}
|
|
599
|
-
return { total:
|
|
1099
|
+
return { total: pool.totalCount ?? 0, idle: pool.idleCount ?? 0, waiting: pool.waitingCount ?? 0 };
|
|
600
1100
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
608
|
-
|
|
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) {
|