@vibeorm/adapter-pg 2.4.1 → 4.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/dist/index.js CHANGED
@@ -2,24 +2,181 @@
2
2
  // packages/adapter-pg/src/index.ts
3
3
  import {
4
4
  abortTransactionOnServerFailure,
5
+ createDeferredControl,
5
6
  observeRowChanges,
6
7
  readRowChanges,
7
8
  statementRowChanges,
9
+ createAdapterLifecycleGate,
8
10
  runPinnedSession,
11
+ runShutdownCleanup,
12
+ transactionLifecycleRefusal,
9
13
  createSavepointScope,
10
14
  engineStatementBudgetMs,
11
15
  isSavepointRollbackFailedError,
12
- isTransactionDeadlineError,
16
+ readOnlyRequested,
13
17
  runNestedSavepoint,
14
18
  runRootScope,
15
19
  scopeTurn,
20
+ settleFailedTransaction,
16
21
  startTransactionBudget,
17
22
  transactionAbortedError,
18
- transactionOutcomeUnknownError,
19
23
  validateTransactionDeadline
20
24
  } from "@vibeorm/runtime/adapter-kit";
21
25
  import { VibeError as VibeError4 } from "@vibeorm/schema";
26
+ import pg2 from "pg";
27
+
28
+ // packages/adapter-pg/src/batch.ts
22
29
  import pg from "pg";
30
+ var prepareValue = (() => {
31
+ const utils = pg.utils;
32
+ return typeof utils?.prepareValue === "function" ? utils.prepareValue : undefined;
33
+ })();
34
+ function supportsBatch(params) {
35
+ if (prepareValue === undefined)
36
+ return false;
37
+ const client = params.client;
38
+ if (client === null || typeof client !== "object" || client.pipeline === true)
39
+ return false;
40
+ const connection = client.connection;
41
+ return connection !== null && typeof connection === "object" && typeof connection.parse === "function" && typeof connection.bind === "function" && typeof connection.describe === "function" && typeof connection.execute === "function" && typeof connection.sync === "function";
42
+ }
43
+ function prepareBatchValues(params) {
44
+ const prepare = prepareValue;
45
+ return params.values.map((value) => prepare(value));
46
+ }
47
+ var COMMAND_TAG = /^([A-Za-z]+)(?: (\d+))?(?: (\d+))?/;
48
+ function isServerError(error) {
49
+ if (typeof error !== "object" || error === null)
50
+ return false;
51
+ const candidate = error;
52
+ return typeof candidate.code === "string" && /^[0-9A-Z]{5}$/.test(candidate.code) && typeof candidate.severity === "string";
53
+ }
54
+
55
+ class PgBatch {
56
+ done;
57
+ callback = undefined;
58
+ steps;
59
+ types;
60
+ outcomes = [];
61
+ parsers = [];
62
+ fields = [];
63
+ rows = [];
64
+ rowError = undefined;
65
+ settled = false;
66
+ resolve = () => {
67
+ return;
68
+ };
69
+ constructor(params) {
70
+ this.steps = params.steps;
71
+ this.types = params.types;
72
+ this.done = new Promise((resolve) => {
73
+ this.resolve = resolve;
74
+ });
75
+ }
76
+ submit(connection) {
77
+ const writer = connection;
78
+ writer.stream?.cork?.();
79
+ try {
80
+ for (const step of this.steps) {
81
+ writer.parse({ text: step.text, types: [] });
82
+ writer.bind({ values: step.values ?? [] });
83
+ writer.describe({ type: "P", name: "" });
84
+ writer.execute({ portal: "", rows: 0 });
85
+ }
86
+ writer.sync();
87
+ } finally {
88
+ writer.stream?.uncork?.();
89
+ }
90
+ }
91
+ handleRowDescription(message) {
92
+ this.fields = message.fields;
93
+ this.parsers = message.fields.map((field) => this.types.getTypeParser(field.dataTypeID, field.format ?? "text"));
94
+ this.rows = [];
95
+ }
96
+ handleDataRow(message) {
97
+ if (this.settled || this.rowError !== undefined)
98
+ return;
99
+ const row = {};
100
+ try {
101
+ for (let index = 0;index < this.fields.length; index++) {
102
+ const raw = message.fields[index] ?? null;
103
+ row[this.fields[index].name] = raw === null ? null : this.parsers[index](raw);
104
+ }
105
+ } catch (error) {
106
+ this.rowError = { error };
107
+ return;
108
+ }
109
+ this.rows.push(row);
110
+ }
111
+ handleCommandComplete(message) {
112
+ if (this.settled)
113
+ return;
114
+ if (this.rowError !== undefined) {
115
+ this.outcomes.push({ status: "failed", error: this.rowError.error });
116
+ this.rowError = undefined;
117
+ this.rows = [];
118
+ this.fields = [];
119
+ this.parsers = [];
120
+ return;
121
+ }
122
+ const match = COMMAND_TAG.exec(message.text ?? message.command ?? "");
123
+ const command = match?.[1];
124
+ const rowCount = match?.[3] !== undefined ? Number.parseInt(match[3], 10) : match?.[2] !== undefined ? Number.parseInt(match[2], 10) : null;
125
+ this.outcomes.push({ status: "ok", result: { rows: this.rows, rowCount, ...command === undefined ? {} : { command } } });
126
+ this.rows = [];
127
+ this.fields = [];
128
+ this.parsers = [];
129
+ }
130
+ handleEmptyQuery() {
131
+ if (this.settled)
132
+ return;
133
+ this.outcomes.push({ status: "ok", result: { rows: [], rowCount: null } });
134
+ }
135
+ handleError(error) {
136
+ if (this.settled)
137
+ return;
138
+ if (!isServerError(error)) {
139
+ this.finish({ transportError: error });
140
+ return;
141
+ }
142
+ this.outcomes.push({ status: "failed", error: this.rowError === undefined ? error : this.rowError.error });
143
+ while (this.outcomes.length < this.steps.length)
144
+ this.outcomes.push({ status: "skipped" });
145
+ this.finish({});
146
+ }
147
+ handleReadyForQuery() {
148
+ this.finish({});
149
+ }
150
+ handlePortalSuspended() {}
151
+ handleCopyInResponse(connection) {
152
+ connection.sendCopyFail?.("COPY FROM STDIN is not supported inside a batch");
153
+ }
154
+ handleCopyData() {}
155
+ finish(params) {
156
+ if (this.settled)
157
+ return;
158
+ this.settled = true;
159
+ this.callback?.(params.transportError ?? null);
160
+ this.resolve({
161
+ steps: this.outcomes,
162
+ ...params.transportError === undefined ? {} : { transportError: params.transportError }
163
+ });
164
+ }
165
+ }
166
+ async function runBatch(params) {
167
+ const batch = new PgBatch({ steps: params.steps, types: params.types });
168
+ params.client.query.call(params.client, batch);
169
+ return batch.done;
170
+ }
171
+ function firstBatchFailure(params) {
172
+ const { outcome } = params;
173
+ const index = outcome.steps.findIndex((step) => step.status === "failed");
174
+ if (index !== -1)
175
+ return { index, error: outcome.steps[index].error };
176
+ if (outcome.transportError !== undefined)
177
+ return { index: outcome.steps.length, error: outcome.transportError };
178
+ return;
179
+ }
23
180
 
