kern-db 0.4.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import { B as takeOnlyRow, C as renderFragment, D as bindParameter, E as assertUntypedValue, H as toValueRows, L as createFieldDecodeError, O as encodeParameter, P as ResultMode, R as createResultDecodeError, V as toMissingResult, a as SqlQuery, c as StatementSource, l as startSqlQuery, v as ParameterBinder, y as SqlFragment, z as isRecordKey } from "./query-Dxiy1HJD.mjs";
1
+ import { B as takeOnlyRow, C as renderFragment, D as bindParameter, E as assertUntypedValue, H as toValueRows, L as createFieldDecodeError, O as encodeParameter, P as ResultMode, R as createResultDecodeError, V as toMissingResult, a as SqlQuery, c as StatementSource, l as startSqlQuery, v as ParameterBinder, y as SqlFragment, z as isRecordKey } from "./query-DpxSVheu.mjs";
2
2
  /** Quotes `name` as a PostgreSQL identifier, which keeps its case and any characters in it. */
3
3
  export declare function quoteIdentifier(name: string): string;
4
4
  export { type ParameterBinder, type ResultMode, SqlFragment, SqlQuery, type StatementSource, assertUntypedValue, bindParameter, createFieldDecodeError, createResultDecodeError, encodeParameter, isRecordKey, renderFragment, startSqlQuery, takeOnlyRow, toMissingResult, toValueRows };
@@ -3,88 +3,6 @@ import { S as createStatement, _ as sql, f as compileFragment, n as startSqlQuer
3
3
  import { i as setBrand, n as QueryError, r as isInstance, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
4
  import { n as formatArray } from "./array-BLt8ZNR5.mjs";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
- // src/transaction/release.ts
7
- /**
8
- * Returns a connection to the driver and passes a failed release to `onReleaseError`, or to `console.error` without one.
9
- *
10
- * The result or error of the transaction is decided before its release, so a failed release must not change it: a committed transaction that rejected would be retried.
11
- * Reporting the failure cannot change it either.
12
- * The error of a handler that throws or rejects goes to `console.error` with the failure of the release, and a `console.error` that throws is ignored.
13
- */
14
- async function releaseConnection(connection, onReleaseError) {
15
- try {
16
- await connection.release();
17
- } catch (error) {
18
- reportReleaseError(error, onReleaseError);
19
- }
20
- }
21
- /** Passes a failed release to `onReleaseError`, or to `console.error` without one, without throwing. */
22
- function reportReleaseError(error, onReleaseError) {
23
- if (!onReleaseError) {
24
- logReleaseError(error);
25
- return;
26
- }
27
- try {
28
- Promise.resolve(onReleaseError(error)).catch((handlerError) => logReleaseError(error, { error: handlerError }));
29
- } catch (handlerError) {
30
- logReleaseError(error, { error: handlerError });
31
- }
32
- }
33
- /**
34
- * Writes a failed release to `console.error`, preceded by the error of `onReleaseError` when the handler failed.
35
- *
36
- * A console that throws, such as one redirected to a failing logger, leaves nowhere to report to, so its error is ignored.
37
- */
38
- function logReleaseError(error, handlerFailure) {
39
- try {
40
- if (handlerFailure) console.error("kern-db: onReleaseError failed", handlerFailure.error);
41
- console.error("kern-db: releasing a connection failed", error);
42
- } catch {}
43
- }
44
- // src/transaction/abort.ts
45
- /**
46
- * Runs `start` and settles as its promise, or rejects with the reason of `signal` when it aborts first and `abort`, called on the abort, returns `true`.
47
- *
48
- * It listens from before `start`, which may abort the signal before it returns, and stops once the promise of `start` settles, such as for the signal that `node:test` aborts after every test.
49
- * A later rejection of that promise stays handled, and a value it resolves with after the rejection goes to `discard`, which handles its own failures.
50
- */
51
- function settleUnlessAborted(start, signal, abort, discard) {
52
- const settled = Promise.withResolvers();
53
- let abandoned = false;
54
- const onAbort = () => {
55
- if (abort()) {
56
- abandoned = true;
57
- settled.reject(signal.reason);
58
- }
59
- };
60
- signal.addEventListener("abort", onAbort, { once: true });
61
- let running;
62
- try {
63
- running = start();
64
- } catch (error) {
65
- signal.removeEventListener("abort", onAbort);
66
- settled.reject(error);
67
- return settled.promise;
68
- }
69
- running.then((value) => {
70
- signal.removeEventListener("abort", onAbort);
71
- if (abandoned) discard?.(value);
72
- else settled.resolve(value);
73
- }, (error) => {
74
- signal.removeEventListener("abort", onAbort);
75
- settled.reject(error);
76
- });
77
- return settled.promise;
78
- }
79
- /**
80
- * Acquires a connection for a test transaction, rejecting with the reason of `signal` when it aborts first.
81
- *
82
- * A connection acquired after the abort returns to the pool as best-effort cleanup: the test transaction has already rejected, so nothing awaits the release, and its failure goes to `onReleaseError`.
83
- */
84
- function acquireUnlessAborted(driver, signal, onReleaseError) {
85
- if (signal.aborted) return Promise.reject(signal.reason);
86
- return settleUnlessAborted(() => driver.acquire(), signal, () => true, (connection) => void releaseConnection(connection, onReleaseError));
87
- }
88
6
  // src/transaction/advisory-lock.ts
89
7
  /** Pause of `pollLock` in milliseconds after its first attempt, which doubles after each attempt up to `maxPollDelay`. */
90
8
  const firstPollDelay = 50;
@@ -300,6 +218,88 @@ function createSettingStatement(settings, local) {
300
218
  }
301
219
  return createStatement(text, parameters);
302
220
  }
221
+ // src/transaction/release.ts
222
+ /**
223
+ * Returns a connection to the driver and passes a failed release to `onReleaseError`, or to `console.error` without one.
224
+ *
225
+ * The result or error of the transaction is decided before its release, so a failed release must not change it: a committed transaction that rejected would be retried.
226
+ * Reporting the failure cannot change it either.
227
+ * The error of a handler that throws or rejects goes to `console.error` with the failure of the release, and a `console.error` that throws is ignored.
228
+ */
229
+ async function releaseConnection(connection, onReleaseError) {
230
+ try {
231
+ await connection.release();
232
+ } catch (error) {
233
+ reportReleaseError(error, onReleaseError);
234
+ }
235
+ }
236
+ /** Passes a failed release to `onReleaseError`, or to `console.error` without one, without throwing. */
237
+ function reportReleaseError(error, onReleaseError) {
238
+ if (!onReleaseError) {
239
+ logReleaseError(error);
240
+ return;
241
+ }
242
+ try {
243
+ Promise.resolve(onReleaseError(error)).catch((handlerError) => logReleaseError(error, { error: handlerError }));
244
+ } catch (handlerError) {
245
+ logReleaseError(error, { error: handlerError });
246
+ }
247
+ }
248
+ /**
249
+ * Writes a failed release to `console.error`, preceded by the error of `onReleaseError` when the handler failed.
250
+ *
251
+ * A console that throws, such as one redirected to a failing logger, leaves nowhere to report to, so its error is ignored.
252
+ */
253
+ function logReleaseError(error, handlerFailure) {
254
+ try {
255
+ if (handlerFailure) console.error("kern-db: onReleaseError failed", handlerFailure.error);
256
+ console.error("kern-db: releasing a connection failed", error);
257
+ } catch {}
258
+ }
259
+ // src/transaction/test-levels/abort.ts
260
+ /**
261
+ * Runs `start` and settles as its promise, or rejects with the reason of `signal` when it aborts first and `abort`, called on the abort, returns `true`.
262
+ *
263
+ * It listens from before `start`, which may abort the signal before it returns, and stops once the promise of `start` settles, such as for the signal that `node:test` aborts after every test.
264
+ * A later rejection of that promise stays handled, and a value it resolves with after the rejection goes to `discard`, which handles its own failures.
265
+ */
266
+ function settleUnlessAborted(start, signal, abort, discard) {
267
+ const settled = Promise.withResolvers();
268
+ let abandoned = false;
269
+ const onAbort = () => {
270
+ if (abort()) {
271
+ abandoned = true;
272
+ settled.reject(signal.reason);
273
+ }
274
+ };
275
+ signal.addEventListener("abort", onAbort, { once: true });
276
+ let running;
277
+ try {
278
+ running = start();
279
+ } catch (error) {
280
+ signal.removeEventListener("abort", onAbort);
281
+ settled.reject(error);
282
+ return settled.promise;
283
+ }
284
+ running.then((value) => {
285
+ signal.removeEventListener("abort", onAbort);
286
+ if (abandoned) discard?.(value);
287
+ else settled.resolve(value);
288
+ }, (error) => {
289
+ signal.removeEventListener("abort", onAbort);
290
+ settled.reject(error);
291
+ });
292
+ return settled.promise;
293
+ }
294
+ /**
295
+ * Acquires a connection for a test transaction, rejecting with the reason of `signal` when it aborts first.
296
+ *
297
+ * A connection acquired after the abort returns to the pool as best-effort cleanup: the test transaction has already rejected, so nothing awaits the release, and its failure goes to `onReleaseError`.
298
+ */
299
+ function acquireUnlessAborted(driver, signal, onReleaseError) {
300
+ if (signal.aborted) return Promise.reject(signal.reason);
301
+ return settleUnlessAborted(() => driver.acquire(), signal, () => true, (connection) => void releaseConnection(connection, onReleaseError));
302
+ }
303
303
  // src/transaction/level.ts
304
304
  /** Options of a level started without options, shared so that such a start allocates none. */
305
305
  const noLevelOptions = {};
@@ -346,7 +346,8 @@ function createLevel(connection, parent, transaction, isolation) {
346
346
  endedBy: void 0,
347
347
  restore: void 0,
348
348
  isolation: enclosing ? enclosing.isolation : isolation,
349
- locks: void 0
349
+ locks: void 0,
350
+ turns: void 0
350
351
  };
351
352
  }
