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.
Files changed (105) hide show
  1. package/README.md +2 -1
  2. package/dist/DBModel.d.ts.map +1 -1
  3. package/dist/DBModel.js +3 -0
  4. package/dist/DBModel.js.map +1 -1
  5. package/dist/Middleware.d.ts +19 -2
  6. package/dist/Middleware.d.ts.map +1 -1
  7. package/dist/Middleware.js +1 -0
  8. package/dist/Middleware.js.map +1 -1
  9. package/dist/decorators.d.ts.map +1 -1
  10. package/dist/decorators.js +8 -0
  11. package/dist/decorators.js.map +1 -1
  12. package/dist/drivers/MysqlHelper.d.ts +1 -1
  13. package/dist/drivers/MysqlHelper.d.ts.map +1 -1
  14. package/dist/drivers/MysqlHelper.js.map +1 -1
  15. package/dist/drivers/PostgresHelper.d.ts +1 -1
  16. package/dist/drivers/PostgresHelper.d.ts.map +1 -1
  17. package/dist/drivers/PostgresHelper.js.map +1 -1
  18. package/dist/drivers/SqliteHelper.d.ts +1 -1
  19. package/dist/drivers/SqliteHelper.d.ts.map +1 -1
  20. package/dist/drivers/SqliteHelper.js.map +1 -1
  21. package/dist/index.cjs +99 -33
  22. package/dist/scp/connection-routing.d.ts +38 -1
  23. package/dist/scp/connection-routing.d.ts.map +1 -1
  24. package/dist/scp/connection-routing.js +64 -5
  25. package/dist/scp/connection-routing.js.map +1 -1
  26. package/dist/scp/dbmodel-runtime.d.ts +2 -1
  27. package/dist/scp/dbmodel-runtime.d.ts.map +1 -1
  28. package/dist/scp/dbmodel-runtime.js +2 -1
  29. package/dist/scp/dbmodel-runtime.js.map +1 -1
  30. package/dist/scp/decorator-adapter.d.ts +22 -0
  31. package/dist/scp/decorator-adapter.d.ts.map +1 -1
  32. package/dist/scp/decorator-adapter.js +26 -0
  33. package/dist/scp/decorator-adapter.js.map +1 -1
  34. package/dist/scp/emit/emitter.d.ts +21 -10
  35. package/dist/scp/emit/emitter.d.ts.map +1 -1
  36. package/dist/scp/emit/emitter.js +119 -51
  37. package/dist/scp/emit/emitter.js.map +1 -1
  38. package/dist/scp/emit/endpoint.d.ts +3 -8
  39. package/dist/scp/emit/endpoint.d.ts.map +1 -1
  40. package/dist/scp/exec-context.d.ts +39 -17
  41. package/dist/scp/exec-context.d.ts.map +1 -1
  42. package/dist/scp/exec-context.js +69 -35
  43. package/dist/scp/exec-context.js.map +1 -1
  44. package/dist/scp/grouping.d.ts.map +1 -1
  45. package/dist/scp/grouping.js +0 -0
  46. package/dist/scp/grouping.js.map +1 -1
  47. package/dist/scp/index.cjs +251 -629
  48. package/dist/scp/index.d.ts +6 -11
  49. package/dist/scp/index.d.ts.map +1 -1
  50. package/dist/scp/index.js +10 -28
  51. package/dist/scp/index.js.map +1 -1
  52. package/dist/scp/index.mjs +251 -617
  53. package/dist/scp/leaf-transport.d.ts +127 -39
  54. package/dist/scp/leaf-transport.d.ts.map +1 -1
  55. package/dist/scp/leaf-transport.js +30 -24
  56. package/dist/scp/leaf-transport.js.map +1 -1
  57. package/dist/scp/leaves.d.ts +57 -20
  58. package/dist/scp/leaves.d.ts.map +1 -1
  59. package/dist/scp/leaves.js +224 -65
  60. package/dist/scp/leaves.js.map +1 -1
  61. package/dist/scp/makesql/compile-select.d.ts +28 -1
  62. package/dist/scp/makesql/compile-select.d.ts.map +1 -1
  63. package/dist/scp/makesql/compile-select.js +19 -17
  64. package/dist/scp/makesql/compile-select.js.map +1 -1
  65. package/dist/scp/makesql/index.d.ts +2 -2
  66. package/dist/scp/makesql/index.d.ts.map +1 -1
  67. package/dist/scp/makesql/index.js +4 -6
  68. package/dist/scp/makesql/index.js.map +1 -1
  69. package/dist/scp/makesql/tx.d.ts +27 -69
  70. package/dist/scp/makesql/tx.d.ts.map +1 -1
  71. package/dist/scp/makesql/tx.js +26 -448
  72. package/dist/scp/makesql/tx.js.map +1 -1
  73. package/dist/scp/middleware.d.ts +4 -4
  74. package/dist/scp/middleware.d.ts.map +1 -1
  75. package/dist/scp/middleware.js +2 -2
  76. package/dist/scp/middleware.js.map +1 -1
  77. package/dist/scp/relation.d.ts +31 -12
  78. package/dist/scp/relation.d.ts.map +1 -1
  79. package/dist/scp/relation.js +16 -6
  80. package/dist/scp/relation.js.map +1 -1
  81. package/dist/scp/typed-object.d.ts +23 -19
  82. package/dist/scp/typed-object.d.ts.map +1 -1
  83. package/dist/scp/typed-object.js +24 -30
  84. package/dist/scp/typed-object.js.map +1 -1
  85. package/dist/scp/write-bundle.d.ts +21 -70
  86. package/dist/scp/write-bundle.d.ts.map +1 -1
  87. package/dist/scp/write-bundle.js +15 -79
  88. package/dist/scp/write-bundle.js.map +1 -1
  89. package/dist/types.d.ts +12 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js.map +1 -1
  92. package/docs/architecture.md +452 -0
  93. package/docs/design/phase-a-execution-context.md +2 -0
  94. package/package.json +14 -3
  95. package/dist/scp/writes.d.ts +0 -251
  96. package/dist/scp/writes.d.ts.map +0 -1
  97. package/dist/scp/writes.js +0 -176
  98. package/dist/scp/writes.js.map +0 -1
  99. package/docs/proposal/contracts-architecture-feasibility.md +0 -328
  100. package/docs/proposal/contracts-architecture.md +0 -579
  101. package/docs/proposal/litedbmodel-v2-scp-architecture.md +0 -433
  102. package/docs/proposal/sql-dynamic-expansion-spec.md +0 -175
  103. package/docs/proposal/v0-coverage-matrix.md +0 -157
  104. package/docs/proposal/v2-completeness-plan.md +0 -90
  105. package/docs/proposal/v2-sql-parity-checklist.md +0 -162