24
181
  // packages/adapter-pg/src/errors.ts
25
182
  import { VibeError } from "@vibeorm/schema";
@@ -39,16 +196,18 @@ var PG_SQLSTATE_ERROR_CODES = Object.freeze({
39
196
  "40001": "VIBE_TRANSACTION",
40
197
  "40P01": "VIBE_TRANSACTION",
41
198
  "57014": "VIBE_TRANSACTION",
42
- "55P03": "VIBE_LOCK_NOT_AVAILABLE"
199
+ "55P03": "VIBE_LOCK_NOT_AVAILABLE",
200
+ "25006": "VIBE_TRANSACTION"
43
201
  });
44
202
  var PG_SQLSTATE_REASONS = Object.freeze({
45
203
  "23P01": "exclusion-violation",
46
204
  "57014": "timeout",
47
205
  "55P03": "lock-not-available",
48
206
  "40P01": "deadlock",
49
- "40001": "serialization-failure"
207
+ "40001": "serialization-failure",
208
+ "25006": "read-only-transaction"
50
209
  });
51
- var SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
210
+ var SQLSTATE_PATTERN = /^(?=[A-Z]*\d)[0-9A-Z]{5}$/;
52
211
  function readStringField(params) {
53
212
  const value = params.source[params.key];
54
213
  return typeof value === "string" && value.length > 0 ? value : undefined;
@@ -247,8 +406,17 @@ var ISOLATION_LEVEL_SQL = Object.freeze({
247
406
  Serializable: "SERIALIZABLE"
248
407
  });
249
408
  function beginStatement(params) {
250
- const { isolationLevel } = params;
251
- return isolationLevel === undefined ? "BEGIN" : `BEGIN ISOLATION LEVEL ${ISOLATION_LEVEL_SQL[isolationLevel]}`;
409
+ const { isolationLevel, readOnly } = params;
410
+ const modes = [];
411
+ if (isolationLevel !== undefined)
412
+ modes.push(`ISOLATION LEVEL ${ISOLATION_LEVEL_SQL[isolationLevel]}`);
413
+ if (readOnly === true)
414
+ modes.push("READ ONLY");
415
+ return modes.length === 0 ? "BEGIN" : `BEGIN ${modes.join(", ")}`;
416
+ }
417
+ function openTransactionStatement(params) {
418
+ const begin = beginStatement(params);
419
+ return params.timeout === undefined ? begin : `${begin}; ${statementTimeoutStatement({ timeout: params.timeout })}`;
252
420
  }
253
421
  function statementTimeoutStatement(params) {
254
422
  const { timeout } = params;
@@ -276,13 +444,17 @@ import { createSavepointCounter, nextSavepointName } from "@vibeorm/runtime/adap
276
444
  import {
277
445
  DEFAULT_DEADLINE_CLOCK,
278
446
  engineStatementBudgetMs as engineStatementBudgetMs2,
279
- isTransactionDeadlineError as isTransactionDeadlineError2,
447
+ isTransactionDeadlineError,
280
448
  startTransactionBudget as startTransactionBudget2,
281
449
  transactionClosedError,
282
450
  transactionDeadlineError,
283
- transactionOutcomeUnknownError as transactionOutcomeUnknownError2,
451
+ transactionOutcomeUnknownError,
284
452
  validateTransactionDeadline as validateTransactionDeadline2
285
453
  } from "@vibeorm/runtime/adapter-kit";
454
+ var MAX_STATEMENT_TIMEOUT_MS = 2147483647;
455
+ var ignore = () => {
456
+ return;
457
+ };
286
458
  function finalResult(params) {
287
459
  return Array.isArray(params.result) ? params.result.at(-1) ?? { rows: [], rowCount: 0 } : params.result;
288
460
  }
@@ -302,13 +474,58 @@ function commitOutcomeUnknownError(params) {
302
474
  cause: params.cause
303
475
  });
304
476
  }
