litedbmodel 1.2.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +6 -0
- package/dist/DBModel.js.map +1 -1
- package/dist/scp/authoring-sql.d.ts +56 -0
- package/dist/scp/authoring-sql.d.ts.map +1 -0
- package/dist/scp/authoring-sql.js +83 -0
- package/dist/scp/authoring-sql.js.map +1 -0
- package/dist/scp/authoring.d.ts +155 -0
- package/dist/scp/authoring.d.ts.map +1 -0
- package/dist/scp/authoring.js +162 -0
- package/dist/scp/authoring.js.map +1 -0
- package/dist/scp/bridge.d.ts +64 -0
- package/dist/scp/bridge.d.ts.map +1 -0
- package/dist/scp/bridge.js +329 -0
- package/dist/scp/bridge.js.map +1 -0
- package/dist/scp/catalog.d.ts +91 -0
- package/dist/scp/catalog.d.ts.map +1 -0
- package/dist/scp/catalog.js +203 -0
- package/dist/scp/catalog.js.map +1 -0
- package/dist/scp/codegen.d.ts +131 -0
- package/dist/scp/codegen.d.ts.map +1 -0
- package/dist/scp/codegen.js +137 -0
- package/dist/scp/codegen.js.map +1 -0
- package/dist/scp/compile-dialect.d.ts +39 -0
- package/dist/scp/compile-dialect.d.ts.map +1 -0
- package/dist/scp/compile-dialect.js +62 -0
- package/dist/scp/compile-dialect.js.map +1 -0
- package/dist/scp/compile-sqlite.d.ts +114 -0
- package/dist/scp/compile-sqlite.d.ts.map +1 -0
- package/dist/scp/compile-sqlite.js +149 -0
- package/dist/scp/compile-sqlite.js.map +1 -0
- package/dist/scp/dialect.d.ts +141 -0
- package/dist/scp/dialect.d.ts.map +1 -0
- package/dist/scp/dialect.js +194 -0
- package/dist/scp/dialect.js.map +1 -0
- package/dist/scp/errors.d.ts +36 -0
- package/dist/scp/errors.d.ts.map +1 -0
- package/dist/scp/errors.js +72 -0
- package/dist/scp/errors.js.map +1 -0
- package/dist/scp/guard.d.ts +47 -0
- package/dist/scp/guard.d.ts.map +1 -0
- package/dist/scp/guard.js +137 -0
- package/dist/scp/guard.js.map +1 -0
- package/dist/scp/index.cjs +4744 -0
- package/dist/scp/index.d.ts +49 -0
- package/dist/scp/index.d.ts.map +1 -0
- package/dist/scp/index.js +164 -0
- package/dist/scp/index.js.map +1 -0
- package/dist/scp/index.mjs +2000 -0
- package/dist/scp/ir.d.ts +92 -0
- package/dist/scp/ir.d.ts.map +1 -0
- package/dist/scp/ir.js +28 -0
- package/dist/scp/ir.js.map +1 -0
- package/dist/scp/relation.d.ts +136 -0
- package/dist/scp/relation.d.ts.map +1 -0
- package/dist/scp/relation.js +191 -0
- package/dist/scp/relation.js.map +1 -0
- package/dist/scp/render.d.ts +40 -0
- package/dist/scp/render.d.ts.map +1 -0
- package/dist/scp/render.js +170 -0
- package/dist/scp/render.js.map +1 -0
- package/dist/scp/runtime.d.ts +295 -0
- package/dist/scp/runtime.d.ts.map +1 -0
- package/dist/scp/runtime.js +665 -0
- package/dist/scp/runtime.js.map +1 -0
- package/dist/scp/typed-object.d.ts +88 -0
- package/dist/scp/typed-object.d.ts.map +1 -0
- package/dist/scp/typed-object.js +163 -0
- package/dist/scp/typed-object.js.map +1 -0
- package/dist/scp/write-plan.d.ts +161 -0
- package/dist/scp/write-plan.d.ts.map +1 -0
- package/dist/scp/write-plan.js +528 -0
- package/dist/scp/write-plan.js.map +1 -0
- package/dist/scp/write-runtime.d.ts +80 -0
- package/dist/scp/write-runtime.d.ts.map +1 -0
- package/dist/scp/write-runtime.js +167 -0
- package/dist/scp/write-runtime.js.map +1 -0
- package/dist/scp/writes.d.ts +251 -0
- package/dist/scp/writes.d.ts.map +1 -0
- package/dist/scp/writes.js +176 -0
- package/dist/scp/writes.js.map +1 -0
- package/docs/proposal/contracts-architecture-feasibility.md +328 -0
- package/docs/proposal/contracts-architecture.md +579 -0
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +382 -0
- package/docs/proposal/sql-dynamic-expansion-spec.md +175 -0
- package/package.json +27 -5
- package/docs/README_REVIEW.md +0 -23
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* litedbmodel v2 SCP — the thin TS runtime (WS3, #23; spec §3 / §10 / §11).
|
|
4
|
+
*
|
|
5
|
+
* Consumes bc's runtime-core (`runBehavior` = plan stage execution + map/wire/skip
|
|
6
|
+
* propagation + Φ output assembly) and adds ONLY the SQL-backend concerns (spec §11):
|
|
7
|
+
* Backend-Compile bridge (WS1↔WS3), per-Catalog Handlers (render → driver execute →
|
|
8
|
+
* row→model assembly), and Error Mapping. It re-implements NO generic execution.
|
|
9
|
+
*
|
|
10
|
+
* ## Execution pipeline (spec §3)
|
|
11
|
+
*
|
|
12
|
+
* validate → fragment select (SKIP) → array expand → Expression eval → bind → SQL execute
|
|
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).
|
|
32
|
+
*
|
|
33
|
+
* ## Input normalization (SSoT — defaults live in the schema, not code)
|
|
34
|
+
*
|
|
35
|
+
* An OPTIONAL Input Port that the caller omits is normalized to `null` (present-as-null) so
|
|
36
|
+
* a SKIP guard `{ne:[{refOpt:[port]}, null]}` evaluates to `false` and drops its fragment —
|
|
37
|
+
* absent-key SKIP via `refOpt`. This is driven by the component's `inputPorts` schema
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.SqlFailure = void 0;
|
|
43
|
+
exports.buildHandlers = buildHandlers;
|
|
44
|
+
exports.normalizeInput = normalizeInput;
|
|
45
|
+
exports.executeBehavior = executeBehavior;
|
|
46
|
+
exports.compileBundle = compileBundle;
|
|
47
|
+
exports.executeBundle = executeBundle;
|
|
48
|
+
exports.compileWriteBundle = compileWriteBundle;
|
|
49
|
+
exports.compileCompositeWriteBundle = compileCompositeWriteBundle;
|
|
50
|
+
exports.executeCompositeCommand = executeCompositeCommand;
|
|
51
|
+
exports.executeCommand = executeCommand;
|
|
52
|
+
exports.executeTransactionBundle = executeTransactionBundle;
|
|
53
|
+
exports.read = read;
|
|
54
|
+
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
|
+
const errors_1 = require("./errors");
|
|
60
|
+
Object.defineProperty(exports, "SqlFailure", { enumerable: true, get: function () { return errors_1.SqlFailure; } });
|
|
61
|
+
const relation_1 = require("./relation");
|
|
62
|
+
const typed_object_1 = require("./typed-object");
|
|
63
|
+
const bridge_2 = require("./bridge");
|
|
64
|
+
const write_plan_1 = require("./write-plan");
|
|
65
|
+
const write_runtime_1 = require("./write-runtime");
|
|
66
|
+
const writes_1 = require("./writes");
|
|
67
|
+
const dialect_1 = require("./dialect");
|
|
68
|
+
/** The synthetic port that carries a SQL node's render scope (see module doc). */
|
|
69
|
+
const SCOPE_PORT = '__scope';
|
|
70
|
+
// ── Value normalization at the SQL boundary ───────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* bc evaluates integers to `bigint`. Convert a rendered param to a driver-bindable value:
|
|
73
|
+
* a safe-range `bigint` → JS `number` (v1 uses numbers; keeps result parity), an
|
|
74
|
+
* out-of-safe-range `bigint` stays `bigint` (better-sqlite3 binds it losslessly). Arrays /
|
|
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);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Collect every `refOpt` head across a compiled op (static params + fragment tree). */
|
|
173
|
+
function operationRefOptHeads(op, heads) {
|
|
174
|
+
for (const p of op.params)
|
|
175
|
+
collectRefOptHeads(p, heads);
|
|
176
|
+
if (op.where !== null)
|
|
177
|
+
collectFragmentRefOptHeads(op.where, heads);
|
|
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);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* All OPTIONAL heads across a set of compiled ops: the SKIP-guarded heads (a `when`-guarded
|
|
193
|
+
* fragment declares its driving input optional) plus every `refOpt`-accessed head (the
|
|
194
|
+
* author declared it optional via `opt(…)`). These are normalized to present-as-null so
|
|
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).
|
|
278
|
+
*
|
|
279
|
+
* Exported so the mode-3 codegen path (WS7f) binds the IDENTICAL SQL handlers into bc's
|
|
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`).
|
|
335
|
+
*/
|
|
336
|
+
function executeBehavior(contract, input, options) {
|
|
337
|
+
return executeBundle(compileBundle(contract, options.entry, options.relations, options.dialect), input, options);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Backend-Compile ONE behavior method of a contract into the serializable {@link SqlBundle}
|
|
341
|
+
* (spec §8) — the published multi-language artifact. This runs the WS1↔WS3 bridge and the
|
|
342
|
+
* surrogate rewrite ONCE, on the TS side (spec §10: the dialect axis is compiled once), and
|
|
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).
|
|
346
|
+
*/
|
|
347
|
+
function compileBundle(contract, entry, relations = [], dialectName = 'sqlite') {
|
|
348
|
+
const dialect = (0, dialect_1.dialectFor)(dialectName);
|
|
349
|
+
const component = entry
|
|
350
|
+
? contract.components.find((c) => c.name === entry)
|
|
351
|
+
: contract.components[0];
|
|
352
|
+
if (component === undefined) {
|
|
353
|
+
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
|
+
const relationOps = {};
|
|
363
|
+
for (const decl of relations) {
|
|
364
|
+
if (relationOps[decl.name] !== undefined) {
|
|
365
|
+
throw new Error(`scp runtime: duplicate relation declaration '${decl.name}'`);
|
|
366
|
+
}
|
|
367
|
+
relationOps[decl.name] = (0, relation_1.compileRelationOp)(decl);
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
irVersion: contract.ir.irVersion,
|
|
371
|
+
exprVersion: contract.ir.exprVersion,
|
|
372
|
+
dialect: dialectName,
|
|
373
|
+
component: surrogate,
|
|
374
|
+
operations,
|
|
375
|
+
optionalHeads: [...optionalHeadsOf(operations)],
|
|
376
|
+
relations: relationOps,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Execute a {@link SqlBundle} (the §8 published artifact) end-to-end: feed bc `runBehavior`
|
|
381
|
+
* the bundle's surrogate component (plan / map / wire / output orchestration) with SQL
|
|
382
|
+
* handlers that render the bundle's `CompiledOperation`s and run REAL SQLite. This is the
|
|
383
|
+
* SAME code path a thin per-language runtime follows — it consumes ONLY the serialized
|
|
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).
|
|
386
|
+
*
|
|
387
|
+
* @throws {SqlFailure} a mapped driver failure re-surfaced at the boundary.
|
|
388
|
+
*/
|
|
389
|
+
function executeBundle(bundle, input, options) {
|
|
390
|
+
const surrogate = bundle.component;
|
|
391
|
+
const ir = {
|
|
392
|
+
irVersion: bundle.irVersion,
|
|
393
|
+
exprVersion: bundle.exprVersion,
|
|
394
|
+
components: [surrogate],
|
|
395
|
+
};
|
|
396
|
+
const handlers = buildHandlers(options.db, bundle.operations, (0, dialect_1.dialectFor)(bundle.dialect));
|
|
397
|
+
const optionalHeads = new Set(bundle.optionalHeads);
|
|
398
|
+
const normalized = normalizeInput(surrogate, optionalHeads, input);
|
|
399
|
+
try {
|
|
400
|
+
return (0, behavior_contracts_1.runBehavior)(ir, handlers, normalized, surrogate.name);
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
// A `fail`-policy node's `{error}` re-throws through runPlan as `OP_FAILED` carrying the
|
|
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);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// ── Write-time relations: Command bundle + 1-tx execution (WS5, #25 — spec §6) ──
|
|
410
|
+
/**
|
|
411
|
+
* 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): exactly one write node whose
|
|
413
|
+
* `onWrite` lifecycle the transaction plan wraps. More than one write node ⇒ the multi-write DAG
|
|
414
|
+
* derivation deferred to WS8 (rejected loudly, never half-built).
|
|
415
|
+
*/
|
|
416
|
+
function baseWriteNodeOf(component) {
|
|
417
|
+
const writes = component.body.filter((n) => !('cond' in n) && !('map' in n) && (n.component === 'Insert' || n.component === 'Update' || n.component === 'Delete'));
|
|
418
|
+
if (writes.length === 0) {
|
|
419
|
+
throw new Error(`scp write: Command '${component.name}' has no base write (Insert/Update/Delete) node`);
|
|
420
|
+
}
|
|
421
|
+
if (writes.length > 1) {
|
|
422
|
+
throw new Error(`scp write: Command '${component.name}' has ${writes.length} base write nodes; WS5 initial ` +
|
|
423
|
+
`scope is a SINGLE-statement Command + write-time relations (multi-write DAG is WS8, spec §6/§13).`);
|
|
424
|
+
}
|
|
425
|
+
return writes[0];
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Backend-Compile a Command method + its `entityWrites` save contract into a {@link SqlBundle}
|
|
429
|
+
* that carries the derived, gate-first write-time-relations {@link TransactionPlan} (spec §6/§8).
|
|
430
|
+
* The base write op is Backend-Compiled ONCE (the same {@link compileNode} bridge), and
|
|
431
|
+
* {@link deriveTransactionPlan} lowers the lifecycle's §6 effect arrays around it into the ordered
|
|
432
|
+
* statement list. The whole plan is pure JSON (it round-trips inside the bundle), so a WS7 runtime
|
|
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).
|
|
437
|
+
*/
|
|
438
|
+
function compileWriteBundle(contract, entry, writes, phase, dialectName = 'sqlite') {
|
|
439
|
+
const dialect = (0, dialect_1.dialectFor)(dialectName);
|
|
440
|
+
const component = contract.components.find((c) => c.name === entry);
|
|
441
|
+
if (component === undefined) {
|
|
442
|
+
throw new Error(`scp write: entry component '${entry}' not found in contract`);
|
|
443
|
+
}
|
|
444
|
+
const lifecycle = (0, writes_1.lifecycleFor)(writes, phase);
|
|
445
|
+
if (lifecycle === undefined) {
|
|
446
|
+
throw new Error(`scp write: the '${phase}' lifecycle is not declared in the entityWrites save contract`);
|
|
447
|
+
}
|
|
448
|
+
const writeNode = baseWriteNodeOf(component);
|
|
449
|
+
const baseOp = (0, bridge_2.compileNode)(writeNode, dialect);
|
|
450
|
+
const base = { op: baseOp, label: `${baseOp.component} ${writeNode.component}` };
|
|
451
|
+
const plan = (0, write_plan_1.deriveTransactionPlan)(phase, [base], lifecycle, dialect);
|
|
452
|
+
// The bundle still carries the surrogate component/operations for structural parity with a read
|
|
453
|
+
// bundle (WS7 uniformity), but the write path executes via the transaction plan, not runBehavior.
|
|
454
|
+
const operations = {};
|
|
455
|
+
for (const [id, op] of (0, bridge_1.compileComponentNodes)(component, dialect))
|
|
456
|
+
operations[id] = op;
|
|
457
|
+
const surrogate = surrogateComponent(component, operations);
|
|
458
|
+
return {
|
|
459
|
+
irVersion: contract.ir.irVersion,
|
|
460
|
+
exprVersion: contract.ir.exprVersion,
|
|
461
|
+
dialect: dialectName,
|
|
462
|
+
component: surrogate,
|
|
463
|
+
operations,
|
|
464
|
+
optionalHeads: [...optionalHeadsOf(operations)],
|
|
465
|
+
relations: {},
|
|
466
|
+
transaction: plan,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Backend-Compile a COMPOSITE (multi-write) Command into a {@link SqlBundle} carrying ONE derived,
|
|
471
|
+
* gate-first, TOPOLOGICALLY-ORDERED transaction plan (spec §6 nested write / §14 "write-time tx DAG
|
|
472
|
+
* 導出"). Each {@link CompositeWriteEntry} contributes a named base write + its effects; a later
|
|
473
|
+
* member depends on an earlier via `$.ref.<name>.*` (e.g. a child INSERT keyed by the parent's
|
|
474
|
+
* RETURNING id). {@link deriveTransactionPlan} builds the data-dependency graph + gate-first
|
|
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.
|
|
481
|
+
*/
|
|
482
|
+
function compileCompositeWriteBundle(contract, entries, phase, dialectName = 'sqlite') {
|
|
483
|
+
if (entries.length < 2) {
|
|
484
|
+
throw new Error('scp write: a composite write bundle needs at least 2 named write members (use compileWriteBundle for a single write).');
|
|
485
|
+
}
|
|
486
|
+
const dialect = (0, dialect_1.dialectFor)(dialectName);
|
|
487
|
+
const bases = [];
|
|
488
|
+
const operations = {};
|
|
489
|
+
let firstComponent;
|
|
490
|
+
for (const e of entries) {
|
|
491
|
+
const component = contract.components.find((c) => c.name === e.entry);
|
|
492
|
+
if (component === undefined)
|
|
493
|
+
throw new Error(`scp write: entry component '${e.entry}' not found in contract`);
|
|
494
|
+
if (firstComponent === undefined)
|
|
495
|
+
firstComponent = component;
|
|
496
|
+
const writeNode = baseWriteNodeOf(component);
|
|
497
|
+
const baseOp = (0, bridge_2.compileNode)(writeNode, dialect);
|
|
498
|
+
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
|
+
}
|
|
502
|
+
// The lifecycle arg is unused in composite scope (each base carries its own effects); pass empty.
|
|
503
|
+
const plan = (0, write_plan_1.deriveTransactionPlan)(phase, bases, { effects: {} }, dialect);
|
|
504
|
+
const surrogate = surrogateComponent(firstComponent, operations);
|
|
505
|
+
return {
|
|
506
|
+
irVersion: contract.ir.irVersion,
|
|
507
|
+
exprVersion: contract.ir.exprVersion,
|
|
508
|
+
dialect: dialectName,
|
|
509
|
+
component: surrogate,
|
|
510
|
+
operations,
|
|
511
|
+
optionalHeads: [...optionalHeadsOf(operations)],
|
|
512
|
+
relations: {},
|
|
513
|
+
transaction: plan,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* 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): Backend-Compile the members into a
|
|
519
|
+
* bundle with the derived tx-DAG plan, then run {@link executeTransactionBundle}.
|
|
520
|
+
*/
|
|
521
|
+
function executeCompositeCommand(contract, entries, phase, input, options) {
|
|
522
|
+
const bundle = compileCompositeWriteBundle(contract, entries, phase, options.dialect);
|
|
523
|
+
return executeTransactionBundle(bundle, input, options);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Execute a Command end-to-end as ONE real SQLite transaction with gate-first short-circuit
|
|
527
|
+
* (spec §6): Backend-Compile the Command + its save contract into a {@link SqlBundle} with a
|
|
528
|
+
* transaction plan, then run {@link executeTransactionBundle}. This is the v2 public write path
|
|
529
|
+
* ("write-time relations が自動導出 → 1 tx", spec §2.3).
|
|
530
|
+
*
|
|
531
|
+
* @throws {SqlFailure} a mapped driver failure (the transaction ROLLBACKs first).
|
|
532
|
+
*/
|
|
533
|
+
function executeCommand(contract, writes, phase, input, options) {
|
|
534
|
+
const bundle = compileWriteBundle(contract, options.entry, writes, phase, options.dialect);
|
|
535
|
+
return executeTransactionBundle(bundle, input, options);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Execute a {@link SqlBundle}'s derived transaction plan (spec §6/§8) as ONE real SQLite
|
|
539
|
+
* transaction. This is the SAME code path a thin per-language runtime follows: it consumes ONLY
|
|
540
|
+
* the serialized {@link TransactionPlan} (pure JSON) + the render pipeline + a SQL driver, never
|
|
541
|
+
* re-deriving the plan. The bundle round-trip test parses this from JSON (no TS state) to prove
|
|
542
|
+
* WS7 self-sufficiency.
|
|
543
|
+
*
|
|
544
|
+
* @throws if the bundle carries no transaction plan (it is not a write-time-relations Command
|
|
545
|
+
* bundle), or {@link SqlFailure} on a mapped driver failure.
|
|
546
|
+
*/
|
|
547
|
+
function executeTransactionBundle(bundle, input, options) {
|
|
548
|
+
if (bundle.transaction === undefined) {
|
|
549
|
+
throw new Error('scp write: this bundle carries no transaction plan (not a write-time-relations Command bundle)');
|
|
550
|
+
}
|
|
551
|
+
return (0, write_runtime_1.executeTransaction)(options.db, bundle.transaction, input, (0, dialect_1.dialectFor)(bundle.dialect));
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The v2 typed-object read surface (spec §4/§5/§12): run a read behavior whose output is a
|
|
555
|
+
* row list (a Select), then wrap each raw row in a plain TYPED-OBJECT (own props = data only,
|
|
556
|
+
* NOT a DBModel instance) with read relations attached.
|
|
557
|
+
*
|
|
558
|
+
* - A relation named in `options.with` is DECLARATIVELY selected: batch-prefetched ONCE over
|
|
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.
|
|
564
|
+
*
|
|
565
|
+
* Both surfaces share the ONE compiled relation op in the bundle (spec §5). The base row list
|
|
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).
|
|
570
|
+
*/
|
|
571
|
+
function read(contract, input, options) {
|
|
572
|
+
return readBundle(compileBundle(contract, options.entry, options.relations), input, options);
|
|
573
|
+
}
|
|
574
|
+
/** {@link read} against an already-compiled {@link SqlBundle} (the published §8 artifact). */
|
|
575
|
+
function readBundle(bundle, input, options) {
|
|
576
|
+
const out = executeBundle(bundle, input, options);
|
|
577
|
+
if (!Array.isArray(out)) {
|
|
578
|
+
throw new Error(`scp read: the read behavior output is not a row list (got ${out === null ? 'null' : typeof out}); ` +
|
|
579
|
+
`the typed-object read surface expects a Select-shaped output`);
|
|
580
|
+
}
|
|
581
|
+
const rawRows = out;
|
|
582
|
+
const readOpts = {
|
|
583
|
+
...(options.with !== undefined ? { with: options.with } : {}),
|
|
584
|
+
...(options.hydrate !== undefined ? { hydrate: options.hydrate } : {}),
|
|
585
|
+
};
|
|
586
|
+
return (0, typed_object_1.buildResultSet)(rawRows, bundle.relations, options.db, readOpts);
|
|
587
|
+
}
|
|
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
|
+
//# sourceMappingURL=runtime.js.map
|