turbine-orm 0.81.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, the state one open transaction shares
|
|
3
|
+
*
|
|
4
|
+
* Everything a `$transaction` hands out (table accessors, raw SQL, nested
|
|
5
|
+
* `$transaction` blocks, streams) runs on ONE pooled connection. This module
|
|
6
|
+
* owns what those share:
|
|
7
|
+
*
|
|
8
|
+
* - whether the transaction is still open. Once it settles, a `tx` that was
|
|
9
|
+
* captured somewhere (an un-awaited promise, an `async` `forEach`, a timer)
|
|
10
|
+
* must not send anything: its connection is back in the pool and may
|
|
11
|
+
* already be running another request's transaction.
|
|
12
|
+
* - the first statement failure. PostgreSQL aborts a transaction at its first
|
|
13
|
+
* error and answers the COMMIT that follows with `ROLLBACK`; that failure is
|
|
14
|
+
* what the caller needs to see as the cause.
|
|
15
|
+
* - savepoint blocks. Savepoints are a stack on the connection, so two blocks
|
|
16
|
+
* open at once would share it and a rollback in one would undo the other.
|
|
17
|
+
* Blocks that share a parent therefore run one at a time, and a statement
|
|
18
|
+
* at the parent's level waits for a block opened before it.
|
|
19
|
+
* - what is still running: statements on the wire (what a cancel request
|
|
20
|
+
* stops, and what a ROLLBACK must wait for) and work started but not
|
|
21
|
+
* settled (which, when the callback returns, means a missing `await`).
|
|
22
|
+
* - whether the server's transaction is already aborted. On PostgreSQL a
|
|
23
|
+
* failed statement leaves the transaction ignoring every statement until a
|
|
24
|
+
* rollback to a savepoint set before it, so a statement sent then is
|
|
25
|
+
* refused here, with the failure that caused it, instead of coming back as
|
|
26
|
+
* an untyped SQLSTATE 25P02 that no retry would recognise.
|
|
27
|
+
* - which `tx` a statement came through. A nested block's callback gets its
|
|
28
|
+
* own handle, and a statement through an OUTER handle from inside the
|
|
29
|
+
* block is refused: it would run in the block's savepoint and be undone
|
|
30
|
+
* with it.
|
|
31
|
+
* - transaction-control SQL on the raw paths, which is refused (see
|
|
32
|
+
* {@link TransactionState.runRaw}).
|
|
33
|
+
*
|
|
34
|
+
* A leaf (errors.ts, the zero-import transaction-control.ts, and type-only
|
|
35
|
+
* imports), so query/ can reach it without an import edge back to client.ts: a QueryInterface inside a transaction holds
|
|
36
|
+
* only its pool, and that pool carries this state under
|
|
37
|
+
* {@link TRANSACTION_STATE}.
|
|
38
|
+
*/
|
|
39
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
40
|
+
import { DeadlockError, LockNotAvailableError, SerializationFailureError, TransactionRolledBackError, ValidationError, wrapPgError, } from './errors.js';
|
|
41
|
+
import { findTransactionControl, mayHoldTransactionControl, } from './transaction-control.js';
|
|
42
|
+
/** The key a transaction-scoped pool carries its {@link TransactionState} under. */
|
|
43
|
+
export const TRANSACTION_STATE = Symbol.for('turbine-orm.transactionState');
|
|
44
|
+
/** The savepoint block the current async context is running inside, if any. */
|
|
45
|
+
const currentFrame = new AsyncLocalStorage();
|
|
46
|
+
export class TransactionState {
|
|
47
|
+
client;
|
|
48
|
+
dialect;
|
|
49
|
+
/** True when multi-statement writes run inside their own savepoint (see {@link withSavepoint}). */
|
|
50
|
+
savepointWrites;
|
|
51
|
+
endedAs;
|
|
52
|
+
failure;
|
|
53
|
+
/** A statement cut short that may still have run: the transaction must roll back (see {@link abort}). */
|
|
54
|
+
aborted;
|
|
55
|
+
/**
|
|
56
|
+
* The server's own error that left the PostgreSQL transaction in its aborted
|
|
57
|
+
* state, until a rollback to a savepoint set before it. Only recorded where
|
|
58
|
+
* a failed statement aborts the transaction (see {@link abortsOnError}) and
|
|
59
|
+
* only for a failure the SERVER reported: a client-side error (a value the
|
|
60
|
+
* driver could not serialize) never reached the transaction and changes
|
|
61
|
+
* nothing about what it will accept.
|
|
62
|
+
*/
|
|
63
|
+
dead;
|
|
64
|
+
/** True where one failed statement makes the server ignore the rest of the transaction (PostgreSQL). */
|
|
65
|
+
abortsOnError;
|
|
66
|
+
/** The lexical rules for finding transaction-control SQL in raw text, or undefined where none apply (PowDB). */
|
|
67
|
+
controlSyntax;
|
|
68
|
+
tick = 0;
|
|
69
|
+
savepointCount = 0;
|
|
70
|
+
root;
|
|
71
|
+
/** Frames whose SAVEPOINT is set and not yet released or rolled back, outermost first. */
|
|
72
|
+
openFrames = [];
|
|
73
|
+
/** Statements sent on the connection that have not answered yet. */
|
|
74
|
+
onWire = 0;
|
|
75
|
+
/** Statements and savepoint blocks started through the transaction that have not settled. */
|
|
76
|
+
started = 0;
|
|
77
|
+
idleWaiters = [];
|
|
78
|
+
/** Settles at the first {@link end}, releasing anything still queued behind a block. */
|
|
79
|
+
endedSignal;
|
|
80
|
+
signalEnded;
|
|
81
|
+
api;
|
|
82
|
+
constructor(client, dialect, options = {}) {
|
|
83
|
+
this.client = client;
|
|
84
|
+
this.dialect = dialect;
|
|
85
|
+
this.root = {
|
|
86
|
+
state: this,
|
|
87
|
+
parent: undefined,
|
|
88
|
+
handle: true,
|
|
89
|
+
tail: Promise.resolve(),
|
|
90
|
+
children: 0,
|
|
91
|
+
openedAt: 0,
|
|
92
|
+
open: true,
|
|
93
|
+
};
|
|
94
|
+
this.savepointWrites = options.userTransaction === true && dialectHasSavepoints(dialect);
|
|
95
|
+
this.api = options.api ?? '$transaction';
|
|
96
|
+
this.abortsOnError = dialect.capabilities.abortsTransactionOnError;
|
|
97
|
+
this.controlSyntax = dialect.capabilities.statementSyntax ?? undefined;
|
|
98
|
+
this.endedSignal = new Promise((resolve) => {
|
|
99
|
+
this.signalEnded = resolve;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Whether the transaction has settled (see {@link end}). */
|
|
103
|
+
get ended() {
|
|
104
|
+
return this.endedAs !== undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The first statement failure not undone by a savepoint rollback, if any.
|
|
108
|
+
* Where the server aborted the transaction, the failure that did it: that is
|
|
109
|
+
* the one a retry decision follows.
|
|
110
|
+
*/
|
|
111
|
+
get firstFailure() {
|
|
112
|
+
return this.dead?.error ?? this.failure?.error;
|
|
113
|
+
}
|
|
114
|
+
/** The scope of the transaction itself: what the `tx` handed to the callback is bound to. */
|
|
115
|
+
get rootScope() {
|
|
116
|
+
return this.root;
|
|
117
|
+
}
|
|
118
|
+
/** Why the transaction must roll back instead of committing (see {@link abort}), if it must. */
|
|
119
|
+
get abortedWith() {
|
|
120
|
+
return this.aborted?.error;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether a statement or savepoint block started through the transaction is
|
|
124
|
+
* still running. True when the callback settles means work it started was
|
|
125
|
+
* not awaited.
|
|
126
|
+
*/
|
|
127
|
+
get busy() {
|
|
128
|
+
return this.started > 0;
|
|
129
|
+
}
|
|
130
|
+
/** Whether a statement is on the wire right now (what a cancel request would stop). */
|
|
131
|
+
get running() {
|
|
132
|
+
return this.onWire > 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Run the transaction's callback. Statements and blocks it starts are then
|
|
136
|
+
* known to sit at the transaction's top level, which is what lets a block
|
|
137
|
+
* whose async context was lost be told apart from a sibling.
|
|
138
|
+
*/
|
|
139
|
+
enter(fn) {
|
|
140
|
+
return currentFrame.run(this.root, fn);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve `true` once no statement is on the wire, or `false` after `ms`.
|
|
144
|
+
* Called after {@link end}, when nothing new can be sent.
|
|
145
|
+
*/
|
|
146
|
+
whenIdle(ms) {
|
|
147
|
+
if (this.onWire === 0)
|
|
148
|
+
return Promise.resolve(true);
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
151
|
+
this.idleWaiters.push(() => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Mark the transaction settled. Called as soon as the callback settles,
|
|
159
|
+
* BEFORE the COMMIT or ROLLBACK, and again with the outcome; from the first
|
|
160
|
+
* call on every statement issued through the transaction is refused.
|
|
161
|
+
*/
|
|
162
|
+
end(how) {
|
|
163
|
+
if (this.endedAs === undefined)
|
|
164
|
+
this.signalEnded();
|
|
165
|
+
this.endedAs = how;
|
|
166
|
+
}
|
|
167
|
+
/** Throw the closed-transaction error when the transaction has settled. */
|
|
168
|
+
assertOpen() {
|
|
169
|
+
if (this.endedAs !== undefined)
|
|
170
|
+
throw closedTransactionError(this.endedAs, this.api);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Run a statement issued THROUGH the transaction (an accessor, raw SQL, a
|
|
174
|
+
* stream): refused once the transaction has settled, and its failure recorded
|
|
175
|
+
* as a candidate cause for a later ROLLBACK-at-COMMIT.
|
|
176
|
+
*
|
|
177
|
+
* `scope` is the handle the statement came through (the transaction's own
|
|
178
|
+
* `tx`, or the one a nested block's callback received). A statement through
|
|
179
|
+
* an OUTER handle from inside an open block is refused: it would run in the
|
|
180
|
+
* block's savepoint and be undone with it.
|
|
181
|
+
*
|
|
182
|
+
* A statement issued while a savepoint block started before it at the same
|
|
183
|
+
* level is still open waits for that block. Sent at once it would run inside
|
|
184
|
+
* the block's savepoint and be undone if the block rolled back, after it had
|
|
185
|
+
* already reported success.
|
|
186
|
+
*
|
|
187
|
+
* On PostgreSQL a statement sent after one failed is not sent at all: the
|
|
188
|
+
* server would ignore it (SQLSTATE 25P02) and the failure that caused that
|
|
189
|
+
* would be lost. `recovery` marks the one statement that is sent regardless,
|
|
190
|
+
* a rollback to a savepoint the caller set by hand (see {@link runRaw}).
|
|
191
|
+
*/
|
|
192
|
+
async run(statement, scope = this.root, recovery = false) {
|
|
193
|
+
this.assertOpen();
|
|
194
|
+
this.assertNotFromInsideBlock(scope, 'A statement');
|
|
195
|
+
this.started++;
|
|
196
|
+
try {
|
|
197
|
+
const inside = currentFrame.getStore();
|
|
198
|
+
if (inside?.state === this && inside.children > 0) {
|
|
199
|
+
await Promise.race([inside.tail, this.endedSignal]);
|
|
200
|
+
this.assertOpen();
|
|
201
|
+
}
|
|
202
|
+
if (!recovery)
|
|
203
|
+
this.assertNotAborted();
|
|
204
|
+
const result = await this.send(statement);
|
|
205
|
+
if (recovery)
|
|
206
|
+
this.clearAbort();
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
this.started--;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* {@link run} for a statement the caller wrote (`tx.raw`, `tx.rawUnsafe`,
|
|
215
|
+
* `tx.sql`, the connection `$connection()` and `transaction()` hand out).
|
|
216
|
+
*
|
|
217
|
+
* Transaction-control SQL is refused with E003 before anything is sent.
|
|
218
|
+
* `COMMIT`, `ROLLBACK` or `BEGIN` ends or reshapes the transaction behind the
|
|
219
|
+
* state that tracks it, so later writes would autocommit and the callback's
|
|
220
|
+
* own throw would keep them. A savepoint of the caller's own sits on the
|
|
221
|
+
* connection's stack beside the ones Turbine sets, so those are refused too
|
|
222
|
+
* inside `$transaction`, where `tx.$transaction(...)` is the savepoint.
|
|
223
|
+
*
|
|
224
|
+
* The raw `transaction()` API has no such block, so `SAVEPOINT`, `RELEASE`
|
|
225
|
+
* and `ROLLBACK TO` stay available there: the only way to recover from a
|
|
226
|
+
* failed statement without losing the transaction. A `ROLLBACK TO` is also
|
|
227
|
+
* the one statement sent while the transaction is aborted, and when it
|
|
228
|
+
* succeeds the abort is over.
|
|
229
|
+
*/
|
|
230
|
+
async runRaw(sql, statement, scope = this.root) {
|
|
231
|
+
return this.run(statement, scope, this.screenRaw(sql));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Check raw SQL for transaction control without sending it: throws E003 for
|
|
235
|
+
* a statement the caller may not send, and answers whether the text rolls
|
|
236
|
+
* back to a savepoint (the one control statement that is allowed to run in an
|
|
237
|
+
* aborted transaction).
|
|
238
|
+
*/
|
|
239
|
+
screenRaw(sql) {
|
|
240
|
+
if (this.controlSyntax === undefined || typeof sql !== 'string' || !mayHoldTransactionControl(sql))
|
|
241
|
+
return false;
|
|
242
|
+
const found = findTransactionControl(sql, this.controlSyntax);
|
|
243
|
+
if (found.length === 0)
|
|
244
|
+
return false;
|
|
245
|
+
const refused = found.find((s) => !(this.api === 'transaction' && SAVEPOINT_KINDS.has(s.kind)));
|
|
246
|
+
if (refused !== undefined)
|
|
247
|
+
throw transactionControlError(refused, this.api);
|
|
248
|
+
return found.every((s) => s.kind === 'rollback-to');
|
|
249
|
+
}
|
|
250
|
+
/** Put one statement on the wire, counted for {@link whenIdle} and recorded on failure. */
|
|
251
|
+
async send(statement) {
|
|
252
|
+
this.onWire++;
|
|
253
|
+
try {
|
|
254
|
+
return await statement();
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
this.noteFailure(err);
|
|
258
|
+
if (this.abortsOnError && reportedByServer(err))
|
|
259
|
+
this.noteDead(err);
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
this.onWire--;
|
|
264
|
+
if (this.onWire === 0 && this.idleWaiters.length > 0) {
|
|
265
|
+
const waiters = this.idleWaiters;
|
|
266
|
+
this.idleWaiters = [];
|
|
267
|
+
for (const wake of waiters)
|
|
268
|
+
wake();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Record `err` as the first failure, unless one is already recorded. */
|
|
273
|
+
noteFailure(err) {
|
|
274
|
+
if (this.failure === undefined)
|
|
275
|
+
this.failure = { error: err, at: ++this.tick };
|
|
276
|
+
}
|
|
277
|
+
/** Record `err` as what aborted the server's transaction, unless something already did. */
|
|
278
|
+
noteDead(err) {
|
|
279
|
+
if (this.dead === undefined)
|
|
280
|
+
this.dead = { error: err, at: ++this.tick };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Mark the transaction as one that must not commit, because of `err`: a
|
|
284
|
+
* statement timed out and was cancelled, and whether it ran cannot be
|
|
285
|
+
* relied on. The server aborts the transaction when the cancel lands, but a
|
|
286
|
+
* cancel that arrives after the statement finished does nothing, and a
|
|
287
|
+
* COMMIT would then keep it. A rollback to a savepoint set before the
|
|
288
|
+
* statement clears the mark, as it clears the failure.
|
|
289
|
+
*
|
|
290
|
+
* Statements after it are refused like those after any failed statement: the
|
|
291
|
+
* transaction cannot commit, so there is nothing to send them for.
|
|
292
|
+
*/
|
|
293
|
+
abort(err) {
|
|
294
|
+
this.noteFailure(err);
|
|
295
|
+
if (this.aborted === undefined)
|
|
296
|
+
this.aborted = { error: err, at: ++this.tick };
|
|
297
|
+
// The cancel's own error (57014) may already be recorded as the cause; this one says more.
|
|
298
|
+
if (this.abortsOnError)
|
|
299
|
+
this.dead = { error: err, at: this.dead?.at ?? ++this.tick };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Refuse a statement the PostgreSQL transaction would ignore, with the
|
|
303
|
+
* failure that aborted it. The same error a COMMIT in this state gets
|
|
304
|
+
* ({@link rolledBackError}), so a deadlock, serialization conflict or lock
|
|
305
|
+
* wait that was caught and followed by one more statement is still the
|
|
306
|
+
* retryable class `$retry` looks for.
|
|
307
|
+
*/
|
|
308
|
+
assertNotAborted() {
|
|
309
|
+
if (this.dead !== undefined)
|
|
310
|
+
throw rolledBackError(this.dead.error, 'statement', this.api);
|
|
311
|
+
}
|
|
312
|
+
/** The caller rolled back to a savepoint of their own and the server accepted it: the abort is over. */
|
|
313
|
+
clearAbort() {
|
|
314
|
+
if (this.dead === undefined)
|
|
315
|
+
return;
|
|
316
|
+
if (this.failure !== undefined && this.failure.error === this.dead.error)
|
|
317
|
+
this.failure = undefined;
|
|
318
|
+
this.dead = undefined;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Refuse `what` issued through `scope` when the async context it was issued
|
|
322
|
+
* from is inside a block opened through a handle nested under `scope`.
|
|
323
|
+
*
|
|
324
|
+
* `tx.$transaction(async (inner) => { await tx.users.update(...) })` sends
|
|
325
|
+
* the update through the OUTER handle, but the connection is inside the
|
|
326
|
+
* block's savepoint, so a throw from the block would undo an update the
|
|
327
|
+
* caller placed outside it. The block's callback has its own handle for work
|
|
328
|
+
* inside it. A context this state cannot see (a timer created before the
|
|
329
|
+
* transaction, a callback API that drops the async context) is not refused:
|
|
330
|
+
* it cannot be told apart from a statement at the right level.
|
|
331
|
+
*/
|
|
332
|
+
assertNotFromInsideBlock(scope, what) {
|
|
333
|
+
const here = currentFrame.getStore();
|
|
334
|
+
if (here === undefined || here.state !== this || here === scope)
|
|
335
|
+
return;
|
|
336
|
+
let inner;
|
|
337
|
+
for (let frame = here; frame !== undefined; frame = frame.parent) {
|
|
338
|
+
if (frame === scope) {
|
|
339
|
+
if (inner !== undefined)
|
|
340
|
+
throw outerHandleError(what);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// A write's own savepoint belongs to the handle that issued the write.
|
|
344
|
+
if (frame.handle && frame.open && inner === undefined)
|
|
345
|
+
inner = frame;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Run `block` inside a savepoint: released when it resolves, rolled back to
|
|
350
|
+
* when it rejects, so the rest of the transaction is unaffected either way.
|
|
351
|
+
*
|
|
352
|
+
* Blocks that share a parent run one at a time. A savepoint is a position on
|
|
353
|
+
* the connection's one stack, so a block that opened second sits inside the
|
|
354
|
+
* first: rolling the first back would discard the second even after it had
|
|
355
|
+
* released and reported success. A block started from INSIDE another block's
|
|
356
|
+
* callback is nested under it and does not wait.
|
|
357
|
+
*
|
|
358
|
+
* A block whose callback swallowed a statement failure resolves normally,
|
|
359
|
+
* but PostgreSQL refuses to RELEASE a savepoint inside an aborted
|
|
360
|
+
* transaction. That case rolls the savepoint back and rejects with the same
|
|
361
|
+
* rolled-back error a COMMIT gets, carrying the failure as `cause`, so the
|
|
362
|
+
* outer transaction stays usable.
|
|
363
|
+
*
|
|
364
|
+
* This is the savepoint of ONE multi-statement write, which belongs to the
|
|
365
|
+
* handle that issued it. {@link withBlock} is a block the caller opened.
|
|
366
|
+
*/
|
|
367
|
+
withSavepoint(block) {
|
|
368
|
+
return this.openSavepoint(block, false, this.root);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Run `block` as a nested `tx.$transaction(...)`: a savepoint (see
|
|
372
|
+
* {@link withSavepoint}) whose callback receives a handle of its own, bound to
|
|
373
|
+
* the scope it is given. `caller` is the handle `$transaction` was called on.
|
|
374
|
+
*/
|
|
375
|
+
withBlock(block, caller) {
|
|
376
|
+
return this.openSavepoint(block, true, caller);
|
|
377
|
+
}
|
|
378
|
+
async openSavepoint(block, handle, caller) {
|
|
379
|
+
this.assertOpen();
|
|
380
|
+
if (handle)
|
|
381
|
+
this.assertNotFromInsideBlock(caller, 'A nested `$transaction`');
|
|
382
|
+
const inside = currentFrame.getStore();
|
|
383
|
+
const known = inside?.state === this;
|
|
384
|
+
// No frame of this transaction means the caller's async context was lost
|
|
385
|
+
// (a callback API that does not carry it, such as a timer created before
|
|
386
|
+
// the transaction). Such a block cannot be told apart from a sibling, and
|
|
387
|
+
// waiting as one could mean waiting on its own ancestor forever, so it
|
|
388
|
+
// nests under the innermost open savepoint: where the connection puts it.
|
|
389
|
+
const parent = known ? inside : (this.openFrames[this.openFrames.length - 1] ?? this.root);
|
|
390
|
+
const previous = parent.tail;
|
|
391
|
+
let releaseSlot;
|
|
392
|
+
const mine = new Promise((resolve) => {
|
|
393
|
+
releaseSlot = resolve;
|
|
394
|
+
});
|
|
395
|
+
parent.tail = known ? mine : Promise.all([previous, mine]).then(() => undefined);
|
|
396
|
+
parent.children++;
|
|
397
|
+
this.started++;
|
|
398
|
+
let frame;
|
|
399
|
+
try {
|
|
400
|
+
if (known) {
|
|
401
|
+
await Promise.race([previous, this.endedSignal]);
|
|
402
|
+
this.assertOpen();
|
|
403
|
+
}
|
|
404
|
+
this.assertNotAborted();
|
|
405
|
+
const name = `sp_${++this.savepointCount}`;
|
|
406
|
+
await this.control(this.dialect.savepointStatement(name));
|
|
407
|
+
const opened = {
|
|
408
|
+
state: this,
|
|
409
|
+
parent,
|
|
410
|
+
handle,
|
|
411
|
+
tail: Promise.resolve(),
|
|
412
|
+
children: 0,
|
|
413
|
+
openedAt: ++this.tick,
|
|
414
|
+
open: true,
|
|
415
|
+
};
|
|
416
|
+
frame = opened;
|
|
417
|
+
this.openFrames.push(opened);
|
|
418
|
+
let result;
|
|
419
|
+
try {
|
|
420
|
+
result = await currentFrame.run(opened, () => block(opened));
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
await this.rollbackTo(name, opened);
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
// A failure the block caught (or a statement its timeout cut short) leaves
|
|
427
|
+
// the server's savepoint unusable, and PostgreSQL would refuse the RELEASE
|
|
428
|
+
// anyway, so the block rolls back without asking.
|
|
429
|
+
const doomed = this.doomedSince(opened);
|
|
430
|
+
if (doomed !== undefined) {
|
|
431
|
+
await this.rollbackTo(name, opened);
|
|
432
|
+
throw rolledBackError(doomed.error, 'savepoint');
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
await this.control(this.dialect.releaseSavepointStatement(name));
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
const cause = this.failureSince(opened) ?? err;
|
|
439
|
+
await this.rollbackTo(name, opened);
|
|
440
|
+
throw rolledBackError(cause, 'savepoint');
|
|
441
|
+
}
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
if (frame !== undefined) {
|
|
446
|
+
frame.open = false;
|
|
447
|
+
const at = this.openFrames.lastIndexOf(frame);
|
|
448
|
+
if (at !== -1)
|
|
449
|
+
this.openFrames.splice(at, 1);
|
|
450
|
+
}
|
|
451
|
+
parent.children--;
|
|
452
|
+
this.started--;
|
|
453
|
+
releaseSlot();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/** The recorded failure, when it happened after `frame`'s savepoint was set. */
|
|
457
|
+
failureSince(frame) {
|
|
458
|
+
return this.failure !== undefined && this.failure.at > frame.openedAt ? this.failure.error : undefined;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* What stops `frame` from being released: a statement cut short by its
|
|
462
|
+
* timeout, or (on PostgreSQL) a failure the server aborted the savepoint for,
|
|
463
|
+
* either one after the savepoint was set.
|
|
464
|
+
*/
|
|
465
|
+
doomedSince(frame) {
|
|
466
|
+
if (this.aborted !== undefined && this.aborted.at > frame.openedAt)
|
|
467
|
+
return this.aborted;
|
|
468
|
+
if (this.dead !== undefined && this.dead.at > frame.openedAt)
|
|
469
|
+
return this.dead;
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
async rollbackTo(name, frame) {
|
|
473
|
+
try {
|
|
474
|
+
await this.control(this.dialect.rollbackToSavepointStatement(name));
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// The transaction cannot take the rollback (it settled, or the connection
|
|
478
|
+
// is gone). The error that brought the block down is the one to report.
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (this.failureSince(frame) !== undefined)
|
|
482
|
+
this.failure = undefined;
|
|
483
|
+
if (this.aborted !== undefined && this.aborted.at > frame.openedAt)
|
|
484
|
+
this.aborted = undefined;
|
|
485
|
+
if (this.dead !== undefined && this.dead.at > frame.openedAt)
|
|
486
|
+
this.dead = undefined;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* A savepoint statement: refused once settled, failures recorded and typed.
|
|
490
|
+
* Typed INSIDE the statement, so the failure the state records is the Turbine
|
|
491
|
+
* error and not the driver's.
|
|
492
|
+
*/
|
|
493
|
+
async control(sql) {
|
|
494
|
+
this.assertOpen();
|
|
495
|
+
await this.send(async () => {
|
|
496
|
+
try {
|
|
497
|
+
await this.client.query(sql);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
throw wrapPgError(err);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** The state carried by a transaction-scoped pool, or undefined for any other pool. */
|
|
506
|
+
export function transactionStateOf(pool) {
|
|
507
|
+
if (pool === null || typeof pool !== 'object')
|
|
508
|
+
return undefined;
|
|
509
|
+
const state = pool[TRANSACTION_STATE];
|
|
510
|
+
return state instanceof TransactionState ? state : undefined;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Run a multi-statement operation as one unit inside the caller's transaction.
|
|
514
|
+
*
|
|
515
|
+
* When `pool` belongs to a transaction the caller opened (`$transaction`,
|
|
516
|
+
* `$withSession`), `operation` runs inside its own savepoint: if it throws
|
|
517
|
+
* part way through, the statements it already ran are rolled back and the
|
|
518
|
+
* caller's transaction carries on as if the operation had never started. On
|
|
519
|
+
* any other pool (no transaction, or Turbine's own implicit transaction, which
|
|
520
|
+
* is atomic already) it simply runs `operation`.
|
|
521
|
+
*
|
|
522
|
+
* Every user-facing write that issues more than one statement inside a
|
|
523
|
+
* transaction already goes through this from `TransactionClient`'s accessors
|
|
524
|
+
* (nested creates and updates, lookup-first upserts); call it from any new
|
|
525
|
+
* multi-statement operation that can be reached some other way.
|
|
526
|
+
*/
|
|
527
|
+
export function withSavepoint(pool, operation) {
|
|
528
|
+
const state = transactionStateOf(pool);
|
|
529
|
+
return state?.savepointWrites ? state.withSavepoint(operation) : operation();
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Send COMMIT and refuse to report success when the server rolled back instead.
|
|
533
|
+
*
|
|
534
|
+
* PostgreSQL answers COMMIT in an aborted transaction with the command tag
|
|
535
|
+
* `ROLLBACK` and no error. Other engines never set `command` on a COMMIT
|
|
536
|
+
* result, so the check is inert for them.
|
|
537
|
+
*/
|
|
538
|
+
export async function commitTransaction(client, commitSql, firstFailure, api = '$transaction') {
|
|
539
|
+
let result;
|
|
540
|
+
try {
|
|
541
|
+
result = await client.query(commitSql);
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
throw wrapPgError(err);
|
|
545
|
+
}
|
|
546
|
+
if (commitWasRolledBack(result))
|
|
547
|
+
throw rolledBackError(firstFailure, 'transaction', api);
|
|
548
|
+
}
|
|
549
|
+
/** Whether a COMMIT result reports that the server rolled the transaction back. */
|
|
550
|
+
export function commitWasRolledBack(result) {
|
|
551
|
+
return result?.command === 'ROLLBACK';
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The error for work that was rolled back although the caller expected it to
|
|
555
|
+
* commit (a transaction) or be kept (a savepoint block).
|
|
556
|
+
*
|
|
557
|
+
* A retryable first failure (deadlock, serialization conflict, lock not
|
|
558
|
+
* available) keeps its class, so `$retry` and `isRetryable` checks retry the
|
|
559
|
+
* whole transaction, which is the documented response to all three. Anything
|
|
560
|
+
* else is a {@link TransactionRolledBackError} (E019) with the first failure as
|
|
561
|
+
* `cause`: nothing was committed, and the server did the rolling back.
|
|
562
|
+
*/
|
|
563
|
+
export function rolledBackError(firstFailure, scope, api = '$transaction') {
|
|
564
|
+
const head = scope === 'transaction'
|
|
565
|
+
? 'The transaction was rolled back, not committed: a statement inside it failed, and PostgreSQL ' +
|
|
566
|
+
'aborts a transaction at its first error, so COMMIT ended it as a ROLLBACK and none of its writes were saved.'
|
|
567
|
+
: scope === 'savepoint'
|
|
568
|
+
? 'The nested `$transaction` block was rolled back, not released: a statement inside it failed, and ' +
|
|
569
|
+
'PostgreSQL aborts at the first error, so none of the writes inside the block were kept. The outer ' +
|
|
570
|
+
'transaction is usable again.'
|
|
571
|
+
: 'This statement was not sent: an earlier statement in the transaction failed, and PostgreSQL ignores ' +
|
|
572
|
+
'every statement after a failure until the transaction, or the savepoint it ran in, is rolled back. ' +
|
|
573
|
+
'Nothing in the transaction can be saved now.';
|
|
574
|
+
const message = `${head}${firstFailure !== undefined ? ' The statement that failed first is attached as `cause`.' : ''} ` +
|
|
575
|
+
'Catching an error inside the callback does not undo the abort. To recover from an expected failure and ' +
|
|
576
|
+
'keep the rest of the work, ' +
|
|
577
|
+
(api === '$transaction'
|
|
578
|
+
? 'run the statement that can fail in its own nested `tx.$transaction(...)`, which rolls back only that savepoint.'
|
|
579
|
+
: 'send `SAVEPOINT s` before the statement that can fail and `ROLLBACK TO SAVEPOINT s` when it does, which ' +
|
|
580
|
+
'undoes only that statement.');
|
|
581
|
+
// A failure that came through a raw driver connection (`transaction()`, `$connection()`,
|
|
582
|
+
// `tx.sql`) is the driver's error, not yet a typed one; its class decides whether to retry.
|
|
583
|
+
const typed = wrapPgError(firstFailure);
|
|
584
|
+
if (typed instanceof LockNotAvailableError)
|
|
585
|
+
return new LockNotAvailableError({ message, cause: firstFailure });
|
|
586
|
+
if (typed instanceof SerializationFailureError) {
|
|
587
|
+
return new SerializationFailureError({ message, cause: firstFailure });
|
|
588
|
+
}
|
|
589
|
+
if (typed instanceof DeadlockError)
|
|
590
|
+
return new DeadlockError({ message, cause: firstFailure });
|
|
591
|
+
return new TransactionRolledBackError(message, firstFailure !== undefined ? { cause: firstFailure } : undefined);
|
|
592
|
+
}
|
|
593
|
+
/** The error for a statement sent through a transaction that has settled. */
|
|
594
|
+
export function closedTransactionError(how, api = '$transaction') {
|
|
595
|
+
const handle = api === '$transaction' ? '`tx`' : 'the client `transaction()` passed in';
|
|
596
|
+
return new ValidationError(`This transaction ${how}, so a query issued through ${handle} afterwards was refused rather than sent on a ` +
|
|
597
|
+
`connection that no longer belongs to it. Every query on it has to finish inside the \`${api}\` callback. ` +
|
|
598
|
+
'The usual causes are a missing `await`, an `async` callback passed to `forEach` (use `for ... of` or ' +
|
|
599
|
+
'`await Promise.all(...)`), or the handle kept in a variable or a timer. Run work that belongs after the ' +
|
|
600
|
+
'transaction on the Turbine client itself.');
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The error for a callback that returned while work it started through the
|
|
604
|
+
* transaction was still running. Committing then would save whatever part of
|
|
605
|
+
* that work had run, so the transaction is rolled back instead.
|
|
606
|
+
*/
|
|
607
|
+
export function unawaitedWorkError(api) {
|
|
608
|
+
return new ValidationError(`The \`${api}\` callback returned while a query it started on the transaction was still running, so the ` +
|
|
609
|
+
'transaction was rolled back instead of committing part of that work. Await every query before the ' +
|
|
610
|
+
'callback returns. The usual causes are a missing `await`, an `async` callback passed to `forEach` (use ' +
|
|
611
|
+
'`for ... of` or `await Promise.all(...)`), or a promise that was started and not returned.');
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The error for a statement (or a nested `$transaction`) issued through an
|
|
615
|
+
* outer `tx` from inside one of its open nested blocks.
|
|
616
|
+
*/
|
|
617
|
+
function outerHandleError(what) {
|
|
618
|
+
return new ValidationError(`${what} was issued through the outer \`tx\` from inside one of its nested \`$transaction\` blocks. The ` +
|
|
619
|
+
"connection is inside that block's savepoint, so the work would be undone if the block rolled back, " +
|
|
620
|
+
"although the outer transaction goes on to commit. Use the `tx` the block's callback received for work " +
|
|
621
|
+
'inside the block (`tx.$transaction(async (inner) => { await inner.users.update(...) })`), or issue this ' +
|
|
622
|
+
'outside the block.');
|
|
623
|
+
}
|
|
624
|
+
const SAVEPOINT_KINDS = new Set(['savepoint', 'release', 'rollback-to']);
|
|
625
|
+
/** The error for transaction-control SQL on a transaction's raw paths: what it would do, and what to do instead. */
|
|
626
|
+
function transactionControlError(statement, api) {
|
|
627
|
+
const where = api === '$transaction' ? 'inside a `$transaction` callback' : 'inside a `transaction()` callback';
|
|
628
|
+
const ends = 'it would end the transaction behind the back of the code that manages it, so every later statement ' +
|
|
629
|
+
'would run on its own and be kept even if the callback then throws';
|
|
630
|
+
let advice;
|
|
631
|
+
let reason;
|
|
632
|
+
switch (statement.kind) {
|
|
633
|
+
case 'commit':
|
|
634
|
+
reason = ends;
|
|
635
|
+
advice = 'The transaction commits when the callback returns. Return from the callback to commit.';
|
|
636
|
+
break;
|
|
637
|
+
case 'rollback':
|
|
638
|
+
reason = ends;
|
|
639
|
+
advice =
|
|
640
|
+
api === '$transaction'
|
|
641
|
+
? 'Throw an error from the callback to roll the transaction back, or run the work you may want to ' +
|
|
642
|
+
"undo in a nested `tx.$transaction(...)`, which rolls back only that block's savepoint."
|
|
643
|
+
: 'Throw an error from the callback to roll the transaction back, or send `SAVEPOINT s` first and ' +
|
|
644
|
+
'`ROLLBACK TO SAVEPOINT s` to undo part of the work.';
|
|
645
|
+
break;
|
|
646
|
+
case 'begin':
|
|
647
|
+
reason = 'a transaction is already open, and a second BEGIN does not nest';
|
|
648
|
+
advice =
|
|
649
|
+
api === '$transaction'
|
|
650
|
+
? 'For a block that can roll back on its own, use `tx.$transaction(...)`, which sets a savepoint.'
|
|
651
|
+
: 'For a block that can roll back on its own, send `SAVEPOINT s`.';
|
|
652
|
+
break;
|
|
653
|
+
case 'set-transaction':
|
|
654
|
+
reason = 'the transaction has already begun, so its characteristics can no longer be changed';
|
|
655
|
+
advice = 'Pass `isolationLevel` and `readOnly` as the options of `$transaction(fn, options)` (or `transaction`).';
|
|
656
|
+
break;
|
|
657
|
+
case 'prepare':
|
|
658
|
+
reason = ends;
|
|
659
|
+
advice = 'Prepare the transaction from a connection you manage yourself, outside Turbine.';
|
|
660
|
+
break;
|
|
661
|
+
default:
|
|
662
|
+
// savepoint, release, rollback-to: only reachable inside `$transaction`.
|
|
663
|
+
reason =
|
|
664
|
+
'a savepoint written by hand sits on the connection beside the ones `tx.$transaction` sets, and ' +
|
|
665
|
+
'a rollback to the wrong one discards work the transaction state still counts as kept';
|
|
666
|
+
advice =
|
|
667
|
+
'Use a nested `tx.$transaction(async (inner) => { ... })`, which sets, releases and rolls back its own savepoint.';
|
|
668
|
+
}
|
|
669
|
+
return new ValidationError(`${statement.label} cannot be sent ${where}: ${reason}. ${advice}`);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Whether the SERVER reported `err`: a driver error carrying a SQLSTATE and a
|
|
673
|
+
* severity, directly or as the `cause` of the Turbine error wrapping it. A
|
|
674
|
+
* client-side failure (a value the driver could not serialize, a closed
|
|
675
|
+
* client) has neither, and says nothing about the state of the transaction.
|
|
676
|
+
*/
|
|
677
|
+
function reportedByServer(err) {
|
|
678
|
+
let current = err;
|
|
679
|
+
for (let depth = 0; depth < 4 && current !== null && typeof current === 'object'; depth++) {
|
|
680
|
+
const { code, severity } = current;
|
|
681
|
+
if (typeof code === 'string' && typeof severity === 'string' && /^[0-9A-Z]{5}$/.test(code))
|
|
682
|
+
return true;
|
|
683
|
+
current = current.cause;
|
|
684
|
+
}
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
/** Whether `dialect` can set a savepoint at all (PowDB cannot, and says so by throwing). */
|
|
688
|
+
function dialectHasSavepoints(dialect) {
|
|
689
|
+
try {
|
|
690
|
+
dialect.savepointStatement('sp_probe');
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
}
|