@vibeorm/adapter-pg 4.2.0 → 5.0.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 +35 -5
- package/dist/batch.d.ts +8 -0
- package/dist/client-turns.d.ts +44 -0
- package/dist/connection-group.d.ts +62 -0
- package/dist/connection-string.d.ts +55 -0
- package/dist/errors.d.ts +8 -0
- package/dist/index.d.ts +42 -5
- package/dist/index.js +1326 -75
- package/dist/index.js.map +13 -9
- package/dist/pool-config.d.ts +25 -0
- package/dist/statement-cache.d.ts +24 -5
- package/dist/timestamp-text.d.ts +34 -0
- package/dist/transaction-sql.d.ts +22 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -16,17 +16,22 @@ import {
|
|
|
16
16
|
transactionLifecycleRefusal,
|
|
17
17
|
createSavepointScope,
|
|
18
18
|
engineStatementBudgetMs,
|
|
19
|
+
findRefusedSetting,
|
|
19
20
|
isSavepointRollbackFailedError,
|
|
20
21
|
readOnlyRequested,
|
|
22
|
+
readTransactionSettings,
|
|
23
|
+
renderTransactionSettings,
|
|
24
|
+
transactionSettingRefusedError,
|
|
21
25
|
runNestedSavepoint,
|
|
22
26
|
runRootScope,
|
|
23
27
|
scopeTurn,
|
|
24
28
|
settleFailedTransaction,
|
|
25
29
|
startTransactionBudget,
|
|
26
|
-
transactionAbortedError,
|
|
27
|
-
validateTransactionDeadline
|
|
30
|
+
transactionAbortedError as transactionAbortedError2,
|
|
31
|
+
validateTransactionDeadline,
|
|
32
|
+
parseExtendedDateTimeText
|
|
28
33
|
} from "@vibeorm/runtime/adapter-kit";
|
|
29
|
-
import { VibeError as
|
|
34
|
+
import { VibeError as VibeError7 } from "@vibeorm/schema";
|
|
30
35
|
import pg3 from "pg";
|
|
31
36
|
|
|
32
37
|
// packages/adapter-pg/src/batch.ts
|
|
@@ -35,6 +40,9 @@ var prepareValue = (() => {
|
|
|
35
40
|
const utils = pg.utils;
|
|
36
41
|
return typeof utils?.prepareValue === "function" ? utils.prepareValue : undefined;
|
|
37
42
|
})();
|
|
43
|
+
function batchDriverAvailable() {
|
|
44
|
+
return prepareValue !== undefined;
|
|
45
|
+
}
|
|
38
46
|
function supportsBatch(params) {
|
|
39
47
|
if (prepareValue === undefined)
|
|
40
48
|
return false;
|
|
@@ -93,13 +101,20 @@ class PgBatch {
|
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
handleRowDescription(message) {
|
|
104
|
+
this.rows = [];
|
|
105
|
+
if (this.steps[this.outcomes.length]?.discardRows === true) {
|
|
106
|
+
this.fields = [];
|
|
107
|
+
this.parsers = [];
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
96
110
|
this.fields = message.fields;
|
|
97
111
|
this.parsers = message.fields.map((field) => this.types.getTypeParser(field.dataTypeID, field.format ?? "text"));
|
|
98
|
-
this.rows = [];
|
|
99
112
|
}
|
|
100
113
|
handleDataRow(message) {
|
|
101
114
|
if (this.settled || this.rowError !== undefined)
|
|
102
115
|
return;
|
|
116
|
+
if (this.steps[this.outcomes.length]?.discardRows === true)
|
|
117
|
+
return;
|
|
103
118
|
const row = {};
|
|
104
119
|
try {
|
|
105
120
|
for (let index = 0;index < this.fields.length; index++) {
|
|
@@ -182,9 +197,180 @@ function firstBatchFailure(params) {
|
|
|
182
197
|
return;
|
|
183
198
|
}
|
|
184
199
|
|
|
200
|
+
// packages/adapter-pg/src/connection-group.ts
|
|
201
|
+
import { VibeError } from "@vibeorm/schema";
|
|
202
|
+
import { transactionAbortedError } from "@vibeorm/runtime/adapter-kit";
|
|
203
|
+
function connectionGroupMisuse(params) {
|
|
204
|
+
return new VibeError({ code: "VIBE_INTERNAL", message: `pipeline lane group: ${params.message}`, meta: { reason: "pipeline-lane-group" } });
|
|
205
|
+
}
|
|
206
|
+
function openConnectionGroup(params) {
|
|
207
|
+
const { driver } = params;
|
|
208
|
+
const seats = [];
|
|
209
|
+
let sealed = false;
|
|
210
|
+
let sent = false;
|
|
211
|
+
const settle = (entries, outcome) => {
|
|
212
|
+
let failure;
|
|
213
|
+
entries.forEach((seat, index) => {
|
|
214
|
+
const issued = seat.issued;
|
|
215
|
+
if (issued === undefined)
|
|
216
|
+
return;
|
|
217
|
+
const step = outcome.steps[index];
|
|
218
|
+
if (step === undefined) {
|
|
219
|
+
const error = driver.mapError(outcome.transportError);
|
|
220
|
+
failure ??= error;
|
|
221
|
+
issued.reject(error);
|
|
222
|
+
} else if (step.status === "ok") {
|
|
223
|
+
issued.resolve(step.result.rows);
|
|
224
|
+
} else if (step.status === "failed") {
|
|
225
|
+
const error = driver.mapError(step.error);
|
|
226
|
+
failure ??= error;
|
|
227
|
+
issued.reject(error);
|
|
228
|
+
} else {
|
|
229
|
+
issued.reject(transactionAbortedError({ cause: failure }));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const flushIfReady = () => {
|
|
234
|
+
if (!sealed || sent)
|
|
235
|
+
return;
|
|
236
|
+
for (const seat of seats)
|
|
237
|
+
if (seat.open && seat.issued === undefined && !seat.done)
|
|
238
|
+
return;
|
|
239
|
+
sent = true;
|
|
240
|
+
const entries = seats.filter((seat) => seat.issued !== undefined);
|
|
241
|
+
if (entries.length === 0)
|
|
242
|
+
return;
|
|
243
|
+
const sending = new Promise((resolve) => {
|
|
244
|
+
resolve(driver.send(entries.map((seat) => seat.issued.entry)));
|
|
245
|
+
});
|
|
246
|
+
sending.then((outcome) => {
|
|
247
|
+
settle(entries, outcome);
|
|
248
|
+
}, (error) => {
|
|
249
|
+
const mapped = driver.mapError(error);
|
|
250
|
+
for (const seat of entries)
|
|
251
|
+
seat.issued?.reject(mapped);
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
const refused = (message) => () => Promise.reject(connectionGroupMisuse({ message }));
|
|
255
|
+
const subAdapter = (seat) => ({
|
|
256
|
+
...driver.base,
|
|
257
|
+
execute: (call) => {
|
|
258
|
+
if (seat.issued !== undefined || seat.done)
|
|
259
|
+
return Promise.reject(connectionGroupMisuse({ message: "a sub-lane issues ONE statement" }));
|
|
260
|
+
if (!seat.open || sent)
|
|
261
|
+
return Promise.reject(connectionGroupMisuse({ message: "a closed sub-lane issues nothing" }));
|
|
262
|
+
if (call.execution !== undefined) {
|
|
263
|
+
seat.done = true;
|
|
264
|
+
flushIfReady();
|
|
265
|
+
return Promise.reject(connectionGroupMisuse({ message: "a sub-lane statement takes no read options" }));
|
|
266
|
+
}
|
|
267
|
+
const pending = new Promise((resolve, reject) => {
|
|
268
|
+
const step = driver.prepare(call);
|
|
269
|
+
seat.issued = { entry: { step, call }, resolve, reject };
|
|
270
|
+
});
|
|
271
|
+
if (seat.issued === undefined) {
|
|
272
|
+
seat.done = true;
|
|
273
|
+
flushIfReady();
|
|
274
|
+
return pending.then(undefined, (error) => {
|
|
275
|
+
throw driver.mapError(error);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
flushIfReady();
|
|
279
|
+
return pending;
|
|
280
|
+
},
|
|
281
|
+
executeUnsafe: refused("a sub-lane runs no raw statement"),
|
|
282
|
+
transaction: refused("a sub-lane opens no transaction"),
|
|
283
|
+
connect: refused("a sub-lane does not connect"),
|
|
284
|
+
disconnect: refused("a sub-lane does not disconnect")
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
lane: () => {
|
|
288
|
+
if (sealed)
|
|
289
|
+
throw connectionGroupMisuse({ message: "a sub-lane is opened only before the group is sealed" });
|
|
290
|
+
const seat = { index: seats.length, open: true, done: false };
|
|
291
|
+
seats.push(seat);
|
|
292
|
+
return {
|
|
293
|
+
adapter: subAdapter(seat),
|
|
294
|
+
close: () => {
|
|
295
|
+
if (!seat.open)
|
|
296
|
+
return;
|
|
297
|
+
seat.open = false;
|
|
298
|
+
flushIfReady();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
seal: () => {
|
|
303
|
+
if (sealed)
|
|
304
|
+
return;
|
|
305
|
+
sealed = true;
|
|
306
|
+
flushIfReady();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// packages/adapter-pg/src/connection-string.ts
|
|
312
|
+
import { VibeError as VibeError2 } from "@vibeorm/schema";
|
|
313
|
+
function lastValue(params) {
|
|
314
|
+
const values = params.search.getAll(params.key);
|
|
315
|
+
return values.length === 0 ? undefined : values[values.length - 1];
|
|
316
|
+
}
|
|
317
|
+
function honourSystemRootCert(params) {
|
|
318
|
+
const { connectionString, option } = params;
|
|
319
|
+
const queryStart = connectionString.indexOf("?");
|
|
320
|
+
if (queryStart === -1)
|
|
321
|
+
return connectionString;
|
|
322
|
+
const hashStart = connectionString.indexOf("#", queryStart);
|
|
323
|
+
const queryEnd = hashStart === -1 ? connectionString.length : hashStart;
|
|
324
|
+
const search = new URLSearchParams(connectionString.slice(queryStart + 1, queryEnd));
|
|
325
|
+
if (lastValue({ search, key: "sslrootcert" }) !== "system")
|
|
326
|
+
return connectionString;
|
|
327
|
+
const sslmode = lastValue({ search, key: "sslmode" });
|
|
328
|
+
const ssl = lastValue({ search, key: "ssl" });
|
|
329
|
+
const weaker = sslmode !== undefined && sslmode !== "verify-full" ? { parameter: "sslmode", value: sslmode } : ssl !== undefined && ssl !== "true" && ssl !== "1" ? { parameter: "ssl", value: ssl } : undefined;
|
|
330
|
+
if (weaker !== undefined) {
|
|
331
|
+
throw new VibeError2({
|
|
332
|
+
code: "VIBE_CONFIG",
|
|
333
|
+
message: `pgAdapter \`${option}\` combines sslrootcert=system with ${weaker.parameter}=${JSON.stringify(weaker.value)}. ` + "libpq allows sslrootcert=system only with sslmode=verify-full (its default when sslmode is omitted): any certificate " + "the system trusts would otherwise vouch for a server that is not yours. Use sslmode=verify-full, or remove the setting.",
|
|
334
|
+
meta: { adapter: "pg", reason: "sslrootcert-system-weak-sslmode", option, parameter: weaker.parameter, value: weaker.value }
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
search.delete("sslrootcert");
|
|
338
|
+
search.delete("ssl");
|
|
339
|
+
search.set("sslmode", "verify-full");
|
|
340
|
+
const query = search.toString().replace(/\+/g, "%20");
|
|
341
|
+
return `${connectionString.slice(0, queryStart)}?${query}${connectionString.slice(queryEnd)}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// packages/adapter-pg/src/client-turns.ts
|
|
345
|
+
function createClientTurns() {
|
|
346
|
+
let outstanding = 0;
|
|
347
|
+
let failure;
|
|
348
|
+
let previous = Promise.resolve();
|
|
349
|
+
const release = () => {
|
|
350
|
+
outstanding -= 1;
|
|
351
|
+
failure = undefined;
|
|
352
|
+
};
|
|
353
|
+
const fail = (error) => {
|
|
354
|
+
outstanding -= 1;
|
|
355
|
+
failure = { error };
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
run: (send) => {
|
|
359
|
+
const running = outstanding === 0 ? new Promise((resolve) => {
|
|
360
|
+
resolve(send());
|
|
361
|
+
}) : previous.then(send);
|
|
362
|
+
outstanding += 1;
|
|
363
|
+
previous = running.then(release, fail);
|
|
364
|
+
return running;
|
|
365
|
+
},
|
|
366
|
+
outstanding: () => outstanding,
|
|
367
|
+
lastFailure: () => failure
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
185
371
|
// packages/adapter-pg/src/read-cancellation.ts
|
|
186
372
|
import pg2 from "pg";
|
|
187
|
-
import { VibeError as
|
|
373
|
+
import { VibeError as VibeError4 } from "@vibeorm/schema";
|
|
188
374
|
import { assertReadExecution, readCancelledError } from "@vibeorm/runtime/adapter-kit";
|
|
189
375
|
|
|
190
376
|
// packages/adapter-pg/src/config.ts
|
|
@@ -193,7 +379,7 @@ var PG_READ_CANCELLATION = Object.freeze({
|
|
|
193
379
|
});
|
|
194
380
|
|
|
195
381
|
// packages/adapter-pg/src/errors.ts
|
|
196
|
-
import { VibeError } from "@vibeorm/schema";
|
|
382
|
+
import { VibeError as VibeError3 } from "@vibeorm/schema";
|
|
197
383
|
var PG_SQLSTATE_ERROR_CODES = Object.freeze({
|
|
198
384
|
"23P01": "VIBE_EXCLUSION_VIOLATION",
|
|
199
385
|
"23505": "VIBE_UNIQUE_VIOLATION",
|
|
@@ -221,6 +407,11 @@ var PG_SQLSTATE_REASONS = Object.freeze({
|
|
|
221
407
|
"40001": "serialization-failure",
|
|
222
408
|
"25006": "read-only-transaction"
|
|
223
409
|
});
|
|
410
|
+
var NAMED_STATEMENT_POOLER_HINT = "Hint: with preparedStatements: true every query must reach the backend that prepared its name. " + "Use a direct connection, session pooling, or PgBouncer 1.21+ with max_prepared_statements > 0; " + "behind a transaction-mode pooler without prepared-statement support, turn preparedStatements off.";
|
|
411
|
+
var PG_SQLSTATE_HINTS = Object.freeze({
|
|
412
|
+
"26000": NAMED_STATEMENT_POOLER_HINT,
|
|
413
|
+
"42P05": NAMED_STATEMENT_POOLER_HINT
|
|
414
|
+
});
|
|
224
415
|
var SQLSTATE_PATTERN = /^(?=[A-Z]*\d)[0-9A-Z]{5}$/;
|
|
225
416
|
function readStringField(params) {
|
|
226
417
|
const value = params.source[params.key];
|
|
@@ -235,7 +426,7 @@ function pgErrorSqlstate(params) {
|
|
|
235
426
|
}
|
|
236
427
|
function mapPgDriverError(params) {
|
|
237
428
|
const { error } = params;
|
|
238
|
-
if (error instanceof
|
|
429
|
+
if (error instanceof VibeError3)
|
|
239
430
|
return error;
|
|
240
431
|
const source = error !== null && typeof error === "object" ? error : {};
|
|
241
432
|
const sqlstate = pgErrorSqlstate({ error });
|
|
@@ -255,8 +446,10 @@ function mapPgDriverError(params) {
|
|
|
255
446
|
const reason = sqlstate === undefined ? undefined : PG_SQLSTATE_REASONS[sqlstate];
|
|
256
447
|
if (reason !== undefined)
|
|
257
448
|
meta.reason = reason;
|
|
258
|
-
const
|
|
259
|
-
|
|
449
|
+
const driverMessage = readStringField({ source, key: "message" }) ?? (typeof error === "string" ? error : "unknown postgres driver error");
|
|
450
|
+
const hint = sqlstate === undefined ? undefined : PG_SQLSTATE_HINTS[sqlstate];
|
|
451
|
+
const message = hint === undefined ? driverMessage : `${driverMessage}. ${hint}`;
|
|
452
|
+
return new VibeError3({ code, message, meta, cause: error });
|
|
260
453
|
}
|
|
261
454
|
async function runMapped(fn) {
|
|
262
455
|
try {
|
|
@@ -279,14 +472,14 @@ function createPgReadCancellation(params) {
|
|
|
279
472
|
if (query.execution?.cancellation !== "running")
|
|
280
473
|
return query.send();
|
|
281
474
|
if (params.target.pipeline === true || (params.target.connectionParameters?.query_timeout ?? 0) > 0) {
|
|
282
|
-
throw new
|
|
475
|
+
throw new VibeError4({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Running cancellation requires a non-pipelined target without local query_timeout", meta: { capability: "readCancellation", reason: "target-lifetime" } });
|
|
283
476
|
}
|
|
284
477
|
const address = params.connectionString;
|
|
285
478
|
if (address === undefined)
|
|
286
|
-
throw new
|
|
479
|
+
throw new VibeError4({ code: "VIBE_INTERNAL", message: "Running cancellation requires a control address" });
|
|
287
480
|
const token = `vibe_cancel_${crypto.randomUUID()}`;
|
|
288
481
|
if (!URL.canParse(address))
|
|
289
|
-
throw new
|
|
482
|
+
throw new VibeError4({ code: "VIBE_CONFIG", message: "The PostgreSQL cancellation control address must be a URL" });
|
|
290
483
|
const url = new URL(address);
|
|
291
484
|
url.searchParams.delete("query_timeout");
|
|
292
485
|
url.searchParams.set("connect_timeout", String(PG_READ_CANCELLATION.controlTimeoutMs / 1000));
|
|
@@ -333,7 +526,7 @@ function createPgReadCancellation(params) {
|
|
|
333
526
|
if (preflightAborted)
|
|
334
527
|
throw readCancelledError({ stage: "waiting-for-connection" });
|
|
335
528
|
if (witness === undefined || typeof witness.pid !== "number" || typeof witness.backend_start !== "string" || typeof witness.datname !== "string" || typeof witness.usename !== "string" || witness.tag !== token) {
|
|
336
|
-
throw new
|
|
529
|
+
throw new VibeError4({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Cannot prove the exact PostgreSQL cancellation target", meta: { capability: "readCancellation", reason: "target-witness" } });
|
|
337
530
|
}
|
|
338
531
|
assertReadExecution({ execution: query.execution, effect: "read", support, provider: "postgresql", stage: "waiting-for-connection" });
|
|
339
532
|
listener = () => {
|
|
@@ -350,7 +543,7 @@ function createPgReadCancellation(params) {
|
|
|
350
543
|
throw error;
|
|
351
544
|
};
|
|
352
545
|
settlementTimer = setTimeout(() => {
|
|
353
|
-
controlFailure = new
|
|
546
|
+
controlFailure = new VibeError4({ code: "VIBE_ADAPTER", message: "PostgreSQL read did not settle after cancellation; its exact session was discarded", meta: { reason: "read-cancellation-unsettled", databaseCancelled: false } });
|
|
354
547
|
if (!terminated) {
|
|
355
548
|
terminated = true;
|
|
356
549
|
params.terminate();
|
|
@@ -362,7 +555,7 @@ function createPgReadCancellation(params) {
|
|
|
362
555
|
})).then((result) => {
|
|
363
556
|
signalSent = rowsOf(result)[0]?.signalled === true;
|
|
364
557
|
if (!signalSent)
|
|
365
|
-
failControl(new
|
|
558
|
+
failControl(new VibeError4({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "The control endpoint did not signal the exact PostgreSQL target", meta: { capability: "readCancellation", reason: "target-not-signalled", databaseCancelled: false } }));
|
|
366
559
|
}, failControl);
|
|
367
560
|
cancel.then(() => {
|
|
368
561
|
return;
|
|
@@ -393,12 +586,12 @@ function createPgReadCancellation(params) {
|
|
|
393
586
|
}, () => {
|
|
394
587
|
return;
|
|
395
588
|
});
|
|
396
|
-
const sqlstate = error instanceof
|
|
589
|
+
const sqlstate = error instanceof VibeError4 ? error.meta.sqlstate : pgErrorSqlstate({ error });
|
|
397
590
|
if (terminated && controlFailure !== undefined && sqlstate === undefined)
|
|
398
591
|
throw controlFailure;
|
|
399
592
|
if (attempted && signalSent && sqlstate === "57014") {
|
|
400
593
|
const cancelled = readCancelledError({ stage: "running", databaseCancelled: true, cause: error });
|
|
401
|
-
throw new
|
|
594
|
+
throw new VibeError4({ code: cancelled.code, message: cancelled.message, meta: { ...cancelled.meta, sqlstate: "57014" }, cause: error });
|
|
402
595
|
}
|
|
403
596
|
throw error;
|
|
404
597
|
});
|
|
@@ -480,7 +673,7 @@ function serializeParamsRaw(params) {
|
|
|
480
673
|
}
|
|
481
674
|
|
|
482
675
|
// packages/adapter-pg/src/pool-config.ts
|
|
483
|
-
import { VibeError as
|
|
676
|
+
import { VibeError as VibeError5 } from "@vibeorm/schema";
|
|
484
677
|
var PLAN_CACHE_MODES = ["auto", "force_custom_plan", "force_generic_plan"];
|
|
485
678
|
function isPlanCacheMode(value) {
|
|
486
679
|
return typeof value === "string" && PLAN_CACHE_MODES.includes(value);
|
|
@@ -489,7 +682,7 @@ function assertPlanCacheMode(params) {
|
|
|
489
682
|
const { value } = params;
|
|
490
683
|
if (isPlanCacheMode(value))
|
|
491
684
|
return value;
|
|
492
|
-
throw new
|
|
685
|
+
throw new VibeError5({
|
|
493
686
|
code: "VIBE_CONFIG",
|
|
494
687
|
message: `planCacheMode must be one of ${PLAN_CACHE_MODES.map((mode) => `"${mode}"`).join(", ")}, received ${JSON.stringify(value)}`,
|
|
495
688
|
meta: { planCacheMode: value, allowed: PLAN_CACHE_MODES }
|
|
@@ -515,32 +708,76 @@ function placePlanCacheMode(params) {
|
|
|
515
708
|
url.searchParams.set("options", merged);
|
|
516
709
|
return { connectionString: url.toString(), options: merged };
|
|
517
710
|
}
|
|
711
|
+
function assertIdleTimeout(params) {
|
|
712
|
+
const { value } = params;
|
|
713
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 2147483647)
|
|
714
|
+
return value;
|
|
715
|
+
throw new VibeError5({
|
|
716
|
+
code: "VIBE_CONFIG",
|
|
717
|
+
message: "pgAdapter `idleTimeout` must be integer milliseconds from 0 through 2,147,483,647 (0 keeps idle connections open).",
|
|
718
|
+
meta: { adapter: "pg", reason: "invalid-idle-timeout", option: "idleTimeout" }
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function assertPoolMin(params) {
|
|
722
|
+
const { value, max } = params;
|
|
723
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= max)
|
|
724
|
+
return value;
|
|
725
|
+
throw new VibeError5({
|
|
726
|
+
code: "VIBE_CONFIG",
|
|
727
|
+
message: `pgAdapter \`min\` must be an integer from 0 through \`max\` (${max}).`,
|
|
728
|
+
meta: { adapter: "pg", reason: "invalid-pool-min", option: "min", max }
|
|
729
|
+
});
|
|
730
|
+
}
|
|
518
731
|
function buildPoolConfig(params) {
|
|
519
|
-
const { max, statementTimeout, connectionTimeout } = params;
|
|
732
|
+
const { max, min, statementTimeout, connectionTimeout, idleTimeout } = params;
|
|
520
733
|
const placement = placePlanCacheMode({
|
|
521
734
|
connectionString: params.connectionString,
|
|
522
735
|
planCacheMode: params.planCacheMode ?? "auto"
|
|
523
736
|
});
|
|
524
737
|
const config = { connectionString: placement.connectionString, max };
|
|
738
|
+
if (min !== undefined)
|
|
739
|
+
config.min = min;
|
|
525
740
|
if (statementTimeout !== undefined)
|
|
526
741
|
config.statement_timeout = statementTimeout;
|
|
527
742
|
if (connectionTimeout !== undefined)
|
|
528
743
|
config.connectionTimeoutMillis = connectionTimeout;
|
|
744
|
+
if (idleTimeout !== undefined)
|
|
745
|
+
config.idleTimeoutMillis = idleTimeout;
|
|
529
746
|
if (placement.options !== undefined)
|
|
530
747
|
config.options = placement.options;
|
|
531
748
|
return config;
|
|
532
749
|
}
|
|
533
750
|
|
|
534
751
|
// packages/adapter-pg/src/statement-cache.ts
|
|
752
|
+
import { createHash } from "crypto";
|
|
535
753
|
var DEFAULT_PROMOTE_AFTER = 2;
|
|
536
754
|
var SIGHTINGS_PER_NAME = 4;
|
|
755
|
+
var NAME_PREFIX = "vibeorm_";
|
|
756
|
+
var BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
757
|
+
function statementNameFor(params) {
|
|
758
|
+
const digest = createHash("sha256").update(params.text, "utf8").digest();
|
|
759
|
+
let name = NAME_PREFIX;
|
|
760
|
+
let buffer = 0;
|
|
761
|
+
let bits = 0;
|
|
762
|
+
for (const byte of digest) {
|
|
763
|
+
buffer = buffer << 8 | byte;
|
|
764
|
+
bits += 8;
|
|
765
|
+
while (bits >= 5) {
|
|
766
|
+
bits -= 5;
|
|
767
|
+
name += BASE32_ALPHABET[buffer >>> bits & 31];
|
|
768
|
+
}
|
|
769
|
+
buffer &= (1 << bits) - 1;
|
|
770
|
+
}
|
|
771
|
+
if (bits > 0)
|
|
772
|
+
name += BASE32_ALPHABET[buffer << 5 - bits & 31];
|
|
773
|
+
return name;
|
|
774
|
+
}
|
|
537
775
|
function createStatementNameCache(params) {
|
|
538
776
|
const max = Math.max(0, params.max);
|
|
539
777
|
const promoteAfter = Math.max(1, params.promoteAfter ?? DEFAULT_PROMOTE_AFTER);
|
|
540
778
|
const sightingsMax = max * SIGHTINGS_PER_NAME;
|
|
541
779
|
const names = new Map;
|
|
542
780
|
const sightings = new Map;
|
|
543
|
-
let counter = 0;
|
|
544
781
|
return {
|
|
545
782
|
getName(nameParams) {
|
|
546
783
|
const { text } = nameParams;
|
|
@@ -562,7 +799,7 @@ function createStatementNameCache(params) {
|
|
|
562
799
|
return;
|
|
563
800
|
}
|
|
564
801
|
sightings.delete(text);
|
|
565
|
-
const name =
|
|
802
|
+
const name = statementNameFor({ text });
|
|
566
803
|
names.set(text, name);
|
|
567
804
|
return name;
|
|
568
805
|
},
|
|
@@ -575,8 +812,88 @@ function createStatementNameCache(params) {
|
|
|
575
812
|
};
|
|
576
813
|
}
|
|
577
814
|
|
|
815
|
+
// packages/adapter-pg/src/timestamp-text.ts
|
|
816
|
+
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
817
|
+
var DIGIT_POSITIONS = [0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18];
|
|
818
|
+
function parsePlainTimestampText(params) {
|
|
819
|
+
const { text, zoned } = params;
|
|
820
|
+
const length = text.length;
|
|
821
|
+
if (length < 19 || text.charCodeAt(4) !== 45 || text.charCodeAt(7) !== 45 || text.charCodeAt(10) !== 32 || text.charCodeAt(13) !== 58 || text.charCodeAt(16) !== 58) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
for (let position = 0;position < DIGIT_POSITIONS.length; position++) {
|
|
825
|
+
const code = text.charCodeAt(DIGIT_POSITIONS[position]);
|
|
826
|
+
if (code < 48 || code > 57)
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const year = (text.charCodeAt(0) - 48) * 1000 + (text.charCodeAt(1) - 48) * 100 + (text.charCodeAt(2) - 48) * 10 + (text.charCodeAt(3) - 48);
|
|
830
|
+
const month = (text.charCodeAt(5) - 48) * 10 + (text.charCodeAt(6) - 48);
|
|
831
|
+
const day = (text.charCodeAt(8) - 48) * 10 + (text.charCodeAt(9) - 48);
|
|
832
|
+
const hour = (text.charCodeAt(11) - 48) * 10 + (text.charCodeAt(12) - 48);
|
|
833
|
+
const minute = (text.charCodeAt(14) - 48) * 10 + (text.charCodeAt(15) - 48);
|
|
834
|
+
const second = (text.charCodeAt(17) - 48) * 10 + (text.charCodeAt(18) - 48);
|
|
835
|
+
if (year < 1000 || month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59)
|
|
836
|
+
return;
|
|
837
|
+
if (day > 28) {
|
|
838
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
839
|
+
const days = month === 2 ? leap ? 29 : 28 : DAYS_IN_MONTH[month - 1];
|
|
840
|
+
if (day > days)
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
let index = 19;
|
|
844
|
+
let milliseconds = 0;
|
|
845
|
+
if (index < length && text.charCodeAt(index) === 46) {
|
|
846
|
+
index++;
|
|
847
|
+
const start = index;
|
|
848
|
+
while (index < length) {
|
|
849
|
+
const code = text.charCodeAt(index);
|
|
850
|
+
if (code < 48 || code > 57)
|
|
851
|
+
break;
|
|
852
|
+
if (index - start < 3)
|
|
853
|
+
milliseconds = milliseconds * 10 + (code - 48);
|
|
854
|
+
index++;
|
|
855
|
+
}
|
|
856
|
+
const digits = index - start;
|
|
857
|
+
if (digits === 0 || digits > 6)
|
|
858
|
+
return;
|
|
859
|
+
if (digits === 1)
|
|
860
|
+
milliseconds *= 100;
|
|
861
|
+
else if (digits === 2)
|
|
862
|
+
milliseconds *= 10;
|
|
863
|
+
}
|
|
864
|
+
let offsetMinutes = 0;
|
|
865
|
+
if (zoned) {
|
|
866
|
+
if (index + 3 > length)
|
|
867
|
+
return;
|
|
868
|
+
const sign = text.charCodeAt(index);
|
|
869
|
+
if (sign !== 43 && sign !== 45)
|
|
870
|
+
return;
|
|
871
|
+
const hours1 = text.charCodeAt(index + 1) - 48;
|
|
872
|
+
const hours2 = text.charCodeAt(index + 2) - 48;
|
|
873
|
+
if (hours1 < 0 || hours1 > 9 || hours2 < 0 || hours2 > 9)
|
|
874
|
+
return;
|
|
875
|
+
offsetMinutes = (hours1 * 10 + hours2) * 60;
|
|
876
|
+
index += 3;
|
|
877
|
+
if (index < length) {
|
|
878
|
+
if (index + 3 !== length || text.charCodeAt(index) !== 58)
|
|
879
|
+
return;
|
|
880
|
+
const minutes1 = text.charCodeAt(index + 1) - 48;
|
|
881
|
+
const minutes2 = text.charCodeAt(index + 2) - 48;
|
|
882
|
+
if (minutes1 < 0 || minutes1 > 9 || minutes2 < 0 || minutes2 > 9)
|
|
883
|
+
return;
|
|
884
|
+
offsetMinutes += minutes1 * 10 + minutes2;
|
|
885
|
+
index = length;
|
|
886
|
+
}
|
|
887
|
+
if (sign === 45)
|
|
888
|
+
offsetMinutes = -offsetMinutes;
|
|
889
|
+
}
|
|
890
|
+
if (index !== length)
|
|
891
|
+
return;
|
|
892
|
+
return new Date(Date.UTC(year, month - 1, day, hour, minute, second, milliseconds) - offsetMinutes * 60000);
|
|
893
|
+
}
|
|
894
|
+
|
|
578
895
|
// packages/adapter-pg/src/transaction-sql.ts
|
|
579
|
-
import { VibeError as
|
|
896
|
+
import { VibeError as VibeError6 } from "@vibeorm/schema";
|
|
580
897
|
import { refuseNestedTransactionOptions } from "@vibeorm/runtime/adapter-kit";
|
|
581
898
|
var ISOLATION_LEVEL_SQL = Object.freeze({
|
|
582
899
|
ReadCommitted: "READ COMMITTED",
|
|
@@ -599,7 +916,7 @@ function openTransactionStatement(params) {
|
|
|
599
916
|
function statementTimeoutStatement(params) {
|
|
600
917
|
const { timeout } = params;
|
|
601
918
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
602
|
-
throw new
|
|
919
|
+
throw new VibeError6({
|
|
603
920
|
code: "VIBE_VALIDATION",
|
|
604
921
|
message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
|
|
605
922
|
meta: { timeout }
|
|
@@ -616,6 +933,12 @@ function classifyRawTransactionControl(params) {
|
|
|
616
933
|
return "close";
|
|
617
934
|
return null;
|
|
618
935
|
}
|
|
936
|
+
var NOT_PROVABLY_ONE_STATEMENT = /[;$\u0000]|--|\/\*/;
|
|
937
|
+
var OWN_TURN_VERBS = /^\s*(?:begin|start|commit|end|rollback|abort|savepoint|release|prepare|copy)\b/i;
|
|
938
|
+
function isSingleStatementText(params) {
|
|
939
|
+
const { text } = params;
|
|
940
|
+
return text.trim() !== "" && !NOT_PROVABLY_ONE_STATEMENT.test(text) && !OWN_TURN_VERBS.test(text);
|
|
941
|
+
}
|
|
619
942
|
|
|
620
943
|
// packages/adapter-pg/src/index.ts
|
|
621
944
|
import { createSavepointCounter, nextSavepointName } from "@vibeorm/runtime/adapter-kit";
|
|
@@ -629,23 +952,56 @@ import {
|
|
|
629
952
|
transactionOutcomeUnknownError,
|
|
630
953
|
validateTransactionDeadline as validateTransactionDeadline2
|
|
631
954
|
} from "@vibeorm/runtime/adapter-kit";
|
|
955
|
+
function laneGroupMisuse(params) {
|
|
956
|
+
return new VibeError7({ code: "VIBE_INTERNAL", message: `pipeline lane group: ${params.message}`, meta: { reason: "pipeline-lane-group" } });
|
|
957
|
+
}
|
|
632
958
|
var MAX_STATEMENT_TIMEOUT_MS = 2147483647;
|
|
633
959
|
var ignore = () => {
|
|
634
960
|
return;
|
|
635
961
|
};
|
|
962
|
+
function predecessorClosedError() {
|
|
963
|
+
return new VibeError7({
|
|
964
|
+
code: "VIBE_TRANSACTION",
|
|
965
|
+
message: "pipeline(): an earlier lane of this ordered scope closed without issuing its statement, so this statement was not sent " + "(the operation before it failed before it reached the database)",
|
|
966
|
+
meta: { reason: "pipeline-predecessor-closed" }
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
function orderedLaneMisuse(params) {
|
|
970
|
+
return new VibeError7({ code: "VIBE_INTERNAL", message: `pipeline(): ${params.message}`, meta: { reason: "pipeline-ordered-lane" } });
|
|
971
|
+
}
|
|
972
|
+
var COMMIT_STEP = Object.freeze({ text: "COMMIT" });
|
|
973
|
+
function commitSentRefusal() {
|
|
974
|
+
return new VibeError7({
|
|
975
|
+
code: "VIBE_TRANSACTION",
|
|
976
|
+
message: "this transaction already sent its COMMIT together with its last statement, so it accepts no further statement",
|
|
977
|
+
meta: { provider: PROVIDER, reason: "commit-sent" }
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function finalStatementMisuse() {
|
|
981
|
+
return new VibeError7({
|
|
982
|
+
code: "VIBE_INTERNAL",
|
|
983
|
+
message: "withCommit(): a view carries ONE statement, the transaction's last",
|
|
984
|
+
meta: { reason: "with-commit-second-statement" }
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
function beforeCommit(params) {
|
|
988
|
+
const { outcome, commitIndex } = params;
|
|
989
|
+
const steps = outcome.steps.slice(0, commitIndex);
|
|
990
|
+
return steps.length < commitIndex && outcome.transportError !== undefined ? { steps, transportError: outcome.transportError } : { steps };
|
|
991
|
+
}
|
|
636
992
|
function finalResult(params) {
|
|
637
993
|
return Array.isArray(params.result) ? params.result.at(-1) ?? { rows: [], rowCount: 0 } : params.result;
|
|
638
994
|
}
|
|
639
995
|
var POOL_STATEMENT_NAMES = new WeakMap;
|
|
640
996
|
function isConnectionLoss(params) {
|
|
641
997
|
const { error } = params;
|
|
642
|
-
if (!(error instanceof
|
|
998
|
+
if (!(error instanceof VibeError7))
|
|
643
999
|
return true;
|
|
644
1000
|
const sqlstate = error.meta.sqlstate;
|
|
645
1001
|
return typeof sqlstate !== "string" || sqlstate.startsWith("08") || ["57P01", "57P02", "57P03"].includes(sqlstate);
|
|
646
1002
|
}
|
|
647
1003
|
function commitOutcomeUnknownError(params) {
|
|
648
|
-
return new
|
|
1004
|
+
return new VibeError7({
|
|
649
1005
|
code: "VIBE_TRANSACTION",
|
|
650
1006
|
message: "the connection was lost while COMMIT was in flight, so the transaction's outcome is unknown: the write may have committed. The connection was discarded; inspect the data before retrying.",
|
|
651
1007
|
meta: { provider: PROVIDER, reason: "transaction-outcome-unknown", outcome: "unknown", stage: "commit" },
|
|
@@ -674,7 +1030,7 @@ function observeReadyForQuery(params) {
|
|
|
674
1030
|
client.on("end", onEnd);
|
|
675
1031
|
return {
|
|
676
1032
|
reusableAfter: async ({ error, named }) => {
|
|
677
|
-
if (!(error instanceof
|
|
1033
|
+
if (!(error instanceof VibeError7))
|
|
678
1034
|
return false;
|
|
679
1035
|
const sqlstate = error.meta.sqlstate;
|
|
680
1036
|
if (typeof sqlstate !== "string" || NEVER_REUSE_SQLSTATE_CLASSES.has(sqlstate.slice(0, 2)))
|
|
@@ -711,8 +1067,10 @@ function guardBackgroundErrors(params) {
|
|
|
711
1067
|
}
|
|
712
1068
|
var DIALECT = "postgres";
|
|
713
1069
|
var PROVIDER = "postgresql";
|
|
714
|
-
var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
|
|
1070
|
+
var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native", bytes: "buffer" });
|
|
715
1071
|
var ROUND_TRIPS = "network";
|
|
1072
|
+
var READ_SNAPSHOT = Object.freeze({ isolationLevel: "RepeatableRead", accessMode: "readOnly" });
|
|
1073
|
+
var PIPELINING = "batched";
|
|
716
1074
|
var BUDGETS = Object.freeze({
|
|
717
1075
|
acquireTimeout: true,
|
|
718
1076
|
statementTimeout: "all-statements",
|
|
@@ -725,16 +1083,21 @@ var TIMESTAMP_OID = 1114;
|
|
|
725
1083
|
var TIMESTAMP_ARRAY_OID = 1115;
|
|
726
1084
|
var DATE_OID = 1082;
|
|
727
1085
|
var DATE_ARRAY_OID = 1182;
|
|
1086
|
+
var TIMESTAMPTZ_OID = 1184;
|
|
1087
|
+
var TIMESTAMPTZ_ARRAY_OID = 1185;
|
|
728
1088
|
var driverTypeParser = pg3.types.getTypeParser;
|
|
729
1089
|
var ZONE_LESS_TEXT = /^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)?$/;
|
|
730
1090
|
function parseZoneLessAsUtc(value) {
|
|
1091
|
+
const plain = parsePlainTimestampText({ text: value, zoned: false });
|
|
1092
|
+
if (plain !== undefined)
|
|
1093
|
+
return plain;
|
|
731
1094
|
if (ZONE_LESS_TEXT.test(value)) {
|
|
732
1095
|
const iso = value.includes(" ") ? `${value.replace(" ", "T").replace(/(\.\d{3})\d+$/, "$1")}Z` : value;
|
|
733
1096
|
const parsed = new Date(iso);
|
|
734
1097
|
if (!Number.isNaN(parsed.getTime()))
|
|
735
1098
|
return parsed;
|
|
736
1099
|
}
|
|
737
|
-
return driverTypeParser(TIMESTAMP_OID, "text")(value);
|
|
1100
|
+
return parseExtendedDateTimeText({ text: value }) ?? driverTypeParser(TIMESTAMP_OID, "text")(value);
|
|
738
1101
|
}
|
|
739
1102
|
function parseZoneLessArrayAsUtc(value) {
|
|
740
1103
|
const mapElements = (element) => {
|
|
@@ -746,6 +1109,28 @@ function parseZoneLessArrayAsUtc(value) {
|
|
|
746
1109
|
};
|
|
747
1110
|
return mapElements(driverTypeParser(TEXT_ARRAY_OID, "text")(value));
|
|
748
1111
|
}
|
|
1112
|
+
var pgTypesArrayParser = (() => {
|
|
1113
|
+
const candidate = pg3.types.arrayParser;
|
|
1114
|
+
return typeof candidate?.create === "function" ? candidate : undefined;
|
|
1115
|
+
})();
|
|
1116
|
+
function recogniseBuiltinParser(params) {
|
|
1117
|
+
const parser = driverTypeParser(params.oid, "text");
|
|
1118
|
+
return parser.name === params.name && params.peers.every((peer) => driverTypeParser(peer, "text") === parser) ? parser : undefined;
|
|
1119
|
+
}
|
|
1120
|
+
var BUILTIN_TIMESTAMPTZ_PARSER = recogniseBuiltinParser({
|
|
1121
|
+
oid: TIMESTAMPTZ_OID,
|
|
1122
|
+
peers: [TIMESTAMP_OID, DATE_OID],
|
|
1123
|
+
name: "parseDate"
|
|
1124
|
+
});
|
|
1125
|
+
var BUILTIN_TIMESTAMPTZ_ARRAY_PARSER = pgTypesArrayParser === undefined || BUILTIN_TIMESTAMPTZ_PARSER === undefined ? undefined : recogniseBuiltinParser({ oid: TIMESTAMPTZ_ARRAY_OID, peers: [TIMESTAMP_ARRAY_OID, DATE_ARRAY_OID], name: "parseDateArray" });
|
|
1126
|
+
function parseTimestamptzText(value) {
|
|
1127
|
+
return parsePlainTimestampText({ text: value, zoned: true }) ?? BUILTIN_TIMESTAMPTZ_PARSER(value);
|
|
1128
|
+
}
|
|
1129
|
+
function parseTimestamptzArrayText(value) {
|
|
1130
|
+
if (!value)
|
|
1131
|
+
return null;
|
|
1132
|
+
return pgTypesArrayParser.create(value, (entry) => entry !== null ? parseTimestamptzText(entry) : entry).parse();
|
|
1133
|
+
}
|
|
749
1134
|
var EXACT_TYPES = {
|
|
750
1135
|
getTypeParser(oid, format) {
|
|
751
1136
|
if (format === undefined || format === "text") {
|
|
@@ -755,6 +1140,14 @@ var EXACT_TYPES = {
|
|
|
755
1140
|
return parseZoneLessAsUtc;
|
|
756
1141
|
if (oid === TIMESTAMP_ARRAY_OID || oid === DATE_ARRAY_OID)
|
|
757
1142
|
return parseZoneLessArrayAsUtc;
|
|
1143
|
+
if (oid === TIMESTAMPTZ_OID) {
|
|
1144
|
+
const driver = driverTypeParser(oid, "text");
|
|
1145
|
+
return driver === BUILTIN_TIMESTAMPTZ_PARSER ? parseTimestamptzText : driver;
|
|
1146
|
+
}
|
|
1147
|
+
if (oid === TIMESTAMPTZ_ARRAY_OID) {
|
|
1148
|
+
const driver = driverTypeParser(oid, "text");
|
|
1149
|
+
return driver === BUILTIN_TIMESTAMPTZ_ARRAY_PARSER ? parseTimestamptzArrayText : driver;
|
|
1150
|
+
}
|
|
758
1151
|
}
|
|
759
1152
|
return driverTypeParser(oid, format);
|
|
760
1153
|
}
|
|
@@ -764,23 +1157,52 @@ function takeAddress(params) {
|
|
|
764
1157
|
if (options?.pool !== undefined && options.pool !== null)
|
|
765
1158
|
return { pool: options.pool };
|
|
766
1159
|
const connectionString = options?.connectionString;
|
|
767
|
-
if (typeof connectionString === "string" && connectionString.trim() !== "")
|
|
768
|
-
return { connectionString };
|
|
769
|
-
|
|
1160
|
+
if (typeof connectionString === "string" && connectionString.trim() !== "") {
|
|
1161
|
+
return { connectionString: honourSystemRootCert({ connectionString, option: "connectionString" }) };
|
|
1162
|
+
}
|
|
1163
|
+
throw new VibeError7({
|
|
770
1164
|
code: "VIBE_CONFIG",
|
|
771
1165
|
message: "pgAdapter needs an explicit address: pass `connectionString` (a PostgreSQL URL) or an existing `pool`. " + "The adapter does not read DATABASE_URL or the PG* environment variables.",
|
|
772
1166
|
meta: { adapter: "pg", reason: "missing-address" }
|
|
773
1167
|
});
|
|
774
1168
|
}
|
|
1169
|
+
function transactionsTakeOneRoundTrip(params) {
|
|
1170
|
+
const { address } = params;
|
|
1171
|
+
if (!batchDriverAvailable())
|
|
1172
|
+
return false;
|
|
1173
|
+
const config = "pool" in address ? address.pool.options ?? {} : { connectionString: address.connectionString };
|
|
1174
|
+
if (config.pipeline === true)
|
|
1175
|
+
return false;
|
|
1176
|
+
let timeout = config.query_timeout;
|
|
1177
|
+
const connectionString = config.connectionString;
|
|
1178
|
+
if (typeof connectionString === "string") {
|
|
1179
|
+
if (!URL.canParse(connectionString))
|
|
1180
|
+
return false;
|
|
1181
|
+
const fromUrl = new URL(connectionString).searchParams.get("query_timeout");
|
|
1182
|
+
if (fromUrl !== null)
|
|
1183
|
+
timeout = fromUrl;
|
|
1184
|
+
}
|
|
1185
|
+
timeout ||= pg3.defaults.query_timeout;
|
|
1186
|
+
return !(Number(timeout ?? 0) > 0);
|
|
1187
|
+
}
|
|
775
1188
|
function pgAdapter(options) {
|
|
776
1189
|
const address = takeAddress({ options });
|
|
777
|
-
const cancellationAddress = options.cancellationConnectionString
|
|
1190
|
+
const cancellationAddress = options.cancellationConnectionString !== undefined ? honourSystemRootCert({ connectionString: options.cancellationConnectionString, option: "cancellationConnectionString" }) : ("connectionString" in address) ? address.connectionString : undefined;
|
|
778
1191
|
const readCancellation = cancellationAddress === undefined ? "before-dispatch" : "running";
|
|
779
1192
|
const usePrepared = options?.preparedStatements ?? false;
|
|
780
1193
|
const stmtCacheMax = options?.stmtCacheMax ?? 1000;
|
|
781
1194
|
const planCacheMode = options?.planCacheMode === undefined ? "auto" : assertPlanCacheMode({ value: options.planCacheMode });
|
|
1195
|
+
const idleTimeout = "pool" in address || options?.idleTimeout === undefined ? undefined : assertIdleTimeout({ value: options.idleTimeout });
|
|
1196
|
+
if (options?.min !== undefined && "pool" in address) {
|
|
1197
|
+
throw new VibeError7({
|
|
1198
|
+
code: "VIBE_CONFIG",
|
|
1199
|
+
message: "pgAdapter `min` configures the pool the adapter builds \u2014 it cannot be applied to a caller-supplied `pool`; set `min` (and `idleTimeoutMillis`) on that pool instead",
|
|
1200
|
+
meta: { adapter: "pg", reason: "pool-min-with-injected-pool", option: "min" }
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
const poolMin = options?.min === undefined ? undefined : assertPoolMin({ value: options.min, max: options.max ?? 10 });
|
|
782
1204
|
if (planCacheMode !== "auto" && "pool" in address) {
|
|
783
|
-
throw new
|
|
1205
|
+
throw new VibeError7({
|
|
784
1206
|
code: "VIBE_CONFIG",
|
|
785
1207
|
message: "planCacheMode configures the pool the adapter builds \u2014 it cannot be applied to a caller-supplied `pool`; " + 'set `options: "-c plan_cache_mode=\u2026"` on that pool instead',
|
|
786
1208
|
meta: { planCacheMode }
|
|
@@ -802,8 +1224,10 @@ function pgAdapter(options) {
|
|
|
802
1224
|
const config = buildPoolConfig({
|
|
803
1225
|
connectionString: address.connectionString,
|
|
804
1226
|
max: options?.max ?? 10,
|
|
1227
|
+
...poolMin === undefined ? {} : { min: poolMin },
|
|
805
1228
|
...options?.statementTimeout === undefined ? {} : { statementTimeout: options.statementTimeout },
|
|
806
1229
|
...options?.connectionTimeout === undefined ? {} : { connectionTimeout: options.connectionTimeout },
|
|
1230
|
+
...idleTimeout === undefined ? {} : { idleTimeout },
|
|
807
1231
|
planCacheMode
|
|
808
1232
|
});
|
|
809
1233
|
const PoolCtor = pg3.Pool;
|
|
@@ -850,16 +1274,29 @@ function pgAdapter(options) {
|
|
|
850
1274
|
return { rows: final.rows, affectedRows: final.rowCount ?? 0 };
|
|
851
1275
|
}
|
|
852
1276
|
function createClientAdapter(params) {
|
|
853
|
-
const { dispatch, scope, budget, deferred, cancellation } = params;
|
|
854
|
-
const
|
|
1277
|
+
const { dispatch, scope, budget, deferred, cancellation, pipelines, lane, connection, final } = params;
|
|
1278
|
+
const laneOf = lane === undefined ? {} : { lane };
|
|
1279
|
+
const finalOf = () => {
|
|
1280
|
+
if (final === undefined)
|
|
1281
|
+
return {};
|
|
1282
|
+
if (final.used)
|
|
1283
|
+
throw finalStatementMisuse();
|
|
1284
|
+
final.used = true;
|
|
1285
|
+
return { commit: true, ...final.discardRows === true ? { discardRows: true } : {} };
|
|
1286
|
+
};
|
|
1287
|
+
const waitAsLane = (wait) => lane === undefined || pipelines === null ? wait : pipelines.waitAs({ lane, wait });
|
|
1288
|
+
const rawClient = {
|
|
1289
|
+
query: (config) => dispatch({ config, batchable: (config.values?.length ?? 0) > 0 || isSingleStatementText({ text: config.text }), ...laneOf, ...finalOf() })
|
|
1290
|
+
};
|
|
855
1291
|
const savepointControl = (statement, cleanup) => {
|
|
856
|
-
if (deferred !== null && deferred.routeSavepoint({ text: statement }) !== "send")
|
|
1292
|
+
if (deferred !== null && deferred.routeSavepoint({ text: statement, cleanup }) !== "send")
|
|
857
1293
|
return;
|
|
858
1294
|
return runMapped(() => dispatch({
|
|
859
1295
|
config: { text: statement },
|
|
860
1296
|
stage: "nested",
|
|
861
1297
|
batchable: true,
|
|
862
|
-
...cleanup ? { cleanup: true } : {}
|
|
1298
|
+
...cleanup ? { cleanup: true } : {},
|
|
1299
|
+
...laneOf
|
|
863
1300
|
}));
|
|
864
1301
|
};
|
|
865
1302
|
return {
|
|
@@ -869,6 +1306,74 @@ function pgAdapter(options) {
|
|
|
869
1306
|
budgets: BUDGETS,
|
|
870
1307
|
readCancellation: cancellation.support,
|
|
871
1308
|
roundTrips: ROUND_TRIPS,
|
|
1309
|
+
readSnapshot: READ_SNAPSHOT,
|
|
1310
|
+
pipelining: pipelines === null ? "ordered" : PIPELINING,
|
|
1311
|
+
connectionIdentity: connection,
|
|
1312
|
+
...params.topLevel !== true || deferred === null ? {} : {
|
|
1313
|
+
deferStatement: (statement) => {
|
|
1314
|
+
budget.assertUsable({ stage: "statement" });
|
|
1315
|
+
deferred.defer({ text: statement.text, values: prepareBatchValues({ values: statement.values }) });
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1318
|
+
...params.commits !== true || deferred === null || scope.depth !== 0 || final !== undefined ? {} : {
|
|
1319
|
+
withCommit: (options) => createClientAdapter({
|
|
1320
|
+
dispatch,
|
|
1321
|
+
scope,
|
|
1322
|
+
budget,
|
|
1323
|
+
deferred,
|
|
1324
|
+
cancellation,
|
|
1325
|
+
pipelines,
|
|
1326
|
+
...laneOf,
|
|
1327
|
+
connection,
|
|
1328
|
+
final: { used: false, ...options?.rows === "discard" ? { discardRows: true } : {} }
|
|
1329
|
+
})
|
|
1330
|
+
},
|
|
1331
|
+
...pipelines === null ? {} : {
|
|
1332
|
+
...params.ordered === true ? { orderedPipeline: true } : {},
|
|
1333
|
+
pipeline: ({ lanes: count, ordered }) => {
|
|
1334
|
+
budget.assertUsable({ stage: "statement" });
|
|
1335
|
+
if (lane?.seat !== undefined)
|
|
1336
|
+
throw laneGroupMisuse({ message: "a sub-lane opens no pipeline scope" });
|
|
1337
|
+
if (lane?.group !== undefined)
|
|
1338
|
+
throw laneGroupMisuse({ message: "a lane opens no pipeline scope while its group is open" });
|
|
1339
|
+
if (!Number.isSafeInteger(count) || count < 1) {
|
|
1340
|
+
throw new VibeError7({ code: "VIBE_VALIDATION", message: "pipeline() needs a positive whole number of lanes", meta: { lanes: count } });
|
|
1341
|
+
}
|
|
1342
|
+
if (ordered === true && params.ordered !== true) {
|
|
1343
|
+
throw new VibeError7({
|
|
1344
|
+
code: "VIBE_VALIDATION",
|
|
1345
|
+
message: "pipeline({ ordered: true }) is offered only on a handle that declares orderedPipeline (the top-level or a savepoint handle of a transaction without a deadline, outside a pipeline lane)",
|
|
1346
|
+
meta: { ordered: true, reason: "ordered-pipeline-undeclared" }
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
const laneHandle = (opened) => ({
|
|
1350
|
+
adapter: createClientAdapter({ dispatch, scope, budget, deferred, cancellation, pipelines, lane: opened, connection, ...params.commits === true && opened.seat === undefined ? { commits: true } : {} }),
|
|
1351
|
+
close: () => {
|
|
1352
|
+
pipelines.close(opened);
|
|
1353
|
+
},
|
|
1354
|
+
...opened.order === undefined ? {} : {
|
|
1355
|
+
follow: () => {
|
|
1356
|
+
budget.assertUsable({ stage: "statement" });
|
|
1357
|
+
return laneHandle(pipelines.follow(opened));
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
...opened.order !== undefined || opened.seat !== undefined || lane !== undefined ? {} : {
|
|
1361
|
+
group: (options) => {
|
|
1362
|
+
const opening = pipelines.group(opened, options?.final === true && params.commits === true);
|
|
1363
|
+
if (opening === undefined)
|
|
1364
|
+
return;
|
|
1365
|
+
return {
|
|
1366
|
+
lane: () => laneHandle(pipelines.seat(opening)),
|
|
1367
|
+
seal: () => {
|
|
1368
|
+
pipelines.seal(opening);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
return (ordered === true ? pipelines.openOrdered(count) : pipelines.open(count)).map(laneHandle);
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
872
1377
|
rowChanges: () => {
|
|
873
1378
|
budget.assertUsable({ stage: "statement" });
|
|
874
1379
|
return readRowChanges({ scope });
|
|
@@ -877,29 +1382,39 @@ function pgAdapter(options) {
|
|
|
877
1382
|
return runReadExecution({ ...execParams, support: cancellation.support, provider: PROVIDER, run: async () => {
|
|
878
1383
|
const turn = scopeTurn({ scope });
|
|
879
1384
|
if (turn !== undefined)
|
|
880
|
-
await turn;
|
|
1385
|
+
await waitAsLane(turn);
|
|
881
1386
|
budget.assertUsable({ stage: "statement" });
|
|
882
1387
|
assertReadExecution2({ ...execParams, support: cancellation.support, provider: PROVIDER, stage: "waiting-for-connection" });
|
|
883
|
-
const queryable = { query: (config) => dispatch({ config, batchable: execParams.execution?.cancellation !== "running", execution: execParams.execution, onReadControl: execParams.onReadControl }) };
|
|
1388
|
+
const queryable = { query: (config) => dispatch({ config, batchable: execParams.execution?.cancellation !== "running", execution: execParams.execution, onReadControl: execParams.onReadControl, ...laneOf, ...finalOf() }) };
|
|
884
1389
|
return runExecute({ queryable, scope, ...execParams }).catch((error) => abortTransactionOnServerFailure({ scope, error }));
|
|
885
1390
|
} });
|
|
886
1391
|
},
|
|
887
1392
|
async executeUnsafe(execParams) {
|
|
888
1393
|
const turn = scopeTurn({ scope });
|
|
889
1394
|
if (turn !== undefined)
|
|
890
|
-
await turn;
|
|
1395
|
+
await waitAsLane(turn);
|
|
891
1396
|
budget.assertUsable({ stage: "statement" });
|
|
892
1397
|
return runExecuteUnsafe({ queryable: rawClient, scope, ...execParams }).catch((error) => abortTransactionOnServerFailure({ scope, error }));
|
|
893
1398
|
},
|
|
894
1399
|
async transaction(fn, nestedOptions) {
|
|
895
1400
|
refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
|
|
896
1401
|
budget.assertUsable({ stage: "nested" });
|
|
1402
|
+
if (lane?.group !== undefined)
|
|
1403
|
+
throw laneGroupMisuse({ message: "a lane opens no savepoint while its group is open" });
|
|
1404
|
+
if (lane?.seat !== undefined)
|
|
1405
|
+
throw laneGroupMisuse({ message: "a sub-lane opens no savepoint" });
|
|
1406
|
+
pipelines?.flush();
|
|
1407
|
+
const leave = lane === undefined || pipelines === null ? ignore : pipelines.enter(lane);
|
|
897
1408
|
return runNestedSavepoint({
|
|
898
1409
|
scope,
|
|
899
1410
|
execute: (statement) => savepointControl(statement, false),
|
|
900
1411
|
cleanup: (statement) => savepointControl(statement, true),
|
|
901
|
-
body: (child) =>
|
|
902
|
-
|
|
1412
|
+
body: (child) => {
|
|
1413
|
+
leave();
|
|
1414
|
+
const ordered = lane === undefined && params.ordered === true ? { ordered: true } : {};
|
|
1415
|
+
return fn(createClientAdapter({ dispatch, scope: child, budget, deferred, cancellation, pipelines, connection, ...laneOf, ...ordered }));
|
|
1416
|
+
}
|
|
1417
|
+
}).finally(leave);
|
|
903
1418
|
},
|
|
904
1419
|
async connect() {
|
|
905
1420
|
budget.assertUsable({ stage: "statement" });
|
|
@@ -1034,7 +1549,8 @@ function pgAdapter(options) {
|
|
|
1034
1549
|
validateTransactionDeadline({
|
|
1035
1550
|
options: transactionOptions,
|
|
1036
1551
|
support: BUDGETS.transactionDeadline,
|
|
1037
|
-
provider: PROVIDER
|
|
1552
|
+
provider: PROVIDER,
|
|
1553
|
+
settings: "transaction-local"
|
|
1038
1554
|
});
|
|
1039
1555
|
const openingOptions = {
|
|
1040
1556
|
isolationLevel: transactionOptions?.isolationLevel,
|
|
@@ -1042,7 +1558,25 @@ function pgAdapter(options) {
|
|
|
1042
1558
|
timeout: transactionOptions?.timeout
|
|
1043
1559
|
};
|
|
1044
1560
|
const openText = openTransactionStatement(openingOptions);
|
|
1561
|
+
const settings = readTransactionSettings({ settings: transactionOptions?.settings, provider: PROVIDER });
|
|
1562
|
+
const settingsStatement = settings.length === 0 ? undefined : renderTransactionSettings({ settings });
|
|
1045
1563
|
const client = await runMapped(() => getPool().connect());
|
|
1564
|
+
const turns = createClientTurns();
|
|
1565
|
+
let gathering = null;
|
|
1566
|
+
let gatheringsPending = 0;
|
|
1567
|
+
const runTurn = (send) => {
|
|
1568
|
+
gathering = null;
|
|
1569
|
+
return turns.run(send);
|
|
1570
|
+
};
|
|
1571
|
+
const onClient = {
|
|
1572
|
+
query: (config) => runTurn(() => client.query(config)),
|
|
1573
|
+
get pipeline() {
|
|
1574
|
+
return client.pipeline;
|
|
1575
|
+
},
|
|
1576
|
+
get connectionParameters() {
|
|
1577
|
+
return client.connectionParameters;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1046
1580
|
let connectionFailure;
|
|
1047
1581
|
const unguard = guardBackgroundErrors({
|
|
1048
1582
|
client,
|
|
@@ -1054,22 +1588,68 @@ function pgAdapter(options) {
|
|
|
1054
1588
|
const scope = createSavepointScope();
|
|
1055
1589
|
const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
|
|
1056
1590
|
const capable = supportsBatch({ client });
|
|
1057
|
-
const deferOpening = capable && (openingOptions.timeout === undefined || openingOptions.timeout <= MAX_STATEMENT_TIMEOUT_MS);
|
|
1591
|
+
const deferOpening = (settingsStatement === undefined || transactionOptions?.deferSettings === true) && capable && (openingOptions.timeout === undefined || openingOptions.timeout <= MAX_STATEMENT_TIMEOUT_MS);
|
|
1058
1592
|
const opening = deferOpening ? [beginStatement(openingOptions), ...openingOptions.timeout === undefined ? [] : [statementTimeoutStatement({ timeout: openingOptions.timeout })]] : [];
|
|
1059
|
-
const deferred = createDeferredControl({ opening });
|
|
1593
|
+
const deferred = createDeferredControl({ opening, ...budget.totalMs === null ? { deferReleases: true } : {} });
|
|
1594
|
+
const settingsDeferred = deferOpening && settingsStatement !== undefined;
|
|
1595
|
+
if (settingsDeferred)
|
|
1596
|
+
deferred.defer({ text: settingsStatement.text, values: prepareBatchValues({ values: settingsStatement.values }) });
|
|
1597
|
+
const openingLength = opening.length + (settingsDeferred ? 1 : 0);
|
|
1598
|
+
const openingStepError = (index, error) => {
|
|
1599
|
+
const mapped = mapPgDriverError({ error });
|
|
1600
|
+
if (!settingsDeferred || index !== opening.length || typeof mapped.meta.sqlstate !== "string")
|
|
1601
|
+
return mapped;
|
|
1602
|
+
return transactionSettingRefusedError({ provider: PROVIDER, settings, setting: findRefusedSetting({ settings, cause: mapped }), cause: mapped, deferred: true });
|
|
1603
|
+
};
|
|
1060
1604
|
let openingFailure;
|
|
1061
1605
|
let inFlight = null;
|
|
1606
|
+
const commits = capable && budget.totalMs === null && !((client.connectionParameters?.query_timeout ?? 0) > 0);
|
|
1607
|
+
let ridden;
|
|
1608
|
+
let ordinaryOpen = 0;
|
|
1609
|
+
const ridesNow = () => commits && ridden === undefined && !cancellationQueue && !cancellationAttempted && connectionFailure === undefined && openingFailure === undefined && scope.state.open === null && scope.state.aborted === null && scope.state.poisoned === null;
|
|
1610
|
+
const settleRidden = (entry, outcome, commitIndex) => {
|
|
1611
|
+
const step = outcome.steps[commitIndex];
|
|
1612
|
+
const answeredError = step?.status === "failed" ? mapPgDriverError({ error: step.error }) : undefined;
|
|
1613
|
+
if (step === undefined || answeredError !== undefined && isConnectionLoss({ error: answeredError })) {
|
|
1614
|
+
commitProgress = "unanswered";
|
|
1615
|
+
discard = true;
|
|
1616
|
+
budget.close({ outcome: "unknown" });
|
|
1617
|
+
entry.state = "lost";
|
|
1618
|
+
entry.error = commitOutcomeUnknownError({ cause: answeredError ?? outcome.transportError ?? firstBatchFailure({ outcome })?.error });
|
|
1619
|
+
} else if (step.status === "skipped") {
|
|
1620
|
+
commitProgress = "not-sent";
|
|
1621
|
+
entry.state = "skipped";
|
|
1622
|
+
} else if (step.status === "failed" || step.result.command === "ROLLBACK") {
|
|
1623
|
+
commitProgress = "answered";
|
|
1624
|
+
entry.state = "failed";
|
|
1625
|
+
entry.error = answeredError ?? transactionAbortedError2({ cause: scope.state.aborted?.cause });
|
|
1626
|
+
} else {
|
|
1627
|
+
entry.state = "committed";
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
const trackRidden = (batch, commitIndex) => {
|
|
1631
|
+
commitProgress = "sent";
|
|
1632
|
+
const entry = { settled: Promise.resolve(), state: "in-flight" };
|
|
1633
|
+
entry.settled = batch.then((outcome) => {
|
|
1634
|
+
settleRidden(entry, outcome, commitIndex);
|
|
1635
|
+
}, (error) => {
|
|
1636
|
+
settleRidden(entry, { steps: [], transportError: error }, commitIndex);
|
|
1637
|
+
});
|
|
1638
|
+
ridden = entry;
|
|
1639
|
+
};
|
|
1062
1640
|
const sendWithControl = (dispatchParams) => {
|
|
1063
1641
|
const { config, cleanup } = dispatchParams;
|
|
1064
1642
|
const handOver = () => {
|
|
1065
1643
|
assertReadExecution2({ execution: dispatchParams.execution, effect: "read", support: readCancellation, provider: PROVIDER, stage: "waiting-for-connection" });
|
|
1066
1644
|
dispatchParams.sent?.();
|
|
1067
|
-
return cancellation.run({ config, execution: dispatchParams.execution, onReadControl: dispatchParams.onReadControl, send: () =>
|
|
1645
|
+
return cancellation.run({ config, execution: dispatchParams.execution, onReadControl: dispatchParams.onReadControl, send: () => onClient.query(config) });
|
|
1068
1646
|
};
|
|
1069
1647
|
if (cleanup !== true && cancellationAttempted)
|
|
1070
|
-
throw
|
|
1648
|
+
throw transactionAbortedError2({ cause: readCancelledError2({ stage: "running" }) });
|
|
1071
1649
|
if (cleanup !== true && openingFailure !== undefined)
|
|
1072
|
-
throw
|
|
1650
|
+
throw transactionAbortedError2({ cause: openingFailure });
|
|
1651
|
+
if (cleanup !== true && ridden !== undefined)
|
|
1652
|
+
throw commitSentRefusal();
|
|
1073
1653
|
let bound;
|
|
1074
1654
|
if (cleanup !== true && budget.totalMs !== null) {
|
|
1075
1655
|
if (scope.state.poisoned !== null)
|
|
@@ -1083,30 +1663,45 @@ function pgAdapter(options) {
|
|
|
1083
1663
|
if (bound === undefined)
|
|
1084
1664
|
return handOver();
|
|
1085
1665
|
const stage = dispatchParams.stage ?? "statement";
|
|
1086
|
-
return runMapped(() =>
|
|
1666
|
+
return runMapped(() => onClient.query({ text: bound })).then(() => {
|
|
1087
1667
|
budget.assertUsable({ stage });
|
|
1088
1668
|
return handOver();
|
|
1089
1669
|
});
|
|
1090
1670
|
}
|
|
1091
1671
|
const pendingControl = cleanup !== true && deferred.hasPending();
|
|
1092
|
-
|
|
1672
|
+
const rides = dispatchParams.commit === true && cleanup !== true && dispatchParams.batchable === true && bound === undefined && ordinaryOpen === 1 && ridesNow();
|
|
1673
|
+
if (!pendingControl && bound === undefined && !rides) {
|
|
1674
|
+
if ((gathering?.phase === "turn" || turns.outstanding() > 0) && gathers(dispatchParams) && deferred.opened())
|
|
1675
|
+
return gatherInTurn(dispatchParams);
|
|
1093
1676
|
return handOver();
|
|
1094
|
-
|
|
1677
|
+
}
|
|
1678
|
+
const statement = dispatchParams.batchable === true ? [{ text: config.text, values: prepareBatchValues({ values: config.values ?? [] }), ...rides && dispatchParams.discardRows === true ? { discardRows: true } : {} }] : [];
|
|
1679
|
+
assertReadExecution2({ execution: dispatchParams.execution, effect: "read", support: readCancellation, provider: PROVIDER, stage: "waiting-for-connection" });
|
|
1095
1680
|
const carriesOpening = !deferred.opened();
|
|
1096
|
-
const control = pendingControl ? [...deferred.
|
|
1681
|
+
const control = pendingControl ? [...deferred.takeSteps()] : [];
|
|
1097
1682
|
if (bound !== undefined)
|
|
1098
|
-
control.push(bound);
|
|
1099
|
-
const openingSteps = carriesOpening ?
|
|
1100
|
-
assertReadExecution2({ execution: dispatchParams.execution, effect: "read", support: readCancellation, provider: PROVIDER, stage: "waiting-for-connection" });
|
|
1683
|
+
control.push({ text: bound });
|
|
1684
|
+
const openingSteps = carriesOpening ? openingLength : 0;
|
|
1101
1685
|
if (statement.length > 0)
|
|
1102
1686
|
dispatchParams.sent?.();
|
|
1103
|
-
const
|
|
1687
|
+
const commitIndex = control.length + statement.length;
|
|
1688
|
+
const batch = runTurn(() => runBatch({ client, steps: [...control, ...statement, ...rides ? [COMMIT_STEP] : []], types: EXACT_TYPES }));
|
|
1689
|
+
if (rides)
|
|
1690
|
+
trackRidden(batch, commitIndex);
|
|
1691
|
+
const burst = batch.then((received) => {
|
|
1692
|
+
const outcome = rides ? beforeCommit({ outcome: received, commitIndex }) : received;
|
|
1104
1693
|
const failure = firstBatchFailure({ outcome });
|
|
1105
1694
|
if (failure !== undefined) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1695
|
+
const openingFailed = failure.index < openingSteps || outcome.transportError !== undefined && openingSteps > 0;
|
|
1696
|
+
if (openingFailed) {
|
|
1697
|
+
openingFailure = openingStepError(failure.index, failure.error);
|
|
1108
1698
|
}
|
|
1109
|
-
|
|
1699
|
+
if (gatheringsPending > 0) {
|
|
1700
|
+
const mapped = openingFailed && openingFailure !== undefined ? openingFailure : mapPgDriverError({ error: failure.error });
|
|
1701
|
+
if (typeof mapped.meta.sqlstate === "string")
|
|
1702
|
+
markTransactionAborted({ scope, cause: mapped });
|
|
1703
|
+
}
|
|
1704
|
+
throw openingFailed && settingsDeferred && failure.index === opening.length && openingFailure !== undefined ? openingFailure : failure.error;
|
|
1110
1705
|
}
|
|
1111
1706
|
if (statement.length === 0)
|
|
1112
1707
|
return handOver();
|
|
@@ -1122,35 +1717,564 @@ function pgAdapter(options) {
|
|
|
1122
1717
|
}
|
|
1123
1718
|
return burst;
|
|
1124
1719
|
};
|
|
1125
|
-
const
|
|
1720
|
+
const guarded = (run) => {
|
|
1126
1721
|
try {
|
|
1127
|
-
return
|
|
1722
|
+
return run();
|
|
1128
1723
|
} catch (error) {
|
|
1129
1724
|
return Promise.reject(error);
|
|
1130
1725
|
}
|
|
1131
1726
|
};
|
|
1727
|
+
const send = (dispatchParams) => guarded(() => sendWithControl(dispatchParams));
|
|
1132
1728
|
let tail = Promise.resolve();
|
|
1133
1729
|
const activeDispatches = new Set;
|
|
1134
1730
|
let cancellationQueue = false;
|
|
1731
|
+
const lanes = new Set;
|
|
1732
|
+
let queued = [];
|
|
1733
|
+
const settleQueued = (entry, outcome) => {
|
|
1734
|
+
if (entry.settled)
|
|
1735
|
+
return;
|
|
1736
|
+
entry.settled = true;
|
|
1737
|
+
entry.lane.waiting -= 1;
|
|
1738
|
+
if ("result" in outcome) {
|
|
1739
|
+
entry.resolve(outcome.result);
|
|
1740
|
+
} else {
|
|
1741
|
+
entry.failure = outcome.error;
|
|
1742
|
+
entry.reject(outcome.error);
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
const distributeBurst = (burst) => {
|
|
1746
|
+
const { outcome, live, controlSteps, openingSteps } = burst;
|
|
1747
|
+
const failure = firstBatchFailure({ outcome });
|
|
1748
|
+
if (failure === undefined) {
|
|
1749
|
+
live.forEach((entry, index) => {
|
|
1750
|
+
const step = outcome.steps[controlSteps + index];
|
|
1751
|
+
settleQueued(entry, { result: step?.status === "ok" ? step.result : { rows: [], rowCount: 0 } });
|
|
1752
|
+
});
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
const mapped = failure.index < openingSteps ? openingStepError(failure.index, failure.error) : mapPgDriverError({ error: failure.error });
|
|
1756
|
+
if (failure.index < openingSteps || outcome.transportError !== undefined && openingSteps > 0)
|
|
1757
|
+
openingFailure = mapped;
|
|
1758
|
+
if (typeof mapped.meta.sqlstate === "string")
|
|
1759
|
+
markTransactionAborted({ scope, cause: mapped });
|
|
1760
|
+
const answered = outcome.transportError === undefined;
|
|
1761
|
+
if (failure.index < controlSteps) {
|
|
1762
|
+
const later = answered || openingFailure !== undefined ? transactionAbortedError2({ cause: openingFailure ?? mapped }) : mapped;
|
|
1763
|
+
live.forEach((entry, index) => {
|
|
1764
|
+
settleQueued(entry, { error: index === 0 ? mapped : later });
|
|
1765
|
+
});
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
let abortCause = mapped;
|
|
1769
|
+
live.forEach((entry, index) => {
|
|
1770
|
+
const position = controlSteps + index;
|
|
1771
|
+
const step = outcome.steps[position];
|
|
1772
|
+
if (step === undefined) {
|
|
1773
|
+
settleQueued(entry, { error: position === failure.index || openingFailure === undefined ? mapped : transactionAbortedError2({ cause: openingFailure }) });
|
|
1774
|
+
} else if (step.status === "ok") {
|
|
1775
|
+
settleQueued(entry, { result: step.result });
|
|
1776
|
+
} else if (step.status === "failed") {
|
|
1777
|
+
const error = position === failure.index ? mapped : mapPgDriverError({ error: step.error });
|
|
1778
|
+
if (typeof error.meta.sqlstate === "string")
|
|
1779
|
+
abortCause = error;
|
|
1780
|
+
settleQueued(entry, { error });
|
|
1781
|
+
} else {
|
|
1782
|
+
settleQueued(entry, { error: transactionAbortedError2({ cause: abortCause }) });
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
};
|
|
1786
|
+
const sendRefusal = () => ridden !== undefined ? commitSentRefusal() : cancellationAttempted ? transactionAbortedError2({ cause: readCancelledError2({ stage: "running" }) }) : openingFailure !== undefined ? transactionAbortedError2({ cause: openingFailure }) : scope.state.poisoned ?? (scope.state.aborted === null ? undefined : transactionAbortedError2({ cause: scope.state.aborted.cause }));
|
|
1787
|
+
const ridesQueued = (live) => {
|
|
1788
|
+
const last = live.at(-1);
|
|
1789
|
+
if (last === undefined || last.commit !== true && last.lane.seat?.group.final !== true || queued.length > 0 || inFlight !== null || ordinaryOpen > 0 || !ridesNow())
|
|
1790
|
+
return false;
|
|
1791
|
+
const counts = new Map;
|
|
1792
|
+
for (const entry of live) {
|
|
1793
|
+
if (entry.commit !== true && entry.lane.order?.followed !== true && entry.lane.seat?.group.final !== true)
|
|
1794
|
+
return false;
|
|
1795
|
+
counts.set(entry.lane, (counts.get(entry.lane) ?? 0) + 1);
|
|
1796
|
+
}
|
|
1797
|
+
for (const open of lanes) {
|
|
1798
|
+
const count = counts.get(open) ?? 0;
|
|
1799
|
+
if (count > 0 && open.waiting === count)
|
|
1800
|
+
continue;
|
|
1801
|
+
const group = open.group;
|
|
1802
|
+
if (count === 0 && group?.final === true && group.sealed && open.waiting === 1 && live.some((entry) => entry.lane.seat?.group === group))
|
|
1803
|
+
continue;
|
|
1804
|
+
if (count === 0 && open.final === true && open.waiting === 0)
|
|
1805
|
+
continue;
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
return true;
|
|
1809
|
+
};
|
|
1810
|
+
const sendQueued = (entries, options) => {
|
|
1811
|
+
const inTurn = options?.inTurn === true;
|
|
1812
|
+
const refusal = sendRefusal();
|
|
1813
|
+
const live = [];
|
|
1814
|
+
for (const entry of entries) {
|
|
1815
|
+
if (refusal !== undefined)
|
|
1816
|
+
settleQueued(entry, { error: refusal });
|
|
1817
|
+
else if (entry.execution?.signal?.aborted === true)
|
|
1818
|
+
settleQueued(entry, { error: readCancelledError2({ stage: "waiting-for-connection" }) });
|
|
1819
|
+
else
|
|
1820
|
+
live.push(entry);
|
|
1821
|
+
}
|
|
1822
|
+
if (live.length === 0)
|
|
1823
|
+
return Promise.resolve();
|
|
1824
|
+
const carriesOpening = !inTurn && !deferred.opened();
|
|
1825
|
+
const rides = !inTurn && live.length === entries.length && ridesQueued(live);
|
|
1826
|
+
const control = inTurn ? [] : deferred.takeSteps();
|
|
1827
|
+
const openingSteps = carriesOpening ? openingLength : 0;
|
|
1828
|
+
const commitIndex = control.length + live.length;
|
|
1829
|
+
const sendBatch = () => runBatch({ client, steps: [...control, ...live.map((entry) => ({ text: entry.text, values: entry.values })), ...rides ? [COMMIT_STEP] : []], types: EXACT_TYPES });
|
|
1830
|
+
const batch = inTurn ? new Promise((resolve) => {
|
|
1831
|
+
resolve(sendBatch());
|
|
1832
|
+
}) : runTurn(sendBatch);
|
|
1833
|
+
if (rides)
|
|
1834
|
+
trackRidden(batch, commitIndex);
|
|
1835
|
+
const burst = batch.then((received) => {
|
|
1836
|
+
distributeBurst({ outcome: rides ? beforeCommit({ outcome: received, commitIndex }) : received, live, controlSteps: control.length, openingSteps });
|
|
1837
|
+
}).then(undefined, (error) => {
|
|
1838
|
+
if (openingSteps > 0)
|
|
1839
|
+
openingFailure ??= mapPgDriverError({ error });
|
|
1840
|
+
for (const entry of live)
|
|
1841
|
+
settleQueued(entry, { error });
|
|
1842
|
+
});
|
|
1843
|
+
if (carriesOpening) {
|
|
1844
|
+
const settled = burst.then(() => {
|
|
1845
|
+
if (inFlight === settled)
|
|
1846
|
+
inFlight = null;
|
|
1847
|
+
});
|
|
1848
|
+
inFlight = settled;
|
|
1849
|
+
}
|
|
1850
|
+
return burst;
|
|
1851
|
+
};
|
|
1852
|
+
const gathers = (dispatchParams) => dispatchParams.commit !== true && coalescable(dispatchParams);
|
|
1853
|
+
const closeGathering = () => {
|
|
1854
|
+
gathering = null;
|
|
1855
|
+
};
|
|
1856
|
+
const gatheredLane = { open: false, waiting: 0 };
|
|
1857
|
+
const joinGathering = (group, dispatchParams, values) => new Promise((resolve, reject) => {
|
|
1858
|
+
group.members.push({ params: dispatchParams, values, resolve, reject });
|
|
1859
|
+
});
|
|
1860
|
+
const sendGathered = (members, options) => sendQueued(members.map((member) => {
|
|
1861
|
+
gatheredLane.waiting += 1;
|
|
1862
|
+
return {
|
|
1863
|
+
text: member.params.config.text,
|
|
1864
|
+
values: member.values,
|
|
1865
|
+
...member.params.execution === undefined || options?.inTurn === true ? {} : { execution: member.params.execution },
|
|
1866
|
+
lane: gatheredLane,
|
|
1867
|
+
resolve: member.resolve,
|
|
1868
|
+
reject: member.reject,
|
|
1869
|
+
settled: false
|
|
1870
|
+
};
|
|
1871
|
+
}), options);
|
|
1872
|
+
const gatherBehind = (dispatchParams, behind) => {
|
|
1873
|
+
const values = prepareBatchValues({ values: dispatchParams.config.values ?? [] });
|
|
1874
|
+
const open = gathering;
|
|
1875
|
+
if (open !== null && open.phase === "opening" && open.behind === behind)
|
|
1876
|
+
return joinGathering(open, dispatchParams, values);
|
|
1877
|
+
const group = { phase: "opening", behind, members: [] };
|
|
1878
|
+
gathering = group;
|
|
1879
|
+
gatheringsPending += 1;
|
|
1880
|
+
const joined = joinGathering(group, dispatchParams, values);
|
|
1881
|
+
behind.then(() => {
|
|
1882
|
+
if (gathering === group)
|
|
1883
|
+
gathering = null;
|
|
1884
|
+
gatheringsPending -= 1;
|
|
1885
|
+
const [only, ...more] = group.members;
|
|
1886
|
+
if (only !== undefined && more.length === 0) {
|
|
1887
|
+
send(only.params).then(only.resolve, only.reject);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
sendGathered(group.members);
|
|
1891
|
+
});
|
|
1892
|
+
return joined;
|
|
1893
|
+
};
|
|
1894
|
+
const gatherInTurn = (dispatchParams) => {
|
|
1895
|
+
assertReadExecution2({ execution: dispatchParams.execution, effect: "read", support: readCancellation, provider: PROVIDER, stage: "waiting-for-connection" });
|
|
1896
|
+
const values = prepareBatchValues({ values: dispatchParams.config.values ?? [] });
|
|
1897
|
+
const open = gathering;
|
|
1898
|
+
if (open !== null && open.phase === "turn")
|
|
1899
|
+
return joinGathering(open, dispatchParams, values);
|
|
1900
|
+
const group = { phase: "turn", members: [] };
|
|
1901
|
+
gatheringsPending += 1;
|
|
1902
|
+
const joined = joinGathering(group, dispatchParams, values);
|
|
1903
|
+
turns.run(() => sendTurnGroup(group));
|
|
1904
|
+
gathering = group;
|
|
1905
|
+
return joined;
|
|
1906
|
+
};
|
|
1907
|
+
const sendTurnGroup = (group) => {
|
|
1908
|
+
if (gathering === group)
|
|
1909
|
+
gathering = null;
|
|
1910
|
+
gatheringsPending -= 1;
|
|
1911
|
+
const [only, ...more] = group.members;
|
|
1912
|
+
if (only !== undefined && more.length === 0) {
|
|
1913
|
+
const query = new Promise((resolve) => {
|
|
1914
|
+
resolve(client.query(only.params.config));
|
|
1915
|
+
});
|
|
1916
|
+
query.then(only.resolve, only.reject);
|
|
1917
|
+
return query;
|
|
1918
|
+
}
|
|
1919
|
+
const prior = turns.lastFailure();
|
|
1920
|
+
if (prior !== undefined) {
|
|
1921
|
+
const mapped = mapPgDriverError({ error: prior.error });
|
|
1922
|
+
if (typeof mapped.meta.sqlstate === "string")
|
|
1923
|
+
markTransactionAborted({ scope, cause: mapped });
|
|
1924
|
+
}
|
|
1925
|
+
return sendGathered(group.members, { inTurn: true });
|
|
1926
|
+
};
|
|
1927
|
+
const sendAlone = (entry, alone) => {
|
|
1928
|
+
const refusal = sendRefusal();
|
|
1929
|
+
if (refusal !== undefined) {
|
|
1930
|
+
settleQueued(entry, { error: refusal });
|
|
1931
|
+
return Promise.resolve();
|
|
1932
|
+
}
|
|
1933
|
+
return dispatchOrdinary(alone).then((result) => {
|
|
1934
|
+
settleQueued(entry, { result });
|
|
1935
|
+
}, (error) => {
|
|
1936
|
+
const mapped = mapPgDriverError({ error });
|
|
1937
|
+
if (typeof mapped.meta.sqlstate === "string")
|
|
1938
|
+
markTransactionAborted({ scope, cause: mapped });
|
|
1939
|
+
settleQueued(entry, { error });
|
|
1940
|
+
});
|
|
1941
|
+
};
|
|
1942
|
+
const prefixOf = (lane) => {
|
|
1943
|
+
const order = lane.order;
|
|
1944
|
+
if (order === undefined)
|
|
1945
|
+
return "issued";
|
|
1946
|
+
let state = "issued";
|
|
1947
|
+
for (let index = 0;index < order.index; index++) {
|
|
1948
|
+
const earlier = order.scope.lanes[index];
|
|
1949
|
+
if (earlier.order.issued > 0)
|
|
1950
|
+
continue;
|
|
1951
|
+
if (!earlier.open)
|
|
1952
|
+
return "closed";
|
|
1953
|
+
state = "open";
|
|
1954
|
+
}
|
|
1955
|
+
return state;
|
|
1956
|
+
};
|
|
1957
|
+
const refuseOrphaned = () => {
|
|
1958
|
+
if (!queued.some((entry) => prefixOf(entry.lane) === "closed"))
|
|
1959
|
+
return;
|
|
1960
|
+
const kept = [];
|
|
1961
|
+
for (const entry of queued) {
|
|
1962
|
+
if (prefixOf(entry.lane) === "closed")
|
|
1963
|
+
settleQueued(entry, { error: predecessorClosedError() });
|
|
1964
|
+
else
|
|
1965
|
+
kept.push(entry);
|
|
1966
|
+
}
|
|
1967
|
+
queued = kept;
|
|
1968
|
+
};
|
|
1969
|
+
const orderedSend = (entries, send) => () => {
|
|
1970
|
+
const live = [];
|
|
1971
|
+
for (const entry of entries) {
|
|
1972
|
+
const failed = entry.lane.order?.scope.failed;
|
|
1973
|
+
if (failed === undefined)
|
|
1974
|
+
live.push(entry);
|
|
1975
|
+
else
|
|
1976
|
+
settleQueued(entry, { error: sendRefusal() ?? transactionAbortedError2({ cause: failed.cause }) });
|
|
1977
|
+
}
|
|
1978
|
+
if (live.length === 0)
|
|
1979
|
+
return Promise.resolve();
|
|
1980
|
+
return send(live).then(() => {
|
|
1981
|
+
for (const entry of live) {
|
|
1982
|
+
const order = entry.lane.order;
|
|
1983
|
+
if (order !== undefined && entry.failure !== undefined)
|
|
1984
|
+
order.scope.failed ??= { cause: mapPgDriverError({ error: entry.failure }) };
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
};
|
|
1988
|
+
let orderedTail = null;
|
|
1989
|
+
const flushOrdered = () => {
|
|
1990
|
+
refuseOrphaned();
|
|
1991
|
+
const ready = [];
|
|
1992
|
+
const held = [];
|
|
1993
|
+
for (const entry of queued)
|
|
1994
|
+
(prefixOf(entry.lane) === "issued" ? ready : held).push(entry);
|
|
1995
|
+
queued = held;
|
|
1996
|
+
if (ready.length === 0)
|
|
1997
|
+
return;
|
|
1998
|
+
ready.sort((first, second) => (first.lane.order?.index ?? 0) - (second.lane.order?.index ?? 0));
|
|
1999
|
+
const sends = [];
|
|
2000
|
+
let run = [];
|
|
2001
|
+
for (const entry of ready) {
|
|
2002
|
+
const alone = entry.alone;
|
|
2003
|
+
if (alone === undefined) {
|
|
2004
|
+
run.push(entry);
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
if (run.length > 0) {
|
|
2008
|
+
sends.push(orderedSend(run, sendQueued));
|
|
2009
|
+
run = [];
|
|
2010
|
+
}
|
|
2011
|
+
sends.push(orderedSend([entry], () => sendAlone(entry, alone)));
|
|
2012
|
+
}
|
|
2013
|
+
if (run.length > 0)
|
|
2014
|
+
sends.push(orderedSend(run, sendQueued));
|
|
2015
|
+
const before = [orderedTail, inFlight].filter((wait) => wait !== null);
|
|
2016
|
+
let chain = before.length === 0 ? sends.shift()() : Promise.all(before).then(ignore);
|
|
2017
|
+
for (const next of sends)
|
|
2018
|
+
chain = chain.then(next);
|
|
2019
|
+
const tail = chain;
|
|
2020
|
+
orderedTail = tail;
|
|
2021
|
+
tail.then(() => {
|
|
2022
|
+
if (orderedTail === tail)
|
|
2023
|
+
orderedTail = null;
|
|
2024
|
+
});
|
|
2025
|
+
};
|
|
2026
|
+
const flushQueue = () => {
|
|
2027
|
+
if (queued.length === 0)
|
|
2028
|
+
return;
|
|
2029
|
+
closeGathering();
|
|
2030
|
+
if (queued.some((entry) => entry.lane.order !== undefined)) {
|
|
2031
|
+
flushOrdered();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const entries = queued.some((entry) => entry.lane.seat !== undefined) ? seatOrder(queued) : queued;
|
|
2035
|
+
queued = [];
|
|
2036
|
+
if (inFlight === null)
|
|
2037
|
+
sendQueued(entries);
|
|
2038
|
+
else
|
|
2039
|
+
inFlight.then(() => {
|
|
2040
|
+
sendQueued(entries);
|
|
2041
|
+
});
|
|
2042
|
+
};
|
|
2043
|
+
const flushIfReady = () => {
|
|
2044
|
+
if (queued.length === 0)
|
|
2045
|
+
return;
|
|
2046
|
+
for (const open of lanes)
|
|
2047
|
+
if (open.waiting === 0)
|
|
2048
|
+
return;
|
|
2049
|
+
flushQueue();
|
|
2050
|
+
};
|
|
2051
|
+
const seatOrder = (entries) => [...entries].sort((first, second) => (first.lane.seat?.index ?? -1) - (second.lane.seat?.index ?? -1));
|
|
2052
|
+
const endGroupIfOver = (opening) => {
|
|
2053
|
+
if (!opening.sealed || opening.live > 0 || opening.parent.group !== opening)
|
|
2054
|
+
return;
|
|
2055
|
+
opening.parent.group = undefined;
|
|
2056
|
+
opening.parent.waiting -= 1;
|
|
2057
|
+
};
|
|
2058
|
+
const closeLane = (lane) => {
|
|
2059
|
+
if (!lane.open)
|
|
2060
|
+
return;
|
|
2061
|
+
lane.open = false;
|
|
2062
|
+
lanes.delete(lane);
|
|
2063
|
+
if (lane.order !== undefined)
|
|
2064
|
+
refuseOrphaned();
|
|
2065
|
+
if (lane.seat !== undefined) {
|
|
2066
|
+
lane.seat.group.live -= 1;
|
|
2067
|
+
endGroupIfOver(lane.seat.group);
|
|
2068
|
+
}
|
|
2069
|
+
flushIfReady();
|
|
2070
|
+
};
|
|
2071
|
+
const pipelines = {
|
|
2072
|
+
open: (count) => Array.from({ length: count }, () => {
|
|
2073
|
+
const opened = { open: true, waiting: 0 };
|
|
2074
|
+
lanes.add(opened);
|
|
2075
|
+
return opened;
|
|
2076
|
+
}),
|
|
2077
|
+
openOrdered: (count) => {
|
|
2078
|
+
const ordered = { lanes: [] };
|
|
2079
|
+
for (let index = 0;index < count; index++) {
|
|
2080
|
+
const opened = { open: true, waiting: 0, order: { scope: ordered, index, issued: 0, followed: false } };
|
|
2081
|
+
ordered.lanes.push(opened);
|
|
2082
|
+
lanes.add(opened);
|
|
2083
|
+
}
|
|
2084
|
+
return [...ordered.lanes];
|
|
2085
|
+
},
|
|
2086
|
+
follow: (lane) => {
|
|
2087
|
+
const order = lane.order;
|
|
2088
|
+
if (order === undefined)
|
|
2089
|
+
throw orderedLaneMisuse({ message: "follow() exists only on the lanes of an ordered scope" });
|
|
2090
|
+
if (!lane.open)
|
|
2091
|
+
throw orderedLaneMisuse({ message: "follow() on a closed lane" });
|
|
2092
|
+
if (order.followed || order.scope.lanes.at(-1) !== lane)
|
|
2093
|
+
throw orderedLaneMisuse({ message: "follow() opens one lane after the LAST lane of its scope" });
|
|
2094
|
+
if (order.issued > 0)
|
|
2095
|
+
throw orderedLaneMisuse({ message: "follow() must come before the lane issues its statement" });
|
|
2096
|
+
order.followed = true;
|
|
2097
|
+
const next = { open: true, waiting: 0, order: { scope: order.scope, index: order.scope.lanes.length, issued: 0, followed: false } };
|
|
2098
|
+
order.scope.lanes.push(next);
|
|
2099
|
+
lanes.add(next);
|
|
2100
|
+
return next;
|
|
2101
|
+
},
|
|
2102
|
+
close: closeLane,
|
|
2103
|
+
waitAs: ({ lane, wait }) => {
|
|
2104
|
+
if (!lane.open)
|
|
2105
|
+
return wait;
|
|
2106
|
+
lane.waiting += 1;
|
|
2107
|
+
flushIfReady();
|
|
2108
|
+
return wait.then((value) => {
|
|
2109
|
+
lane.waiting -= 1;
|
|
2110
|
+
return value;
|
|
2111
|
+
}, (error) => {
|
|
2112
|
+
lane.waiting -= 1;
|
|
2113
|
+
throw error;
|
|
2114
|
+
});
|
|
2115
|
+
},
|
|
2116
|
+
enter: (lane) => {
|
|
2117
|
+
if (!lane.open)
|
|
2118
|
+
return ignore;
|
|
2119
|
+
lane.waiting += 1;
|
|
2120
|
+
flushIfReady();
|
|
2121
|
+
let left = false;
|
|
2122
|
+
return () => {
|
|
2123
|
+
if (left)
|
|
2124
|
+
return;
|
|
2125
|
+
left = true;
|
|
2126
|
+
lane.waiting -= 1;
|
|
2127
|
+
};
|
|
2128
|
+
},
|
|
2129
|
+
flush: flushQueue,
|
|
2130
|
+
group: (lane, final) => {
|
|
2131
|
+
if (!lane.open || lane.order !== undefined || lane.seat !== undefined || lane.gate === true || lane.group !== undefined || lane.final === true)
|
|
2132
|
+
return;
|
|
2133
|
+
if (!capable || budget.totalMs !== null || cancellationQueue || sendRefusal() !== undefined)
|
|
2134
|
+
return;
|
|
2135
|
+
if ((client.connectionParameters?.query_timeout ?? 0) > 0)
|
|
2136
|
+
return;
|
|
2137
|
+
for (const other of lanes) {
|
|
2138
|
+
if (other !== lane && (other.final !== true || other.waiting > 0))
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
const gate = { open: true, waiting: 0, gate: true };
|
|
2142
|
+
const opening = { parent: lane, gate, opened: 0, live: 0, sealed: false, final };
|
|
2143
|
+
lanes.add(gate);
|
|
2144
|
+
lane.group = opening;
|
|
2145
|
+
lane.waiting += 1;
|
|
2146
|
+
return opening;
|
|
2147
|
+
},
|
|
2148
|
+
seat: (opening) => {
|
|
2149
|
+
if (opening.sealed)
|
|
2150
|
+
throw laneGroupMisuse({ message: "a sub-lane is opened only before the group is sealed" });
|
|
2151
|
+
if (!opening.parent.open)
|
|
2152
|
+
throw laneGroupMisuse({ message: "a sub-lane of a closed lane" });
|
|
2153
|
+
const seat = { open: true, waiting: 0, seat: { group: opening, index: opening.opened, issued: false } };
|
|
2154
|
+
opening.opened += 1;
|
|
2155
|
+
opening.live += 1;
|
|
2156
|
+
lanes.add(seat);
|
|
2157
|
+
return seat;
|
|
2158
|
+
},
|
|
2159
|
+
seal: (opening) => {
|
|
2160
|
+
if (opening.sealed)
|
|
2161
|
+
return;
|
|
2162
|
+
opening.sealed = true;
|
|
2163
|
+
endGroupIfOver(opening);
|
|
2164
|
+
closeLane(opening.gate);
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
const endPipeline = () => {
|
|
2168
|
+
for (const open of lanes)
|
|
2169
|
+
open.open = false;
|
|
2170
|
+
lanes.clear();
|
|
2171
|
+
flushQueue();
|
|
2172
|
+
};
|
|
2173
|
+
const enqueue = (dispatchParams, lane, alone) => {
|
|
2174
|
+
if (cancellationAttempted)
|
|
2175
|
+
throw transactionAbortedError2({ cause: readCancelledError2({ stage: "running" }) });
|
|
2176
|
+
if (openingFailure !== undefined)
|
|
2177
|
+
throw transactionAbortedError2({ cause: openingFailure });
|
|
2178
|
+
if (ridden !== undefined)
|
|
2179
|
+
throw commitSentRefusal();
|
|
2180
|
+
assertReadExecution2({ execution: dispatchParams.execution, effect: "read", support: readCancellation, provider: PROVIDER, stage: "waiting-for-connection" });
|
|
2181
|
+
const values = alone === true ? [] : prepareBatchValues({ values: dispatchParams.config.values ?? [] });
|
|
2182
|
+
const pending = new Promise((resolve, reject) => {
|
|
2183
|
+
lane.waiting += 1;
|
|
2184
|
+
queued.push({
|
|
2185
|
+
text: dispatchParams.config.text,
|
|
2186
|
+
values,
|
|
2187
|
+
...dispatchParams.execution === undefined ? {} : { execution: dispatchParams.execution },
|
|
2188
|
+
...alone === true ? { alone: dispatchParams } : {},
|
|
2189
|
+
...dispatchParams.commit === true ? { commit: true } : {},
|
|
2190
|
+
lane,
|
|
2191
|
+
resolve,
|
|
2192
|
+
reject,
|
|
2193
|
+
settled: false
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
if (lane.order !== undefined)
|
|
2197
|
+
lane.order.issued += 1;
|
|
2198
|
+
activeDispatches.add(pending);
|
|
2199
|
+
pending.then(() => activeDispatches.delete(pending), () => activeDispatches.delete(pending));
|
|
2200
|
+
flushIfReady();
|
|
2201
|
+
return pending;
|
|
2202
|
+
};
|
|
2203
|
+
const coalescable = (dispatchParams) => capable && budget.totalMs === null && !cancellationQueue && !((client.connectionParameters?.query_timeout ?? 0) > 0) && dispatchParams.batchable === true && dispatchParams.cleanup !== true && dispatchParams.stage === undefined && dispatchParams.sent === undefined && dispatchParams.execution?.cancellation !== "running";
|
|
1135
2204
|
const dispatch = (dispatchParams) => {
|
|
2205
|
+
const lane = dispatchParams.lane?.open === true ? dispatchParams.lane : undefined;
|
|
2206
|
+
if (lane !== undefined && dispatchParams.cleanup !== true) {
|
|
2207
|
+
if (lane.group !== undefined)
|
|
2208
|
+
return Promise.reject(laneGroupMisuse({ message: "a lane issues nothing while its group is open" }));
|
|
2209
|
+
const seat = lane.seat;
|
|
2210
|
+
if (seat !== undefined) {
|
|
2211
|
+
if (seat.issued)
|
|
2212
|
+
return Promise.reject(laneGroupMisuse({ message: "a sub-lane issues ONE statement" }));
|
|
2213
|
+
if (!coalescable(dispatchParams))
|
|
2214
|
+
return Promise.reject(laneGroupMisuse({ message: "a sub-lane statement must be able to join a batch" }));
|
|
2215
|
+
seat.issued = true;
|
|
2216
|
+
}
|
|
2217
|
+
if (dispatchParams.commit === true)
|
|
2218
|
+
lane.final = true;
|
|
2219
|
+
}
|
|
2220
|
+
const order = lane?.order;
|
|
2221
|
+
if (lane !== undefined && order !== undefined && dispatchParams.cleanup !== true) {
|
|
2222
|
+
return guarded(() => {
|
|
2223
|
+
if (order.followed && order.issued > 0)
|
|
2224
|
+
throw orderedLaneMisuse({ message: "a lane that has a follower may issue ONE statement" });
|
|
2225
|
+
return coalescable(dispatchParams) ? enqueue(dispatchParams, lane) : enqueue(dispatchParams, lane, true);
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
if (lane !== undefined && coalescable(dispatchParams))
|
|
2229
|
+
return guarded(() => enqueue(dispatchParams, lane));
|
|
2230
|
+
if (lane === undefined)
|
|
2231
|
+
return dispatchOrdinary(dispatchParams);
|
|
2232
|
+
const leave = pipelines.enter(lane);
|
|
2233
|
+
flushQueue();
|
|
2234
|
+
return dispatchOrdinary(dispatchParams).then((result) => {
|
|
2235
|
+
leave();
|
|
2236
|
+
return result;
|
|
2237
|
+
}, (error) => {
|
|
2238
|
+
leave();
|
|
2239
|
+
throw error;
|
|
2240
|
+
});
|
|
2241
|
+
};
|
|
2242
|
+
const dispatchOrdinary = (dispatchParams) => {
|
|
1136
2243
|
if (dispatchParams.execution?.cancellation === "running" && !cancellationQueue) {
|
|
2244
|
+
flushQueue();
|
|
1137
2245
|
cancellationQueue = true;
|
|
2246
|
+
closeGathering();
|
|
1138
2247
|
tail = Promise.allSettled([...activeDispatches]);
|
|
1139
2248
|
}
|
|
1140
2249
|
if (budget.totalMs === null && !cancellationQueue) {
|
|
1141
|
-
|
|
2250
|
+
ordinaryOpen += 1;
|
|
2251
|
+
const behind = gathering?.phase === "opening" ? gathering.behind ?? null : inFlight;
|
|
2252
|
+
let pending;
|
|
2253
|
+
if (behind === null)
|
|
2254
|
+
pending = send(dispatchParams);
|
|
2255
|
+
else if (gathers(dispatchParams))
|
|
2256
|
+
pending = guarded(() => gatherBehind(dispatchParams, behind));
|
|
2257
|
+
else {
|
|
2258
|
+
closeGathering();
|
|
2259
|
+
pending = behind.then(() => send(dispatchParams));
|
|
2260
|
+
}
|
|
1142
2261
|
activeDispatches.add(pending);
|
|
1143
|
-
|
|
2262
|
+
const settle = () => {
|
|
2263
|
+
activeDispatches.delete(pending);
|
|
2264
|
+
ordinaryOpen -= 1;
|
|
2265
|
+
};
|
|
2266
|
+
pending.then(settle, settle);
|
|
1144
2267
|
return pending;
|
|
1145
2268
|
}
|
|
1146
2269
|
const pending = tail.then(() => send(dispatchParams));
|
|
1147
2270
|
tail = pending.then(ignore, ignore);
|
|
2271
|
+
closeGathering();
|
|
1148
2272
|
return pending;
|
|
1149
2273
|
};
|
|
1150
2274
|
let discard = false;
|
|
1151
2275
|
let released = false;
|
|
1152
2276
|
let cancellationAttempted = false;
|
|
1153
|
-
const cancellation = createPgReadCancellation({ connectionString: cancellationAddress, target:
|
|
2277
|
+
const cancellation = createPgReadCancellation({ connectionString: cancellationAddress, target: onClient, terminate: () => {
|
|
1154
2278
|
if (!released) {
|
|
1155
2279
|
released = true;
|
|
1156
2280
|
client.release(true);
|
|
@@ -1161,20 +2285,77 @@ function pgAdapter(options) {
|
|
|
1161
2285
|
markTransactionAborted({ scope, cause: readCancelledError2({ stage: "running" }) });
|
|
1162
2286
|
} });
|
|
1163
2287
|
let commitProgress = "not-sent";
|
|
2288
|
+
let openingRollback;
|
|
2289
|
+
const openWithSettings = async (statement) => {
|
|
2290
|
+
let failed;
|
|
2291
|
+
if (capable) {
|
|
2292
|
+
const control = [beginStatement(openingOptions), ...openingOptions.timeout === undefined ? [] : [statementTimeoutStatement({ timeout: openingOptions.timeout })]];
|
|
2293
|
+
const outcome = await turns.run(() => runBatch({
|
|
2294
|
+
client,
|
|
2295
|
+
steps: [...control.map((text) => ({ text })), { text: statement.text, values: prepareBatchValues({ values: statement.values }) }],
|
|
2296
|
+
types: EXACT_TYPES
|
|
2297
|
+
}));
|
|
2298
|
+
const failure = firstBatchFailure({ outcome });
|
|
2299
|
+
if (failure === undefined)
|
|
2300
|
+
return;
|
|
2301
|
+
if (failure.index < control.length || outcome.transportError !== undefined)
|
|
2302
|
+
throw mapPgDriverError({ error: failure.error });
|
|
2303
|
+
failed = { error: failure.error };
|
|
2304
|
+
} else {
|
|
2305
|
+
await runMapped(() => onClient.query({ text: openText }));
|
|
2306
|
+
failed = await onClient.query({ text: statement.text, values: [...statement.values] }).then(() => {
|
|
2307
|
+
return;
|
|
2308
|
+
}, (error) => ({ error }));
|
|
2309
|
+
if (failed === undefined)
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const cause = mapPgDriverError({ error: failed.error });
|
|
2313
|
+
if (typeof cause.meta.sqlstate !== "string")
|
|
2314
|
+
throw cause;
|
|
2315
|
+
openingRollback = await onClient.query({ text: "ROLLBACK" }).then(() => "acknowledged", () => "failed");
|
|
2316
|
+
if (openingRollback === "failed")
|
|
2317
|
+
discard = true;
|
|
2318
|
+
const setting = findRefusedSetting({ settings, cause });
|
|
2319
|
+
throw transactionSettingRefusedError({ provider: PROVIDER, settings, setting, cause });
|
|
2320
|
+
};
|
|
1164
2321
|
try {
|
|
1165
|
-
if (!
|
|
1166
|
-
await
|
|
2322
|
+
if (settingsStatement !== undefined && !settingsDeferred)
|
|
2323
|
+
await openWithSettings(settingsStatement);
|
|
2324
|
+
else if (!deferOpening)
|
|
2325
|
+
await runMapped(() => onClient.query({ text: openText }));
|
|
1167
2326
|
const result = await runRootScope({
|
|
1168
2327
|
scope,
|
|
1169
|
-
body: () => fn(createClientAdapter({
|
|
2328
|
+
body: () => fn(createClientAdapter({
|
|
2329
|
+
dispatch,
|
|
2330
|
+
scope,
|
|
2331
|
+
budget,
|
|
2332
|
+
deferred: capable ? deferred : null,
|
|
2333
|
+
cancellation,
|
|
2334
|
+
pipelines: capable ? pipelines : null,
|
|
2335
|
+
connection: client,
|
|
2336
|
+
topLevel: true,
|
|
2337
|
+
ordered: capable && budget.totalMs === null && !((client.connectionParameters?.query_timeout ?? 0) > 0),
|
|
2338
|
+
commits
|
|
2339
|
+
}))
|
|
1170
2340
|
});
|
|
2341
|
+
endPipeline();
|
|
2342
|
+
const riding = ridden;
|
|
2343
|
+
if (riding !== undefined) {
|
|
2344
|
+
await riding.settled;
|
|
2345
|
+
if (riding.state === "committed") {
|
|
2346
|
+
budget.close({ outcome: "committed" });
|
|
2347
|
+
return result;
|
|
2348
|
+
}
|
|
2349
|
+
throw riding.state === "skipped" ? transactionAbortedError2({ cause: scope.state.aborted?.cause }) : riding.error;
|
|
2350
|
+
}
|
|
1171
2351
|
budget.assertUsable({ stage: "commit" });
|
|
1172
2352
|
if (!deferred.opened()) {
|
|
1173
2353
|
budget.close({ outcome: "committed" });
|
|
1174
2354
|
return result;
|
|
1175
2355
|
}
|
|
1176
2356
|
if (openingFailure !== undefined)
|
|
1177
|
-
throw
|
|
2357
|
+
throw transactionAbortedError2({ cause: openingFailure });
|
|
2358
|
+
deferred.dropReleases();
|
|
1178
2359
|
const commit = await runMapped(() => dispatch({
|
|
1179
2360
|
config: { text: "COMMIT" },
|
|
1180
2361
|
stage: "commit",
|
|
@@ -1198,18 +2379,29 @@ function pgAdapter(options) {
|
|
|
1198
2379
|
});
|
|
1199
2380
|
if (finalResult({ result: commit }).command === "ROLLBACK") {
|
|
1200
2381
|
commitProgress = "answered";
|
|
1201
|
-
throw
|
|
2382
|
+
throw transactionAbortedError2({ cause: scope.state.aborted?.cause });
|
|
1202
2383
|
}
|
|
1203
2384
|
budget.close({ outcome: "committed" });
|
|
1204
2385
|
return result;
|
|
1205
|
-
} catch (
|
|
2386
|
+
} catch (caught) {
|
|
2387
|
+
endPipeline();
|
|
2388
|
+
const riding = ridden;
|
|
2389
|
+
if (riding !== undefined)
|
|
2390
|
+
await riding.settled;
|
|
2391
|
+
if (riding?.state === "committed") {
|
|
2392
|
+
budget.close({ outcome: "committed" });
|
|
2393
|
+
throw caught;
|
|
2394
|
+
}
|
|
2395
|
+
const error = riding?.state === "lost" ? riding.error : caught;
|
|
1206
2396
|
if (isSavepointRollbackFailedError(error) || scope.state.poisoned !== null) {
|
|
1207
2397
|
discard = true;
|
|
1208
2398
|
budget.close({ outcome: "unknown" });
|
|
1209
2399
|
}
|
|
1210
2400
|
let rollback = "not-needed";
|
|
1211
2401
|
try {
|
|
1212
|
-
if (
|
|
2402
|
+
if (openingRollback !== undefined)
|
|
2403
|
+
rollback = openingRollback;
|
|
2404
|
+
else if (deferred.opened()) {
|
|
1213
2405
|
await dispatch({ config: { text: "ROLLBACK" }, cleanup: true });
|
|
1214
2406
|
rollback = "acknowledged";
|
|
1215
2407
|
}
|
|
@@ -1233,9 +2425,56 @@ function pgAdapter(options) {
|
|
|
1233
2425
|
}
|
|
1234
2426
|
},
|
|
1235
2427
|
async connect() {
|
|
1236
|
-
const
|
|
1237
|
-
|
|
2428
|
+
const opened = await Promise.allSettled(Array.from({ length: Math.max(1, poolMin ?? 0) }, () => runMapped(() => getPool().connect())));
|
|
2429
|
+
for (const outcome of opened) {
|
|
2430
|
+
if (outcome.status === "fulfilled")
|
|
2431
|
+
outcome.value.release();
|
|
2432
|
+
}
|
|
2433
|
+
const failed = opened.find((outcome) => outcome.status === "rejected");
|
|
2434
|
+
if (failed !== undefined)
|
|
2435
|
+
throw failed.reason;
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
const sendConnectionGroup = async (entries) => {
|
|
2439
|
+
const rootPool = getPool();
|
|
2440
|
+
const client = rootViaPoolQuery ? undefined : await runMapped(() => rootPool.connect());
|
|
2441
|
+
if (client === undefined || !hasReadyForQuerySeam({ client }) || !supportsBatch({ client }) || (client.connectionParameters?.query_timeout ?? 0) > 0) {
|
|
2442
|
+
client?.release();
|
|
2443
|
+
const steps = [];
|
|
2444
|
+
for (const entry of entries) {
|
|
2445
|
+
if (steps.some((step) => step.status === "failed")) {
|
|
2446
|
+
steps.push({ status: "skipped" });
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
steps.push(await root.execute(entry.call).then((rows) => ({ status: "ok", result: { rows, rowCount: rows.length } }), (error) => ({ status: "failed", error })));
|
|
2450
|
+
}
|
|
2451
|
+
return { steps };
|
|
1238
2452
|
}
|
|
2453
|
+
const seam = observeReadyForQuery({ client });
|
|
2454
|
+
let reuse = false;
|
|
2455
|
+
const outcome = await runBatch({ client, steps: entries.map((entry) => entry.step), types: EXACT_TYPES }).then((settled) => settled, (error) => ({ steps: [], transportError: error }));
|
|
2456
|
+
const failure = firstBatchFailure({ outcome });
|
|
2457
|
+
if (failure === undefined)
|
|
2458
|
+
reuse = true;
|
|
2459
|
+
else if (outcome.transportError === undefined)
|
|
2460
|
+
reuse = await seam.reusableAfter({ error: mapPgDriverError({ error: failure.error }), named: false });
|
|
2461
|
+
seam.detach();
|
|
2462
|
+
client.release(!reuse);
|
|
2463
|
+
return outcome;
|
|
2464
|
+
};
|
|
2465
|
+
const connectionGroupBase = {
|
|
2466
|
+
dialect: DIALECT,
|
|
2467
|
+
provider: PROVIDER,
|
|
2468
|
+
wire: WIRE,
|
|
2469
|
+
budgets: BUDGETS,
|
|
2470
|
+
roundTrips: ROUND_TRIPS,
|
|
2471
|
+
formatArrayParam: (values) => new PgArrayParam(values)
|
|
2472
|
+
};
|
|
2473
|
+
const connectionGroupDriver = {
|
|
2474
|
+
base: connectionGroupBase,
|
|
2475
|
+
prepare: (call) => ({ text: call.text, values: prepareBatchValues({ values: serializeParams({ values: call.values }) }) }),
|
|
2476
|
+
send: (entries) => lifecycle.admit({ run: () => sendConnectionGroup(entries) }),
|
|
2477
|
+
mapError: (error) => mapPgDriverError({ error })
|
|
1239
2478
|
};
|
|
1240
2479
|
return {
|
|
1241
2480
|
dialect: DIALECT,
|
|
@@ -1244,7 +2483,19 @@ function pgAdapter(options) {
|
|
|
1244
2483
|
budgets: BUDGETS,
|
|
1245
2484
|
readCancellation,
|
|
1246
2485
|
roundTrips: ROUND_TRIPS,
|
|
2486
|
+
readSnapshot: READ_SNAPSHOT,
|
|
2487
|
+
pipelining: PIPELINING,
|
|
1247
2488
|
transactionOutcome: "reported",
|
|
2489
|
+
transactionSettings: "transaction-local",
|
|
2490
|
+
...transactionsTakeOneRoundTrip({ address }) ? {
|
|
2491
|
+
oneRoundTripTransactions: true,
|
|
2492
|
+
connectionGroup: () => {
|
|
2493
|
+
const closed = lifecycle.refusal();
|
|
2494
|
+
if (closed !== undefined)
|
|
2495
|
+
throw closed;
|
|
2496
|
+
return rootViaPoolQuery ? undefined : openConnectionGroup({ driver: connectionGroupDriver });
|
|
2497
|
+
}
|
|
2498
|
+
} : {},
|
|
1248
2499
|
withSession({ run }) {
|
|
1249
2500
|
return lifecycle.admit({ run: () => root.withSession({ run: (executor) => lifecycle.inCallback({ run: () => run(executor) }) }) });
|
|
1250
2501
|
},
|
|
@@ -1297,4 +2548,4 @@ export {
|
|
|
1297
2548
|
validateTransactionDeadline2 as validateTransactionDeadline
|
|
1298
2549
|
};
|
|
1299
2550
|
|
|
1300
|
-
//# debugId=
|
|
2551
|
+
//# debugId=96B1BE63991D190764756E2164756E21
|