477
+ var NEVER_REUSE_SQLSTATE_CLASSES = new Set(["08", "53", "57", "58", "XX"]);
478
+ var STALE_STATEMENT_SQLSTATES = new Set(["0A000", "26000", "42P05"]);
479
+ function observeReadyForQuery(params) {
480
+ const client = params.client;
481
+ let backgroundFailure = false;
482
+ let settle = () => {
483
+ return;
484
+ };
485
+ const firstEvent = new Promise((resolve) => {
486
+ settle = resolve;
487
+ });
488
+ const onDrain = () => settle("drain");
489
+ const onError = () => {
490
+ backgroundFailure = true;
491
+ settle("error");
492
+ };
493
+ const onEnd = () => settle("end");
494
+ client.on("error", onError);
495
+ client.on("drain", onDrain);
496
+ client.on("end", onEnd);
497
+ return {
498
+ reusableAfter: async ({ error, named }) => {
499
+ if (!(error instanceof VibeError4))
500
+ return false;
501
+ const sqlstate = error.meta.sqlstate;
502
+ if (typeof sqlstate !== "string" || NEVER_REUSE_SQLSTATE_CLASSES.has(sqlstate.slice(0, 2)))
503
+ return false;
504
+ if (named && STALE_STATEMENT_SQLSTATES.has(sqlstate))
505
+ return false;
506
+ const event = await firstEvent;
507
+ return event === "drain" && !backgroundFailure && client.getTransactionStatus() === "I";
508
+ },
509
+ detach: () => {
510
+ client.removeListener("error", onError);
511
+ client.removeListener("drain", onDrain);
512
+ client.removeListener("end", onEnd);
513
+ }
514
+ };
515
+ }
516
+ function hasReadyForQuerySeam(params) {
517
+ const client = params.client;
518
+ return typeof client.on === "function" && typeof client.removeListener === "function" && typeof client.once === "function" && typeof client.getTransactionStatus === "function" && client.pipeline !== true;
519
+ }
305
520
  function guardBackgroundErrors(params) {
306
- const { client } = params;
521
+ const { client, onError } = params;
307
522
  if (typeof client.on !== "function")
308
523
  return () => {
309
524
  return;
310
525
  };
311
- const swallow = () => {};
526
+ const swallow = (error) => {
527
+ onError?.(error);
528
+ };
312
529
  client.on("error", swallow);
313
530
  return () => {
314
531
  client.removeListener?.("error", swallow);
@@ -317,10 +534,12 @@ function guardBackgroundErrors(params) {
317
534
  var DIALECT = "postgres";
318
535
  var PROVIDER = "postgresql";
319
536
  var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
537
+ var ROUND_TRIPS = "network";
320
538
  var BUDGETS = Object.freeze({
321
539
  acquireTimeout: true,
322
540
  statementTimeout: "all-statements",
323
- transactionDeadline: "cancel-running-statements"
541
+ transactionDeadline: "cancel-running-statements",
542
+ accessModes: Object.freeze(["readOnly"])
324
543
  });
325
544
  var NUMERIC_ARRAY_OID = 1231;
326
545
  var TEXT_ARRAY_OID = 1009;
@@ -328,7 +547,7 @@ var TIMESTAMP_OID = 1114;
328
547
  var TIMESTAMP_ARRAY_OID = 1115;
329
548
  var DATE_OID = 1082;
330
549
  var DATE_ARRAY_OID = 1182;
331
- var driverTypeParser = pg.types.getTypeParser;
550
+ var driverTypeParser = pg2.types.getTypeParser;
332
551
  var ZONE_LESS_TEXT = /^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)?$/;
333
552
  function parseZoneLessAsUtc(value) {
334
553
  if (ZONE_LESS_TEXT.test(value)) {
@@ -362,11 +581,25 @@ var EXACT_TYPES = {
362
581
  return driverTypeParser(oid, format);
363
582
  }
364
583
  };
584
+ function takeAddress(params) {
585
+ const options = params.options;
586
+ if (options?.pool !== undefined && options.pool !== null)
587
+ return { pool: options.pool };
588
+ const connectionString = options?.connectionString;
589
+ if (typeof connectionString === "string" && connectionString.trim() !== "")
590
+ return { connectionString };
591
+ throw new VibeError4({
592
+ code: "VIBE_CONFIG",
593
+ 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.",
594
+ meta: { adapter: "pg", reason: "missing-address" }
595
+ });
596
+ }
365
597
  function pgAdapter(options) {
598
+ const address = takeAddress({ options });
366
599
  const usePrepared = options?.preparedStatements ?? false;
367
600
  const stmtCacheMax = options?.stmtCacheMax ?? 1000;
368
601
  const planCacheMode = options?.planCacheMode === undefined ? "auto" : assertPlanCacheMode({ value: options.planCacheMode });
369
- if (planCacheMode !== "auto" && options?.pool !== undefined) {
602
+ if (planCacheMode !== "auto" && "pool" in address) {
370
603
  throw new VibeError4({
371
604
  code: "VIBE_CONFIG",
372
605
  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',
@@ -377,22 +610,23 @@ function pgAdapter(options) {
377
610
  let ownsPool = false;
378
611
  let rawSession = null;
379
612
  let rawSessionUnguard = null;
613
+ let rootViaPoolQuery = false;
380
614
  function getPool() {
381
615
  if (pool !== null)
382
616
  return pool;
383
- if (options?.pool !== undefined) {
384
- pool = options.pool;
617
+ if ("pool" in address) {
618
+ pool = address.pool;
385
619
  ownsPool = false;
386
620
  return pool;
387
621
  }
388
622
  const config = buildPoolConfig({
389
- connectionString: options?.connectionString ?? process.env.DATABASE_URL,
623
+ connectionString: address.connectionString,
390
624
  max: options?.max ?? 10,
391
625
  ...options?.statementTimeout === undefined ? {} : { statementTimeout: options.statementTimeout },
392
626
  ...options?.connectionTimeout === undefined ? {} : { connectionTimeout: options.connectionTimeout },
393
627
  planCacheMode
394
628
  });
395
- const PoolCtor = pg.Pool;
629
+ const PoolCtor = pg2.Pool;
396
630
  pool = new PoolCtor(config);
397
631
  pool.on?.("error", () => {});
398
632
  ownsPool = true;
@@ -420,7 +654,7 @@ function pgAdapter(options) {
420
654
  if (results.length === 0)
421
655
  observeRowChanges({ scope: params.scope, count: statementRowChanges({ effect: params.effect }) });
422
656
  for (const result of results)
423
- observeRowChanges({ scope: params.scope, count: statementRowChanges({ effect: params.effect, count: result.rowCount }) });
657
+ observeRowChanges({ scope: params.scope, count: statementRowChanges({ effect: params.effect, count: result.rowCount, rows: result.rows?.length, resultRows: result.rows }) });
424
658
  }
425
659
  async function runExecute(params) {
426
660
  const config = buildQueryConfig({ text: params.text, values: params.values });
@@ -436,13 +670,25 @@ function pgAdapter(options) {
436
670
  return { rows: final.rows, affectedRows: final.rowCount ?? 0 };
437
671
  }
438
672
  function createClientAdapter(params) {
439
- const { dispatch, scope, budget } = params;
440
- const client = { query: (config) => dispatch({ config }) };
673
+ const { dispatch, scope, budget, deferred } = params;
674
+ const client = { query: (config) => dispatch({ config, batchable: true }) };
675
+ const rawClient = { query: (config) => dispatch({ config, batchable: (config.values?.length ?? 0) > 0 }) };
676
+ const savepointControl = (statement, cleanup) => {
677
+ if (deferred !== null && deferred.routeSavepoint({ text: statement }) !== "send")
678
+ return;
679
+ return runMapped(() => dispatch({
680
+ config: { text: statement },
681
+ stage: "nested",
682
+ batchable: true,
683
+ ...cleanup ? { cleanup: true } : {}
684
+ }));
685
+ };
441
686
  return {
442
687
  dialect: DIALECT,
443
688
  provider: PROVIDER,
444
689
  wire: WIRE,
445
690
  budgets: BUDGETS,
691
+ roundTrips: ROUND_TRIPS,
446
692
  rowChanges: () => {
447
693
  budget.assertUsable({ stage: "statement" });
448
694
  return readRowChanges({ scope });
@@ -459,33 +705,61 @@ function pgAdapter(options) {
459
705
  if (turn !== undefined)
460
706
  await turn;
461
707
  budget.assertUsable({ stage: "statement" });
462
- return runExecuteUnsafe({ queryable: client, scope, ...execParams }).catch((error) => abortTransactionOnServerFailure({ scope, error }));
708
+ return runExecuteUnsafe({ queryable: rawClient, scope, ...execParams }).catch((error) => abortTransactionOnServerFailure({ scope, error }));
463
709
  },
464
710
  async transaction(fn, nestedOptions) {
465
711
  refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
466
712
  budget.assertUsable({ stage: "nested" });
467
713
  return runNestedSavepoint({
468
714
  scope,
469
- execute: (statement) => runMapped(() => dispatch({
470
- config: { text: statement },
471
- stage: "nested",
472
- cleanup: statement.startsWith("ROLLBACK TO SAVEPOINT ")
473
- })),
474
- body: (child) => fn(createClientAdapter({ dispatch, scope: child, budget }))
715
+ execute: (statement) => savepointControl(statement, false),
716
+ cleanup: (statement) => savepointControl(statement, true),
717
+ body: (child) => fn(createClientAdapter({ dispatch, scope: child, budget, deferred }))
475
718
  });
476
719
  },
477
- async connect() {},
478
- async disconnect() {},
720
+ async connect() {
721
+ budget.assertUsable({ stage: "statement" });
722
+ throw transactionLifecycleRefusal({ method: "connect" });
723
+ },
724
+ async disconnect() {
725
+ budget.assertUsable({ stage: "statement" });
726
+ throw transactionLifecycleRefusal({ method: "disconnect" });
727
+ },
479
728
  formatArrayParam(values) {
480
729
  return new PgArrayParam(values);
481
730
  }
482
731
  };
483
732
  }
484
- return {
485
- dialect: DIALECT,
486
- provider: PROVIDER,
487
- wire: WIRE,
488
- budgets: BUDGETS,
733
+ function closeOwnedResources() {
734
+ return runShutdownCleanup({
735
+ steps: [
736
+ async () => {
737
+ if (rawSession === null)
738
+ return;
739
+ const session = rawSession;
740
+ const unguard = rawSessionUnguard;
741
+ rawSession = null;
742
+ rawSessionUnguard = null;
743
+ await runMapped(() => session.query({ text: "ROLLBACK" })).then(() => {
744
+ unguard?.();
745
+ session.release();
746
+ }, (error) => {
747
+ unguard?.();
748
+ session.release(true);
749
+ throw error;
750
+ });
751
+ },
752
+ async () => {
753
+ if (pool === null || !ownsPool)
754
+ return;
755
+ const closing = pool;
756
+ await runMapped(() => closing.end());
757
+ }
758
+ ]
759
+ });
760
+ }
761
+ const lifecycle = createAdapterLifecycleGate({ driver: "pg", cleanup: closeOwnedResources });
762
+ const root = {
489
763
  async withSession({ run }) {
490
764
  const client = await runMapped(() => getPool().connect());
491
765
  let backgroundFailure;
@@ -508,7 +782,29 @@ function pgAdapter(options) {
508
782
  });
509
783
  },
510
784
  async execute(params) {
511
- return runExecute({ queryable: getPool(), ...params });
785
+ const config = buildQueryConfig({ text: params.text, values: params.values });
786
+ const rootPool = getPool();
787
+ if (!rootViaPoolQuery) {
788
+ const client = await runMapped(() => rootPool.connect());
789
+ if (hasReadyForQuerySeam({ client })) {
790
+ const seam = observeReadyForQuery({ client });
791
+ let reuse = false;
792
+ try {
793
+ const result = await runMapped(() => client.query(config));
794
+ reuse = true;
795
+ return finalResult({ result }).rows;
796
+ } catch (error) {
797
+ reuse = await seam.reusableAfter({ error, named: config.name !== undefined });
798
+ throw error;
799
+ } finally {
800
+ seam.detach();
801
+ client.release(!reuse);
802
+ }
803
+ }
804
+ client.release();
805
+ rootViaPoolQuery = true;
806
+ }
807
+ return finalResult({ result: await runMapped(() => rootPool.query(config)) }).rows;
512
808
  },
513
809
  async executeUnsafe(params) {
514
810
  const control = classifyRawTransactionControl({ text: params.text });
@@ -549,55 +845,145 @@ function pgAdapter(options) {
549
845
  support: BUDGETS.transactionDeadline,
550
846
  provider: PROVIDER
551
847
  });
848
+ const openingOptions = {
849
+ isolationLevel: transactionOptions?.isolationLevel,
850
+ readOnly: readOnlyRequested({ options: transactionOptions, provider: PROVIDER }),
851
+ timeout: transactionOptions?.timeout
852
+ };
853
+ const openText = openTransactionStatement(openingOptions);
552
854
  const client = await runMapped(() => getPool().connect());
553
- const unguard = guardBackgroundErrors({ client });
855
+ let connectionFailure;
856
+ const unguard = guardBackgroundErrors({
857
+ client,
858
+ onError: (cause) => {
859
+ if (connectionFailure === undefined)
860
+ connectionFailure = { cause };
861
+ }
862
+ });
554
863
  const scope = createSavepointScope();
555
864
  const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
556
- let tail = Promise.resolve();
557
- const dispatch = (dispatchParams) => {
558
- if (budget.totalMs === null)
559
- return client.query(dispatchParams.config);
560
- const pending = tail.then(async () => {
561
- if (!dispatchParams.cleanup) {
562
- if (scope.state.poisoned !== null)
563
- throw scope.state.poisoned;
564
- const stage = dispatchParams.stage ?? "statement";
865
+ const capable = supportsBatch({ client });
866
+ const deferOpening = capable && (openingOptions.timeout === undefined || openingOptions.timeout <= MAX_STATEMENT_TIMEOUT_MS);
867
+ const opening = deferOpening ? [beginStatement(openingOptions), ...openingOptions.timeout === undefined ? [] : [statementTimeoutStatement({ timeout: openingOptions.timeout })]] : [];
868
+ const deferred = createDeferredControl({ opening });
869
+ let openingFailure;
870
+ let inFlight = null;
871
+ const sendWithControl = (dispatchParams) => {
872
+ const { config, cleanup } = dispatchParams;
873
+ const handOver = () => {
874
+ dispatchParams.sent?.();
875
+ return client.query(config);
876
+ };
877
+ if (cleanup !== true && openingFailure !== undefined)
878
+ throw transactionAbortedError({ cause: openingFailure });
879
+ let bound;
880
+ if (cleanup !== true && budget.totalMs !== null) {
881
+ if (scope.state.poisoned !== null)
882
+ throw scope.state.poisoned;
883
+ budget.assertUsable({ stage: dispatchParams.stage ?? "statement" });
884
+ const ms = engineStatementBudgetMs({ budget });
885
+ if (ms !== null)
886
+ bound = statementTimeoutStatement({ timeout: ms });
887
+ }
888
+ if (!capable) {
889
+ if (bound === undefined)
890
+ return handOver();
891
+ const stage = dispatchParams.stage ?? "statement";
892
+ return runMapped(() => client.query({ text: bound })).then(() => {
565
893
  budget.assertUsable({ stage });
566
- const ms = engineStatementBudgetMs({ budget });
567
- if (ms !== null) {
568
- await runMapped(() => client.query({ text: statementTimeoutStatement({ timeout: ms }) }));
569
- budget.assertUsable({ stage });
894
+ return handOver();
895
+ });
896
+ }
897
+ const pendingControl = cleanup !== true && deferred.hasPending();
898
+ if (!pendingControl && bound === undefined)
899
+ return handOver();
900
+ const statement = dispatchParams.batchable === true ? [{ text: config.text, values: prepareBatchValues({ values: config.values ?? [] }) }] : [];
901
+ const carriesOpening = !deferred.opened();
902
+ const control = pendingControl ? [...deferred.take()] : [];
903
+ if (bound !== undefined)
904
+ control.push(bound);
905
+ const openingSteps = carriesOpening ? opening.length : 0;
906
+ if (statement.length > 0)
907
+ dispatchParams.sent?.();
908
+ const burst = runBatch({ client, steps: [...control.map((text) => ({ text })), ...statement], types: EXACT_TYPES }).then((outcome) => {
909
+ const failure = firstBatchFailure({ outcome });
910
+ if (failure !== undefined) {
911
+ if (failure.index < openingSteps || outcome.transportError !== undefined && openingSteps > 0) {
912
+ openingFailure = mapPgDriverError({ error: failure.error });
570
913
  }
914
+ throw failure.error;
571
915
  }
572
- return client.query(dispatchParams.config);
573
- });
574
- tail = pending.then(() => {
575
- return;
576
- }, () => {
577
- return;
916
+ if (statement.length === 0)
917
+ return handOver();
918
+ const last = outcome.steps.at(-1);
919
+ return last?.status === "ok" ? last.result : { rows: [], rowCount: 0 };
578
920
  });
921
+ if (budget.totalMs === null && carriesOpening) {
922
+ const settled = burst.then(ignore, ignore).then(() => {
923
+ if (inFlight === settled)
924
+ inFlight = null;
925
+ });
926
+ inFlight = settled;
927
+ }
928
+ return burst;
929
+ };
930
+ const send = (dispatchParams) => {
931
+ try {
932
+ return sendWithControl(dispatchParams);
933
+ } catch (error) {
934
+ return Promise.reject(error);
935
+ }
936
+ };
937
+ let tail = Promise.resolve();
938
+ const dispatch = (dispatchParams) => {
939
+ if (budget.totalMs === null) {
940
+ if (inFlight !== null)
941
+ return inFlight.then(() => dispatch(dispatchParams));
942
+ return send(dispatchParams);
943
+ }
944
+ const pending = tail.then(() => send(dispatchParams));
945
+ tail = pending.then(ignore, ignore);
579
946
  return pending;
580
947
  };
581
948
  let discard = false;
949
+ let commitProgress = "not-sent";
582
950
  try {
583
- await runMapped(() => client.query({ text: beginStatement({ isolationLevel: transactionOptions?.isolationLevel }) }));
584
- if (transactionOptions?.timeout !== undefined) {
585
- const text = statementTimeoutStatement({ timeout: transactionOptions.timeout });
586
- await runMapped(() => client.query({ text }));
587
- }
951
+ if (!deferOpening)
952
+ await runMapped(() => client.query({ text: openText }));
588
953
  const result = await runRootScope({
589
954
  scope,
590
- body: () => fn(createClientAdapter({ dispatch, scope, budget }))
955
+ body: () => fn(createClientAdapter({ dispatch, scope, budget, deferred: capable ? deferred : null }))
591
956
  });
592
957
  budget.assertUsable({ stage: "commit" });
593
- const commit = await runMapped(() => dispatch({ config: { text: "COMMIT" }, stage: "commit" })).catch((cause) => {
594
- if (!isConnectionLoss({ error: cause }))
958
+ if (!deferred.opened()) {
959
+ budget.close({ outcome: "committed" });
960
+ return result;
961
+ }
962
+ if (openingFailure !== undefined)
963
+ throw transactionAbortedError({ cause: openingFailure });
964
+ const commit = await runMapped(() => dispatch({
965
+ config: { text: "COMMIT" },
966
+ stage: "commit",
967
+ batchable: true,
968
+ sent: () => {
969
+ if (connectionFailure !== undefined)
970
+ throw mapPgDriverError({ error: connectionFailure.cause });
971
+ commitProgress = "sent";
972
+ }
973
+ })).catch((cause) => {
974
+ if (commitProgress === "not-sent")
595
975
  throw cause;
976
+ if (!isConnectionLoss({ error: cause })) {
977
+ commitProgress = "answered";
978
+ throw cause;
979
+ }
980
+ commitProgress = "unanswered";
596
981
  discard = true;
597
982
  budget.close({ outcome: "unknown" });
598
983
  throw commitOutcomeUnknownError({ cause });
599
984
  });
600
985
  if (finalResult({ result: commit }).command === "ROLLBACK") {
986
+ commitProgress = "answered";
601
987
  throw transactionAbortedError({ cause: scope.state.aborted?.cause });
602
988
  }
603
989
  budget.close({ outcome: "committed" });
@@ -607,21 +993,25 @@ function pgAdapter(options) {
607
993
  discard = true;
608
994
  budget.close({ outcome: "unknown" });
609
995
  }
996
+ let rollback = "not-needed";
610
997
  try {
611
- await dispatch({ config: { text: "ROLLBACK" }, cleanup: true });
612
- budget.close({ outcome: "rolled-back" });
998
+ if (deferred.opened()) {
999
+ await dispatch({ config: { text: "ROLLBACK" }, cleanup: true });
1000
+ rollback = "acknowledged";
1001
+ }
613
1002
  } catch {
614
1003
  discard = true;
615
- budget.close({ outcome: "unknown" });
616
- if (budget.totalMs !== null && isTransactionDeadlineError(error)) {
617
- throw transactionOutcomeUnknownError({
618
- provider: PROVIDER,
619
- totalMs: budget.totalMs,
620
- cause: error
621
- });
622
- }
1004
+ rollback = "failed";
623
1005
  }
624
- throw error;
1006
+ throw settleFailedTransaction({
1007
+ error,
1008
+ provider: PROVIDER,
1009
+ budget,
1010
+ scope,
1011
+ commit: commitProgress,
1012
+ rollback,
1013
+ connection: discard ? "discarded" : "returned"
1014
+ });
625
1015
  } finally {
626
1016
  unguard();
627
1017
  client.release(discard);
@@ -630,24 +1020,32 @@ function pgAdapter(options) {
630
1020
  async connect() {
631
1021
  const client = await runMapped(() => getPool().connect());
632
1022
  client.release();
1023
+ }
1024
+ };
1025
+ return {
1026
+ dialect: DIALECT,
1027
+ provider: PROVIDER,
1028
+ wire: WIRE,
1029
+ budgets: BUDGETS,
1030
+ roundTrips: ROUND_TRIPS,
1031
+ transactionOutcome: "reported",
1032
+ withSession({ run }) {
1033
+ return lifecycle.admit({ run: () => root.withSession({ run: (executor) => lifecycle.inCallback({ run: () => run(executor) }) }) });
633
1034
  },
634
- async disconnect() {
635
- if (rawSession !== null) {
636
- const session = rawSession;
637
- const unguard = rawSessionUnguard;
638
- rawSession = null;
639
- rawSessionUnguard = null;
640
- try {
641
- await session.query({ text: "ROLLBACK" });
642
- } catch {}
643
- unguard?.();
644
- session.release();
645
- }
646
- if (pool !== null && ownsPool) {
647
- const closing = pool;
648
- pool = null;
649
- await runMapped(() => closing.end());
650
- }
1035
+ execute(params) {
1036
+ return lifecycle.admit({ run: () => root.execute(params) });
1037
+ },
1038
+ executeUnsafe(params) {
1039
+ return lifecycle.admit({ run: () => root.executeUnsafe(params) });
1040
+ },
1041
+ transaction(fn, transactionOptions) {
1042
+ return lifecycle.admit({ run: () => root.transaction((txAdapter) => lifecycle.inCallback({ run: () => fn(txAdapter) }), transactionOptions) });
1043
+ },
1044
+ connect() {
1045
+ return lifecycle.admit({ run: () => root.connect() });
1046
+ },
1047
+ disconnect() {
1048
+ return lifecycle.disconnect();
651
1049
  },
652
1050
  formatArrayParam(values) {
653
1051
  return new PgArrayParam(values);
@@ -666,7 +1064,7 @@ export {
666
1064
  createSavepointCounter,
667
1065
  createStatementNameCache,
668
1066
  engineStatementBudgetMs2 as engineStatementBudgetMs,
669
- isTransactionDeadlineError2 as isTransactionDeadlineError,
1067
+ isTransactionDeadlineError,
670
1068
  mapPgDriverError,
671
1069
  nextSavepointName,
672
1070
  pgAdapter,
@@ -679,8 +1077,8 @@ export {
679
1077
  statementTimeoutStatement,
680
1078
  transactionClosedError,
681
1079
  transactionDeadlineError,
682
- transactionOutcomeUnknownError2 as transactionOutcomeUnknownError,
1080
+ transactionOutcomeUnknownError,
683
1081
  validateTransactionDeadline2 as validateTransactionDeadline
684
1082
  };
685
1083
 
686
- //# debugId=2E6D6B100B9B0A5B64756E2164756E21
1084
+ //# debugId=9151AD009082F4C664756E2164756E21