352
353
  /**
@@ -433,6 +434,11 @@ async function runLevel(storage, level, callback, commits, signal) {
433
434
  rejection = { error };
434
435
  }
435
436
  }
437
+ if (!rejection && level.turns && !level.cleanup && !level.turns.tryTake()) try {
438
+ await level.turns.take(signal);
439
+ } catch {
440
+ endLevel(level, false, "abort");
441
+ }
436
442
  if (level.cleanup) return getEndedOutcome(level.cleanup, level.endedBy === "abort" ? signal?.reason : createClosedError(level, rejection));
437
443
  if (!rejection && level.child) rejection = { error: /* @__PURE__ */ new Error(level.session ? "Session ended with an error: a transaction of it was still running when its callback finished" : "Transaction was rolled back: a nested transaction was still running when its callback finished") };
438
444
  if (!rejection && level.rollbackOnly) rejection = { error: new Error("Transaction was rolled back: a nested transaction failed to end", { cause: level.rollbackOnly.error }) };
@@ -442,6 +448,12 @@ async function runLevel(storage, level, callback, commits, signal) {
442
448
  rejection: combineRejections(rejection, cleanup)
443
449
  };
444
450
  }
451
+ /** Runs `callback` in a started `level` in `storage` with `runLevel`, returning its result or throwing its rejection. */
452
+ async function runStarted(storage, level, callback, commits, signal) {
453
+ const { result, rejection } = await runLevel(storage, level, callback, commits, signal);
454
+ if (rejection) throw rejection.error;
455
+ return result;
456
+ }
445
457
  /**
446
458
  * Ends `level` once, whoever ends it: later calls return the cleanup already started.
447
459
  *
@@ -456,6 +468,7 @@ function endLevel(level, commit, endedBy) {
456
468
  return level.cleanup;
457
469
  }
458
470
  level.state = "closing";
471
+ level.turns?.close();
459
472
  level.cleanup = cleanUpLevel(level, commit, level.child && endLevel(level.child, false, "enclosing level"));
460
473
  return level.cleanup;
461
474
  }
@@ -556,223 +569,6 @@ function combineRejections(earlier, cleanup) {
556
569
  function suppress(error, earlier, message) {
557
570
  return { error: earlier ? new SuppressedError(error, earlier.error, message) : error };
558
571
  }
559
- // src/transaction/fixture-stack.ts
560
- /** Open test fixtures of a transaction coordinator and the queue of the changes of their stack. */
561
- var TestFixtureStack = class {
562
- #driver;
563
- #onReleaseError;
564
- /** Async context of the levels of the coordinator, which tells whether a call runs inside a level. */
565
- #storage;
566
- /** Open test fixtures, the innermost last. */
567
- #fixtures = [];
568
- /** Changes of the fixture stack, run one after another so that each one sees the stack the previous one left. */
569
- #transitions = Promise.resolve();
570
- /**
571
- * The `close` functions of the test fixtures that are still opening, from the call that starts the opening until the fixture enters the stack or the opening ends otherwise.
572
- *
573
- * An opening holds the queue of the fixture stack while its seed runs, so a closing of the stack queued behind it would wait for a seed that may never settle.
574
- * Closing the stack therefore calls these first: each one tells its opening to stop waiting for the seed.
575
- */
576
- #openings = /* @__PURE__ */ new Set();
577
- /**
578
- * Outermost fixture in the stack whose closing is queued but has not finished.
579
- *
580
- * Openings that start after that closing is requested nest outside of the fixture once it closes, so a repeated closing of it, or a closing of a fixture nested in it, must not close them.
581
- */
582
- #closing;
583
- constructor(driver, storage, onReleaseError) {
584
- this.#driver = driver;
585
- this.#storage = storage;
586
- this.#onReleaseError = onReleaseError;
587
- }
588
- /**
589
- * Starts opening a test fixture with `options`, see `startTestFixture` of the coordinator, or, given `refusal`, returns a fixture that rejects with it.
590
- *
591
- * The `close` of a refused fixture resolves once the earlier changes of the stack end.
592
- */
593
- start(seed, refusal, options) {
594
- let level;
595
- let closeRequested = false;
596
- const closing = Promise.withResolvers();
597
- let closedRejection;
598
- const close = () => {
599
- if (this.#storage.getStore()) return Promise.reject(/* @__PURE__ */ new Error("Cannot close a test fixture inside a transaction"));
600
- closeRequested = true;
601
- closing.resolve();
602
- if (level && this.#fixtures.includes(level)) {
603
- const opened = level;
604
- if (this.#closing && this.#closing.depth <= opened.depth) return this.#enqueue(() => this.#closeFixture(opened));
605
- this.#closing = opened;
606
- return this.#closeAfterOpenings(() => this.#enqueue(async () => {
607
- try {
608
- await this.#closeFixture(opened);
609
- } finally {
610
- if (this.#closing === opened) this.#closing = void 0;
611
- }
612
- }));
613
- }
614
- return this.#enqueue(() => {
615
- const rejection = closedRejection;
616
- closedRejection = void 0;
617
- if (rejection) throw rejection.error;
618
- });
619
- };
620
- if (refusal) return {
621
- fixture: Promise.reject(refusal),
622
- close
623
- };
624
- /**
625
- * Runs stages 2 and 3 of the fixture and puts it in the stack, returning `undefined` when closing was requested and the fixture rolled back cleanly.
626
- *
627
- * It rejects with a failure to start the fixture, with the rejection of `seed`, or with a failure to roll back, which suppresses the rejection of `seed`.
628
- */
629
- const open = async () => {
630
- if (closeRequested) return;
631
- const parent = this.#fixtures.at(-1);
632
- const started = parent ? await this.#startInFixture(parent, options) : await this.#beginFixture(options?.isolation);
633
- started.state = "open";
634
- const outcome = closeRequested ? void 0 : await Promise.race([this.#seedFixture(started, seed), closing.promise]);
635
- if (outcome && !outcome.rejection && !closeRequested) {
636
- this.#fixtures.push(started);
637
- level = started;
638
- this.#openings.delete(close);
639
- const { result } = outcome;
640
- return {
641
- get data() {
642
- if (started.state !== "open") throw new Error("Test fixture is closed");
643
- return result;
644
- },
645
- close,
646
- [Symbol.asyncDispose]: close
647
- };
648
- }
649
- const rejection = combineRejections(outcome?.rejection, { rejection: await this.#endFixture(started) });
650
- if (rejection) throw rejection.error;
651
- };
652
- this.#openings.add(close);
653
- return {
654
- fixture: this.#enqueue(async () => {
655
- let opened;
656
- try {
657
- opened = await open();
658
- } catch (error) {
659
- if (!closeRequested) throw error;
660
- closedRejection = { error };
661
- } finally {
662
- this.#openings.delete(close);
663
- }
664
- if (!opened) throw new Error("Test fixture was closed before it opened");
665
- return opened;
666
- }),
667
- close
668
- };
669
- }
670
- /**
671
- * Starts the level of a test transaction in the innermost test fixture as a change of the fixture stack, resolving to `undefined` when no fixture is open.
672
- *
673
- * Aborted while it waits for its turn, it rejects at once and its change starts nothing, so that a test that has already ended never preempts the active level of the fixture.
674
- * From its turn on, it settles with its change.
675
- */
676
- startTestLevel(options) {
677
- const signal = options?.signal;
678
- let waiting = true;
679
- const start = () => this.#enqueue(() => {
680
- if (signal?.aborted) return;
681
- waiting = false;
682
- const fixture = this.#fixtures.at(-1);
683
- return fixture && this.#startInFixture(fixture, options);
684
- });
685
- return signal ? settleUnlessAborted(start, signal, () => waiting) : start();
686
- }
687
- /**
688
- * Closes every fixture, including those still opening, rejecting with the failures of all of them.
689
- *
690
- * Unlike closing a fixture, it closes every opening, including one started after a closing of the stack was requested, since nothing may stay open.
691
- */
692
- close() {
693
- return this.#closeAfterOpenings(() => this.#enqueue(() => {
694
- const root = this.#fixtures[0];
695
- return root && this.#closeFixture(root);
696
- }));
697
- }
698
- /**
699
- * Closes the fixtures still opening and runs `closeStack`, rejecting with the failures of all of them.
700
- *
701
- * A seed that never settles holds the queue of the fixture stack, so closing is requested for every opening before `closeStack` queues its change, and the rollback does not wait for the seeds.
702
- */
703
- async #closeAfterOpenings(closeStack) {
704
- const closings = [];
705
- for (const closeOpening of this.#openings) closings.push(closeOpening());
706
- closings.push(closeStack());
707
- let rejection;
708
- for (const closing of await Promise.allSettled(closings)) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test fixture failed");
709
- if (rejection) throw rejection.error;
710
- }
711
- /**
712
- * Runs `seed` in a savepoint of a fixture that is released when it resolves, returning its result or rejection.
713
- *
714
- * The savepoint has its own context, so late code of `seed` cannot reach the fixture after it ends.
715
- */
716
- async #seedFixture(fixture, seed) {
717
- try {
718
- return await runLevel(this.#storage, await startNestedLevel(fixture, false), seed, true);
719
- } catch (error) {
720
- return { rejection: { error } };
721
- }
722
- }
723
- /** Closes an opened fixture as a change of the fixture stack. */
724
- async #closeFixture(fixture) {
725
- const index = this.#fixtures.indexOf(fixture);
726
- if (index < 0) return;
727
- let rejection;
728
- try {
729
- rejection = await this.#endFixture(fixture);
730
- } finally {
731
- this.#fixtures.length = index;
732
- }
733
- if (rejection) throw rejection.error;
734
- }
735
- /** Rolls back a fixture and releases the connection of the first one, returning the error to reject with. */
736
- async #endFixture(fixture) {
737
- const { rejection } = await endLevel(fixture, false);
738
- if (!fixture.parent) await releaseConnection(fixture.connection, this.#onReleaseError);
739
- return rejection;
740
- }
741
- /** Runs a change of the fixture stack after the earlier ones. */
742
- #enqueue(transition) {
743
- const result = this.#transitions.then(transition);
744
- this.#transitions = result.catch(() => {});
745
- return result;
746
- }
747
- /** Starts the first test fixture: a transaction at `isolation` on a reserved connection. */
748
- async #beginFixture(isolation) {
749
- const connection = await this.#driver.acquire();
750
- try {
751
- await connection.execute(getBeginStatement(isolation));
752
- } catch (error) {
753
- await releaseConnection(connection, this.#onReleaseError);
754
- throw error;
755
- }
756
- return createLevel(connection, void 0, false, isolation);
757
- }
758
- /**
759
- * Starts a nested level of a test fixture with `options`, preempting its active one.
760
- *
761
- * A fixture runs one nested level at a time, so a new one ends the previous one, such as a test that timed out.
762
- * A level whose `signal` aborts while it preempts rejects with its reason instead of starting, and a failure to start after the abort suppresses the reason.
763
- */
764
- async #startInFixture(fixture, options) {
765
- const signal = options?.signal;
766
- if (fixture.child) {
767
- const cleanup = await endLevel(fixture.child, false, "preemption");
768
- if (cleanup.rejection) throw suppressAbortReason(new Error("Cannot start a level of the test fixture: ending its active level failed", { cause: cleanup.rejection.error }), signal);
769
- }
770
- const unavailable = createUnavailableError(fixture, "start a level of the test fixture");
771
- if (unavailable) throw suppressAbortReason(unavailable, signal);
772
- signal?.throwIfAborted();
773
- return startNestedLevel(fixture, false, options);
774
- }
775
- };
776
572
  // src/error-condition-codes.ts