@@ -1,47 +1,36 @@
1
1
  "use strict";
2
2
  /**
3
- * litedbmodel v2 SCP — write-time relations → ordered SQL transaction plan, re-expressed on
4
- * the `makeSQL` model (epic #43/#45 Phase B; spec §6 / §14). This REPLACES the reduced-spine
5
- * `../write-plan.ts` + `../write-runtime.ts` (`CompiledOperation` + `renderOperation` +
6
- * FragmentTree). No reduced IR is emitted anywhere: every statement's SQL is COMPLETE tuned
7
- * text (byte-identical to what the v1 write path sends — `SELECT 1 …`, `INSERT … ON CONFLICT
8
- * DO NOTHING`, `UPDATE … SET c = c + ?`, the outbox INSERT), and its `params` are closed-set
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
- * ## Gate-first is a real execution behavior (spec §6 "Gate First")
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
- * Each gate statement carries a {@link GateRule} the runtime evaluates AFTER executing it: a
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
- * ## 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.
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 tuned guard INSERT text for a gate-first `idempotency`/`unique` statement — byte-identical
118
- * to the v1 write path: SQLite/Postgres emit `INSERT INTO … VALUES (…) ON CONFLICT DO NOTHING`;
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
- function guardInsert(dialect, table, columns, placeholders) {
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/compileWriteBundle (the decorator-adapter write path).`);
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/compileWriteBundle (the decorator-adapter write path).`);
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 compileWriteGroup(base, lifecycle, nextId, dialect, seqRef) {
649
- const e = lifecycle.effects;
650
- const nodes = [];
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: true, returning: hasReturn, bigint: false }, { execAsync: ctx, dialect });
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[writes_1.ENTITY_ROOT] = entity;
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];