litedbmodel 2.2.2 → 2.2.4
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 +2 -1
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +3 -0
- package/dist/DBModel.js.map +1 -1
- package/dist/Middleware.d.ts +19 -2
- package/dist/Middleware.d.ts.map +1 -1
- package/dist/Middleware.js +1 -0
- package/dist/Middleware.js.map +1 -1
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +8 -0
- package/dist/decorators.js.map +1 -1
- package/dist/drivers/MysqlHelper.d.ts +1 -1
- package/dist/drivers/MysqlHelper.d.ts.map +1 -1
- package/dist/drivers/MysqlHelper.js.map +1 -1
- package/dist/drivers/PostgresHelper.d.ts +1 -1
- package/dist/drivers/PostgresHelper.d.ts.map +1 -1
- package/dist/drivers/PostgresHelper.js.map +1 -1
- package/dist/drivers/SqliteHelper.d.ts +1 -1
- package/dist/drivers/SqliteHelper.d.ts.map +1 -1
- package/dist/drivers/SqliteHelper.js.map +1 -1
- package/dist/index.cjs +105 -74
- package/dist/scp/connection-routing.d.ts +38 -1
- package/dist/scp/connection-routing.d.ts.map +1 -1
- package/dist/scp/connection-routing.js +64 -5
- package/dist/scp/connection-routing.js.map +1 -1
- package/dist/scp/dbmodel-runtime.d.ts +2 -1
- package/dist/scp/dbmodel-runtime.d.ts.map +1 -1
- package/dist/scp/dbmodel-runtime.js +2 -1
- package/dist/scp/dbmodel-runtime.js.map +1 -1
- package/dist/scp/decorator-adapter.d.ts +22 -0
- package/dist/scp/decorator-adapter.d.ts.map +1 -1
- package/dist/scp/decorator-adapter.js +26 -0
- package/dist/scp/decorator-adapter.js.map +1 -1
- package/dist/scp/emit/emitter.d.ts +21 -10
- package/dist/scp/emit/emitter.d.ts.map +1 -1
- package/dist/scp/emit/emitter.js +119 -51
- package/dist/scp/emit/emitter.js.map +1 -1
- package/dist/scp/emit/endpoint.d.ts +3 -8
- package/dist/scp/emit/endpoint.d.ts.map +1 -1
- package/dist/scp/exec-context.d.ts +39 -17
- package/dist/scp/exec-context.d.ts.map +1 -1
- package/dist/scp/exec-context.js +69 -35
- package/dist/scp/exec-context.js.map +1 -1
- package/dist/scp/grouping.d.ts.map +1 -1
- package/dist/scp/grouping.js +0 -0
- package/dist/scp/grouping.js.map +1 -1
- package/dist/scp/index.cjs +257 -670
- package/dist/scp/index.d.ts +6 -11
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +10 -28
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +257 -658
- package/dist/scp/leaf-transport.d.ts +127 -39
- package/dist/scp/leaf-transport.d.ts.map +1 -1
- package/dist/scp/leaf-transport.js +30 -24
- package/dist/scp/leaf-transport.js.map +1 -1
- package/dist/scp/leaves.d.ts +57 -20
- package/dist/scp/leaves.d.ts.map +1 -1
- package/dist/scp/leaves.js +224 -65
- package/dist/scp/leaves.js.map +1 -1
- package/dist/scp/makesql/compile-select.d.ts +28 -1
- package/dist/scp/makesql/compile-select.d.ts.map +1 -1
- package/dist/scp/makesql/compile-select.js +19 -17
- package/dist/scp/makesql/compile-select.js.map +1 -1
- package/dist/scp/makesql/index.d.ts +2 -2
- package/dist/scp/makesql/index.d.ts.map +1 -1
- package/dist/scp/makesql/index.js +4 -6
- package/dist/scp/makesql/index.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts +43 -89
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +45 -496
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/makesql/writeouttype.d.ts +5 -7
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
- package/dist/scp/makesql/writeouttype.js +13 -19
- package/dist/scp/makesql/writeouttype.js.map +1 -1
- package/dist/scp/middleware.d.ts +4 -4
- package/dist/scp/middleware.d.ts.map +1 -1
- package/dist/scp/middleware.js +2 -2
- package/dist/scp/middleware.js.map +1 -1
- package/dist/scp/relation.d.ts +31 -12
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +16 -6
- package/dist/scp/relation.js.map +1 -1
- package/dist/scp/typed-object.d.ts +23 -19
- package/dist/scp/typed-object.d.ts.map +1 -1
- package/dist/scp/typed-object.js +24 -30
- package/dist/scp/typed-object.js.map +1 -1
- package/dist/scp/write-bundle.d.ts +21 -70
- package/dist/scp/write-bundle.d.ts.map +1 -1
- package/dist/scp/write-bundle.js +16 -80
- package/dist/scp/write-bundle.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/BENCHMARK.md +26 -22
- package/docs/COMPARISON.md +8 -4
- package/docs/api/README.md +1 -1
- package/docs/api/classes/Conditions.md +10 -10
- package/docs/api/classes/DBHandler.md +17 -17
- package/docs/api/classes/DBModel.md +111 -104
- package/docs/api/classes/LimitExceededError.md +8 -8
- package/docs/api/classes/Middleware.md +18 -18
- package/docs/api/classes/SqlRaw.md +6 -6
- package/docs/api/classes/SqlRef.md +7 -7
- package/docs/api/classes/Values.md +6 -6
- package/docs/api/classes/WriteInReadOnlyContextError.md +5 -5
- package/docs/api/classes/WriteOutsideTransactionError.md +5 -5
- package/docs/api/functions/belongsTo.md +2 -2
- package/docs/api/functions/closeAllPools.md +2 -2
- package/docs/api/functions/condsToRecord.md +2 -2
- package/docs/api/functions/createMiddleware.md +2 -2
- package/docs/api/functions/createPostgresDriver.md +2 -2
- package/docs/api/functions/createSqliteDriver.md +2 -2
- package/docs/api/functions/dbCast.md +3 -3
- package/docs/api/functions/dbCastIn.md +2 -2
- package/docs/api/functions/dbDynamic.md +3 -3
- package/docs/api/functions/dbFalse.md +2 -2
- package/docs/api/functions/dbImmediate.md +2 -2
- package/docs/api/functions/dbIn.md +2 -2
- package/docs/api/functions/dbNotNull.md +2 -2
- package/docs/api/functions/dbNow.md +2 -2
- package/docs/api/functions/dbNull.md +2 -2
- package/docs/api/functions/dbRaw.md +2 -2
- package/docs/api/functions/dbTrue.md +2 -2
- package/docs/api/functions/dbUuid.md +3 -3
- package/docs/api/functions/dbUuidIn.md +2 -2
- package/docs/api/functions/formatLocalDate.md +25 -0
- package/docs/api/functions/formatUTCDate.md +25 -0
- package/docs/api/functions/getDBConfig.md +2 -2
- package/docs/api/functions/getDBHandler.md +2 -2
- package/docs/api/functions/getTransactionConnection.md +2 -2
- package/docs/api/functions/getTransactionContext.md +2 -2
- package/docs/api/functions/hasMany.md +2 -2
- package/docs/api/functions/hasOne.md +2 -2
- package/docs/api/functions/initDBHandler.md +2 -2
- package/docs/api/functions/isAnySqlFragment.md +2 -2
- package/docs/api/functions/isConnectionError.md +26 -0
- package/docs/api/functions/isSqlCondition.md +2 -2
- package/docs/api/functions/isSqlFragment.md +2 -2
- package/docs/api/functions/isSqlRaw.md +2 -2
- package/docs/api/functions/isSqlRef.md +2 -2
- package/docs/api/functions/isSqlTypedFragment.md +2 -2
- package/docs/api/functions/model.md +6 -42
- package/docs/api/functions/parentRef.md +2 -2
- package/docs/api/functions/sql.md +7 -7
- package/docs/api/globals.md +57 -22
- package/docs/api/interfaces/Column.md +31 -31
- package/docs/api/interfaces/ColumnOptions.md +5 -4
- package/docs/api/interfaces/CreatedMiddlewareClass.md +7 -7
- package/docs/api/interfaces/DBConfig.md +13 -11
- package/docs/api/interfaces/DBConfigOptions.md +8 -8
- package/docs/api/interfaces/DeleteOptions.md +4 -4
- package/docs/api/interfaces/InsertOptions.md +8 -8
- package/docs/api/interfaces/LimitConfig.md +4 -4
- package/docs/api/interfaces/MiddlewareConfig.md +15 -15
- package/docs/api/interfaces/ModelOptions.md +8 -7
- package/docs/api/interfaces/PkeyResult.md +4 -4
- package/docs/api/interfaces/SelectOptions.md +17 -16
- package/docs/api/interfaces/SqlCondition.md +6 -6
- package/docs/api/interfaces/SqlFragment.md +5 -5
- package/docs/api/interfaces/SqlTypedFragment.md +7 -7
- package/docs/api/interfaces/TransactionOptions.md +7 -7
- package/docs/api/interfaces/UpdateManyOptions.md +4 -4
- package/docs/api/interfaces/UpdateOptions.md +4 -4
- package/docs/architecture.md +452 -0
- package/docs/benchmark-chart.svg +225 -249
- package/docs/design/phase-a-execution-context.md +2 -0
- package/package.json +20 -5
- package/dist/scp/writes.d.ts +0 -251
- package/dist/scp/writes.d.ts.map +0 -1
- package/dist/scp/writes.js +0 -176
- package/dist/scp/writes.js.map +0 -1
- package/docs/proposal/contracts-architecture-feasibility.md +0 -328
- package/docs/proposal/contracts-architecture.md +0 -579
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +0 -433
- package/docs/proposal/sql-dynamic-expansion-spec.md +0 -175
- package/docs/proposal/v0-coverage-matrix.md +0 -157
- package/docs/proposal/v2-completeness-plan.md +0 -90
- package/docs/proposal/v2-sql-parity-checklist.md +0 -162
package/dist/scp/makesql/tx.js
CHANGED
|
@@ -1,47 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* litedbmodel v2 SCP — write
|
|
4
|
-
* the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* bc Expression IR refs resolved at execute time against the accumulated transaction scope.
|
|
3
|
+
* litedbmodel v2 SCP — the write descriptor → makeSQL statement compiler ({@link compileWriteNode}),
|
|
4
|
+
* the BATCH transaction-plan derivation ({@link deriveBatchPlan}), and the live async 1-tx runtime
|
|
5
|
+
* ({@link executeTransactionAsync}). No reduced IR is emitted anywhere: every statement's SQL is
|
|
6
|
+
* COMPLETE tuned text (byte-identical to what the v1 write path sends — `INSERT … ON CONFLICT DO
|
|
7
|
+
* NOTHING`, `UPDATE … SET c = ?`, `DELETE FROM … WHERE …`), and its `params` are closed-set bc
|
|
8
|
+
* Expression IR refs resolved at execute time against the transaction scope.
|
|
10
9
|
*
|
|
11
10
|
* A statement op is exactly a `makeSQL` template: `{ sql, params }` where `sql` carries `?`
|
|
12
11
|
* placeholders and `params` are Expression IR (`{ref:[…]}` / a literal / a `{obj:{…}}` payload)
|
|
13
12
|
* that bc evaluates per statement — then the concrete values assemble + render + bind through
|
|
14
13
|
* the SAME `assembleMakeSQL` / `renderPlaceholders` the read path uses.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
15
|
+
* A BATCH write (createMany / updateMany / deleteMany) is ONE logical op that produces N grouped
|
|
16
|
+
* statements; {@link deriveBatchPlan} lowers those into a gate-free {@link TransactionPlan}
|
|
17
|
+
* (`entityFrom` null, every statement `role:'body'`), run in the declared order as ONE transaction.
|
|
18
|
+
* The single-row and read paths are lowered by `bc generate` into a native module per language and
|
|
19
|
+
* never reach this runtime; there is no runtime read-compile and no runtime single-write compile.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ## Path lowering (spec §6) — `$.input.*` / `$.entity.*` / `$.ref.<w>.*` → closed-set refs
|
|
22
|
-
*
|
|
23
|
-
* `$.input.<f>` → `{ref:['<f>']}` (bc flat input scope)
|
|
24
|
-
* `$.entity.<f>` → `{ref:['__entity','<f>']}` (the sole body write's RETURNING row)
|
|
25
|
-
* `$.ref.<w>.<f>` → `{ref:['<w>','<f>']}` (a named upstream write's RETURNING row)
|
|
26
|
-
*
|
|
27
|
-
* ## tx-DAG derivation (spec §6 / §14) — gate-first + data-dependency topo order
|
|
28
|
-
*
|
|
29
|
-
* Statements form a data-dependency DAG (`$.ref.<w>.*` consumes an earlier write's RETURNING
|
|
30
|
-
* row via `TxStatement.binds`) with a gate-first constraint. A deterministic Kahn topo sort
|
|
31
|
-
* (stable ascending declaration `seq` tie-break) orders them into a single-transaction plan.
|
|
32
|
-
* Underivable shapes (cycle, dangling `$.ref`, referenced write without RETURNING, duplicate
|
|
33
|
-
* bind, composite `$.entity`) are LOUD rejects — never a silently mis-ordered plan.
|
|
21
|
+
* Every statement executes: a plan either commits or raises. There is no gate that inspects a
|
|
22
|
+
* statement's result and short-circuits the rest — no plan in this repository ever carried one, so
|
|
23
|
+
* the interpretation was removed rather than kept as a path nothing reaches (#256).
|
|
34
24
|
*/
|
|
35
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.IN_SENTINEL = void 0;
|
|
26
|
+
exports.IN_SENTINEL = exports.ENTITY_ROOT = void 0;
|
|
37
27
|
exports.literalize = literalize;
|
|
28
|
+
exports.isBatchPlan = isBatchPlan;
|
|
38
29
|
exports.pgTypeSpecimen = pgTypeSpecimen;
|
|
39
30
|
exports.pkPort = pkPort;
|
|
40
31
|
exports.compileWriteNode = compileWriteNode;
|
|
41
|
-
exports.deriveTransactionPlan = deriveTransactionPlan;
|
|
42
32
|
exports.deriveBatchPlan = deriveBatchPlan;
|
|
43
|
-
exports.executeTransaction = executeTransaction;
|
|
44
|
-
exports.countingDriver = countingDriver;
|
|
45
33
|
exports.renderTxStatement = renderTxStatement;
|
|
46
34
|
exports.executeTransactionAsync = executeTransactionAsync;
|
|
47
35
|
const runtime_1 = require("behavior-contracts/runtime");
|
|
@@ -61,7 +49,6 @@ const leaves_1 = require("../leaves");
|
|
|
61
49
|
const tx_options_1 = require("../tx-options");
|
|
62
50
|
const connection_errors_1 = require("../../connection-errors");
|
|
63
51
|
const DBConditions_1 = require("../../DBConditions");
|
|
64
|
-
const writes_1 = require("../writes");
|
|
65
52
|
/**
|
|
66
53
|
* Encode a CONCRETE value into a bc Expression IR node that `evaluateExpression` returns VERBATIM.
|
|
67
54
|
*
|
|
@@ -103,126 +90,24 @@ function literalize(value) {
|
|
|
103
90
|
}
|
|
104
91
|
return value;
|
|
105
92
|
}
|
|
106
|
-
// ── Path → Expression IR ref (closed-set only) ────────────────────────────────
|
|
107
|
-
function pathToRef(value) {
|
|
108
|
-
const p = (0, writes_1.parseEffectPath)(value);
|
|
109
|
-
if (p.root === 'input')
|
|
110
|
-
return { ref: [p.field] };
|
|
111
|
-
if (p.root === 'entity')
|
|
112
|
-
return { ref: [writes_1.ENTITY_ROOT, p.field] };
|
|
113
|
-
return { ref: [p.writeName, p.field] };
|
|
114
|
-
}
|
|
115
|
-
// ── Per-dialect guard INSERT text (the ON CONFLICT DO NOTHING / INSERT IGNORE SSoT) ──
|
|
116
93
|
/**
|
|
117
|
-
* The
|
|
118
|
-
*
|
|
119
|
-
* MySQL emits `INSERT IGNORE INTO … VALUES (…)`.
|
|
94
|
+
* The reserved scope key the tx runtime binds the plan's `entityFrom` row under, so a later statement
|
|
95
|
+
* that references the written row resolves against it. Reserved: no input field may use this name.
|
|
120
96
|
*/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
if (cols.length === 0)
|
|
135
|
-
throw new Error(`write-plan: requires on '${e.table}' declares no keys`);
|
|
136
|
-
const whereSql = v1EqualityWhereText(cols);
|
|
137
|
-
const op = {
|
|
138
|
-
sql: `SELECT 1 FROM ${e.table} WHERE ${whereSql}`,
|
|
139
|
-
params: cols.map((c) => pathToRef(e.keys[c])),
|
|
140
|
-
};
|
|
141
|
-
return { id: nextId('requires'), role: 'gate:requires', op, gate: 'existsElseRollback', label: `requires ${e.table}` };
|
|
142
|
-
}
|
|
143
|
-
/** `idempotency` → a gate-first token INSERT ON CONFLICT DO NOTHING (duplicate short-circuits). */
|
|
144
|
-
function compileIdempotency(e, nextId, dialect) {
|
|
145
|
-
const op = {
|
|
146
|
-
sql: guardInsert(dialect, e.table, [e.column], '?'),
|
|
147
|
-
params: [pathToRef(e.token)],
|
|
148
|
-
};
|
|
149
|
-
return { id: nextId('idem'), role: 'gate:idempotency', op, gate: 'insertedElseNoop', label: `idempotency ${e.table}` };
|
|
150
|
-
}
|
|
151
|
-
/** `unique` → a gate-first guard-row INSERT ON CONFLICT DO NOTHING (collision → ROLLBACK). */
|
|
152
|
-
function compileUnique(e, nextId, dialect) {
|
|
153
|
-
const scopeCols = e.scope.map((_, i) => `s${i}`);
|
|
154
|
-
const fieldCols = e.fields.map((_, i) => `f${i}`);
|
|
155
|
-
const cols = ['name', ...scopeCols, ...fieldCols];
|
|
156
|
-
const placeholders = cols.map(() => '?').join(', ');
|
|
157
|
-
const params = [
|
|
158
|
-
e.name, // literal discriminator (a closed-set string literal)
|
|
159
|
-
...e.scope.map(pathToRef),
|
|
160
|
-
...e.fields.map(pathToRef),
|
|
161
|
-
];
|
|
162
|
-
const op = { sql: guardInsert(dialect, e.guardTable, cols, placeholders), params };
|
|
163
|
-
return { id: nextId('unique'), role: 'gate:unique', op, gate: 'insertedElseRollback', label: `unique ${e.name}` };
|
|
164
|
-
}
|
|
165
|
-
/** `derive` → a cascade counter update: `UPDATE <table> SET <attr> = <attr> + ? WHERE k = ?`. */
|
|
166
|
-
function compileDerive(e, nextId) {
|
|
167
|
-
const keyCols = Object.keys(e.keys);
|
|
168
|
-
if (keyCols.length === 0)
|
|
169
|
-
throw new Error(`write-plan: derive on '${e.table}' declares no keys`);
|
|
170
|
-
const whereSql = v1EqualityWhereText(keyCols);
|
|
171
|
-
const op = {
|
|
172
|
-
sql: `UPDATE ${e.table} SET ${e.attribute} = ${e.attribute} + ? WHERE ${whereSql}`,
|
|
173
|
-
// SET amount is the first param (before the WHERE keys), matching the v1 param order.
|
|
174
|
-
params: [e.amount, ...keyCols.map((c) => pathToRef(e.keys[c]))],
|
|
175
|
-
};
|
|
176
|
-
return { id: nextId('derive'), role: 'derive', op, label: `derive ${e.table}.${e.attribute}` };
|
|
177
|
-
}
|
|
178
|
-
/** `edges` → M:N intermediate INSERT/DELETE or 1:N FK UPDATE (spec §6 table row `edges`). */
|
|
179
|
-
function compileEdge(e, nextId) {
|
|
180
|
-
if (e.relation === 'm2m') {
|
|
181
|
-
const cols = Object.keys(e.columns);
|
|
182
|
-
if (e.action === 'set') {
|
|
183
|
-
const placeholders = cols.map(() => '?').join(', ');
|
|
184
|
-
const op = {
|
|
185
|
-
sql: `INSERT INTO ${e.table} (${cols.join(', ')}) VALUES (${placeholders})`,
|
|
186
|
-
params: cols.map((c) => pathToRef(e.columns[c])),
|
|
187
|
-
};
|
|
188
|
-
return { id: nextId('edge'), role: 'edge', op, label: `edge m2m link ${e.table}` };
|
|
189
|
-
}
|
|
190
|
-
const whereSql = v1EqualityWhereText(cols);
|
|
191
|
-
const op = {
|
|
192
|
-
sql: `DELETE FROM ${e.table} WHERE ${whereSql}`,
|
|
193
|
-
params: cols.map((c) => pathToRef(e.columns[c])),
|
|
194
|
-
};
|
|
195
|
-
return { id: nextId('edge'), role: 'edge', op, label: `edge m2m unlink ${e.table}` };
|
|
196
|
-
}
|
|
197
|
-
// fk: UPDATE <related> SET <fkCol> = ? (or NULL) WHERE <where keys…>
|
|
198
|
-
const setCols = Object.keys(e.columns);
|
|
199
|
-
const whereCols = Object.keys(e.where);
|
|
200
|
-
const setClauses = setCols.map((c) => (e.action === 'set' ? `${c} = ?` : `${c} = NULL`));
|
|
201
|
-
const setParams = e.action === 'set' ? setCols.map((c) => pathToRef(e.columns[c])) : [];
|
|
202
|
-
const whereSql = v1EqualityWhereText(whereCols);
|
|
203
|
-
const op = {
|
|
204
|
-
sql: `UPDATE ${e.table} SET ${setClauses.join(', ')} WHERE ${whereSql}`,
|
|
205
|
-
params: [...setParams, ...whereCols.map((c) => pathToRef(e.where[c]))],
|
|
206
|
-
};
|
|
207
|
-
return { id: nextId('edge'), role: 'edge', op, label: `edge fk ${e.action} ${e.table}` };
|
|
208
|
-
}
|
|
209
|
-
/** `emits` → an outbox INSERT (same tx): `INSERT INTO <outbox>(type, payload) VALUES(?, ?)`. */
|
|
210
|
-
function compileEmit(e, nextId) {
|
|
211
|
-
const payloadObj = {};
|
|
212
|
-
for (const [k, v] of Object.entries(e.payload))
|
|
213
|
-
payloadObj[k] = pathToRef(v);
|
|
214
|
-
const op = {
|
|
215
|
-
sql: `INSERT INTO ${e.outboxTable} (type, payload) VALUES (?, ?)`,
|
|
216
|
-
params: [e.name, { obj: payloadObj }],
|
|
217
|
-
};
|
|
218
|
-
return { id: nextId('emit'), role: 'emit', op, label: `emit ${e.name}` };
|
|
97
|
+
exports.ENTITY_ROOT = '__entity';
|
|
98
|
+
/**
|
|
99
|
+
* Is this plan a BATCH (createMany / updateMany / deleteMany) rather than a single/composite Command?
|
|
100
|
+
* A batch is ref-free: nothing exposes a row for a later statement to read, so `entityFrom` is null and
|
|
101
|
+
* every statement is a plain `body` with no `binds`.
|
|
102
|
+
*
|
|
103
|
+
* It decides two things that MUST agree, which is why it is ONE function and not a test each side
|
|
104
|
+
* spells for itself: the runtime accumulates `returnedRows` only for a batch, and the codegen out-type
|
|
105
|
+
* ({@link ./writeouttype}) includes the `returnedRows` field only for a batch. Two spellings that drift
|
|
106
|
+
* would type a field the runtime does not fill, or drop one it does (#259).
|
|
107
|
+
*/
|
|
108
|
+
function isBatchPlan(plan) {
|
|
109
|
+
return plan.entityFrom === null && plan.statements.every((s) => s.binds === undefined && s.role === 'body');
|
|
219
110
|
}
|
|
220
|
-
// ============================================================================
|
|
221
|
-
// Authored write node → makeSQL TxOp (the makeSQL re-expression of the bridge's
|
|
222
|
-
// `compileNode` for the write path — reads ports STRUCTURALLY, emits complete SQL
|
|
223
|
-
// text + DEFERRED Expression-IR params resolved at tx execute time).
|
|
224
|
-
// ============================================================================
|
|
225
|
-
/** The reserved column-ref path head that marks an IN-list membership (mirrors the bridge). */
|
|
226
111
|
exports.IN_SENTINEL = '@in';
|
|
227
112
|
function stringPort(ports, name) {
|
|
228
113
|
const v = ports[name];
|
|
@@ -313,21 +198,6 @@ function v1ConditionText(conditions) {
|
|
|
313
198
|
const probe = [];
|
|
314
199
|
return new DBConditions_1.DBConditions(conditions).compile(probe);
|
|
315
200
|
}
|
|
316
|
-
/**
|
|
317
|
-
* The tuned WHERE-body TEXT for a set of PK-equality columns (`k1 = ? AND k2 = ?`), produced by
|
|
318
|
-
* driving the ORIGINAL v1 `DBConditions.compile()` (via {@link v1ConditionText}) — the SAME
|
|
319
|
-
* builder the read path uses — so the write-tx predicate text is byte-identical to v1 by
|
|
320
|
-
* construction (a v1 predicate regression now moves a tx golden). The throwaway probe params are
|
|
321
|
-
* discarded; the real runtime values are the caller's deferred Expression-IR params, bound 1:1
|
|
322
|
-
* with the `?` placeholders. `DBConditions.compile` iterates the condition object in insertion
|
|
323
|
-
* order and joins with ` AND `, so the `?`/param order matches the caller's column order.
|
|
324
|
-
*/
|
|
325
|
-
function v1EqualityWhereText(columns) {
|
|
326
|
-
const conditions = {};
|
|
327
|
-
for (const c of columns)
|
|
328
|
-
conditions[c] = PROBE;
|
|
329
|
-
return v1ConditionText(conditions);
|
|
330
|
-
}
|
|
331
201
|
/**
|
|
332
202
|
* Lower one where-member Expression node → a `<sql, params, columns>` WHERE fragment (deferred
|
|
333
203
|
* params). `columns[i]` is the table column the i-th emitted param binds — parallel to `params` — so
|
|
@@ -552,7 +422,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
552
422
|
if (stringPort(ports, 'batch') === 'true') {
|
|
553
423
|
if (dialect === 'postgres') {
|
|
554
424
|
if (resolveColumnType === undefined)
|
|
555
|
-
throw new Error(`compileWriteNode: batch insert on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts — pass it through compileCreateManyBundle
|
|
425
|
+
throw new Error(`compileWriteNode: batch insert on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts — pass it through compileCreateManyBundle (the decorator-adapter batch write path).`);
|
|
556
426
|
return pgBatchInsert(table, sorted, values, ports, resolveColumnType, dialect);
|
|
557
427
|
}
|
|
558
428
|
const shapeOpts = { tableName: table, columns: sorted, records: [], ...onConflictJsonOpts(ports, sorted), ...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}) };
|
|
@@ -599,7 +469,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
599
469
|
const updateCols = [...setCols].sort();
|
|
600
470
|
if (dialect === 'postgres') {
|
|
601
471
|
if (resolveColumnType === undefined)
|
|
602
|
-
throw new Error(`compileWriteNode: batch update on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts — pass it through compileCreateManyBundle
|
|
472
|
+
throw new Error(`compileWriteNode: batch update on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts — pass it through compileCreateManyBundle (the decorator-adapter batch write path).`);
|
|
603
473
|
return pgBatchUpdate(table, keyCols, updateCols, key, set, ports, resolveColumnType, dialect);
|
|
604
474
|
}
|
|
605
475
|
const shapeOpts = { tableName: table, keyColumns: keyCols, updateColumns: updateCols, records: [], ...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}) };
|
|
@@ -645,149 +515,9 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
645
515
|
throw new Error(`compileWriteNode: catalog component '${component}' has no write compile (SQL writes: Insert/Update/Delete)`);
|
|
646
516
|
}
|
|
647
517
|
}
|
|
648
|
-
function
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const mk = (stmt, isGate) => {
|
|
652
|
-
const consumes = refHeadsOf(stmt).filter((h) => h !== writes_1.ENTITY_ROOT && !isInputHead(h, stmt));
|
|
653
|
-
return { stmt, seq: seqRef.n++, consumes, produces: stmt.binds ?? null, isGate };
|
|
654
|
-
};
|
|
655
|
-
for (const r of e.requires ?? [])
|
|
656
|
-
nodes.push(mk(compileRequires(r, nextId), true));
|
|
657
|
-
if (e.idempotency !== undefined)
|
|
658
|
-
nodes.push(mk(compileIdempotency(e.idempotency, nextId, dialect), true));
|
|
659
|
-
for (const u of e.unique ?? [])
|
|
660
|
-
nodes.push(mk(compileUnique(u, nextId, dialect), true));
|
|
661
|
-
const bodyId = nextId('body');
|
|
662
|
-
const bodyStmt = {
|
|
663
|
-
id: bodyId,
|
|
664
|
-
role: 'body',
|
|
665
|
-
op: base.op,
|
|
666
|
-
label: base.label,
|
|
667
|
-
...(base.name !== undefined ? { binds: base.name } : {}),
|
|
668
|
-
};
|
|
669
|
-
nodes.push(mk(bodyStmt, false));
|
|
670
|
-
for (const d of e.derive ?? [])
|
|
671
|
-
nodes.push(mk(compileDerive(d, nextId), false));
|
|
672
|
-
for (const ed of e.edges ?? [])
|
|
673
|
-
nodes.push(mk(compileEdge(ed, nextId), false));
|
|
674
|
-
for (const em of e.emits ?? [])
|
|
675
|
-
nodes.push(mk(compileEmit(em, nextId), false));
|
|
676
|
-
return { nodes, bodyId };
|
|
677
|
-
}
|
|
678
|
-
function topoOrder(nodes) {
|
|
679
|
-
const byName = new Map();
|
|
680
|
-
for (const n of nodes) {
|
|
681
|
-
if (n.produces !== null) {
|
|
682
|
-
if (byName.has(n.produces)) {
|
|
683
|
-
throw new Error(`write-plan: two writes both bind the name '${n.produces}' — write names must be unique in a composite Command.`);
|
|
684
|
-
}
|
|
685
|
-
byName.set(n.produces, n);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
const succ = new Map();
|
|
689
|
-
const indeg = new Map();
|
|
690
|
-
const nodeById = new Map();
|
|
691
|
-
for (const n of nodes) {
|
|
692
|
-
nodeById.set(n.stmt.id, n);
|
|
693
|
-
succ.set(n.stmt.id, new Set());
|
|
694
|
-
indeg.set(n.stmt.id, 0);
|
|
695
|
-
}
|
|
696
|
-
const addEdge = (fromId, toId) => {
|
|
697
|
-
if (fromId === toId)
|
|
698
|
-
return;
|
|
699
|
-
const s = succ.get(fromId);
|
|
700
|
-
if (!s.has(toId)) {
|
|
701
|
-
s.add(toId);
|
|
702
|
-
indeg.set(toId, indeg.get(toId) + 1);
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
for (const n of nodes) {
|
|
706
|
-
for (const dep of n.consumes) {
|
|
707
|
-
const producer = byName.get(dep);
|
|
708
|
-
if (producer === undefined) {
|
|
709
|
-
throw new Error(`write-plan: statement '${n.stmt.label}' references '$.ref.${dep}.*' but no write in this ` +
|
|
710
|
-
`Command binds the name '${dep}' — a dangling write reference (fail-closed; no silent skip).`);
|
|
711
|
-
}
|
|
712
|
-
addEdge(producer.stmt.id, n.stmt.id);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
const gates = nodes.filter((n) => n.isGate);
|
|
716
|
-
const nonGates = nodes.filter((n) => !n.isGate);
|
|
717
|
-
for (const g of gates)
|
|
718
|
-
for (const b of nonGates)
|
|
719
|
-
addEdge(g.stmt.id, b.stmt.id);
|
|
720
|
-
const ordered = [];
|
|
721
|
-
const ready = nodes.filter((n) => indeg.get(n.stmt.id) === 0).sort((a, b) => a.seq - b.seq);
|
|
722
|
-
while (ready.length > 0) {
|
|
723
|
-
const n = ready.shift();
|
|
724
|
-
ordered.push(n.stmt);
|
|
725
|
-
for (const depId of succ.get(n.stmt.id)) {
|
|
726
|
-
indeg.set(depId, indeg.get(depId) - 1);
|
|
727
|
-
if (indeg.get(depId) === 0) {
|
|
728
|
-
const dn = nodeById.get(depId);
|
|
729
|
-
let lo = 0;
|
|
730
|
-
while (lo < ready.length && ready[lo].seq < dn.seq)
|
|
731
|
-
lo++;
|
|
732
|
-
ready.splice(lo, 0, dn);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
if (ordered.length !== nodes.length) {
|
|
737
|
-
const stuck = nodes.filter((n) => !ordered.includes(n.stmt)).map((n) => n.stmt.label);
|
|
738
|
-
throw new Error(`write-plan: the write-time transaction DAG has a CYCLE (unresolvable ordering among ` +
|
|
739
|
-
`[${stuck.join(', ')}]). This is either a mutual '$.ref' data dependency or a gate that ` +
|
|
740
|
-
`depends on a body row (a gate cannot depend on a not-yet-written row). ESCALATE: the ` +
|
|
741
|
-
`Command's write dependencies are contradictory — no order satisfies both the data ` +
|
|
742
|
-
`dependencies and gate-first. (Fail-closed; the derivation never guesses an order.)`);
|
|
743
|
-
}
|
|
744
|
-
return ordered;
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Derive the ORDERED, gate-first {@link TransactionPlan} from a Command's base write(s) + its
|
|
748
|
-
* lifecycle save contract (spec §6 / §14). Single base write = fixed §6 group; multiple named
|
|
749
|
-
* base writes = a topologically-ordered gate-first tx DAG (`$.ref.<name>.*` data dependencies).
|
|
750
|
-
*/
|
|
751
|
-
function deriveTransactionPlan(phase, bases, lifecycle, dialect = 'sqlite') {
|
|
752
|
-
if (bases.length === 0) {
|
|
753
|
-
throw new Error('write-plan: a Command must declare at least one base write (Insert/Update/Delete).');
|
|
754
|
-
}
|
|
755
|
-
const nextId = makeIdGen();
|
|
756
|
-
const seqRef = { n: 0 };
|
|
757
|
-
const composite = bases.length > 1;
|
|
758
|
-
const allNodes = [];
|
|
759
|
-
const bodyIds = [];
|
|
760
|
-
bases.forEach((base, i) => {
|
|
761
|
-
const lc = base.effects !== undefined ? { effects: base.effects } : i === 0 ? lifecycle : { effects: {} };
|
|
762
|
-
const { nodes, bodyId } = compileWriteGroup(base, lc, nextId, dialect, seqRef);
|
|
763
|
-
allNodes.push(...nodes);
|
|
764
|
-
bodyIds.push(bodyId);
|
|
765
|
-
});
|
|
766
|
-
const statements = topoOrder(allNodes);
|
|
767
|
-
const soleBody = bases[0];
|
|
768
|
-
const soleBodyId = bodyIds[0];
|
|
769
|
-
const usesEntity = referencesHead(statements, writes_1.ENTITY_ROOT);
|
|
770
|
-
const soleBodyReturns = /\breturning\b/i.test(soleBody.op.sql);
|
|
771
|
-
if (usesEntity && composite) {
|
|
772
|
-
throw new Error(`write-plan: a composite (multi-write) Command uses '$.entity.*' — ambiguous which write it ` +
|
|
773
|
-
`denotes. Address each write's row by name via '$.ref.<writeName>.<field>' (fail-closed).`);
|
|
774
|
-
}
|
|
775
|
-
if (usesEntity && !soleBodyReturns) {
|
|
776
|
-
throw new Error(`write-plan: a derive/edges/emits stage references '$.entity.*' but the body write ` +
|
|
777
|
-
`('${soleBody.label}') has no RETURNING clause — the written row cannot be exposed to later ` +
|
|
778
|
-
`stages. Add a RETURNING to the base write (fail-closed; no absent-entity default).`);
|
|
779
|
-
}
|
|
780
|
-
const entityFrom = usesEntity || (!composite && soleBodyReturns) ? soleBodyId : null;
|
|
781
|
-
for (const base of bases) {
|
|
782
|
-
if (base.name === undefined)
|
|
783
|
-
continue;
|
|
784
|
-
if (referencesHead(statements, base.name) && !/\breturning\b/i.test(base.op.sql)) {
|
|
785
|
-
throw new Error(`write-plan: write '${base.name}' is referenced as '$.ref.${base.name}.*' by a later ` +
|
|
786
|
-
`statement but its op has no RETURNING clause — its row cannot be bound for downstream ` +
|
|
787
|
-
`writes. Add a RETURNING to write '${base.name}' (fail-closed; no absent-row default).`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return { phase, entityFrom, statements, onIdempotentHit: 'rollback' };
|
|
518
|
+
function makeIdGen() {
|
|
519
|
+
let n = 0;
|
|
520
|
+
return (role) => `tx_${role}_${n++}`;
|
|
791
521
|
}
|
|
792
522
|
/**
|
|
793
523
|
* Lower a list of CONCRETE batch-write ops (createMany / updateMany / deleteMany — each already
|
|
@@ -817,68 +547,7 @@ function deriveBatchPlan(phase, ops) {
|
|
|
817
547
|
// its own rows. `entityFrom` stays null; the runtime accumulates EVERY body statement's RETURNING
|
|
818
548
|
// rows into `TransactionResult.returnedRows` (ordered by group), reproducing v1 `createMany`'s
|
|
819
549
|
// "all created rows" result while `expectedDbState` proves the persisted state.
|
|
820
|
-
return { phase, entityFrom: null, statements
|
|
821
|
-
}
|
|
822
|
-
/** Collect the ref-heads (`ref[0]`) a statement's params reference. */
|
|
823
|
-
function refHeadsOf(stmt) {
|
|
824
|
-
const heads = new Set();
|
|
825
|
-
collectRefHeads(stmt.op.params, heads);
|
|
826
|
-
return [...heads];
|
|
827
|
-
}
|
|
828
|
-
/** True if a ref-head is an INPUT head (a bare `$.input.<f>` → 1-element `{ref:['<f>']}`). */
|
|
829
|
-
function isInputHead(head, stmt) {
|
|
830
|
-
return refPathLengthFor(stmt.op.params, head) === 1;
|
|
831
|
-
}
|
|
832
|
-
function refPathLengthFor(node, head) {
|
|
833
|
-
let maxLen = 0;
|
|
834
|
-
const walk = (n) => {
|
|
835
|
-
if (n === null || typeof n !== 'object')
|
|
836
|
-
return;
|
|
837
|
-
if (Array.isArray(n)) {
|
|
838
|
-
for (const el of n)
|
|
839
|
-
walk(el);
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
const keys = Object.keys(n);
|
|
843
|
-
if (keys.length === 1 && keys[0] === 'ref') {
|
|
844
|
-
const path = n.ref;
|
|
845
|
-
if (Array.isArray(path) && path[0] === head)
|
|
846
|
-
maxLen = Math.max(maxLen, path.length);
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
for (const k of keys)
|
|
850
|
-
walk(n[k]);
|
|
851
|
-
};
|
|
852
|
-
walk(node);
|
|
853
|
-
return maxLen;
|
|
854
|
-
}
|
|
855
|
-
function referencesHead(statements, head) {
|
|
856
|
-
const heads = new Set();
|
|
857
|
-
for (const s of statements)
|
|
858
|
-
collectRefHeads(s.op.params, heads);
|
|
859
|
-
return heads.has(head);
|
|
860
|
-
}
|
|
861
|
-
function collectRefHeads(node, heads) {
|
|
862
|
-
if (node === null || typeof node !== 'object')
|
|
863
|
-
return;
|
|
864
|
-
if (Array.isArray(node)) {
|
|
865
|
-
for (const el of node)
|
|
866
|
-
collectRefHeads(el, heads);
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const keys = Object.keys(node);
|
|
870
|
-
if (keys.length === 1 && keys[0] === 'ref') {
|
|
871
|
-
const path = node.ref;
|
|
872
|
-
if (Array.isArray(path) && typeof path[0] === 'string')
|
|
873
|
-
heads.add(path[0]);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
for (const k of keys)
|
|
877
|
-
collectRefHeads(node[k], heads);
|
|
878
|
-
}
|
|
879
|
-
/** Coerce a `SqliteDb | ExecutionContext` argument to a ctx (raw driver ⇒ backward-compat wrapper). */
|
|
880
|
-
function asContext(dbOrCtx) {
|
|
881
|
-
return 'connectionFor' in dbOrCtx ? dbOrCtx : (0, exec_context_1.contextForDriver)(dbOrCtx);
|
|
550
|
+
return { phase, entityFrom: null, statements };
|
|
882
551
|
}
|
|
883
552
|
/** bc evaluates ints to bigint; convert a rendered param to a driver-bindable value. */
|
|
884
553
|
function toDriverParam(v, dialect) {
|
|
@@ -917,100 +586,6 @@ function renderStatement(op, scope, dialect) {
|
|
|
917
586
|
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
918
587
|
return { sql: (0, handler_1.renderPlaceholders)(sql, dialect), params };
|
|
919
588
|
}
|
|
920
|
-
function execStatement(ctx, op, scope, dialect) {
|
|
921
|
-
// A tx body statement rides the SAME op-independent transport as every read/write: the `executeSQL`
|
|
922
|
-
// leaf (placeholder render + param encode + central seam). The tx scope-eval/assemble/coerce is done
|
|
923
|
-
// first (`evalAssemble`); the leaf receives raw `?` SQL + concrete params. `write:true` ⇒ the tx-owned
|
|
924
|
-
// connection resolves via `connectionFor` (§3); a SELECT/RETURNING statement reads rows (`returning`
|
|
925
|
-
// ⇒ the row-returning seam), a bare write runs (the `[{changes,…}]` summary).
|
|
926
|
-
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
927
|
-
const hasReturn = /\bselect\b/i.test(sql.slice(0, 8)) || /\breturning\b/i.test(sql);
|
|
928
|
-
const out = (0, leaves_1.executeSQL)({ sql, params, write: true, returning: hasReturn, bigint: false }, { exec: ctx, dialect });
|
|
929
|
-
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
930
|
-
}
|
|
931
|
-
function gateShortCircuit(gate, result) {
|
|
932
|
-
switch (gate) {
|
|
933
|
-
case 'existsElseRollback':
|
|
934
|
-
return result.rows.length === 0 ? 'requires_absent' : null;
|
|
935
|
-
case 'insertedElseRollback':
|
|
936
|
-
return result.changes === 0 ? 'unique_collision' : null;
|
|
937
|
-
case 'insertedElseNoop':
|
|
938
|
-
return result.changes === 0 ? 'idempotent_duplicate' : null;
|
|
939
|
-
default:
|
|
940
|
-
// Fail-CLOSED on an unknown / forward-incompatible gate rule (aligned with Python + Rust): a
|
|
941
|
-
// corrupt or unrecognized gate MUST NOT silently continue (fail-open would let a malformed gate
|
|
942
|
-
// be skipped and the write COMMIT). Throwing here aborts the tx (the caller's catch ROLLBACKs).
|
|
943
|
-
throw new Error(`scp write: unknown gate rule '${String(gate)}'`);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Execute a derived {@link TransactionPlan} as ONE real SQLite transaction with gate-first
|
|
948
|
-
* short-circuit. Accepts a raw {@link SqliteDb} (wrapped via {@link contextForDriver}) or a full
|
|
949
|
-
* {@link ExecutionContext}. The transaction derives a tx-scoped ctx (`withConnection(conn, true)`)
|
|
950
|
-
* that PINS one connection so BEGIN, every body statement, and COMMIT/ROLLBACK all run on the SAME
|
|
951
|
-
* connection (§3, per-execution ownership). For the single-DB SQLite driver the pinned connection
|
|
952
|
-
* is the sole connection; the ownership shows its teeth on the pooled async path
|
|
953
|
-
* ({@link import('../exec-context').withTransactionAsync}), which this mirrors.
|
|
954
|
-
*/
|
|
955
|
-
function executeTransaction(db, plan, input, dialect = 'sqlite') {
|
|
956
|
-
const outer = asContext(db);
|
|
957
|
-
const executed = [];
|
|
958
|
-
const scope = { ...input };
|
|
959
|
-
let entity = null;
|
|
960
|
-
// Batch mode (createMany/updateMany/deleteMany): a gate-free, ref-free plan (no `$.entity`, no
|
|
961
|
-
// gates, no `$.ref` binds) — a pure list of body statements. Only THEN accumulate each body
|
|
962
|
-
// statement's RETURNING rows in order (a composite Command also has `entityFrom:null` but carries
|
|
963
|
-
// `binds`/gates and is NOT batch — its written rows flow via scope refs, not `returnedRows`).
|
|
964
|
-
const isBatch = plan.entityFrom === null &&
|
|
965
|
-
plan.statements.every((s) => s.gate === undefined && s.binds === undefined && s.role === 'body');
|
|
966
|
-
const returnedRows = [];
|
|
967
|
-
// The tx boundary is the sync `ctx.transaction` ({@link withTransactionSync}: pin conn → BEGIN →
|
|
968
|
-
// body → COMMIT/ROLLBACK, inside `runInTransactionScope`). The body runs the derived plan's
|
|
969
|
-
// statements in order via the `executeSQL` transport leaf ({@link execStatement}); a gate
|
|
970
|
-
// short-circuit signals `{commit:false}` so the boundary ROLLBACKs but still returns the reason.
|
|
971
|
-
try {
|
|
972
|
-
return (0, exec_context_1.withTransactionSync)(outer, (ctx) => {
|
|
973
|
-
for (const stmt of plan.statements) {
|
|
974
|
-
const result = execStatement(ctx, stmt.op, scope, dialect);
|
|
975
|
-
executed.push(stmt.id);
|
|
976
|
-
if (stmt.gate !== undefined) {
|
|
977
|
-
const reason = gateShortCircuit(stmt.gate, result);
|
|
978
|
-
if (reason !== null) {
|
|
979
|
-
return { commit: false, value: { committed: false, shortCircuit: { statementId: stmt.id, reason }, entity: null, executed } };
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
if (stmt.id === plan.entityFrom) {
|
|
983
|
-
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
984
|
-
if (entity !== null)
|
|
985
|
-
scope[writes_1.ENTITY_ROOT] = entity;
|
|
986
|
-
}
|
|
987
|
-
if (stmt.binds !== undefined && result.rows.length > 0) {
|
|
988
|
-
scope[stmt.binds] = result.rows[0];
|
|
989
|
-
}
|
|
990
|
-
if (isBatch && stmt.role === 'body' && result.rows.length > 0)
|
|
991
|
-
returnedRows.push(result.rows);
|
|
992
|
-
}
|
|
993
|
-
return { commit: true, value: { committed: true, entity, executed, ...(returnedRows.length > 0 ? { returnedRows } : {}) } };
|
|
994
|
-
}, dialect);
|
|
995
|
-
}
|
|
996
|
-
catch (e) {
|
|
997
|
-
throw (0, errors_1.mapSqliteError)(e);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* A counting {@link SqliteDb} wrapper: forwards every call to the wrapped driver but records
|
|
1002
|
-
* each PREPARED SQL string. Tests use it to PROVE gate-first short-circuit.
|
|
1003
|
-
*/
|
|
1004
|
-
function countingDriver(db) {
|
|
1005
|
-
const prepared = [];
|
|
1006
|
-
const wrapped = {
|
|
1007
|
-
prepare(sql) {
|
|
1008
|
-
prepared.push(sql);
|
|
1009
|
-
return db.prepare(sql);
|
|
1010
|
-
},
|
|
1011
|
-
};
|
|
1012
|
-
return { db: wrapped, prepared };
|
|
1013
|
-
}
|
|
1014
589
|
/** Render one statement op to its dialect SQL text + params (exposed for golden tests). */
|
|
1015
590
|
function renderTxStatement(op, scope, dialect = 'sqlite') {
|
|
1016
591
|
return renderStatement(op, scope, dialect);
|
|
@@ -1038,12 +613,12 @@ async function execStatementAsync(ctx, op, scope, dialect) {
|
|
|
1038
613
|
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
1039
614
|
// A SELECT/RETURNING reads rows; a bare write returns the `[{changes,…}]` summary.
|
|
1040
615
|
const hasReturn = /\bselect\b/i.test(sql.slice(0, 8)) || /\breturning\b/i.test(sql);
|
|
1041
|
-
const out = await (0, leaves_1.executeSQLAsync)({ sql, params, write:
|
|
616
|
+
const out = await (0, leaves_1.executeSQLAsync)({ sql, params, write: { returning: hasReturn } }, { execAsync: ctx, dialect });
|
|
1042
617
|
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
1043
618
|
}
|
|
1044
619
|
/**
|
|
1045
|
-
* Execute a derived {@link TransactionPlan} on a live PG / MySQL connection with
|
|
1046
|
-
*
|
|
620
|
+
* Execute a derived {@link TransactionPlan} on a live PG / MySQL connection with **per-execution
|
|
621
|
+
* connection ownership** (§3). The live-DB WRITE entry (#86).
|
|
1047
622
|
*
|
|
1048
623
|
* ## Ambient-tx JOIN vs. its own envelope (the #86 core)
|
|
1049
624
|
*
|
|
@@ -1086,8 +661,7 @@ function executeTransactionAsync(ctx, plan, input, dialect = 'sqlite', options =
|
|
|
1086
661
|
}
|
|
1087
662
|
/** The plan-executor body of {@link executeTransactionAsync}, split so the guard runs at entry. */
|
|
1088
663
|
function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
1089
|
-
const
|
|
1090
|
-
plan.statements.every((s) => s.gate === undefined && s.binds === undefined && s.role === 'body');
|
|
664
|
+
const batch = isBatchPlan(plan);
|
|
1091
665
|
return (0, exec_context_1.withTransactionAsync)(ctx, async (txCtx) => {
|
|
1092
666
|
const executed = [];
|
|
1093
667
|
const scope = { ...input };
|
|
@@ -1096,47 +670,22 @@ function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
|
1096
670
|
for (const stmt of plan.statements) {
|
|
1097
671
|
const result = await execStatementAsync(txCtx, stmt.op, scope, dialect);
|
|
1098
672
|
executed.push(stmt.id);
|
|
1099
|
-
if (stmt.gate !== undefined) {
|
|
1100
|
-
const reason = gateShortCircuit(stmt.gate, result);
|
|
1101
|
-
if (reason !== null) {
|
|
1102
|
-
// A gate short-circuit ROLLBACKs the whole tx (atomicity): throw the sentinel so
|
|
1103
|
-
// `withTransactionAsync` runs ROLLBACK on the owned connection, then translate to the
|
|
1104
|
-
// structured non-committed result at the boundary.
|
|
1105
|
-
throw new GateShortCircuit(stmt.id, reason, executed);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
673
|
if (stmt.id === plan.entityFrom) {
|
|
1109
674
|
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
1110
675
|
if (entity !== null)
|
|
1111
|
-
scope[
|
|
676
|
+
scope[exports.ENTITY_ROOT] = entity;
|
|
1112
677
|
}
|
|
1113
678
|
if (stmt.binds !== undefined && result.rows.length > 0) {
|
|
1114
679
|
scope[stmt.binds] = result.rows[0];
|
|
1115
680
|
}
|
|
1116
|
-
if (
|
|
681
|
+
if (batch && stmt.role === 'body' && result.rows.length > 0)
|
|
1117
682
|
returnedRows.push(result.rows);
|
|
1118
683
|
}
|
|
1119
684
|
return { committed: true, entity, executed, ...(returnedRows.length > 0 ? { returnedRows } : {}) };
|
|
1120
685
|
}, options, dialect === 'sqlite' ? 'postgres' : dialect, connection_errors_1.isConnectionError).catch((e) => {
|
|
1121
|
-
//
|
|
1122
|
-
// error
|
|
1123
|
-
if (e instanceof GateShortCircuit) {
|
|
1124
|
-
return { committed: false, shortCircuit: { statementId: e.statementId, reason: e.reason }, entity: null, executed: e.executed };
|
|
1125
|
-
}
|
|
686
|
+
// Every error here is a real driver failure (already rolled back by withTransactionAsync) — a plan
|
|
687
|
+
// has no non-error way to not commit, so there is nothing to translate before re-surfacing.
|
|
1126
688
|
throw (0, errors_1.mapSqliteError)(e);
|
|
1127
689
|
});
|
|
1128
690
|
}
|
|
1129
|
-
/** Internal sentinel: a gate short-circuit inside an async tx (ROLLBACK, then report non-committed). */
|
|
1130
|
-
class GateShortCircuit extends Error {
|
|
1131
|
-
statementId;
|
|
1132
|
-
reason;
|
|
1133
|
-
executed;
|
|
1134
|
-
constructor(statementId, reason, executed) {
|
|
1135
|
-
super(`scp write: gate short-circuit at '${statementId}' (${reason})`);
|
|
1136
|
-
this.statementId = statementId;
|
|
1137
|
-
this.reason = reason;
|
|
1138
|
-
this.executed = executed;
|
|
1139
|
-
this.name = 'GateShortCircuit';
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
691
|
//# sourceMappingURL=tx.js.map
|