litedbmodel 2.0.0 → 2.0.2
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/dist/scp/authoring-sql.d.ts +78 -0
- package/dist/scp/authoring-sql.d.ts.map +1 -1
- package/dist/scp/authoring-sql.js +110 -2
- package/dist/scp/authoring-sql.js.map +1 -1
- package/dist/scp/catalog.d.ts +5 -2
- package/dist/scp/catalog.d.ts.map +1 -1
- package/dist/scp/catalog.js +32 -1
- package/dist/scp/catalog.js.map +1 -1
- package/dist/scp/codegen.d.ts +143 -85
- package/dist/scp/codegen.d.ts.map +1 -1
- package/dist/scp/codegen.js +374 -90
- package/dist/scp/codegen.js.map +1 -1
- package/dist/scp/coltype.d.ts +41 -0
- package/dist/scp/coltype.d.ts.map +1 -0
- package/dist/scp/coltype.js +188 -0
- package/dist/scp/coltype.js.map +1 -0
- package/dist/scp/dialect.d.ts +12 -3
- package/dist/scp/dialect.d.ts.map +1 -1
- package/dist/scp/dialect.js +12 -3
- package/dist/scp/dialect.js.map +1 -1
- package/dist/scp/find-filter-guard.d.ts +68 -0
- package/dist/scp/find-filter-guard.d.ts.map +1 -0
- package/dist/scp/find-filter-guard.js +88 -0
- package/dist/scp/find-filter-guard.js.map +1 -0
- package/dist/scp/fragment/assemble.d.ts +61 -0
- package/dist/scp/fragment/assemble.d.ts.map +1 -0
- package/dist/scp/fragment/assemble.js +156 -0
- package/dist/scp/fragment/assemble.js.map +1 -0
- package/dist/scp/fragment/compile-pg.d.ts +107 -0
- package/dist/scp/fragment/compile-pg.d.ts.map +1 -0
- package/dist/scp/fragment/compile-pg.js +142 -0
- package/dist/scp/fragment/compile-pg.js.map +1 -0
- package/dist/scp/fragment/index.d.ts +16 -0
- package/dist/scp/fragment/index.d.ts.map +1 -0
- package/dist/scp/fragment/index.js +34 -0
- package/dist/scp/fragment/index.js.map +1 -0
- package/dist/scp/fragment/model.d.ts +119 -0
- package/dist/scp/fragment/model.d.ts.map +1 -0
- package/dist/scp/fragment/model.js +66 -0
- package/dist/scp/fragment/model.js.map +1 -0
- package/dist/scp/fragment/pg-array-type.d.ts +13 -0
- package/dist/scp/fragment/pg-array-type.d.ts.map +1 -0
- package/dist/scp/fragment/pg-array-type.js +34 -0
- package/dist/scp/fragment/pg-array-type.js.map +1 -0
- package/dist/scp/guard.d.ts +2 -6
- package/dist/scp/guard.d.ts.map +1 -1
- package/dist/scp/guard.js +19 -22
- package/dist/scp/guard.js.map +1 -1
- package/dist/scp/index.cjs +18695 -3921
- package/dist/scp/index.d.ts +41 -24
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +134 -49
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +4830 -1432
- package/dist/scp/makesql/authoring-compile.d.ts +90 -0
- package/dist/scp/makesql/authoring-compile.d.ts.map +1 -0
- package/dist/scp/makesql/authoring-compile.js +494 -0
- package/dist/scp/makesql/authoring-compile.js.map +1 -0
- package/dist/scp/makesql/compile-crud.d.ts +133 -0
- package/dist/scp/makesql/compile-crud.d.ts.map +1 -0
- package/dist/scp/makesql/compile-crud.js +288 -0
- package/dist/scp/makesql/compile-crud.js.map +1 -0
- package/dist/scp/makesql/compile-relation.d.ts +157 -0
- package/dist/scp/makesql/compile-relation.d.ts.map +1 -0
- package/dist/scp/makesql/compile-relation.js +425 -0
- package/dist/scp/makesql/compile-relation.js.map +1 -0
- package/dist/scp/makesql/compile-select.d.ts +52 -0
- package/dist/scp/makesql/compile-select.d.ts.map +1 -0
- package/dist/scp/makesql/compile-select.js +58 -0
- package/dist/scp/makesql/compile-select.js.map +1 -0
- package/dist/scp/makesql/compile.d.ts +63 -0
- package/dist/scp/makesql/compile.d.ts.map +1 -0
- package/dist/scp/makesql/compile.js +99 -0
- package/dist/scp/makesql/compile.js.map +1 -0
- package/dist/scp/makesql/handler.d.ts +58 -0
- package/dist/scp/makesql/handler.d.ts.map +1 -0
- package/dist/scp/makesql/handler.js +113 -0
- package/dist/scp/makesql/handler.js.map +1 -0
- package/dist/scp/makesql/index.d.ts +33 -0
- package/dist/scp/makesql/index.d.ts.map +1 -0
- package/dist/scp/makesql/index.js +114 -0
- package/dist/scp/makesql/index.js.map +1 -0
- package/dist/scp/makesql/ir.d.ts +42 -0
- package/dist/scp/makesql/ir.d.ts.map +1 -0
- package/dist/scp/makesql/ir.js +66 -0
- package/dist/scp/makesql/ir.js.map +1 -0
- package/dist/scp/makesql/json-array.d.ts +84 -0
- package/dist/scp/makesql/json-array.d.ts.map +1 -0
- package/dist/scp/makesql/json-array.js +118 -0
- package/dist/scp/makesql/json-array.js.map +1 -0
- package/dist/scp/makesql/json-batch.d.ts +102 -0
- package/dist/scp/makesql/json-batch.d.ts.map +1 -0
- package/dist/scp/makesql/json-batch.js +269 -0
- package/dist/scp/makesql/json-batch.js.map +1 -0
- package/dist/scp/makesql/makesql.d.ts +99 -0
- package/dist/scp/makesql/makesql.d.ts.map +1 -0
- package/dist/scp/makesql/makesql.js +122 -0
- package/dist/scp/makesql/makesql.js.map +1 -0
- package/dist/scp/makesql/outtype.d.ts +44 -0
- package/dist/scp/makesql/outtype.d.ts.map +1 -0
- package/dist/scp/makesql/outtype.js +197 -0
- package/dist/scp/makesql/outtype.js.map +1 -0
- package/dist/scp/makesql/pool-executor.d.ts +47 -0
- package/dist/scp/makesql/pool-executor.d.ts.map +1 -0
- package/dist/scp/makesql/pool-executor.js +50 -0
- package/dist/scp/makesql/pool-executor.js.map +1 -0
- package/dist/scp/makesql/static-bundle.d.ts +187 -0
- package/dist/scp/makesql/static-bundle.d.ts.map +1 -0
- package/dist/scp/makesql/static-bundle.js +1113 -0
- package/dist/scp/makesql/static-bundle.js.map +1 -0
- package/dist/scp/makesql/tx.d.ts +216 -0
- package/dist/scp/makesql/tx.d.ts.map +1 -0
- package/dist/scp/makesql/tx.js +788 -0
- package/dist/scp/makesql/tx.js.map +1 -0
- package/dist/scp/makesql/writeouttype.d.ts +66 -0
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -0
- package/dist/scp/makesql/writeouttype.js +195 -0
- package/dist/scp/makesql/writeouttype.js.map +1 -0
- package/dist/scp/relation.d.ts +86 -74
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +186 -120
- package/dist/scp/relation.js.map +1 -1
- package/dist/scp/runtime.d.ts +158 -211
- package/dist/scp/runtime.d.ts.map +1 -1
- package/dist/scp/runtime.js +250 -515
- package/dist/scp/runtime.js.map +1 -1
- package/dist/scp/typed-object.d.ts +12 -1
- package/dist/scp/typed-object.d.ts.map +1 -1
- package/dist/scp/typed-object.js +25 -4
- package/dist/scp/typed-object.js.map +1 -1
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +51 -0
- package/docs/proposal/v0-coverage-matrix.md +157 -0
- package/docs/proposal/v2-completeness-plan.md +90 -0
- package/docs/proposal/v2-sql-parity-checklist.md +162 -0
- package/package.json +3 -3
- package/dist/scp/bridge.d.ts +0 -64
- package/dist/scp/bridge.d.ts.map +0 -1
- package/dist/scp/bridge.js +0 -329
- package/dist/scp/bridge.js.map +0 -1
- package/dist/scp/compile-dialect.d.ts +0 -39
- package/dist/scp/compile-dialect.d.ts.map +0 -1
- package/dist/scp/compile-dialect.js +0 -62
- package/dist/scp/compile-dialect.js.map +0 -1
- package/dist/scp/compile-sqlite.d.ts +0 -114
- package/dist/scp/compile-sqlite.d.ts.map +0 -1
- package/dist/scp/compile-sqlite.js +0 -149
- package/dist/scp/compile-sqlite.js.map +0 -1
- package/dist/scp/ir.d.ts +0 -92
- package/dist/scp/ir.d.ts.map +0 -1
- package/dist/scp/ir.js +0 -28
- package/dist/scp/ir.js.map +0 -1
- package/dist/scp/render.d.ts +0 -40
- package/dist/scp/render.d.ts.map +0 -1
- package/dist/scp/render.js +0 -170
- package/dist/scp/render.js.map +0 -1
- package/dist/scp/write-plan.d.ts +0 -161
- package/dist/scp/write-plan.d.ts.map +0 -1
- package/dist/scp/write-plan.js +0 -528
- package/dist/scp/write-plan.js.map +0 -1
- package/dist/scp/write-runtime.d.ts +0 -80
- package/dist/scp/write-runtime.d.ts.map +0 -1
- package/dist/scp/write-runtime.js +0 -167
- package/dist/scp/write-runtime.js.map +0 -1
package/dist/scp/runtime.js
CHANGED
|
@@ -1,417 +1,180 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* litedbmodel v2 SCP — the thin TS runtime (WS3, #23;
|
|
3
|
+
* litedbmodel v2 SCP — the thin TS runtime on the STATIC makeSQL bundle (WS3, #23; makeSQL
|
|
4
|
+
* flip, epic #43/#45 Phase B; spec §3 / §10 / §11).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* makeSQL is the SOLE SCP compile/execute path. A behavior method compiles (SYMBOLICALLY — no
|
|
7
|
+
* concrete input) to a {@link SqlBundle}:
|
|
8
|
+
* - a READ bundle carries a {@link ReadGraph} — the surrogate bc `ComponentGraphIR` (each SQL
|
|
9
|
+
* node → a `makeSQL` node) + per-node static statement templates. bc `runBehavior` drives
|
|
10
|
+
* map iteration / wire binding / Φ output; the makeSQL handler renders each node's statements
|
|
11
|
+
* (value-specs + skip evaluated per-input) and executes real SQL ("bc composes, makeSQL
|
|
12
|
+
* executes"). Read-relation batch ops ride the bundle too.
|
|
13
|
+
* - a WRITE bundle carries a single `makeSQL` statement (the base write) + the derived gate-first
|
|
14
|
+
* {@link TransactionPlan} for a Command with write-time relations.
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* → assembly
|
|
14
|
-
*
|
|
15
|
-
* bc's `runBehavior` owns the orchestration (which node runs when, map iteration, wire
|
|
16
|
-
* binding, output merge). But the SQL-structural ports (`where` fragment tree, `limit`, …)
|
|
17
|
-
* MUST NOT go through bc's generic port evaluation — a `where` `{arr:[…]}` of comparison
|
|
18
|
-
* Expressions would evaluate to bare booleans (losing the SQL structure), and a SKIP guard
|
|
19
|
-
* over an ABSENT optional input would fail-closed with `UNKNOWN_BINDING`. So the runtime:
|
|
20
|
-
*
|
|
21
|
-
* 1. Backend-Compiles every SQL node up front ({@link import('./bridge').compileNode}) to a
|
|
22
|
-
* WS1 `CompiledOperation` (SQL text + fragment tree + closed-set param slots).
|
|
23
|
-
* 2. Rewrites the IR into a SURROGATE graph: each SQL node keeps its `id` / wiring / `map`
|
|
24
|
-
* shape, but its ports collapse to ONE synthetic `__scope` port — a bc `{obj:…}` that
|
|
25
|
-
* re-exports every binding head the op references (`input` names, wire node ids, the
|
|
26
|
-
* `map` element `as`). bc evaluates `__scope` in ITS scope (so `input`, sibling results
|
|
27
|
-
* and the map element all resolve through bc's own machinery) and hands the plain scope
|
|
28
|
-
* to the handler. No SQL-structural port is ever evaluated by bc.
|
|
29
|
-
* 3. Each handler renders its pre-compiled op against `ports.__scope` via WS1's normative
|
|
30
|
-
* {@link renderOperation} (fragment select + array expand + Expression eval), binds the
|
|
31
|
-
* params through the driver, executes REAL SQL, and returns the row list (assembly).
|
|
16
|
+
* The reduced spine (`CompiledOperation` + `FragmentTree` + `renderOperation`) is RETIRED — this
|
|
17
|
+
* module carries no reduced IR. Reads execute via {@link import('./makesql/static-bundle')} (bc
|
|
18
|
+
* `runBehavior` + makeSQL handler); writes via {@link import('./makesql/tx')} (gate-first tx-DAG).
|
|
32
19
|
*
|
|
33
20
|
* ## Input normalization (SSoT — defaults live in the schema, not code)
|
|
34
21
|
*
|
|
35
|
-
* An OPTIONAL
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* (`required !== true` ⇒ optional), NOT an ad-hoc `?? null` in engine code: the schema is
|
|
39
|
-
* the single source of truth for which ports are optional.
|
|
22
|
+
* An OPTIONAL input head the caller omits is normalized to `null` (present-as-null) so a SKIP
|
|
23
|
+
* presence expression drops its fragment (absent-key SKIP). "Optional" comes from the bundle's
|
|
24
|
+
* `optionalHeads` (schema-optional + SKIP-guarded + every `refOpt` head) — the SSoT.
|
|
40
25
|
*/
|
|
41
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
27
|
exports.SqlFailure = void 0;
|
|
43
|
-
exports.buildHandlers = buildHandlers;
|
|
44
|
-
exports.normalizeInput = normalizeInput;
|
|
45
28
|
exports.executeBehavior = executeBehavior;
|
|
46
29
|
exports.compileBundle = compileBundle;
|
|
47
30
|
exports.executeBundle = executeBundle;
|
|
31
|
+
exports.executeBundleAsync = executeBundleAsync;
|
|
32
|
+
exports.executeBehaviorAsync = executeBehaviorAsync;
|
|
48
33
|
exports.compileWriteBundle = compileWriteBundle;
|
|
49
34
|
exports.compileCompositeWriteBundle = compileCompositeWriteBundle;
|
|
35
|
+
exports.compileCreateManyBundle = compileCreateManyBundle;
|
|
36
|
+
exports.compileUpdateManyBundle = compileUpdateManyBundle;
|
|
37
|
+
exports.compileDeleteManyBundle = compileDeleteManyBundle;
|
|
50
38
|
exports.executeCompositeCommand = executeCompositeCommand;
|
|
51
39
|
exports.executeCommand = executeCommand;
|
|
52
40
|
exports.executeTransactionBundle = executeTransactionBundle;
|
|
53
41
|
exports.read = read;
|
|
54
42
|
exports.readBundle = readBundle;
|
|
55
|
-
exports.resolveRelationViaPlan = resolveRelationViaPlan;
|
|
56
|
-
const behavior_contracts_1 = require("behavior-contracts");
|
|
57
|
-
const bridge_1 = require("./bridge");
|
|
58
|
-
const render_1 = require("./render");
|
|
59
43
|
const errors_1 = require("./errors");
|
|
60
44
|
Object.defineProperty(exports, "SqlFailure", { enumerable: true, get: function () { return errors_1.SqlFailure; } });
|
|
61
45
|
const relation_1 = require("./relation");
|
|
62
46
|
const typed_object_1 = require("./typed-object");
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
47
|
+
const static_bundle_1 = require("./makesql/static-bundle");
|
|
48
|
+
const tx_1 = require("./makesql/tx");
|
|
49
|
+
const compile_crud_1 = require("./makesql/compile-crud");
|
|
50
|
+
const makesql_1 = require("./makesql/makesql");
|
|
51
|
+
const writeouttype_1 = require("./makesql/writeouttype");
|
|
66
52
|
const writes_1 = require("./writes");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* objects are IN-list elements / already-flat scalars; strings / booleans / null pass
|
|
76
|
-
* through. This mirrors graphddb's `toPlain` boundary conversion.
|
|
77
|
-
*/
|
|
78
|
-
function toDriverParam(v) {
|
|
79
|
-
if (typeof v === 'bigint') {
|
|
80
|
-
if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER))
|
|
81
|
-
return Number(v);
|
|
82
|
-
return v; // better-sqlite3 binds a BigInt losslessly for i64-range values
|
|
83
|
-
}
|
|
84
|
-
return v;
|
|
85
|
-
}
|
|
86
|
-
// ── Ref-head collection (build the surrogate `__scope` port) ──────────────────
|
|
87
|
-
/** Collect every ref/refOpt path HEAD used inside an Expression IR node. */
|
|
88
|
-
function collectHeads(node, heads) {
|
|
89
|
-
if (node === null || typeof node !== 'object')
|
|
90
|
-
return;
|
|
91
|
-
if (Array.isArray(node)) {
|
|
92
|
-
for (const el of node)
|
|
93
|
-
collectHeads(el, heads);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const keys = Object.keys(node);
|
|
97
|
-
if (keys.length === 1 && (keys[0] === 'ref' || keys[0] === 'refOpt')) {
|
|
98
|
-
const path = node[keys[0]];
|
|
99
|
-
if (Array.isArray(path) && typeof path[0] === 'string')
|
|
100
|
-
heads.add(path[0]);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
for (const k of keys)
|
|
104
|
-
collectHeads(node[k], heads);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Collect every path HEAD accessed via `refOpt` in an Expression IR node. A `refOpt` head
|
|
108
|
-
* is OPTIONAL by construction — the author reached for `opt(…)` / `refOpt` precisely because
|
|
109
|
-
* the binding may be absent (a `coalesce`-defaulted LIMIT, a SKIP presence guard). It is
|
|
110
|
-
* therefore normalized to present-as-null (SSoT: the `refOpt` is the optionality
|
|
111
|
-
* declaration, not an ad-hoc code default).
|
|
112
|
-
*/
|
|
113
|
-
function collectRefOptHeads(node, heads) {
|
|
114
|
-
if (node === null || typeof node !== 'object')
|
|
115
|
-
return;
|
|
116
|
-
if (Array.isArray(node)) {
|
|
117
|
-
for (const el of node)
|
|
118
|
-
collectRefOptHeads(el, heads);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const keys = Object.keys(node);
|
|
122
|
-
if (keys.length === 1 && keys[0] === 'refOpt') {
|
|
123
|
-
const path = node.refOpt;
|
|
124
|
-
if (Array.isArray(path) && typeof path[0] === 'string')
|
|
125
|
-
heads.add(path[0]);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
for (const k of keys)
|
|
129
|
-
collectRefOptHeads(node[k], heads);
|
|
130
|
-
}
|
|
131
|
-
/** Collect ref heads from a whole fragment tree (its `when` guards + param slots). */
|
|
132
|
-
function collectFragmentHeads(node, heads) {
|
|
133
|
-
if ('connector' in node) {
|
|
134
|
-
for (const f of node.fragments)
|
|
135
|
-
collectFragmentHeads(f, heads);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (node.when !== undefined)
|
|
139
|
-
collectHeads(node.when, heads);
|
|
140
|
-
for (const p of node.params)
|
|
141
|
-
collectHeads(p, heads);
|
|
142
|
-
}
|
|
143
|
-
/** Every binding head a compiled op references (static params + fragment tree). */
|
|
144
|
-
function operationHeads(op) {
|
|
145
|
-
const heads = new Set();
|
|
146
|
-
for (const p of op.params)
|
|
147
|
-
collectHeads(p, heads);
|
|
148
|
-
if (op.where !== null)
|
|
149
|
-
collectFragmentHeads(op.where, heads);
|
|
150
|
-
return heads;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Collect the binding heads that live UNDER a SKIP guard: every head in a `when`-guarded
|
|
154
|
-
* fragment's guard OR its params. A SKIP guard is the structural declaration that its
|
|
155
|
-
* driving input is OPTIONAL (spec §7: `cond ? [...] : SKIP`) — so an absent such head is
|
|
156
|
-
* normalized to null (present-as-null), letting `refOpt` drop the fragment. This is
|
|
157
|
-
* schema/structure-driven, not an ad-hoc code default: the SKIP fragment IS the SSoT that
|
|
158
|
-
* the input is optional.
|
|
159
|
-
*/
|
|
160
|
-
function skipGuardedHeads(node, heads) {
|
|
161
|
-
if ('connector' in node) {
|
|
162
|
-
for (const f of node.fragments)
|
|
163
|
-
skipGuardedHeads(f, heads);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (node.when !== undefined) {
|
|
167
|
-
collectHeads(node.when, heads);
|
|
168
|
-
for (const p of node.params)
|
|
169
|
-
collectHeads(p, heads);
|
|
53
|
+
/** The primary catalog node of a component (the first non-cond, non-map body node). */
|
|
54
|
+
function primaryNodeOf(component) {
|
|
55
|
+
for (const n of component.body) {
|
|
56
|
+
if ('cond' in n)
|
|
57
|
+
continue;
|
|
58
|
+
if ('map' in n)
|
|
59
|
+
continue;
|
|
60
|
+
return n;
|
|
170
61
|
}
|
|
62
|
+
return undefined;
|
|
171
63
|
}
|
|
172
|
-
/**
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
/** Collect `refOpt` heads from a whole fragment tree (guards + param slots). */
|
|
180
|
-
function collectFragmentRefOptHeads(node, heads) {
|
|
181
|
-
if ('connector' in node) {
|
|
182
|
-
for (const f of node.fragments)
|
|
183
|
-
collectFragmentRefOptHeads(f, heads);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (node.when !== undefined)
|
|
187
|
-
collectRefOptHeads(node.when, heads);
|
|
188
|
-
for (const p of node.params)
|
|
189
|
-
collectRefOptHeads(p, heads);
|
|
64
|
+
/** Is a component's primary catalog node a write (Insert/Update/Delete)? */
|
|
65
|
+
function isWriteComponent(component) {
|
|
66
|
+
const p = primaryNodeOf(component);
|
|
67
|
+
if (p === undefined || 'map' in p)
|
|
68
|
+
return false;
|
|
69
|
+
const c = p.component;
|
|
70
|
+
return c === 'Insert' || c === 'Update' || c === 'Delete';
|
|
190
71
|
}
|
|
191
72
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* `refOpt` drops/defaults cleanly instead of failing on an absent head.
|
|
196
|
-
*/
|
|
197
|
-
function optionalHeadsOf(compiled) {
|
|
198
|
-
const heads = new Set();
|
|
199
|
-
for (const op of Object.values(compiled)) {
|
|
200
|
-
if (op.where !== null)
|
|
201
|
-
skipGuardedHeads(op.where, heads);
|
|
202
|
-
operationRefOptHeads(op, heads);
|
|
203
|
-
}
|
|
204
|
-
return heads;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Build the surrogate `__scope` port for a SQL node: a bc `{obj:{…}}` re-exporting each
|
|
208
|
-
* referenced head as `{ref:[head]}`. Excludes the IN-sentinel head (`@in` is a column
|
|
209
|
-
* marker, never a real binding). bc evaluates this in its own scope, so `input` names,
|
|
210
|
-
* wire node ids, and the `map` element `as` all resolve; the handler then renders the op
|
|
211
|
-
* against the resulting plain scope. A head that is genuinely absent surfaces as bc's
|
|
212
|
-
* `UNKNOWN_BINDING` — the runtime pre-normalizes OPTIONAL input heads to null so only a
|
|
213
|
-
* real wiring bug reaches that fail-closed path.
|
|
214
|
-
*/
|
|
215
|
-
function scopePort(op) {
|
|
216
|
-
const obj = {};
|
|
217
|
-
for (const head of operationHeads(op)) {
|
|
218
|
-
if (head === '@in')
|
|
219
|
-
continue; // IN-list sentinel column marker, not a binding
|
|
220
|
-
obj[head] = { ref: [head] };
|
|
221
|
-
}
|
|
222
|
-
return { obj };
|
|
223
|
-
}
|
|
224
|
-
/** Rewrite one body node to its surrogate (SQL nodes → single `__scope` port). */
|
|
225
|
-
function surrogateNode(node, compiled) {
|
|
226
|
-
if ('cond' in node)
|
|
227
|
-
return node; // pure Expression — bc evaluates it unchanged
|
|
228
|
-
if ('map' in node) {
|
|
229
|
-
const op = compiled[node.id];
|
|
230
|
-
if (op === undefined)
|
|
231
|
-
return node;
|
|
232
|
-
return { ...node, map: { ...node.map, ports: { [SCOPE_PORT]: scopePort(op) } } };
|
|
233
|
-
}
|
|
234
|
-
const op = compiled[node.id];
|
|
235
|
-
if (op === undefined)
|
|
236
|
-
return node;
|
|
237
|
-
return { ...node, ports: { [SCOPE_PORT]: scopePort(op) } };
|
|
238
|
-
}
|
|
239
|
-
/** Rewrite a whole component to its surrogate graph (structure + wiring preserved). */
|
|
240
|
-
function surrogateComponent(component, compiled) {
|
|
241
|
-
return { ...component, body: component.body.map((n) => surrogateNode(n, compiled)) };
|
|
242
|
-
}
|
|
243
|
-
// ── Handlers (render → execute → assembly) ────────────────────────────────────
|
|
244
|
-
/**
|
|
245
|
-
* Execute one rendered SQL statement against the driver and return the row list (assembly).
|
|
246
|
-
* A SELECT / RETURNING statement returns its rows; a non-returning write returns a
|
|
247
|
-
* single-row summary `[{ changes, lastInsertRowid }]` (the RETURNING-less write shape). A
|
|
248
|
-
* driver error is mapped ({@link mapSqliteError}) and returned as bc `{ error }` so the
|
|
249
|
-
* node's Policy Kind governs propagation.
|
|
250
|
-
*/
|
|
251
|
-
function executeRendered(db, op, scope, dialect) {
|
|
252
|
-
const rendered = (0, render_1.renderOperation)(op, scope, dialect);
|
|
253
|
-
const params = rendered.params.map(toDriverParam);
|
|
254
|
-
let stmt;
|
|
255
|
-
try {
|
|
256
|
-
stmt = db.prepare(rendered.sql);
|
|
257
|
-
}
|
|
258
|
-
catch (e) {
|
|
259
|
-
return { error: (0, errors_1.mapSqliteError)(e).message };
|
|
260
|
-
}
|
|
261
|
-
const hasReturn = op.component === 'Select' || /\breturning\b/i.test(rendered.sql);
|
|
262
|
-
try {
|
|
263
|
-
if (hasReturn) {
|
|
264
|
-
const rows = stmt.all(...params);
|
|
265
|
-
return { ok: rows };
|
|
266
|
-
}
|
|
267
|
-
const info = stmt.run(...params);
|
|
268
|
-
return {
|
|
269
|
-
ok: [{ changes: info.changes, lastInsertRowid: toDriverParam(BigInt(info.lastInsertRowid)) }],
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
catch (e) {
|
|
273
|
-
return { error: (0, errors_1.mapSqliteError)(e).message };
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Build the SQL handler registry: one handler per SQL Catalog name (spec §11 item 4).
|
|
73
|
+
* Execute a published behavior contract (WS2 {@link BehaviorModelContract}) end-to-end via the
|
|
74
|
+
* static makeSQL bundle. Returns the component's output (a read Φ output / row list, or a write's
|
|
75
|
+
* RETURNING rows / `[{changes, lastInsertRowid}]` summary).
|
|
278
76
|
*
|
|
279
|
-
*
|
|
280
|
-
* generated `bind(handlers)` accessor — the generated module bakes the surrogate IR literal and
|
|
281
|
-
* calls the SAME `runBehavior`, so pairing it with THIS handler builder makes the codegen output
|
|
282
|
-
* byte-identical to {@link executeBundle} by construction (not a parallel reimplementation).
|
|
283
|
-
*/
|
|
284
|
-
function buildHandlers(db, compiled, dialect) {
|
|
285
|
-
const handle = (ports, ctx) => {
|
|
286
|
-
const op = compiled[ctx.nodeId];
|
|
287
|
-
if (op === undefined) {
|
|
288
|
-
return { error: `scp runtime: no compiled operation for node '${ctx.nodeId}' (${ctx.component})` };
|
|
289
|
-
}
|
|
290
|
-
const scope = ports[SCOPE_PORT];
|
|
291
|
-
if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
|
|
292
|
-
return { error: `scp runtime: node '${ctx.nodeId}' surrogate scope did not evaluate to an object` };
|
|
293
|
-
}
|
|
294
|
-
return executeRendered(db, op, scope, dialect);
|
|
295
|
-
};
|
|
296
|
-
// One binding per SQL CRUD Catalog name (all share the render→execute handler; the
|
|
297
|
-
// pre-compiled op keyed by nodeId already encodes the per-node operation).
|
|
298
|
-
return { Select: handle, Insert: handle, Update: handle, Delete: handle };
|
|
299
|
-
}
|
|
300
|
-
// ── Input normalization (schema-driven — SSoT) ────────────────────────────────
|
|
301
|
-
/**
|
|
302
|
-
* Normalize the caller input to `null` (present-as-null) for every OPTIONAL binding the
|
|
303
|
-
* caller omitted, so a SKIP guard using `refOpt` evaluates to `false` and drops its
|
|
304
|
-
* fragment (absent-key SKIP). "Optional" is determined from the SSoT, NOT an ad-hoc code
|
|
305
|
-
* default — a head is optional iff EITHER (a) the component's Input Port schema marks the
|
|
306
|
-
* port `required !== true`, OR (b) the head is SKIP-guarded in a compiled fragment (the
|
|
307
|
-
* SKIP fragment declares its driving input optional — spec §7). A REQUIRED, non-SKIP head
|
|
308
|
-
* that is missing is left absent so a real wiring bug surfaces loudly as bc's
|
|
309
|
-
* `UNKNOWN_BINDING` rather than being silently defaulted.
|
|
310
|
-
*/
|
|
311
|
-
function normalizeInput(component, optionalHeads, input) {
|
|
312
|
-
const out = { ...input };
|
|
313
|
-
for (const [port, schema] of Object.entries(component.inputPorts)) {
|
|
314
|
-
if (schema.required !== true && !(port in out))
|
|
315
|
-
out[port] = null;
|
|
316
|
-
}
|
|
317
|
-
for (const head of optionalHeads) {
|
|
318
|
-
if (!(head in out))
|
|
319
|
-
out[head] = null;
|
|
320
|
-
}
|
|
321
|
-
return out;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Execute a published behavior contract (WS2 {@link BehaviorModelContract}) end-to-end:
|
|
325
|
-
* Backend-Compile every SQL node → surrogate IR → bc `runBehavior` (plan / map / wire /
|
|
326
|
-
* output) with SQL handlers → REAL SQLite execution → assembled Φ output.
|
|
327
|
-
*
|
|
328
|
-
* This is α's vertical slice: authoring → IR → Backend-Compile → thin runtime → real
|
|
329
|
-
* SQLite → assembly. The returned Value is the component's `output` (Φ merge) with each SQL
|
|
330
|
-
* node's slot filled by its executed row list.
|
|
331
|
-
*
|
|
332
|
-
* @throws {SqlFailure} a mapped driver failure re-surfaced at the boundary (a `fail`-policy
|
|
333
|
-
* node re-throws through `runPlan` as `OP_FAILED`; the runtime unwraps it back to the
|
|
334
|
-
* structured {@link SqlFailure} so the caller sees `kind` / `policy` / `sqliteCode`).
|
|
77
|
+
* @throws {SqlFailure} a mapped driver failure re-surfaced at the boundary.
|
|
335
78
|
*/
|
|
336
79
|
function executeBehavior(contract, input, options) {
|
|
337
80
|
return executeBundle(compileBundle(contract, options.entry, options.relations, options.dialect), input, options);
|
|
338
81
|
}
|
|
339
82
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* emits pure JSON: no TS runtime state is captured, so `JSON.stringify(bundle)` round-trips
|
|
344
|
-
* losslessly and a thin per-language runtime can execute it with bc runtime-core + a SQL
|
|
345
|
-
* handler alone (proven by the bundle round-trip test).
|
|
83
|
+
* Compile ONE behavior method into the serializable STATIC {@link SqlBundle} (spec §8) — the
|
|
84
|
+
* published multi-language artifact. SYMBOLIC (no concrete input). A read method compiles to a
|
|
85
|
+
* {@link ReadGraph}; a single-write method to a `makeSQL` statement. Pure JSON throughout.
|
|
346
86
|
*/
|
|
347
|
-
function compileBundle(contract, entry, relations = [], dialectName = 'sqlite') {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
? contract.components.find((c) => c.name === entry)
|
|
351
|
-
: contract.components[0];
|
|
352
|
-
if (component === undefined) {
|
|
87
|
+
function compileBundle(contract, entry, relations = [], dialectName = 'sqlite', findFilterModel, resolveColumnType) {
|
|
88
|
+
const component = entry ? contract.components.find((c) => c.name === entry) : contract.components[0];
|
|
89
|
+
if (component === undefined)
|
|
353
90
|
throw new Error(`scp runtime: entry component '${entry ?? '<first>'}' not found in contract`);
|
|
354
|
-
}
|
|
355
|
-
const operations = {};
|
|
356
|
-
for (const [id, op] of (0, bridge_1.compileComponentNodes)(component, dialect))
|
|
357
|
-
operations[id] = op;
|
|
358
|
-
const surrogate = surrogateComponent(component, operations);
|
|
359
|
-
// Backend-Compile every relation declaration ONCE into a pure-JSON relation op (spec §8),
|
|
360
|
-
// dialect-aware (the relation batch SELECTs carry no dialect-divergent text today, but the
|
|
361
|
-
// decl is compiled through the SAME dialect-parameterized path for uniformity/WS7).
|
|
362
91
|
const relationOps = {};
|
|
363
92
|
for (const decl of relations) {
|
|
364
93
|
if (relationOps[decl.name] !== undefined) {
|
|
365
94
|
throw new Error(`scp runtime: duplicate relation declaration '${decl.name}'`);
|
|
366
95
|
}
|
|
367
|
-
relationOps[decl.name] = (0, relation_1.compileRelationOp)(decl);
|
|
96
|
+
relationOps[decl.name] = (0, relation_1.compileRelationOp)({ ...decl, dialect: decl.dialect ?? dialectName });
|
|
97
|
+
}
|
|
98
|
+
if (isWriteComponent(component)) {
|
|
99
|
+
const writeNode = primaryNodeOf(component);
|
|
100
|
+
const op = (0, tx_1.compileWriteNode)(writeNode, dialectName);
|
|
101
|
+
return {
|
|
102
|
+
dialect: dialectName,
|
|
103
|
+
name: component.name,
|
|
104
|
+
statement: { sql: op.sql, params: op.params },
|
|
105
|
+
optionalHeads: [],
|
|
106
|
+
relations: relationOps,
|
|
107
|
+
};
|
|
368
108
|
}
|
|
109
|
+
const readGraph = (0, static_bundle_1.compileReadGraph)(contract, dialectName, entry, findFilterModel, resolveColumnType);
|
|
369
110
|
return {
|
|
370
|
-
irVersion: contract.ir.irVersion,
|
|
371
|
-
exprVersion: contract.ir.exprVersion,
|
|
372
111
|
dialect: dialectName,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
optionalHeads:
|
|
112
|
+
name: readGraph.name,
|
|
113
|
+
readGraph,
|
|
114
|
+
optionalHeads: readGraph.optionalHeads,
|
|
376
115
|
relations: relationOps,
|
|
377
116
|
};
|
|
378
117
|
}
|
|
379
118
|
/**
|
|
380
|
-
* Execute a {@link SqlBundle} (the §8 published artifact) end-to-end
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* bundle + bc runtime-core, never re-running litedbmodel's Backend-Compile. The bundle
|
|
385
|
-
* round-trip test parses this from JSON (no TS state) to prove self-sufficiency (WS7).
|
|
119
|
+
* Execute a {@link SqlBundle} (the §8 published artifact) end-to-end via the static makeSQL
|
|
120
|
+
* runtime + REAL SQLite. Read bundles run the read graph via bc `runBehavior`; single-statement
|
|
121
|
+
* write bundles run the write (RETURNING rows / summary). The SAME code path a thin per-language
|
|
122
|
+
* runtime follows — it consumes ONLY the serialized bundle + bc, never re-running the compile.
|
|
386
123
|
*
|
|
387
124
|
* @throws {SqlFailure} a mapped driver failure re-surfaced at the boundary.
|
|
388
125
|
*/
|
|
389
126
|
function executeBundle(bundle, input, options) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const normalized = normalizeInput(surrogate, optionalHeads, input);
|
|
399
|
-
try {
|
|
400
|
-
return (0, behavior_contracts_1.runBehavior)(ir, handlers, normalized, surrogate.name);
|
|
127
|
+
if (bundle.statement !== undefined && bundle.transaction === undefined) {
|
|
128
|
+
const view = {
|
|
129
|
+
dialect: bundle.dialect,
|
|
130
|
+
name: bundle.name,
|
|
131
|
+
statements: [bundle.statement],
|
|
132
|
+
optionalHeads: bundle.optionalHeads,
|
|
133
|
+
};
|
|
134
|
+
return (0, static_bundle_1.executeStaticWrite)(view, input, options.db);
|
|
401
135
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// mapped-failure message. Re-surface the structured SqlFailure from the message-embedded
|
|
405
|
-
// code if present; otherwise re-throw verbatim.
|
|
406
|
-
throw reErrorToSqlFailure(e);
|
|
136
|
+
if (bundle.readGraph === undefined) {
|
|
137
|
+
throw new Error(`scp runtime: bundle '${bundle.name}' carries neither a read graph nor a write statement`);
|
|
407
138
|
}
|
|
139
|
+
return (0, static_bundle_1.executeReadGraph)(bundle.readGraph, input, options.db);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Execute a READ {@link SqlBundle} end-to-end via the ASYNC PG / MySQL execution model (#40): bc
|
|
143
|
+
* `runBehaviorAsync` drives map iteration / wire binding / Φ output and dispatches independent
|
|
144
|
+
* sibling read nodes in bounded parallel against the pooled async executor. This is the PRODUCTION
|
|
145
|
+
* live-DB read entry point — the async twin of {@link executeBundle} for read bundles.
|
|
146
|
+
*
|
|
147
|
+
* Byte-identical composition (SAME IR, SAME per-node SQL text + params, SAME Φ output) to the
|
|
148
|
+
* serial `executeBundle`; concurrency changes only the wall-clock. A single-relation (zero-sibling)
|
|
149
|
+
* read graph runs exactly one query, so it is identical to the serial path either way.
|
|
150
|
+
*
|
|
151
|
+
* A single-statement WRITE bundle is NOT accepted here — the write-tx path stays sync + serial
|
|
152
|
+
* (`executeBundle` / `executeTransactionBundle`); this async path is READ-only.
|
|
153
|
+
*
|
|
154
|
+
* @throws {SqlFailure} a mapped driver failure re-surfaced at the boundary.
|
|
155
|
+
*/
|
|
156
|
+
async function executeBundleAsync(bundle, input, options) {
|
|
157
|
+
if (bundle.readGraph === undefined) {
|
|
158
|
+
throw new Error(`scp runtime: executeBundleAsync requires a READ bundle ('${bundle.name}' carries no read graph); ` +
|
|
159
|
+
`the write-tx path stays synchronous + serial (use executeBundle / executeTransactionBundle).`);
|
|
160
|
+
}
|
|
161
|
+
return (0, static_bundle_1.executeReadGraphAsync)(bundle.readGraph, input, options.exec);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Compile + execute a READ behavior method via the ASYNC PG / MySQL execution model (#40). The
|
|
165
|
+
* async twin of {@link executeBehavior} for reads: compile the contract to a {@link SqlBundle}
|
|
166
|
+
* (SYMBOLIC), then run its read graph through {@link executeBundleAsync}.
|
|
167
|
+
*
|
|
168
|
+
* @throws {SqlFailure} a mapped driver failure re-surfaced at the boundary.
|
|
169
|
+
*/
|
|
170
|
+
async function executeBehaviorAsync(contract, input, options) {
|
|
171
|
+
return executeBundleAsync(compileBundle(contract, options.entry, options.relations, options.dialect), input, options);
|
|
408
172
|
}
|
|
409
173
|
// ── Write-time relations: Command bundle + 1-tx execution (WS5, #25 — spec §6) ──
|
|
410
174
|
/**
|
|
411
175
|
* Find the single base-write catalog node (`Insert`/`Update`/`Delete`) of a Command component.
|
|
412
|
-
* WS5 initial scope is a single-statement Command (spec §6/§13)
|
|
413
|
-
*
|
|
414
|
-
* derivation deferred to WS8 (rejected loudly, never half-built).
|
|
176
|
+
* WS5 initial scope is a single-statement Command (spec §6/§13). More than one write node ⇒ the
|
|
177
|
+
* multi-write DAG derivation (WS8, rejected loudly here).
|
|
415
178
|
*/
|
|
416
179
|
function baseWriteNodeOf(component) {
|
|
417
180
|
const writes = component.body.filter((n) => !('cond' in n) && !('map' in n) && (n.component === 'Insert' || n.component === 'Update' || n.component === 'Delete'));
|
|
@@ -425,98 +188,157 @@ function baseWriteNodeOf(component) {
|
|
|
425
188
|
return writes[0];
|
|
426
189
|
}
|
|
427
190
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* {@link deriveTransactionPlan} lowers the lifecycle's §6 effect arrays
|
|
432
|
-
*
|
|
433
|
-
* honors the SAME plan via {@link executeTransactionBundle} — no re-derivation.
|
|
434
|
-
*
|
|
435
|
-
* @throws if the method is not a Command, the phase lifecycle is not declared, the scope is not a
|
|
436
|
-
* single base write, or a `$.entity.*` reference lacks a RETURNING body (all fail-closed).
|
|
191
|
+
* Compile a Command method + its `entityWrites` save contract into a {@link SqlBundle} carrying
|
|
192
|
+
* the derived, gate-first write-time-relations {@link TransactionPlan} (spec §6/§8). The base
|
|
193
|
+
* write op is compiled ONCE (the SAME `compileWriteNode` — complete tuned SQL + deferred
|
|
194
|
+
* Expression-IR params); {@link deriveTransactionPlan} lowers the lifecycle's §6 effect arrays
|
|
195
|
+
* around it into the ordered statement list. Pure JSON; a WS7 runtime honors the SAME plan.
|
|
437
196
|
*/
|
|
438
|
-
function compileWriteBundle(contract, entry, writes, phase, dialectName = 'sqlite') {
|
|
439
|
-
const dialect = (0, dialect_1.dialectFor)(dialectName);
|
|
197
|
+
function compileWriteBundle(contract, entry, writes, phase, dialectName = 'sqlite', resolveColumnType) {
|
|
440
198
|
const component = contract.components.find((c) => c.name === entry);
|
|
441
|
-
if (component === undefined)
|
|
199
|
+
if (component === undefined)
|
|
442
200
|
throw new Error(`scp write: entry component '${entry}' not found in contract`);
|
|
443
|
-
}
|
|
444
201
|
const lifecycle = (0, writes_1.lifecycleFor)(writes, phase);
|
|
445
202
|
if (lifecycle === undefined) {
|
|
446
203
|
throw new Error(`scp write: the '${phase}' lifecycle is not declared in the entityWrites save contract`);
|
|
447
204
|
}
|
|
448
205
|
const writeNode = baseWriteNodeOf(component);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
operations[id] = op;
|
|
457
|
-
const surrogate = surrogateComponent(component, operations);
|
|
458
|
-
return {
|
|
459
|
-
irVersion: contract.ir.irVersion,
|
|
460
|
-
exprVersion: contract.ir.exprVersion,
|
|
206
|
+
// MySQL has no native RETURNING; when the base write is an INSERT…RETURNING carrying a PK
|
|
207
|
+
// descriptor, annotate it with the strip-before-execute PK hint so the mysql emulation re-selects
|
|
208
|
+
// by the REAL PK (not a hardcoded `id`). PG/SQLite keep the op verbatim (native RETURNING).
|
|
209
|
+
const baseOp = dialectName === 'mysql' ? (0, tx_1.mysqlPkHint)((0, tx_1.compileWriteNode)(writeNode, dialectName)) : (0, tx_1.compileWriteNode)(writeNode, dialectName);
|
|
210
|
+
const base = { op: baseOp, label: `${writeNode.component}` };
|
|
211
|
+
const plan = (0, tx_1.deriveTransactionPlan)(phase, [base], lifecycle, dialectName);
|
|
212
|
+
const bundle = {
|
|
461
213
|
dialect: dialectName,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
optionalHeads: [
|
|
214
|
+
name: component.name,
|
|
215
|
+
statement: { sql: baseOp.sql, params: baseOp.params },
|
|
216
|
+
optionalHeads: [],
|
|
465
217
|
relations: {},
|
|
466
218
|
transaction: plan,
|
|
467
219
|
};
|
|
220
|
+
// Codegen typed de-box (spec §4.1/§9): when the schema/DDL column-type SoT is supplied, annotate
|
|
221
|
+
// the write bundle with the TransactionResult `outputType` (entity/returnedRows rows typed via the
|
|
222
|
+
// resolver + the write's target/RETURNING) so bc's typed(-raw) emitters materialize a concrete
|
|
223
|
+
// struct for the result. Fail-closed inside `annotateWriteBundleOutType`. Absent resolver → the
|
|
224
|
+
// bundle stays un-annotated (interpret/boxed only, back-compat).
|
|
225
|
+
return resolveColumnType === undefined ? bundle : (0, writeouttype_1.annotateWriteBundleOutType)(bundle, resolveColumnType);
|
|
468
226
|
}
|
|
469
227
|
/**
|
|
470
|
-
*
|
|
471
|
-
* gate-first, TOPOLOGICALLY-ORDERED transaction plan (spec §6 nested write / §14
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* constraint and derives the correct ordered plan (deterministic, stable tie-break; a cycle or
|
|
476
|
-
* dangling ref is ESCALATED). The plan is pure JSON — the 5 language runtimes execute it verbatim
|
|
477
|
-
* (they already bind each statement's `binds` row into scope for downstream `$.ref` resolution).
|
|
478
|
-
*
|
|
479
|
-
* @throws if an entry is missing/not a single-write node, or the DAG cannot be derived (cycle /
|
|
480
|
-
* dangling `$.ref` / missing RETURNING on a referenced write) — all fail-closed.
|
|
228
|
+
* Compile a COMPOSITE (multi-write) Command into a {@link SqlBundle} carrying ONE derived,
|
|
229
|
+
* gate-first, TOPOLOGICALLY-ORDERED transaction plan (spec §6 nested write / §14). Each entry
|
|
230
|
+
* contributes a named base write + its effects; a later member depends on an earlier via
|
|
231
|
+
* `$.ref.<name>.*`. {@link deriveTransactionPlan} builds the DAG + gate-first constraint and
|
|
232
|
+
* derives the ordered plan; a cycle / dangling ref is ESCALATED. Pure JSON.
|
|
481
233
|
*/
|
|
482
234
|
function compileCompositeWriteBundle(contract, entries, phase, dialectName = 'sqlite') {
|
|
483
235
|
if (entries.length < 2) {
|
|
484
236
|
throw new Error('scp write: a composite write bundle needs at least 2 named write members (use compileWriteBundle for a single write).');
|
|
485
237
|
}
|
|
486
|
-
const dialect = (0, dialect_1.dialectFor)(dialectName);
|
|
487
238
|
const bases = [];
|
|
488
|
-
|
|
489
|
-
let
|
|
239
|
+
let firstBaseOp;
|
|
240
|
+
let firstName = '';
|
|
490
241
|
for (const e of entries) {
|
|
491
242
|
const component = contract.components.find((c) => c.name === e.entry);
|
|
492
243
|
if (component === undefined)
|
|
493
244
|
throw new Error(`scp write: entry component '${e.entry}' not found in contract`);
|
|
494
|
-
if (
|
|
495
|
-
|
|
245
|
+
if (firstBaseOp === undefined)
|
|
246
|
+
firstName = component.name;
|
|
496
247
|
const writeNode = baseWriteNodeOf(component);
|
|
497
|
-
const baseOp = (0,
|
|
248
|
+
const baseOp = (0, tx_1.compileWriteNode)(writeNode, dialectName);
|
|
249
|
+
if (firstBaseOp === undefined)
|
|
250
|
+
firstBaseOp = baseOp;
|
|
498
251
|
bases.push({ op: baseOp, label: `${writeNode.component} ${e.name}`, name: e.name, effects: e.lifecycle.effects });
|
|
499
|
-
for (const [id, op] of (0, bridge_1.compileComponentNodes)(component, dialect))
|
|
500
|
-
operations[`${e.name}.${id}`] = op;
|
|
501
252
|
}
|
|
502
|
-
|
|
503
|
-
const plan = (0, write_plan_1.deriveTransactionPlan)(phase, bases, { effects: {} }, dialect);
|
|
504
|
-
const surrogate = surrogateComponent(firstComponent, operations);
|
|
253
|
+
const plan = (0, tx_1.deriveTransactionPlan)(phase, bases, { effects: {} }, dialectName);
|
|
505
254
|
return {
|
|
506
|
-
irVersion: contract.ir.irVersion,
|
|
507
|
-
exprVersion: contract.ir.exprVersion,
|
|
508
255
|
dialect: dialectName,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
optionalHeads: [
|
|
256
|
+
name: firstName,
|
|
257
|
+
statement: { sql: firstBaseOp.sql, params: firstBaseOp.params },
|
|
258
|
+
optionalHeads: [],
|
|
512
259
|
relations: {},
|
|
513
260
|
transaction: plan,
|
|
514
261
|
};
|
|
515
262
|
}
|
|
263
|
+
// ── Batch writes: createMany / updateMany / deleteMany (a v1→v2 regression fix) ──
|
|
264
|
+
//
|
|
265
|
+
// A batch write is ONE LOGICAL operation that produces N grouped SQL statements (createMany with a
|
|
266
|
+
// heterogeneous column-set groups records into one INSERT per group, mirroring `DBModel._insert`;
|
|
267
|
+
// updateMany is one UNNEST/JSON/CASE statement; deleteMany is a PK-set IN-list DELETE). This is
|
|
268
|
+
// DISTINCT from the deferred composite multi-write DAG (`baseWriteNodeOf` still rejects >1 authored
|
|
269
|
+
// base write). The batch compilers (`compileInsertMany`/`compileUpdateMany`/`compileDeleteMany`) copy
|
|
270
|
+
// the v1 builders byte-for-byte; here they are lowered into a gate-free {@link TransactionPlan} of
|
|
271
|
+
// body statements ({@link deriveBatchPlan}), so ALL FIVE runtimes execute the multi-statement batch
|
|
272
|
+
// through the SAME per-statement tx loop with no runtime change (concrete params are literalized to
|
|
273
|
+
// bc IR so they survive the render pass — see `deriveBatchPlan`/`literalize`).
|
|
274
|
+
/** Flatten a batch compiler's `MakeSQL` component to a concrete `{ sql, params }` op. */
|
|
275
|
+
function flattenBatchOp(node, label) {
|
|
276
|
+
const assembled = (0, makesql_1.assembleMakeSQL)(node);
|
|
277
|
+
return { sql: assembled.sql, params: assembled.params, label };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Compile a `createMany` into a batch write {@link SqlBundle} carrying a gate-free
|
|
281
|
+
* {@link TransactionPlan}. Heterogeneous column-set groups become MULTIPLE ordered INSERT
|
|
282
|
+
* statements — byte-identical to what `DBModel._insert` emits per group (via `compileInsertMany`).
|
|
283
|
+
*
|
|
284
|
+
* `pk` (the target PK descriptor) is REQUIRED when the createMany carries a RETURNING clause on the
|
|
285
|
+
* MySQL dialect: a batch INSERT persists N rows, so the MySQL RETURNING emulation must re-select ALL
|
|
286
|
+
* N (a range on the AUTO_INCREMENT column, or the client-supplied PK values), not a single `id`.
|
|
287
|
+
*/
|
|
288
|
+
function compileCreateManyBundle(name, options, dialectName = 'sqlite', resolveColumnType) {
|
|
289
|
+
const components = (0, compile_crud_1.compileInsertMany)(dialectName, options);
|
|
290
|
+
const ops = components.map((c, i) => {
|
|
291
|
+
const flat = flattenBatchOp(c, `createMany group ${i}`);
|
|
292
|
+
// On MySQL, annotate a RETURNING batch INSERT with the PK hint so the driver emulation
|
|
293
|
+
// re-selects every inserted row of the group by the real PK.
|
|
294
|
+
if (dialectName === 'mysql' && options.pk !== undefined && options.returning !== undefined) {
|
|
295
|
+
return { ...flat, ...(0, tx_1.mysqlPkHint)({ sql: flat.sql, params: flat.params, pk: options.pk }) };
|
|
296
|
+
}
|
|
297
|
+
return flat;
|
|
298
|
+
});
|
|
299
|
+
const plan = (0, tx_1.deriveBatchPlan)('create', ops);
|
|
300
|
+
// `ops` is never empty here: `deriveBatchPlan` throws on zero ops, and a `createMany` always groups
|
|
301
|
+
// into >=1 INSERT. Fail LOUDLY rather than emitting an empty `sql` fallback (an empty-ops bundle is a
|
|
302
|
+
// compile bug, not a default — 'defaults in schema, not code'). The `statement` mirrors the batch plan's
|
|
303
|
+
// first (group-0) statement, matching the updateMany/deleteMany bundle shape below.
|
|
304
|
+
const head = ops[0];
|
|
305
|
+
if (head === undefined) {
|
|
306
|
+
throw new Error(`scp write: compileCreateManyBundle('${name}') produced no INSERT statements (empty createMany grouping) — an empty-ops batch bundle is a compile bug, not a silent empty-SQL default.`);
|
|
307
|
+
}
|
|
308
|
+
const bundle = { dialect: dialectName, name, statement: { sql: head.sql, params: head.params }, optionalHeads: [], relations: {}, transaction: plan };
|
|
309
|
+
// Codegen typed de-box (spec §4.1/§9): annotate the batch write with the TransactionResult
|
|
310
|
+
// `outputType` when the schema SoT resolver is supplied (returnedRows rows typed via the RETURNING
|
|
311
|
+
// + target table). Fail-closed. Absent resolver → un-annotated (back-compat).
|
|
312
|
+
return resolveColumnType === undefined ? bundle : (0, writeouttype_1.annotateWriteBundleOutType)(bundle, resolveColumnType);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Compile an `updateMany` into a batch write {@link SqlBundle} (one UNNEST/JSON/CASE statement,
|
|
316
|
+
* byte-identical to `compileUpdateMany` driving the v1 `buildUpdateMany` / JSON-batch builder).
|
|
317
|
+
*/
|
|
318
|
+
function compileUpdateManyBundle(name, options, dialectName = 'sqlite') {
|
|
319
|
+
const op = flattenBatchOp((0, compile_crud_1.compileUpdateMany)(dialectName, options), 'updateMany');
|
|
320
|
+
const plan = (0, tx_1.deriveBatchPlan)('update', [op]);
|
|
321
|
+
return { dialect: dialectName, name, statement: { sql: op.sql, params: op.params }, optionalHeads: [], relations: {}, transaction: plan };
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Compile a `deleteMany` into a batch write {@link SqlBundle} — a PK-set IN-list DELETE (single-key)
|
|
325
|
+
* or one DELETE per composite-key group, driven by the v1 `DBConditions` builder (`compileDeleteMany`).
|
|
326
|
+
*/
|
|
327
|
+
function compileDeleteManyBundle(name, options, dialectName = 'sqlite') {
|
|
328
|
+
const components = (0, compile_crud_1.compileDeleteMany)({ dialect: dialectName, ...options });
|
|
329
|
+
const ops = components.map((c, i) => flattenBatchOp(c, `deleteMany group ${i}`));
|
|
330
|
+
if (ops.length === 0) {
|
|
331
|
+
// Empty key set: nothing to delete. Emit a no-op plan (zero statements is rejected by
|
|
332
|
+
// deriveBatchPlan, so surface a single always-false DELETE keyed by the v1 empty IN-list is
|
|
333
|
+
// NOT needed — an empty deleteMany simply has no statements). Represent as an empty transaction.
|
|
334
|
+
return { dialect: dialectName, name, statement: { sql: '', params: [] }, optionalHeads: [], relations: {}, transaction: { phase: 'remove', entityFrom: null, statements: [], onIdempotentHit: 'rollback' } };
|
|
335
|
+
}
|
|
336
|
+
const plan = (0, tx_1.deriveBatchPlan)('remove', ops);
|
|
337
|
+
return { dialect: dialectName, name, statement: { sql: ops[0].sql, params: ops[0].params }, optionalHeads: [], relations: {}, transaction: plan };
|
|
338
|
+
}
|
|
516
339
|
/**
|
|
517
340
|
* Execute a COMPOSITE (multi-write) Command end-to-end as ONE real SQLite transaction with gate-
|
|
518
|
-
* first short-circuit + DAG-ordered statements (spec §6 / §14)
|
|
519
|
-
* bundle with the derived tx-DAG plan, then run {@link executeTransactionBundle}.
|
|
341
|
+
* first short-circuit + DAG-ordered statements (spec §6 / §14).
|
|
520
342
|
*/
|
|
521
343
|
function executeCompositeCommand(contract, entries, phase, input, options) {
|
|
522
344
|
const bundle = compileCompositeWriteBundle(contract, entries, phase, options.dialect);
|
|
@@ -524,9 +346,8 @@ function executeCompositeCommand(contract, entries, phase, input, options) {
|
|
|
524
346
|
}
|
|
525
347
|
/**
|
|
526
348
|
* Execute a Command end-to-end as ONE real SQLite transaction with gate-first short-circuit
|
|
527
|
-
* (spec §6):
|
|
528
|
-
*
|
|
529
|
-
* ("write-time relations が自動導出 → 1 tx", spec §2.3).
|
|
349
|
+
* (spec §6): compile the Command + its save contract into a {@link SqlBundle} with a transaction
|
|
350
|
+
* plan, then run {@link executeTransactionBundle}. The v2 public write path.
|
|
530
351
|
*
|
|
531
352
|
* @throws {SqlFailure} a mapped driver failure (the transaction ROLLBACKs first).
|
|
532
353
|
*/
|
|
@@ -536,40 +357,28 @@ function executeCommand(contract, writes, phase, input, options) {
|
|
|
536
357
|
}
|
|
537
358
|
/**
|
|
538
359
|
* Execute a {@link SqlBundle}'s derived transaction plan (spec §6/§8) as ONE real SQLite
|
|
539
|
-
* transaction.
|
|
540
|
-
*
|
|
541
|
-
* re-deriving the plan. The bundle round-trip test parses this from JSON (no TS state) to prove
|
|
542
|
-
* WS7 self-sufficiency.
|
|
360
|
+
* transaction. The SAME code path a thin per-language runtime follows: it consumes ONLY the
|
|
361
|
+
* serialized {@link TransactionPlan} (pure JSON) + a SQL driver, never re-deriving the plan.
|
|
543
362
|
*
|
|
544
|
-
* @throws if the bundle carries no transaction plan
|
|
545
|
-
* bundle), or {@link SqlFailure} on a mapped driver failure.
|
|
363
|
+
* @throws if the bundle carries no transaction plan, or {@link SqlFailure} on a driver failure.
|
|
546
364
|
*/
|
|
547
365
|
function executeTransactionBundle(bundle, input, options) {
|
|
548
366
|
if (bundle.transaction === undefined) {
|
|
549
367
|
throw new Error('scp write: this bundle carries no transaction plan (not a write-time-relations Command bundle)');
|
|
550
368
|
}
|
|
551
|
-
return (0,
|
|
369
|
+
return (0, tx_1.executeTransaction)(options.db, bundle.transaction, input, bundle.dialect);
|
|
552
370
|
}
|
|
553
371
|
/**
|
|
554
|
-
* The v2 typed-object read surface (spec §4/§5/§12): run a read behavior whose output is a
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* the whole page (staged, no N+1) and attached as an OWN prop.
|
|
560
|
-
* - Every OTHER declared relation is LAZY: a prototype getter that, on `await result.author`,
|
|
561
|
-
* fires the SAME compiled relation op over the sibling set (batched — still no N+1).
|
|
562
|
-
* - `options.hydrate` recovers host objects (`(raw) => new Domain(raw)`) — the consumer-side
|
|
563
|
-
* method-UX recovery. It stays in the runtime and never enters the bundle.
|
|
372
|
+
* The v2 typed-object read surface (spec §4/§5/§12): run a read behavior whose output is a row
|
|
373
|
+
* list (a Select), then wrap each raw row in a plain TYPED-OBJECT with read relations attached.
|
|
374
|
+
* A relation named in `options.with` is declaratively batch-prefetched; every other declared
|
|
375
|
+
* relation is lazy (a prototype getter firing the SAME compiled op over the sibling set). Both
|
|
376
|
+
* surfaces share the ONE compiled relation op in the bundle.
|
|
564
377
|
*
|
|
565
|
-
*
|
|
566
|
-
* is produced by the SAME {@link executeBundle} path (bc runtime-core + SQL handlers), so the
|
|
567
|
-
* read query itself is fully IR-driven; relations are the runtime's staged-batch concern.
|
|
568
|
-
*
|
|
569
|
-
* @throws if the behavior output is not a row list (the typed-object surface is for reads).
|
|
378
|
+
* @throws if the behavior output is not a row list (the typed-object read surface is for reads).
|
|
570
379
|
*/
|
|
571
380
|
function read(contract, input, options) {
|
|
572
|
-
return readBundle(compileBundle(contract, options.entry, options.relations), input, options);
|
|
381
|
+
return readBundle(compileBundle(contract, options.entry, options.relations, options.dialect), input, options);
|
|
573
382
|
}
|
|
574
383
|
/** {@link read} against an already-compiled {@link SqlBundle} (the published §8 artifact). */
|
|
575
384
|
function readBundle(bundle, input, options) {
|
|
@@ -582,84 +391,10 @@ function readBundle(bundle, input, options) {
|
|
|
582
391
|
const readOpts = {
|
|
583
392
|
...(options.with !== undefined ? { with: options.with } : {}),
|
|
584
393
|
...(options.hydrate !== undefined ? { hydrate: options.hydrate } : {}),
|
|
394
|
+
// CROSS-DB relations (V0 R1): forward the connection registry so a tagged relation routes to
|
|
395
|
+
// its target DB. Absent for a single-DB read (every relation runs on the primary `db`).
|
|
396
|
+
...(options.connections !== undefined ? { connections: options.connections } : {}),
|
|
585
397
|
};
|
|
586
398
|
return (0, typed_object_1.buildResultSet)(rawRows, bundle.relations, options.db, readOpts);
|
|
587
399
|
}
|
|
588
|
-
// ── Staged-batch relation resolution THROUGH bc's plan (spec §5) ──────────────
|
|
589
|
-
/** The synthetic catalog name of a batched-relation map node (runtime-internal, not authored). */
|
|
590
|
-
const RELATION_BATCH_COMPONENT = '__RelationBatch';
|
|
591
|
-
/** The port a batched-relation map element evaluates to: the parent's batch-key value. */
|
|
592
|
-
const RELATION_KEY_PORT = '__key';
|
|
593
|
-
/**
|
|
594
|
-
* Resolve ONE relation for a page of parent rows by driving bc's PLAN mechanism (spec §5:
|
|
595
|
-
* `deriveExecutionPlan` — result path → stage groups + concurrency). This is the staged-batch
|
|
596
|
-
* path: it builds a bc component with a single `map.batched` node over the parent rows and
|
|
597
|
-
* runs it through {@link runBehavior} (the SAME multi-language execution core WS7 languages
|
|
598
|
-
* use). Because the map is `batched`, bc evaluates every parent's key port and invokes the
|
|
599
|
-
* relation handler EXACTLY ONCE with `{items:[…]}` — so the relation op fires ONE batched
|
|
600
|
-
* query for the whole page (structurally no N+1, enforced by bc's batch contract). The
|
|
601
|
-
* handler runs the IDENTICAL compiled {@link RelationOp} the declarative-select and lazy
|
|
602
|
-
* surfaces use, and `into` attaches each parent's resolved child(ren).
|
|
603
|
-
*
|
|
604
|
-
* Returns the parent rows augmented with the relation under `op.name` (per cardinality).
|
|
605
|
-
*/
|
|
606
|
-
function resolveRelationViaPlan(op, parents, db) {
|
|
607
|
-
// One batched map node over the parent list. Each element evaluates its batch-key port
|
|
608
|
-
// (`{ref:[<elemAs>, parentKey]}`); the batched handler receives all keys at once.
|
|
609
|
-
const elemAs = '$p';
|
|
610
|
-
const node = {
|
|
611
|
-
id: 'rel',
|
|
612
|
-
map: {
|
|
613
|
-
over: { ref: ['__parents'] },
|
|
614
|
-
as: elemAs,
|
|
615
|
-
component: RELATION_BATCH_COMPONENT,
|
|
616
|
-
ports: { [RELATION_KEY_PORT]: { ref: [elemAs, op.parentKey] } },
|
|
617
|
-
into: op.name,
|
|
618
|
-
batched: true,
|
|
619
|
-
},
|
|
620
|
-
};
|
|
621
|
-
const component = {
|
|
622
|
-
name: '__resolveRelation',
|
|
623
|
-
inputPorts: { __parents: { required: true } },
|
|
624
|
-
body: [node],
|
|
625
|
-
output: { ref: ['rel'] },
|
|
626
|
-
};
|
|
627
|
-
const ir = {
|
|
628
|
-
irVersion: 1,
|
|
629
|
-
exprVersion: 1,
|
|
630
|
-
components: [component],
|
|
631
|
-
};
|
|
632
|
-
// The batched-relation handler: bc calls it ONCE with every parent's evaluated key port.
|
|
633
|
-
// It runs the relation op's batch query a SINGLE time and returns per-parent results aligned
|
|
634
|
-
// to `items` (bc's MAP_BATCH_RESULT_MISMATCH enforces the 1:1 alignment).
|
|
635
|
-
const handlers = {
|
|
636
|
-
[RELATION_BATCH_COMPONENT]: (portsOrItems) => {
|
|
637
|
-
const items = portsOrItems.items;
|
|
638
|
-
if (!Array.isArray(items)) {
|
|
639
|
-
return { error: `${RELATION_BATCH_COMPONENT}: expected a batched {items:[…]} invocation` };
|
|
640
|
-
}
|
|
641
|
-
// Reconstruct the parent-key-bearing rows from the evaluated key ports, run the op ONCE.
|
|
642
|
-
const keyRows = items.map((it) => ({ [op.parentKey]: it[RELATION_KEY_PORT] }));
|
|
643
|
-
const { batch } = (0, relation_1.runRelationOp)(op, keyRows, db);
|
|
644
|
-
const aligned = keyRows.map((r) => (0, relation_1.distributeToParent)(op, r, batch));
|
|
645
|
-
return { ok: aligned };
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
const out = (0, behavior_contracts_1.runBehavior)(ir, handlers, { __parents: parents }, '__resolveRelation');
|
|
649
|
-
return out;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* If a `runPlan` `OP_FAILED` carries a mapped-failure message, re-surface the structured
|
|
653
|
-
* {@link SqlFailure} (the message embeds the original SQLite code). Non-driver errors are
|
|
654
|
-
* re-thrown verbatim.
|
|
655
|
-
*/
|
|
656
|
-
function reErrorToSqlFailure(e) {
|
|
657
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
658
|
-
const m = /(SQLITE_[A-Z_]+)/.exec(message);
|
|
659
|
-
if (m) {
|
|
660
|
-
// Reconstruct a minimal driver-error shape and re-map to preserve kind/policy/code.
|
|
661
|
-
return (0, errors_1.mapSqliteError)({ code: m[1], message });
|
|
662
|
-
}
|
|
663
|
-
return e;
|
|
664
|
-
}
|
|
665
400
|
//# sourceMappingURL=runtime.js.map
|