litedbmodel 2.2.2 → 2.2.3
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 +99 -33
- 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 +251 -629
- 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 +251 -617
- 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 +27 -69
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +26 -448
- package/dist/scp/makesql/tx.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 +15 -79
- 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/architecture.md +452 -0
- package/docs/design/phase-a-execution-context.md +2 -0
- package/package.json +14 -3
- 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,36 @@
|
|
|
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
|
-
* failing gate short-circuits — the remaining statements never execute and the tx ROLLBACKs.
|
|
21
|
+
* ## Gate short-circuit (still supported by the runtime)
|
|
20
22
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.
|
|
23
|
+
* A statement MAY carry a {@link GateRule} the runtime evaluates AFTER executing it: a failing gate
|
|
24
|
+
* short-circuits — the remaining statements never execute and the tx ROLLBACKs. The batch plans
|
|
25
|
+
* derived here carry no gates; the rule survives for a caller that hands the runtime a gated plan.
|
|
34
26
|
*/
|
|
35
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.IN_SENTINEL = void 0;
|
|
28
|
+
exports.IN_SENTINEL = exports.ENTITY_ROOT = void 0;
|
|
37
29
|
exports.literalize = literalize;
|
|
38
30
|
exports.pgTypeSpecimen = pgTypeSpecimen;
|
|
39
31
|
exports.pkPort = pkPort;
|
|
40
32
|
exports.compileWriteNode = compileWriteNode;
|
|
41
|
-
exports.deriveTransactionPlan = deriveTransactionPlan;
|
|
42
33
|
exports.deriveBatchPlan = deriveBatchPlan;
|
|
43
|
-
exports.executeTransaction = executeTransaction;
|
|
44
|
-
exports.countingDriver = countingDriver;
|
|
45
34
|
exports.renderTxStatement = renderTxStatement;
|
|
46
35
|
exports.executeTransactionAsync = executeTransactionAsync;
|
|
47
36
|
const runtime_1 = require("behavior-contracts/runtime");
|
|
@@ -61,7 +50,6 @@ const leaves_1 = require("../leaves");
|
|
|
61
50
|
const tx_options_1 = require("../tx-options");
|
|
62
51
|
const connection_errors_1 = require("../../connection-errors");
|
|
63
52
|
const DBConditions_1 = require("../../DBConditions");
|
|
64
|
-
const writes_1 = require("../writes");
|
|
65
53
|
/**
|
|
66
54
|
* Encode a CONCRETE value into a bc Expression IR node that `evaluateExpression` returns VERBATIM.
|
|
67
55
|
*
|
|
@@ -103,126 +91,11 @@ function literalize(value) {
|
|
|
103
91
|
}
|
|
104
92
|
return value;
|
|
105
93
|
}
|
|
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
94
|
/**
|
|
117
|
-
* The
|
|
118
|
-
*
|
|
119
|
-
* MySQL emits `INSERT IGNORE INTO … VALUES (…)`.
|
|
95
|
+
* The reserved scope key the tx runtime binds the plan's `entityFrom` row under, so a later statement
|
|
96
|
+
* that references the written row resolves against it. Reserved: no input field may use this name.
|
|
120
97
|
*/
|
|
121
|
-
|
|
122
|
-
const cols = `(${columns.join(', ')})`;
|
|
123
|
-
if (dialect === 'mysql')
|
|
124
|
-
return `INSERT IGNORE INTO ${table} ${cols} VALUES (${placeholders})`;
|
|
125
|
-
return `INSERT INTO ${table} ${cols} VALUES (${placeholders}) ON CONFLICT DO NOTHING`;
|
|
126
|
-
}
|
|
127
|
-
function makeIdGen() {
|
|
128
|
-
let n = 0;
|
|
129
|
-
return (role) => `tx_${role}_${n++}`;
|
|
130
|
-
}
|
|
131
|
-
/** `requires` → a gate-first existence probe: `SELECT 1 FROM <table> WHERE k1 = ? AND …`. */
|
|
132
|
-
function compileRequires(e, nextId) {
|
|
133
|
-
const cols = Object.keys(e.keys);
|
|
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}` };
|
|
219
|
-
}
|
|
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). */
|
|
98
|
+
exports.ENTITY_ROOT = '__entity';
|
|
226
99
|
exports.IN_SENTINEL = '@in';
|
|
227
100
|
function stringPort(ports, name) {
|
|
228
101
|
const v = ports[name];
|
|
@@ -313,21 +186,6 @@ function v1ConditionText(conditions) {
|
|
|
313
186
|
const probe = [];
|
|
314
187
|
return new DBConditions_1.DBConditions(conditions).compile(probe);
|
|
315
188
|
}
|
|
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
189
|
/**
|
|
332
190
|
* Lower one where-member Expression node → a `<sql, params, columns>` WHERE fragment (deferred
|
|
333
191
|
* params). `columns[i]` is the table column the i-th emitted param binds — parallel to `params` — so
|
|
@@ -552,7 +410,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
552
410
|
if (stringPort(ports, 'batch') === 'true') {
|
|
553
411
|
if (dialect === 'postgres') {
|
|
554
412
|
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
|
|
413
|
+
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
414
|
return pgBatchInsert(table, sorted, values, ports, resolveColumnType, dialect);
|
|
557
415
|
}
|
|
558
416
|
const shapeOpts = { tableName: table, columns: sorted, records: [], ...onConflictJsonOpts(ports, sorted), ...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}) };
|
|
@@ -599,7 +457,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
599
457
|
const updateCols = [...setCols].sort();
|
|
600
458
|
if (dialect === 'postgres') {
|
|
601
459
|
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
|
|
460
|
+
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
461
|
return pgBatchUpdate(table, keyCols, updateCols, key, set, ports, resolveColumnType, dialect);
|
|
604
462
|
}
|
|
605
463
|
const shapeOpts = { tableName: table, keyColumns: keyCols, updateColumns: updateCols, records: [], ...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}) };
|
|
@@ -645,149 +503,9 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
645
503
|
throw new Error(`compileWriteNode: catalog component '${component}' has no write compile (SQL writes: Insert/Update/Delete)`);
|
|
646
504
|
}
|
|
647
505
|
}
|
|
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' };
|
|
506
|
+
function makeIdGen() {
|
|
507
|
+
let n = 0;
|
|
508
|
+
return (role) => `tx_${role}_${n++}`;
|
|
791
509
|
}
|
|
792
510
|
/**
|
|
793
511
|
* Lower a list of CONCRETE batch-write ops (createMany / updateMany / deleteMany — each already
|
|
@@ -819,67 +537,6 @@ function deriveBatchPlan(phase, ops) {
|
|
|
819
537
|
// "all created rows" result while `expectedDbState` proves the persisted state.
|
|
820
538
|
return { phase, entityFrom: null, statements, onIdempotentHit: 'rollback' };
|
|
821
539
|
}
|
|
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);
|
|
882
|
-
}
|
|
883
540
|
/** bc evaluates ints to bigint; convert a rendered param to a driver-bindable value. */
|
|
884
541
|
function toDriverParam(v, dialect) {
|
|
885
542
|
if (typeof v === 'bigint') {
|
|
@@ -917,17 +574,6 @@ function renderStatement(op, scope, dialect) {
|
|
|
917
574
|
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
918
575
|
return { sql: (0, handler_1.renderPlaceholders)(sql, dialect), params };
|
|
919
576
|
}
|
|
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
577
|
function gateShortCircuit(gate, result) {
|
|
932
578
|
switch (gate) {
|
|
933
579
|
case 'existsElseRollback':
|
|
@@ -943,74 +589,6 @@ function gateShortCircuit(gate, result) {
|
|
|
943
589
|
throw new Error(`scp write: unknown gate rule '${String(gate)}'`);
|
|
944
590
|
}
|
|
945
591
|
}
|
|
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
592
|
/** Render one statement op to its dialect SQL text + params (exposed for golden tests). */
|
|
1015
593
|
function renderTxStatement(op, scope, dialect = 'sqlite') {
|
|
1016
594
|
return renderStatement(op, scope, dialect);
|
|
@@ -1038,7 +616,7 @@ async function execStatementAsync(ctx, op, scope, dialect) {
|
|
|
1038
616
|
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
1039
617
|
// A SELECT/RETURNING reads rows; a bare write returns the `[{changes,…}]` summary.
|
|
1040
618
|
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:
|
|
619
|
+
const out = await (0, leaves_1.executeSQLAsync)({ sql, params, write: { returning: hasReturn } }, { execAsync: ctx, dialect });
|
|
1042
620
|
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
1043
621
|
}
|
|
1044
622
|
/**
|
|
@@ -1108,7 +686,7 @@ function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
|
1108
686
|
if (stmt.id === plan.entityFrom) {
|
|
1109
687
|
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
1110
688
|
if (entity !== null)
|
|
1111
|
-
scope[
|
|
689
|
+
scope[exports.ENTITY_ROOT] = entity;
|
|
1112
690
|
}
|
|
1113
691
|
if (stmt.binds !== undefined && result.rows.length > 0) {
|
|
1114
692
|
scope[stmt.binds] = result.rows[0];
|