777
573
  /**
778
574
  * Condition names of the SQLSTATE codes of PostgreSQL errors, by code: the codes of type `E` with a condition name, which PL/pgSQL takes in `EXCEPTION WHEN`, under the headings of their classes.
@@ -1068,7 +864,8 @@ function createSessionLevel(connection) {
1068
864
  endedBy: void 0,
1069
865
  restore: void 0,
1070
866
  isolation: void 0,
1071
- locks: void 0
867
+ locks: void 0,
868
+ turns: void 0
1072
869
  };
1073
870
  }
1074
871
  /**
@@ -1120,6 +917,436 @@ async function leaveInTransaction(connection) {
1120
917
  if (!(error instanceof QueryError) || isQueryError(error, "in_failed_sql_transaction")) return;
1121
918
  }
1122
919
  }
920
+ // src/transaction/test-levels/turns.ts
921
+ /**
922
+ * Queue of the units of work that share the connection of a level of the test helpers, which run one at a time in the order they ask for a turn.
923
+ *
924
+ * The holder of the turn passes it to the next waiter with `release`.
925
+ * Once closed, it grants every turn at once, so that its waiters see that the level is closed and reject.
926
+ */
927
+ var TurnQueue = class {
928
+ #held = false;
929
+ #closed = false;
930
+ /** Waiters in the order they asked for a turn; each one, called, takes the turn. */
931
+ #waiters = [];
932
+ /**
933
+ * Resolves once the caller holds the turn, or at once after `close`.
934
+ *
935
+ * Aborted by `signal` while it waits, it leaves the queue and rejects with the reason of the signal; given an aborted signal, it rejects without taking the turn.
936
+ */
937
+ take(signal) {
938
+ if (signal?.aborted) return Promise.reject(signal.reason);
939
+ if (this.#closed) return Promise.resolve();
940
+ if (!this.#held) {
941
+ this.#held = true;
942
+ return Promise.resolve();
943
+ }
944
+ const { promise, resolve, reject } = Promise.withResolvers();
945
+ const onAbort = () => {
946
+ this.#waiters.splice(this.#waiters.indexOf(grant), 1);
947
+ reject(signal.reason);
948
+ };
949
+ const grant = () => {
950
+ signal?.removeEventListener("abort", onAbort);
951
+ resolve();
952
+ };
953
+ signal?.addEventListener("abort", onAbort, { once: true });
954
+ this.#waiters.push(grant);
955
+ return promise;
956
+ }
957
+ /** Takes the turn at once when it is free or the queue is closed, and tells whether it did. */
958
+ tryTake() {
959
+ if (this.#closed) return true;
960
+ if (this.#held) return false;
961
+ this.#held = true;
962
+ return true;
963
+ }
964
+ /** Passes the turn of the caller to the next waiter, or frees it. */
965
+ release() {
966
+ if (this.#closed) return;
967
+ const next = this.#waiters.shift();
968
+ if (next) next();
969
+ else this.#held = false;
970
+ }
971
+ /** Grants the turn to every waiter and to every later caller. */
972
+ close() {
973
+ this.#closed = true;
974
+ for (const grant of this.#waiters.splice(0)) grant();
975
+ }
976
+ };
977
+ /**
978
+ * Runs `start` on its turn on the connection of a level of the test helpers `level`, and passes the turn on once it settles, after the level it started has ended.
979
+ *
980
+ * The code of the level runs statements and transactions at once on its connection, such as the requests of an open test transaction, where PostgreSQL would run a statement inside a nested transaction of other code, so they take turns rather than reject as in a transaction of `transaction`.
981
+ * It rejects without calling `start` when the level accepts no statements on its turn, and with the reason of `signal` when it aborts first.
982
+ */
983
+ async function runInTurn(level, operation, start, signal) {
984
+ const turns = level.turns;
985
+ if (!turns.tryTake()) await turns.take(signal);
986
+ try {
987
+ signal?.throwIfAborted();
988
+ const unavailable = createUnavailableError(level, operation);
989
+ if (unavailable) throw unavailable;
990
+ return await start();
991
+ } finally {
992
+ turns.release();
993
+ }
994
+ }
995
+ /**
996
+ * Runs a statement of `run` on its turn in a savepoint of its own in a level of the test helpers `level`, released when it succeeds and rolled back when it fails.
997
+ *
998
+ * A statement outside a transaction commits or fails alone in production, so its failure must not abort the transaction of the test.
999
+ * A statement sent before the level ends settles with its own result, as on the connection of a transaction: the rollback of the level undoes it, and reports the failures of that rollback.
1000
+ */
1001
+ function runStatementInTurn(level, operation, run) {
1002
+ return runInTurn(level, operation, async () => {
1003
+ const unit = createLevel(level.connection, level, false);
1004
+ level.child = unit;
1005
+ const { connection } = unit;
1006
+ const savepoint = connection.execute(createStatement(`SAVEPOINT ${getSavepointName(unit)}`));
1007
+ unit.state = "open";
1008
+ const running = run(connection);
1009
+ try {
1010
+ await savepoint;
1011
+ } catch (error) {
1012
+ await running.catch(() => {});
1013
+ if (unit.cleanup) throw combineRejections({ error }, await unit.cleanup).error;
1014
+ level.rollbackOnly ??= { error };
1015
+ closeStarting(unit);
1016
+ throw error;
1017
+ }
1018
+ let result;
1019
+ try {
1020
+ result = await running;
1021
+ } catch (error) {
1022
+ const cleanup = await endLevel(unit, false);
1023
+ throw unit.endedBy ? error : combineRejections({ error }, cleanup).error;
1024
+ }
1025
+ const cleanup = await endLevel(unit, true);
1026
+ if (cleanup.rejection && !unit.endedBy) throw cleanup.rejection.error;
1027
+ return result;
1028
+ });
1029
+ }
1030
+ // src/transaction/test-levels/fixture-stack.ts
1031
+ /** Open test fixtures of a transaction coordinator and the queue of the changes of their stack. */
1032
+ var TestFixtureStack = class {
1033
+ #driver;
1034
+ #onReleaseError;
1035
+ /** Async context of the levels of the coordinator, which tells whether a call runs inside a level. */
1036
+ #storage;
1037
+ /** Open test fixtures, the innermost last. */
1038
+ #fixtures = [];
1039
+ /** Changes of the fixture stack, run one after another so that each one sees the stack the previous one left. */
1040
+ #transitions = Promise.resolve();
1041
+ /**
1042
+ * The `close` functions of the test fixtures that are still opening, from the call that starts the opening until the fixture enters the stack or the opening ends otherwise.
1043
+ *
1044
+ * An opening holds the queue of the fixture stack while its seed runs, so a closing of the stack queued behind it would wait for a seed that may never settle.
1045
+ * Closing the stack therefore calls these first: each one tells its opening to stop waiting for the seed.
1046
+ */
1047
+ #openings = /* @__PURE__ */ new Set();
1048
+ /**
1049
+ * Outermost fixture in the stack whose closing is queued but has not finished.
1050
+ *
1051
+ * Openings that start after that closing is requested nest outside of the fixture once it closes, so a repeated closing of it, or a closing of a fixture nested in it, must not close them.
1052
+ */
1053
+ #closing;
1054
+ constructor(driver, storage, onReleaseError) {
1055
+ this.#driver = driver;
1056
+ this.#storage = storage;
1057
+ this.#onReleaseError = onReleaseError;
1058
+ }
1059
+ /**
1060
+ * Starts opening a test fixture with `options`, see `startTestFixture` of the coordinator, or, given `refusal`, returns a fixture that rejects with it.
1061
+ *
1062
+ * The `close` of a refused fixture resolves once the earlier changes of the stack end.
1063
+ */
1064
+ start(seed, refusal, options) {
1065
+ let level;
1066
+ let closeRequested = false;
1067
+ const closing = Promise.withResolvers();
1068
+ let closedRejection;
1069
+ const close = () => {
1070
+ if (this.#storage.getStore()) return Promise.reject(/* @__PURE__ */ new Error("Cannot close a test fixture inside a transaction"));
1071
+ closeRequested = true;
1072
+ closing.resolve();
1073
+ if (level && this.#fixtures.includes(level)) {
1074
+ const opened = level;
1075
+ if (this.#closing && this.#closing.depth <= opened.depth) return this.#enqueue(() => this.#closeFixture(opened));
1076
+ this.#closing = opened;
1077
+ return this.#closeAfterOpenings(() => this.#enqueue(async () => {
1078
+ try {
1079
+ await this.#closeFixture(opened);
1080
+ } finally {
1081
+ if (this.#closing === opened) this.#closing = void 0;
1082
+ }
1083
+ }));
1084
+ }
1085
+ return this.#enqueue(() => {
1086
+ const rejection = closedRejection;
1087
+ closedRejection = void 0;
1088
+ if (rejection) throw rejection.error;
1089
+ });
1090
+ };
1091
+ if (refusal) return {
1092
+ fixture: Promise.reject(refusal),
1093
+ close
1094
+ };
1095
+ /**
1096
+ * Runs stages 2 and 3 of the fixture and puts it in the stack, returning `undefined` when closing was requested and the fixture rolled back cleanly.
1097
+ *
1098
+ * It rejects with a failure to start the fixture, with the rejection of `seed`, or with a failure to roll back, which suppresses the rejection of `seed`.
1099
+ */
1100
+ const open = async () => {
1101
+ if (closeRequested) return;
1102
+ const parent = this.#fixtures.at(-1);
1103
+ const started = parent ? await this.#startInFixture(parent, options) : await this.#beginFixture(options?.isolation);
1104
+ started.state = "open";
1105
+ const outcome = closeRequested ? void 0 : await Promise.race([this.#seedFixture(started, seed), closing.promise]);
1106
+ if (outcome && !outcome.rejection && !closeRequested) {
1107
+ this.#fixtures.push(started);
1108
+ level = started;
1109
+ this.#openings.delete(close);
1110
+ const { result } = outcome;
1111
+ return {
1112
+ get data() {
1113
+ if (started.state !== "open") throw new Error("Test fixture is closed");
1114
+ return result;
1115
+ },
1116
+ close,
1117
+ [Symbol.asyncDispose]: close
1118
+ };
1119
+ }
1120
+ const rejection = combineRejections(outcome?.rejection, { rejection: await this.#endFixture(started) });
1121
+ if (rejection) throw rejection.error;
1122
+ };
1123
+ this.#openings.add(close);
1124
+ return {
1125
+ fixture: this.#enqueue(async () => {
1126
+ let opened;
1127
+ try {
1128
+ opened = await open();
1129
+ } catch (error) {
1130
+ if (!closeRequested) throw error;
1131
+ closedRejection = { error };
1132
+ } finally {
1133
+ this.#openings.delete(close);
1134
+ }
1135
+ if (!opened) throw new Error("Test fixture was closed before it opened");
1136
+ return opened;
1137
+ }),
1138
+ close
1139
+ };
1140
+ }
1141
+ /**
1142
+ * Starts the level of a test transaction in the innermost test fixture as a change of the fixture stack, resolving to `undefined` when no fixture is open.
1143
+ *
1144
+ * Aborted while it waits for its turn, it rejects at once and its change starts nothing, so that a test that has already ended never preempts the active level of the fixture.
1145
+ * From its turn on, it settles with its change.
1146
+ */
1147
+ startTestLevel(options) {
1148
+ const signal = options?.signal;
1149
+ let waiting = true;
1150
+ const start = () => this.#enqueue(() => {
1151
+ if (signal?.aborted) return;
1152
+ waiting = false;
1153
+ const fixture = this.#fixtures.at(-1);
1154
+ return fixture && this.#startInFixture(fixture, options);
1155
+ });
1156
+ return signal ? settleUnlessAborted(start, signal, () => waiting) : start();
1157
+ }
1158
+ /**
1159
+ * Closes every fixture, including those still opening, rejecting with the failures of all of them.
1160
+ *
1161
+ * Unlike closing a fixture, it closes every opening, including one started after a closing of the stack was requested, since nothing may stay open.
1162
+ */
1163
+ close() {
1164
+ return this.#closeAfterOpenings(() => this.#enqueue(() => {
1165
+ const root = this.#fixtures[0];
1166
+ return root && this.#closeFixture(root);
1167
+ }));
1168
+ }
1169
+ /**
1170
+ * Closes the fixtures still opening and runs `closeStack`, rejecting with the failures of all of them.
1171
+ *
1172
+ * A seed that never settles holds the queue of the fixture stack, so closing is requested for every opening before `closeStack` queues its change, and the rollback does not wait for the seeds.
1173
+ */
1174
+ async #closeAfterOpenings(closeStack) {
1175
+ const closings = [];
1176
+ for (const closeOpening of this.#openings) closings.push(closeOpening());
1177
+ closings.push(closeStack());
1178
+ let rejection;
1179
+ for (const closing of await Promise.allSettled(closings)) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test fixture failed");
1180
+ if (rejection) throw rejection.error;
1181
+ }
1182
+ /**
1183
+ * Runs `seed` in a savepoint of a fixture that is released when it resolves, returning its result or rejection.
1184
+ *
1185
+ * The savepoint has its own context, so late code of `seed` cannot reach the fixture after it ends.
1186
+ * Its statements and transactions take turns, as in a test transaction.
1187
+ */
1188
+ async #seedFixture(fixture, seed) {
1189
+ try {
1190
+ const level = await startNestedLevel(fixture, false);
1191
+ level.turns = new TurnQueue();
1192
+ return await runLevel(this.#storage, level, seed, true);
1193
+ } catch (error) {
1194
+ return { rejection: { error } };
1195
+ }
1196
+ }
1197
+ /** Closes an opened fixture as a change of the fixture stack. */
1198
+ async #closeFixture(fixture) {
1199
+ const index = this.#fixtures.indexOf(fixture);
1200
+ if (index < 0) return;
1201
+ let rejection;
1202
+ try {
1203
+ rejection = await this.#endFixture(fixture);
1204
+ } finally {
1205
+ this.#fixtures.length = index;
1206
+ }
1207
+ if (rejection) throw rejection.error;
1208
+ }
1209
+ /** Rolls back a fixture and releases the connection of the first one, returning the error to reject with. */
1210
+ async #endFixture(fixture) {
1211
+ const { rejection } = await endLevel(fixture, false);
1212
+ if (!fixture.parent) await releaseConnection(fixture.connection, this.#onReleaseError);
1213
+ return rejection;
1214
+ }
1215
+ /** Runs a change of the fixture stack after the earlier ones. */
1216
+ #enqueue(transition) {
1217
+ const result = this.#transitions.then(transition);
1218
+ this.#transitions = result.catch(() => {});
1219
+ return result;
1220
+ }
1221
+ /** Starts the first test fixture: a transaction at `isolation` on a reserved connection. */
1222
+ async #beginFixture(isolation) {
1223
+ const connection = await this.#driver.acquire();
1224
+ try {
1225
+ await connection.execute(getBeginStatement(isolation));
1226
+ } catch (error) {
1227
+ await releaseConnection(connection, this.#onReleaseError);
1228
+ throw error;
1229
+ }
1230
+ return createLevel(connection, void 0, false, isolation);
1231
+ }
1232
+ /**
1233
+ * Starts a nested level of a test fixture with `options`, preempting its active one.
1234
+ *
1235
+ * A fixture runs one nested level at a time, so a new one ends the previous one, such as a test that timed out.
1236
+ * A level whose `signal` aborts while it preempts rejects with its reason instead of starting, and a failure to start after the abort suppresses the reason.
1237
+ */
1238
+ async #startInFixture(fixture, options) {
1239
+ const signal = options?.signal;
1240
+ if (fixture.child) {
1241
+ const cleanup = await endLevel(fixture.child, false, "preemption");
1242
+ if (cleanup.rejection) throw suppressAbortReason(new Error("Cannot start a level of the test fixture: ending its active level failed", { cause: cleanup.rejection.error }), signal);
1243
+ }
1244
+ const unavailable = createUnavailableError(fixture, "start a level of the test fixture");
1245
+ if (unavailable) throw suppressAbortReason(unavailable, signal);
1246
+ signal?.throwIfAborted();
1247
+ return startNestedLevel(fixture, false, options);
1248
+ }
1249
+ };
1250
+ // src/transaction/test-levels/open-test-transaction.ts
1251
+ /** Opens the test transactions of a transaction coordinator, and closes those on connections of their own when the database closes. */
1252
+ var OpenTestTransactions = class {
1253
+ #driver;
1254
+ #storage;
1255
+ #fixtures;
1256
+ #onReleaseError;
1257
+ /** The `close` functions of the open test transactions on connections of their own; those in a fixture close with it. */
1258
+ #roots = /* @__PURE__ */ new Set();
1259
+ constructor(driver, storage, fixtures, onReleaseError) {
1260
+ this.#driver = driver;
1261
+ this.#storage = storage;
1262
+ this.#fixtures = fixtures;
1263
+ this.#onReleaseError = onReleaseError;
1264
+ }
1265
+ /**
1266
+ * Opens a test transaction: a savepoint in the innermost open fixture, which preempts its active level, or a transaction on a connection of its own.
1267
+ *
1268
+ * Its statements outside a transaction and its nested levels take turns on its connection, see `runInTurn`.
1269
+ * `getClosedError` returns the error of an opening once closing the database is requested; an opening on a connection of its own that the closing overtakes rolls back and rejects with it, since the closing no longer sees it.
1270
+ */
1271
+ async open(options, getClosedError) {
1272
+ const nested = await this.#fixtures.startTestLevel(options);
1273
+ const level = nested ?? await this.#begin(options?.isolation, getClosedError);
1274
+ if (nested?.cleanup) {
1275
+ const { rejection } = await getEndedOutcome(nested.cleanup, /* @__PURE__ */ new Error("Cannot open a test transaction: its test fixture ended it before it opened"));
1276
+ throw rejection.error;
1277
+ }
1278
+ level.state = "open";
1279
+ level.turns = new TurnQueue();
1280
+ let closing;
1281
+ const close = () => {
1282
+ closing ??= (async () => {
1283
+ const { rejection } = await endLevel(level, false);
1284
+ if (!nested) {
1285
+ this.#roots.delete(close);
1286
+ await releaseConnection(level.connection, this.#onReleaseError);
1287
+ }
1288
+ if (rejection) throw rejection.error;
1289
+ })();
1290
+ return closing;
1291
+ };
1292
+ if (!nested) {
1293
+ const closedError = getClosedError();
1294
+ if (closedError) {
1295
+ let rejection = { error: closedError };
1296
+ try {
1297
+ await close();
1298
+ } catch (error) {
1299
+ rejection = suppress(error, rejection, "Transaction rollback failed");
1300
+ }
1301
+ throw rejection.error;
1302
+ }
1303
+ this.#roots.add(close);
1304
+ }
1305
+ return {
1306
+ run: async (callback) => {
1307
+ if (level.state !== "open") throw new Error("Cannot run a callback in the test transaction: it is already closed");
1308
+ const current = this.#storage.getStore();
1309
+ if (current === level) return callback();
1310
+ if (current) throw new Error("Cannot run a callback in an open test transaction inside a transaction");
1311
+ return this.#storage.run(level, callback);
1312
+ },
1313
+ close,
1314
+ [Symbol.asyncDispose]: close
1315
+ };
1316
+ }
1317
+ /** Rolls back the open test transactions on connections of their own, rejecting with the failures of all of them. */
1318
+ async close() {
1319
+ const closings = [];
1320
+ for (const close of this.#roots) closings.push(close());
1321
+ let rejection;
1322
+ for (const closing of await Promise.allSettled(closings)) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test transaction failed");
1323
+ if (rejection) throw rejection.error;
1324
+ }
1325
+ /**
1326
+ * Begins an open test transaction outside a fixture at `isolation` on a connection of its own.
1327
+ *
1328
+ * Once closing the database is requested, it releases the connection instead of sending `BEGIN`, and rejects with the error of `getClosedError`.
1329
+ */
1330
+ async #begin(isolation, getClosedError) {
1331
+ const closedError = getClosedError();
1332
+ if (closedError) throw closedError;
1333
+ const connection = await this.#driver.acquire();
1334
+ const lateError = getClosedError();
1335
+ if (lateError) {
1336
+ await releaseConnection(connection, this.#onReleaseError);
1337
+ throw lateError;
1338
+ }
1339
+ const level = createLevel(connection, void 0, false, isolation);
1340
+ try {
1341
+ await connection.execute(getBeginStatement(isolation));
1342
+ } catch (error) {
1343
+ closeStarting(level);
1344
+ await releaseConnection(connection, this.#onReleaseError);
1345
+ throw error;
1346
+ }
1347
+ return level;
1348
+ }
1349
+ };
1123
1350
  // src/transaction/coordinator.ts
1124
1351
  /**
1125
1352
  * Routes statements and scripts to the ambient transaction or session of the current async context or to the pool.
@@ -1133,6 +1360,7 @@ var TransactionCoordinator = class {
1133
1360
  #onReleaseError;
1134
1361
  /** Closing of the database, set by the first `close`; from then on, operations outside a transaction reject. */
1135
1362
  #closed;
1363
+ #openTestTransactions;
1136
1364
  /** Rejects statements and transactions outside a test transaction or the seed of a test fixture instead of running them on the pool. */
1137
1365
  test;
1138
1366
  constructor(driver, { test = false, onReleaseError } = {}) {
@@ -1140,19 +1368,23 @@ var TransactionCoordinator = class {
1140
1368
  this.test = test;
1141
1369
  this.#onReleaseError = onReleaseError;
1142
1370
  this.#fixtures = new TestFixtureStack(driver, this.#storage, onReleaseError);
1371
+ this.#openTestTransactions = new OpenTestTransactions(driver, this.#storage, this.#fixtures, onReleaseError);
1143
1372
  }
1144
1373
  execute(statement) {
1145
- const target = this.#route("execute a query");
1374
+ const level = this.#storage.getStore();
1375
+ if (level?.turns) return runStatementInTurn(level, "execute a query", (connection) => connection.execute(statement));
1376
+ const target = this.#route(level, "execute a query");
1146
1377
  return target instanceof Error ? Promise.reject(target) : target.execute(statement);
1147
1378
  }
1148
1379
  /** Executes a script of statements without parameters where `execute` executes a statement. */
1149
1380
  executeScript(text) {
1150
- const target = this.#route("execute a script");
1381
+ const level = this.#storage.getStore();
1382
+ if (level?.turns) return runStatementInTurn(level, "execute a script", (connection) => connection.executeScript(text));
1383
+ const target = this.#route(level, "execute a script");
1151
1384
  return target instanceof Error ? Promise.reject(target) : target.executeScript(text);
1152
1385
  }
1153
- /** Returns the connection of the ambient transaction or session, or the driver of the pool, that runs `operation`, or the error that rejects it. */
1154
- #route(operation) {
1155
- const level = this.#storage.getStore();
1386
+ /** Returns the connection of the ambient transaction or session `level`, or the driver of the pool, that runs `operation`, or the error that rejects it. */
1387
+ #route(level, operation) {
1156
1388
  if (!level) {
1157
1389
  if (this.#closed) return createDatabaseClosedError(operation);
1158
1390
  return this.test ? new Error(describeOutsideTest(operation)) : this.driver;
@@ -1261,6 +1493,7 @@ var TransactionCoordinator = class {
1261
1493
  * Runs `callback` in a transaction that always rolls back, on a coordinator created for tests.
1262
1494
  *
1263
1495
  * Inside a transaction, it runs `callback` in a savepoint nested in it that always rolls back.
1496
+ * Outside a transaction of `transaction`, the statements and nested levels of `callback` take turns on its connection, each statement in a savepoint of its own, see `runInTurn`; when `callback` resolves, the level ends after the statements and nested levels it started.
1264
1497
  * When `signal` aborts, the level stops accepting statements at once and ends without waiting for `callback`.
1265
1498
  * Once the level ends, the transaction rejects with the reason of the signal.
1266
1499
  * A failure to start or end the level after the abort suppresses the reason in a `SuppressedError`.
@@ -1273,7 +1506,24 @@ var TransactionCoordinator = class {
1273
1506
  const parent = this.#storage.getStore();
1274
1507
  if (parent) return this.#runNested(parent, callback, false, options);
1275
1508
  if (this.#closed) return Promise.reject(createDatabaseClosedError("start a test transaction"));
1276
- return this.#fixtures.startTestLevel(options).then((level) => level ? this.#runStarted(level, callback, false, signal) : this.#runRoot(callback, false, options));
1509
+ return this.#fixtures.startTestLevel(options).then((level) => {
1510
+ if (!level) return this.#runRoot(callback, false, options);
1511
+ level.turns = new TurnQueue();
1512
+ return runStarted(this.#storage, level, callback, false, signal);
1513
+ });
1514
+ }
1515
+ /**
1516
+ * Opens a test transaction that the callbacks of `run` share until it closes, see `OpenTestTransactions`.
1517
+ *
1518
+ * It rejects inside a transaction, and once closing the database is requested.
1519
+ */
1520
+ async openTestTransaction(options) {
1521
+ if (!this.test) throw new Error("openTestTransaction requires a database created with test: true");
1522
+ if (this.#storage.getStore()) throw new Error("Cannot open a test transaction inside a transaction");
1523
+ const getClosedError = () => this.#closed ? createDatabaseClosedError("open a test transaction") : void 0;
1524
+ const closedError = getClosedError();
1525
+ if (closedError) throw closedError;
1526
+ return this.#openTestTransactions.open(options, getClosedError);
1277
1527
  }
1278
1528
  /**
1279
1529
  * Opens a test fixture: a transaction on a reserved connection, or a savepoint in the innermost open fixture, and runs `seed` in it.
@@ -1295,15 +1545,15 @@ var TransactionCoordinator = class {
1295
1545
  }
1296
1546
  /** Returns the error that rejects a test fixture created now, or `undefined` when it can open. */
1297
1547
  #getFixtureRefusal() {
1298
- if (!this.test) return /* @__PURE__ */ new Error("createTestFixture requires a database created with test: true");
1548
+ if (!this.test) return /* @__PURE__ */ new Error("openTestFixture requires a database created with test: true");
1299
1549
  if (this.#storage.getStore()) return /* @__PURE__ */ new Error("Cannot create a test fixture inside a transaction");
1300
1550
  if (this.#closed) return createDatabaseClosedError("create a test fixture");
1301
1551
  }
1302
1552
  /**
1303
- * Closes the test fixtures, including those still opening, and then the driver, which waits for the running transactions and sessions to end.
1553
+ * Closes the test fixtures, including those still opening, and the open test transactions on connections of their own, and then the driver, which waits for the running transactions and sessions to end.
1304
1554
  *
1305
- * A driver waits for every reserved connection, so closing it under an open fixture would wait forever.
1306
- * A failed closing of the driver suppresses the failures of closing the fixtures.
1555
+ * A driver waits for every reserved connection, so closing it under an open fixture or test transaction would wait forever.
1556
+ * A failed closing of the driver suppresses the failures of closing the fixtures and test transactions.
1307
1557
  * From the first call, statements, transactions, sessions, and fixtures started outside a transaction and a session reject, while the levels already running can still end; a repeated call returns the same closing.
1308
1558
  */
1309
1559
  close() {
@@ -1312,14 +1562,10 @@ var TransactionCoordinator = class {
1312
1562
  this.#closed ??= this.#close();
1313
1563
  return this.#closed;
1314
1564
  }
1315
- /** Closes the test fixtures and then the driver, see `close`. */
1565
+ /** Closes the test fixtures and the open test transactions on connections of their own, and then the driver, see `close`. */
1316
1566
  async #close() {
1317
1567
  let rejection;
1318
- try {
1319
- await this.#fixtures.close();
1320
- } catch (error) {
1321
- rejection = { error };
1322
- }
1568
+ for (const closing of await Promise.allSettled([this.#fixtures.close(), this.#openTestTransactions.close()])) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test fixture or transaction failed");
1323
1569
  try {
1324
1570
  await this.driver.close();
1325
1571
  } catch (error) {
@@ -1345,6 +1591,7 @@ var TransactionCoordinator = class {
1345
1591
  throw signal.reason;
1346
1592
  }
1347
1593
  const level = createLevel(connection, void 0, commits, options.isolation);
1594
+ if (!commits) level.turns = new TurnQueue();
1348
1595
  const outcome = await this.#runTransaction(level, callback, commits, options);
1349
1596
  await releaseConnection(connection, this.#onReleaseError);
1350
1597
  return settleTransaction(level, outcome);
@@ -1402,15 +1649,15 @@ var TransactionCoordinator = class {
1402
1649
  return outcome.result;
1403
1650
  }
1404
1651
  async #runNested(parent, callback, commits, options) {
1652
+ const start = async () => {
1653
+ const level = await startNestedLevel(parent, commits, options);
1654
+ if (!level.inTransaction) level.turns = new TurnQueue();
1655
+ return runStarted(this.#storage, level, callback, commits, options?.signal);
1656
+ };
1657
+ if (parent.turns) return runInTurn(parent, "start a nested transaction", start, options?.signal);
1405
1658
  const unavailable = createUnavailableError(parent, "start a nested transaction");
1406
1659
  if (unavailable) throw unavailable;
1407
- return this.#runStarted(await startNestedLevel(parent, commits, options), callback, commits, options?.signal);
1408
- }
1409
- /** Runs `callback` in a started level, returning its result or throwing its rejection. */
1410
- async #runStarted(level, callback, commits, signal) {
1411
- const { result, rejection } = await runLevel(this.#storage, level, callback, commits, signal);
1412
- if (rejection) throw rejection.error;
1413
- return result;
1660
+ return start();
1414
1661
  }
1415
1662
  };
1416
1663
  /**
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as TextValue, i as SqlStatement, n as Driver, o as createStatement, r as DriverResult, s as isDriver, t as Connection } from "./driver-CZCBkque.mjs";
2
- import { a as BaseDerivableType, c as Type, d as TypeOutput, f as TypeTransform, h as typeValues, i as AnyType, l as TypeInput, m as isType, n as AcceptedValues, o as DerivableType, p as defineType, r as AnyDerivableType, s as DerivableTypeWithParameters, t as AcceptedNull, u as TypeOptions } from "./type-CbrtxvDF.mjs";
3
- import { A as OneRow, F as ResultRows, I as ValueRows, M as ResultByRows, N as ResultDecodeError, S as param, T as UntypedValue, _ as isDatabase, a as SqlQuery, b as SqlValue, d as DatabaseOptions, f as IsolationLevel, g as createDatabase, h as TransactionOptions, i as InitialSqlQueryState, j as OneValueRows, k as NotFoundError, m as SessionOptions, n as DecodedFields, o as SqlQueryResult, p as PollLockOptions, r as FieldTypes, s as SqlQueryState, t as AnySqlQuery, u as Database, w as sql, x as identifier, y as SqlFragment } from "./query-Dxiy1HJD.mjs";
2
+ import { a as BaseDerivableType, c as Type, d as TypeOutput, f as TypeTransform, h as typeValues, i as AnyType, l as TypeInput, m as isType, n as AcceptedValues, o as DerivableType, p as defineType, r as AnyDerivableType, s as DerivableTypeWithParameters, t as AcceptedNull, u as TypeOptions } from "./type-DU5oJfh_.mjs";
3
+ import { A as OneRow, F as ResultRows, I as ValueRows, M as ResultByRows, N as ResultDecodeError, S as param, T as UntypedValue, _ as isDatabase, a as SqlQuery, b as SqlValue, d as DatabaseOptions, f as IsolationLevel, g as createDatabase, h as TransactionOptions, i as InitialSqlQueryState, j as OneValueRows, k as NotFoundError, m as SessionOptions, n as DecodedFields, o as SqlQueryResult, p as PollLockOptions, r as FieldTypes, s as SqlQueryState, t as AnySqlQuery, u as Database, w as sql, x as identifier, y as SqlFragment } from "./query-DpxSVheu.mjs";
4
4
  /**
5
5
  * Error of a statement that PostgreSQL rejected, such as for a violated constraint; the connection of the statement stays usable.
6
6
  *
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { o as defineType, s as isType } from "./bytea-DNZ_ftio.mjs";
2
2
  import { C as isDriver, S as createStatement, _ as sql, h as param, i as ResultDecodeError, m as identifier, r as NotFoundError } from "./query-FKBN7BMY.mjs";
3
3
  import { a as toDriverError, n as QueryError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
- import { a as TransactionConnectionError, i as isQueryError, o as isOutcomeUnknown, r as isDatabase, t as createDatabase } from "./database-D4ql2RZ0.mjs";
4
+ import { a as TransactionConnectionError, i as isQueryError, o as isOutcomeUnknown, r as isDatabase, t as createDatabase } from "./database-C3LR3pFV.mjs";
5
5
  export { ConnectionError, NotFoundError, QueryError, ResultDecodeError, TransactionConnectionError, createDatabase, createStatement, defineType, identifier, isDatabase, isDriver, isOutcomeUnknown, isQueryError, isType, param, sql, toDriverError };
@@ -1,5 +1,5 @@
1
1
  import { a as TextValue, i as SqlStatement, n as Driver, r as DriverResult } from "./driver-CZCBkque.mjs";
2
- import { c as Type, d as TypeOutput, i as AnyType } from "./type-CbrtxvDF.mjs";
2
+ import { c as Type, d as TypeOutput, i as AnyType } from "./type-DU5oJfh_.mjs";
3
3
  /**
4
4
  * How a query returns its rows: `many` as an array; `one` as one row, rejecting without it; `nullable` as one row or `null` without it; `optional` as one row or `undefined` without it.
5
5
  *
@@ -1,17 +1,39 @@
1
1
  import { n as Driver, t as Connection } from "./driver-CZCBkque.mjs";
2
- import { f as IsolationLevel, u as Database } from "./query-Dxiy1HJD.mjs";
2
+ import { f as IsolationLevel, u as Database } from "./query-DpxSVheu.mjs";
3
3
  /** Test fixture of `useTestFixture`, open while the tests of its `describe` block or test file run. */
4
4
  interface TestFixture<T = unknown> {
5
5
  /** Result of the seed of the fixture; reading it while the fixture is not open throws. */
6
6
  readonly data: T;
7
7
  }
8
- /** Test fixture opened by the transaction coordinator: an outer transaction of a database created for tests, which always rolls back when it closes. */
8
+ /** Open test fixture of `openTestFixture`: an outer transaction of a database created for tests, which always rolls back when it closes. */
9
9
  interface OpenedTestFixture<T = unknown> extends AsyncDisposable {
10
10
  /** Result of the seed of the fixture, such as the records it created; reading it after the fixture closes throws. */
11
11
  readonly data: T;
12
12
  /** Rolls back the fixture and the fixtures opened inside it; closing a closed fixture does nothing. */
13
13
  close(): Promise<void>;
14
14
  }
15
+ /** Open test transaction of `openTestTransaction`, which the callbacks of `run`, such as the requests of one browser test to a server, share until it closes. */
16
+ interface OpenedTestTransaction extends AsyncDisposable {
17
+ /**
18
+ * Runs `callback` in the test transaction, so that its queries run there.
19
+ *
20
+ * Each query of the callback outside a transaction runs as a nested transaction of its own, so that a failed query undoes only itself, as in production.
21
+ * A transaction of a callback runs as a nested transaction that the queries and transactions of the other callbacks wait for.
22
+ * It rejects without calling `callback` once the test transaction is closed, and inside a transaction.
23
+ */
24
+ run<T>(callback: () => Promise<T>): Promise<T>;
25
+ /** Rolls back the test transaction; the later queries of its callbacks fail, and closing it again returns the same closing. */
26
+ close(): Promise<void>;
27
+ }
28
+ /** Options of `openTestTransaction`. */
29
+ interface OpenTestTransactionOptions {
30
+ /**
31
+ * Isolation level of the test transaction, such as `serializable`, for code under test that starts a transaction at that level.
32
+ *
33
+ * Inside a test fixture, the test transaction runs at the level of the fixture: given another level, it rejects.
34
+ */
35
+ isolation?: IsolationLevel;
36
+ }
15
37
  /** Test fixture that is being opened, with a `close` that can be called before it opens. */
16
38
  interface PendingTestFixture<T = unknown> {
17
39
  readonly fixture: Promise<OpenedTestFixture<T>>;
@@ -25,6 +47,12 @@ interface PendingTestFixture<T = unknown> {
25
47
  interface TestHooks {
26
48
  before(hook: () => Promise<void>): unknown;
27
49
  after(hook: () => Promise<void>): unknown;
50
+ /**
51
+ * Hook of a test runner that runs a function once the current test finishes, also when it times out, such as `onTestFinished` of `bun:test`.
52
+ *
53
+ * With it, a test that `wrapTest` declares rolls back its test transaction as soon as the runner gives up on the test, as with the abort signal of `testTransaction`.
54
+ */
55
+ onTestFinished?(hook: () => void): unknown;
28
56
  }
29
57
  /** Options of `testTransaction`. */
30
58
  interface TestTransactionOptions {
@@ -44,7 +72,7 @@ interface TestTransactionOptions {
44
72
  */
45
73
  isolation?: IsolationLevel;
46
74
  }
47
- /** Options of `useTestFixture`. */
75
+ /** Options of `useTestFixture` and `openTestFixture`. */
48
76
  interface TestFixtureOptions {
49
77
  /**
50
78
  * Isolation level of the transaction of the fixture, such as `serializable`, at which the test transactions and fixtures nested in it run.
@@ -59,6 +87,7 @@ interface TestHelpers {
59
87
  * Runs `callback` in a transaction that always rolls back.
60
88
  *
61
89
  * Wrap the body of every test in it, so that every run of the test gets its own transaction.
90
+ * A query of the callback outside a transaction runs in a nested transaction of its own, so that a failed query undoes only itself, as in production.
62
91
  * Inside an existing transaction, it starts a nested transaction that always rolls back.
63
92
  */
64
93
  testTransaction<T>(callback: () => Promise<T>, options?: TestTransactionOptions): Promise<T>;
@@ -71,9 +100,34 @@ interface TestHelpers {
71
100
  * A fixture whose `seed` rejects rolls back and closes.
72
101
  */
73
102
  useTestFixture<T>(seed: () => Promise<T>, options?: TestFixtureOptions): TestFixture<T>;
103
+ /**
104
+ * Wraps the `test` function of `node:test` or `bun:test` so that every test it declares runs in a test transaction.
105
+ *
106
+ * The wrapped function keeps the modifiers of the original, such as `test.skip`, `test.only`, and `test.each(table)`, and its options, such as a timeout.
107
+ * Without `onTestFinished` in the hooks, the test transaction takes the abort signal of the test context, such as `t.signal` of `node:test`.
108
+ * Hooks of the test, such as `beforeEach`, run outside of its test transaction.
109
+ * A test function must return a promise or a value rather than take a `done` callback.
110
+ * With Vitest, run tests in test transactions with `aroundEach` instead, which also covers their hooks: the wrapper hides the parameters of a test function from the fixtures of `test.extend`.
111
+ */
112
+ wrapTest<T extends (...args: never[]) => unknown>(test: T): T;
74
113
  }
75
114
  /** Creates the test helpers of a database created with `test: true`, registering fixtures with the hooks of a test runner. */
76
115
  export declare function createTestHelpers(database: Database, hooks: TestHooks): TestHelpers;
116
+ /**
117
+ * Opens a test fixture of a database created with `test: true` without the hooks of a test runner, such as in a server that runs browser tests, and runs `seed` in it.
118
+ *
119
+ * It resolves once `seed` resolves, with the fixture whose `close` rolls it back; a fixture whose `seed` rejects rolls back and closes.
120
+ * Closing the database closes the fixture too.
121
+ * Fixtures nest as with `useTestFixture`: a new one opens inside the innermost open one, and test transactions run in the innermost one.
122
+ */
123
+ export declare function openTestFixture<T>(database: Database, seed: () => Promise<T>, options?: TestFixtureOptions): Promise<OpenedTestFixture<T>>;
124
+ /**
125
+ * Opens a test transaction of a database created with `test: true` that callbacks share through `run` until it closes, such as the requests of one browser test to a server.
126
+ *
127
+ * Inside an open test fixture, it opens as a nested transaction of the innermost one, and rolls back a test transaction or another open test transaction running there; without a fixture, it opens on a connection of its own.
128
+ * Closing the database closes it too.
129
+ */
130
+ export declare function openTestTransaction(database: Database, options?: OpenTestTransactionOptions): Promise<OpenedTestTransaction>;
77
131
  /** Options of `describeDriverContract`: how to create the driver under test. */
78
132
  interface DriverContractOptions {
79
133
  /** Creates a driver over a new client of the test database with `max` connections. */
@@ -126,4 +180,4 @@ export declare function waitForLock(observer: Pick<Connection, "execute">, pid:
126
180
  * It rejects when no such backend waits in time.
127
181
  */
128
182
  export declare function waitForLockedStatement(observer: Pick<Connection, "execute">, fragment: string, options?: LockWaitOptions): Promise<number>;
129
- export type { DriverContractOptions, DriverContractRunner, LockWaitOptions, OpenedTestFixture, PendingTestFixture, TestFixture, TestFixtureOptions, TestHelpers, TestHooks, TestTransactionOptions };
183
+ export type { DriverContractOptions, DriverContractRunner, LockWaitOptions, OpenTestTransactionOptions, OpenedTestFixture, OpenedTestTransaction, PendingTestFixture, TestFixture, TestFixtureOptions, TestHelpers, TestHooks, TestTransactionOptions };
package/dist/testing.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { r as boolean, t as bytea } from "./bytea-DNZ_ftio.mjs";
2
2
  import { S as createStatement, _ as sql, h as param, m as identifier } from "./query-FKBN7BMY.mjs";
3
3
  import { n as QueryError, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
- import { a as TransactionConnectionError, n as getCoordinator, s as pause, t as createDatabase } from "./database-D4ql2RZ0.mjs";
5
4
  import { t as array } from "./array-BLt8ZNR5.mjs";
5
+ import { a as TransactionConnectionError, n as getCoordinator, s as pause, t as createDatabase } from "./database-C3LR3pFV.mjs";
6
6
  import { _ as interval, a as bigint, b as timestamptz, c as numeric, f as inet, g as date, h as enumOf, m as jsonb, n as char, o as doublePrecision, r as text, s as integer, t as uuid, v as time, y as timestamp } from "./types-Dl2oicjV.mjs";
7
7
  import assert from "node:assert/strict";
8
8
  // src/test-helpers.ts
@@ -10,8 +10,32 @@ import assert from "node:assert/strict";
10
10
  function createTestHelpers(database, hooks) {
11
11
  const coordinator = getCoordinator(database);
12
12
  if (!coordinator.test) throw new Error("createTestHelpers requires a database created with test: true");
13
+ /**
14
+ * Runs a test function in a test transaction, which `onTestFinished`, or else the signal of the test context, aborts once the runner finishes the test.
15
+ *
16
+ * The arguments of a test of `test.each` are a row of its table rather than a context, so its `signal` belongs to the data of the test.
17
+ */
18
+ function runTestInTransaction(callback, parameterized) {
19
+ return function(...args) {
20
+ const run = async () => callback.apply(this, args);
21
+ if (!hooks.onTestFinished) {
22
+ const signal = parameterized ? void 0 : readTestSignal(args[0]);
23
+ return coordinator.testTransaction(run, { signal });
24
+ }
25
+ const controller = new AbortController();
26
+ let finished = false;
27
+ hooks.onTestFinished(() => {
28
+ finished = true;
29
+ controller.abort(/* @__PURE__ */ new Error("The test finished before its test transaction ended"));
30
+ });
31
+ return coordinator.testTransaction(run, { signal: controller.signal }).catch((error) => {
32
+ if (!finished) throw error;
33
+ });
34
+ };
35
+ }
13
36
  return {
14
37
  testTransaction: (callback, options) => coordinator.testTransaction(callback, options),
38
+ wrapTest: (test) => wrapTestFunction(test, void 0, false, runTestInTransaction),
15
39
  useTestFixture(seed, options) {
16
40
  let pending;
17
41
  let fixture;
@@ -38,6 +62,52 @@ function createTestHelpers(database, hooks) {
38
62
  }
39
63
  };
40
64
  }
65
+ /**
66
+ * Opens a test fixture of a database created with `test: true` without the hooks of a test runner, such as in a server that runs browser tests, and runs `seed` in it.
67
+ *
68
+ * It resolves once `seed` resolves, with the fixture whose `close` rolls it back; a fixture whose `seed` rejects rolls back and closes.
69
+ * Closing the database closes the fixture too.
70
+ * Fixtures nest as with `useTestFixture`: a new one opens inside the innermost open one, and test transactions run in the innermost one.
71
+ */
72
+ function openTestFixture(database, seed, options) {
73
+ return getCoordinator(database).createTestFixture(seed, options);
74
+ }
75
+ /**
76
+ * Opens a test transaction of a database created with `test: true` that callbacks share through `run` until it closes, such as the requests of one browser test to a server.
77
+ *
78
+ * Inside an open test fixture, it opens as a nested transaction of the innermost one, and rolls back a test transaction or another open test transaction running there; without a fixture, it opens on a connection of its own.
79
+ * Closing the database closes it too.
80
+ */
81
+ function openTestTransaction(database, options) {
82
+ return getCoordinator(database).openTestTransaction(options);
83
+ }
84
+ /** Reads the abort signal of a test from the first argument of its function, the test context of `node:test`. */
85
+ function readTestSignal(context) {
86
+ if (typeof context === "object" && context !== null && "signal" in context) {
87
+ const { signal } = context;
88
+ return signal instanceof AbortSignal ? signal : void 0;
89
+ }
90
+ }
91
+ /**
92
+ * Wraps a `test` function of a test runner and the functions that its properties and calls return, such as `test.skip` and `test.each(table)`, replacing every function argument with `wrapCallback` of it.
93
+ *
94
+ * Calls a modifier with the object it was read from, since modifiers of `bun:test` reject another `this`.
95
+ * Marks the callbacks of functions reached through `each` as parameterized.
96
+ */
97
+ function wrapTestFunction(target, owner, parameterized, wrapCallback) {
98
+ return new Proxy(target, {
99
+ apply(callee, self, args) {
100
+ const wrappedArgs = [];
101
+ for (const arg of args) wrappedArgs.push(typeof arg === "function" ? wrapCallback(arg, parameterized) : arg);
102
+ const result = Reflect.apply(callee, owner ?? self, wrappedArgs);
103
+ return typeof result === "function" ? wrapTestFunction(result, void 0, parameterized, wrapCallback) : result;
104
+ },
105
+ get(object, key) {
106
+ const value = Reflect.get(object, key);
107
+ return typeof value === "function" ? wrapTestFunction(value, object, parameterized || key === "each", wrapCallback) : value;
108
+ }
109
+ });
110
+ }
41
111
  // src/driver-contract.ts
42
112
  /** Creates helpers that run raw statements on `target`: a database, a driver, or a reserved connection. */
43
113
  function createStatementRunners(target) {
@@ -842,4 +912,4 @@ async function poll(timeout, message, check) {
842
912
  clearTimeout(timer);
843
913
  }
844
914
  }
845
- export { createTestHelpers, describeDriverContract, getBackendPid, waitForLock, waitForLockedStatement };
915
+ export { createTestHelpers, describeDriverContract, getBackendPid, openTestFixture, openTestTransaction, waitForLock, waitForLockedStatement };
@@ -61,7 +61,8 @@ interface BaseDerivableType<Output, Input, Parameters extends unknown[] | undefi
61
61
  /**
62
62
  * Narrows the values of this type to `T` in TypeScript, such as to a union of strings that a `CHECK` constraint allows, or to the shape of JSON values.
63
63
  *
64
- * The values are not validated at runtime. `T` must be among the values that this type both returns and accepts, and `nullable` keeps `null` apart from it.
64
+ * The values are not validated at runtime.
65
+ * `T` must be among the values that this type both returns and accepts, and `nullable` keeps `null` apart from it.
65
66
  */
66
67
  of<T extends AcceptedValues<Exclude<Output, null>, Exclude<Input, null>>>(): DerivableType<T | AcceptedNull<Input>, T | AcceptedNull<Input>, Parameters>;
67
68
  }
package/dist/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as BaseDerivableType, c as Type, o as DerivableType, s as DerivableTypeWithParameters } from "./type-CbrtxvDF.mjs";
1
+ import { a as BaseDerivableType, c as Type, o as DerivableType, s as DerivableTypeWithParameters } from "./type-DU5oJfh_.mjs";
2
2
  /**
3
3
  * Creates a type of one-dimensional arrays of `element` values.
4
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-db",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "PostgreSQL database layer with ambient transactions, typed SQL queries, and test transactions",
5
5
  "keywords": [
6
6
  "database",