litedbmodel 2.0.2 → 2.2.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 +7 -0
- package/dist/DBConditions.d.ts.map +1 -1
- package/dist/DBConditions.js +16 -1
- package/dist/DBConditions.js.map +1 -1
- package/dist/DBHandler.d.ts.map +1 -1
- package/dist/DBHandler.js +6 -0
- package/dist/DBHandler.js.map +1 -1
- package/dist/DBModel.d.ts +84 -2
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +312 -29
- package/dist/DBModel.js.map +1 -1
- package/dist/decorators.d.ts +52 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +127 -24
- package/dist/decorators.js.map +1 -1
- package/dist/drivers/mysql.d.ts.map +1 -1
- package/dist/drivers/mysql.js +12 -2
- package/dist/drivers/mysql.js.map +1 -1
- package/dist/drivers/postgres.d.ts.map +1 -1
- package/dist/drivers/postgres.js +13 -2
- package/dist/drivers/postgres.js.map +1 -1
- package/dist/drivers/sqlite.d.ts +2 -0
- package/dist/drivers/sqlite.d.ts.map +1 -1
- package/dist/drivers/sqlite.js +49 -4
- package/dist/drivers/sqlite.js.map +1 -1
- package/dist/index.cjs +10741 -0
- package/dist/index.d.cts +1 -0
- package/dist/scp/coltype.d.ts +108 -0
- package/dist/scp/coltype.d.ts.map +1 -1
- package/dist/scp/coltype.js +293 -6
- package/dist/scp/coltype.js.map +1 -1
- package/dist/scp/connection-routing.d.ts +312 -0
- package/dist/scp/connection-routing.d.ts.map +1 -0
- package/dist/scp/connection-routing.js +394 -0
- package/dist/scp/connection-routing.js.map +1 -0
- package/dist/scp/dbmodel-runtime.d.ts +101 -0
- package/dist/scp/dbmodel-runtime.d.ts.map +1 -0
- package/dist/scp/dbmodel-runtime.js +174 -0
- package/dist/scp/dbmodel-runtime.js.map +1 -0
- package/dist/scp/decorator-adapter.d.ts +196 -0
- package/dist/scp/decorator-adapter.d.ts.map +1 -0
- package/dist/scp/decorator-adapter.js +286 -0
- package/dist/scp/decorator-adapter.js.map +1 -0
- package/dist/scp/emit/emitter.d.ts +108 -0
- package/dist/scp/emit/emitter.d.ts.map +1 -0
- package/dist/scp/emit/emitter.js +782 -0
- package/dist/scp/emit/emitter.js.map +1 -0
- package/dist/scp/emit/endpoint.d.ts +248 -0
- package/dist/scp/emit/endpoint.d.ts.map +1 -0
- package/dist/scp/emit/endpoint.js +20 -0
- package/dist/scp/emit/endpoint.js.map +1 -0
- package/dist/scp/emit/index.d.ts +11 -0
- package/dist/scp/emit/index.d.ts.map +1 -0
- package/dist/scp/emit/index.js +13 -0
- package/dist/scp/emit/index.js.map +1 -0
- package/dist/scp/errors.d.ts +45 -1
- package/dist/scp/errors.d.ts.map +1 -1
- package/dist/scp/errors.js +47 -1
- package/dist/scp/errors.js.map +1 -1
- package/dist/scp/exec-context.d.ts +390 -0
- package/dist/scp/exec-context.d.ts.map +1 -0
- package/dist/scp/exec-context.js +540 -0
- package/dist/scp/exec-context.js.map +1 -0
- package/dist/scp/grouping.d.ts +32 -0
- package/dist/scp/grouping.d.ts.map +1 -0
- package/dist/scp/grouping.js +0 -0
- package/dist/scp/grouping.js.map +1 -0
- package/dist/scp/index.cjs +6432 -17579
- package/dist/scp/index.d.cts +1 -0
- package/dist/scp/index.d.mts +1 -0
- package/dist/scp/index.d.ts +51 -52
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +189 -152
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +6067 -3659
- package/dist/scp/leaf-transport.d.ts +50 -0
- package/dist/scp/leaf-transport.d.ts.map +1 -0
- package/dist/scp/leaf-transport.js +92 -0
- package/dist/scp/leaf-transport.js.map +1 -0
- package/dist/scp/leaves.d.ts +174 -0
- package/dist/scp/leaves.d.ts.map +1 -0
- package/dist/scp/leaves.js +287 -0
- package/dist/scp/leaves.js.map +1 -0
- package/dist/scp/limit-config.d.ts +118 -0
- package/dist/scp/limit-config.d.ts.map +1 -0
- package/dist/scp/limit-config.js +126 -0
- package/dist/scp/limit-config.js.map +1 -0
- package/dist/scp/makesql/compile-relation.d.ts +49 -23
- package/dist/scp/makesql/compile-relation.d.ts.map +1 -1
- package/dist/scp/makesql/compile-relation.js +101 -76
- package/dist/scp/makesql/compile-relation.js.map +1 -1
- package/dist/scp/makesql/compile-select.d.ts +45 -10
- package/dist/scp/makesql/compile-select.d.ts.map +1 -1
- package/dist/scp/makesql/compile-select.js +50 -10
- package/dist/scp/makesql/compile-select.js.map +1 -1
- package/dist/scp/makesql/handler.d.ts +1 -1
- package/dist/scp/makesql/handler.d.ts.map +1 -1
- package/dist/scp/makesql/index.d.ts +7 -10
- package/dist/scp/makesql/index.d.ts.map +1 -1
- package/dist/scp/makesql/index.js +19 -30
- package/dist/scp/makesql/index.js.map +1 -1
- package/dist/scp/makesql/json-array.d.ts +65 -0
- package/dist/scp/makesql/json-array.d.ts.map +1 -1
- package/dist/scp/makesql/json-array.js +99 -10
- package/dist/scp/makesql/json-array.js.map +1 -1
- package/dist/scp/makesql/json-batch.d.ts.map +1 -1
- package/dist/scp/makesql/json-batch.js +21 -19
- package/dist/scp/makesql/json-batch.js.map +1 -1
- package/dist/scp/makesql/makesql.d.ts +1 -1
- package/dist/scp/makesql/makesql.d.ts.map +1 -1
- package/dist/scp/makesql/mysql-returning.d.ts +93 -0
- package/dist/scp/makesql/mysql-returning.d.ts.map +1 -0
- package/dist/scp/makesql/mysql-returning.js +215 -0
- package/dist/scp/makesql/mysql-returning.js.map +1 -0
- package/dist/scp/makesql/outtype.d.ts +75 -34
- package/dist/scp/makesql/outtype.d.ts.map +1 -1
- package/dist/scp/makesql/outtype.js +91 -118
- package/dist/scp/makesql/outtype.js.map +1 -1
- package/dist/scp/makesql/pool-executor.d.ts +135 -13
- package/dist/scp/makesql/pool-executor.d.ts.map +1 -1
- package/dist/scp/makesql/pool-executor.js +229 -21
- package/dist/scp/makesql/pool-executor.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts +107 -22
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +451 -97
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/makesql/writeouttype.d.ts +7 -8
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
- package/dist/scp/makesql/writeouttype.js +8 -7
- package/dist/scp/makesql/writeouttype.js.map +1 -1
- package/dist/scp/middleware.d.ts +287 -0
- package/dist/scp/middleware.d.ts.map +1 -0
- package/dist/scp/middleware.js +358 -0
- package/dist/scp/middleware.js.map +1 -0
- package/dist/scp/relation.d.ts +80 -9
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +198 -78
- package/dist/scp/relation.js.map +1 -1
- package/dist/scp/tx-options.d.ts +136 -0
- package/dist/scp/tx-options.d.ts.map +1 -0
- package/dist/scp/tx-options.js +184 -0
- package/dist/scp/tx-options.js.map +1 -0
- package/dist/scp/write-bundle.d.ts +131 -0
- package/dist/scp/write-bundle.d.ts.map +1 -0
- package/dist/scp/write-bundle.js +160 -0
- package/dist/scp/write-bundle.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/BENCHMARK.md +58 -56
- package/docs/benchmark-chart.svg +166 -166
- package/docs/design/phase-a-execution-context.md +139 -0
- package/package.json +16 -9
- package/dist/scp/authoring-sql.d.ts +0 -134
- package/dist/scp/authoring-sql.d.ts.map +0 -1
- package/dist/scp/authoring-sql.js +0 -191
- package/dist/scp/authoring-sql.js.map +0 -1
- package/dist/scp/authoring.d.ts +0 -155
- package/dist/scp/authoring.d.ts.map +0 -1
- package/dist/scp/authoring.js +0 -162
- package/dist/scp/authoring.js.map +0 -1
- package/dist/scp/catalog.d.ts +0 -94
- package/dist/scp/catalog.d.ts.map +0 -1
- package/dist/scp/catalog.js +0 -234
- package/dist/scp/catalog.js.map +0 -1
- package/dist/scp/codegen.d.ts +0 -189
- package/dist/scp/codegen.d.ts.map +0 -1
- package/dist/scp/codegen.js +0 -421
- package/dist/scp/codegen.js.map +0 -1
- package/dist/scp/find-filter-guard.d.ts +0 -68
- package/dist/scp/find-filter-guard.d.ts.map +0 -1
- package/dist/scp/find-filter-guard.js +0 -88
- package/dist/scp/find-filter-guard.js.map +0 -1
- package/dist/scp/guard.d.ts +0 -43
- package/dist/scp/guard.d.ts.map +0 -1
- package/dist/scp/guard.js +0 -134
- package/dist/scp/guard.js.map +0 -1
- package/dist/scp/makesql/authoring-compile.d.ts +0 -90
- package/dist/scp/makesql/authoring-compile.d.ts.map +0 -1
- package/dist/scp/makesql/authoring-compile.js +0 -494
- package/dist/scp/makesql/authoring-compile.js.map +0 -1
- package/dist/scp/makesql/ir.d.ts +0 -42
- package/dist/scp/makesql/ir.d.ts.map +0 -1
- package/dist/scp/makesql/ir.js +0 -66
- package/dist/scp/makesql/ir.js.map +0 -1
- package/dist/scp/makesql/static-bundle.d.ts +0 -187
- package/dist/scp/makesql/static-bundle.d.ts.map +0 -1
- package/dist/scp/makesql/static-bundle.js +0 -1113
- package/dist/scp/makesql/static-bundle.js.map +0 -1
- package/dist/scp/runtime.d.ts +0 -242
- package/dist/scp/runtime.d.ts.map +0 -1
- package/dist/scp/runtime.js +0 -400
- package/dist/scp/runtime.js.map +0 -1
|
@@ -1,1113 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* litedbmodel v2 SCP — the STATIC, PORTABLE `makeSQL` bundle + its runtime (epic #43/#45
|
|
4
|
-
* Phase B; design #45 owner-confirmed static-bundle decision).
|
|
5
|
-
*
|
|
6
|
-
* This is the SOLE read/compile path of the makeSQL SCP model, expressed as a compile step
|
|
7
|
-
* that is **symbolic** (no concrete input needed) and a runtime that evaluates value-specs +
|
|
8
|
-
* skip PER-INPUT via behavior-contracts. It REPLACES the reduced-spine `../runtime.ts`
|
|
9
|
-
* `SqlBundle`/`compileBundle`/`executeBundle` for the read path.
|
|
10
|
-
*
|
|
11
|
-
* ## The static bundle shape (design #45)
|
|
12
|
-
*
|
|
13
|
-
* A compiled behavior method is a bundle of ordered {@link StaticStatement} — each a
|
|
14
|
-
* `makeSQL` TEMPLATE:
|
|
15
|
-
* - `sql` — tuned dialect SQL text (`?` placeholders), COMPLETE and value-independent
|
|
16
|
-
* (compiled ONCE; the single-JSON-param array forms + PG `= ANY`/UNNEST keep
|
|
17
|
-
* the text fixed regardless of the runtime array length — no per-input text).
|
|
18
|
-
* - `params` — value-specs = bc Expression IR (`{ref:[…]}` / small ops / literals), 1:1 with
|
|
19
|
-
* the top-level `?`, evaluated AT RUNTIME by bc `evaluateExpression`.
|
|
20
|
-
* - `skip?` — a bc presence expression, evaluated at runtime; when truthy the whole
|
|
21
|
-
* statement (its `sql` and `params`) drops (contributes NOTHING).
|
|
22
|
-
*
|
|
23
|
-
* So the compile step emits a reusable portable artifact; the runtime (this module's
|
|
24
|
-
* {@link executeStaticBundle}) evaluates skip + params per-input via bc, assembles present
|
|
25
|
-
* fragments, renders placeholders, and executes. This unifies read/write/codegen on ONE
|
|
26
|
-
* `SqlBundle` shape and enables multi-language IR-reference execution.
|
|
27
|
-
*
|
|
28
|
-
* ## Symbolic compile — reuses `authoring-compile`'s port decoding, defers evaluation
|
|
29
|
-
*
|
|
30
|
-
* Phase A's `authoring-compile.ts` compiled per-input EAGERLY (it called `evaluateExpression`
|
|
31
|
-
* on every value slot to produce concrete `{sql, params}`). Here we keep the SAME closed-set
|
|
32
|
-
* port decoding (`../bridge.ts` operators, IN-sentinel, SKIP-`cond` collapse) but DEFER the
|
|
33
|
-
* value/skip evaluation to the runtime: a where-member's value operand stays as its raw bc
|
|
34
|
-
* Expression IR, a SKIP-`cond` guard becomes the statement's `skip` expression, and the SQL
|
|
35
|
-
* text is emitted once.
|
|
36
|
-
*/
|
|
37
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.compileSelectNode = compileSelectNode;
|
|
39
|
-
exports.compileStaticBundle = compileStaticBundle;
|
|
40
|
-
exports.executeStaticBundle = executeStaticBundle;
|
|
41
|
-
exports.executeStaticWrite = executeStaticWrite;
|
|
42
|
-
exports.compileReadGraph = compileReadGraph;
|
|
43
|
-
exports.executeReadGraph = executeReadGraph;
|
|
44
|
-
exports.executeReadGraphAsync = executeReadGraphAsync;
|
|
45
|
-
exports.executeReadBehavior = executeReadBehavior;
|
|
46
|
-
exports.renderReadPrimary = renderReadPrimary;
|
|
47
|
-
const behavior_contracts_1 = require("behavior-contracts");
|
|
48
|
-
const tx_1 = require("./tx");
|
|
49
|
-
const makesql_1 = require("./makesql");
|
|
50
|
-
const handler_1 = require("./handler");
|
|
51
|
-
const tx_2 = require("./tx");
|
|
52
|
-
const find_filter_guard_1 = require("../find-filter-guard");
|
|
53
|
-
const outtype_1 = require("./outtype");
|
|
54
|
-
const errors_1 = require("../errors");
|
|
55
|
-
const DBConditions_1 = require("../../DBConditions");
|
|
56
|
-
const DBValues_1 = require("../../DBValues");
|
|
57
|
-
const json_array_1 = require("./json-array");
|
|
58
|
-
const compile_select_1 = require("./compile-select");
|
|
59
|
-
const compile_1 = require("./compile");
|
|
60
|
-
const authoring_sql_1 = require("../authoring-sql");
|
|
61
|
-
function isMap(n) {
|
|
62
|
-
return 'map' in n;
|
|
63
|
-
}
|
|
64
|
-
function nodeRef(n) {
|
|
65
|
-
return isMap(n) ? { component: n.map.component, ports: n.map.ports } : { component: n.component, ports: n.ports };
|
|
66
|
-
}
|
|
67
|
-
function stringPort(ports, name) {
|
|
68
|
-
const v = ports[name];
|
|
69
|
-
if (v === undefined)
|
|
70
|
-
return undefined;
|
|
71
|
-
if (typeof v !== 'string')
|
|
72
|
-
throw new Error(`static-bundle: port '${name}' must be a literal string in the IR (got ${JSON.stringify(v)})`);
|
|
73
|
-
return v;
|
|
74
|
-
}
|
|
75
|
-
function arrPort(ports, name) {
|
|
76
|
-
const v = ports[name];
|
|
77
|
-
if (v === undefined)
|
|
78
|
-
return undefined;
|
|
79
|
-
if (typeof v === 'object' && v !== null && 'arr' in v && Array.isArray(v.arr)) {
|
|
80
|
-
return v.arr;
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`static-bundle: port '${name}' must be an {arr:[...]} literal in the IR (got ${JSON.stringify(v)})`);
|
|
83
|
-
}
|
|
84
|
-
function stringArrayPort(ports, name) {
|
|
85
|
-
const arr = arrPort(ports, name);
|
|
86
|
-
if (arr === undefined)
|
|
87
|
-
return undefined;
|
|
88
|
-
return arr.map((e) => {
|
|
89
|
-
if (typeof e !== 'string')
|
|
90
|
-
throw new Error(`static-bundle: '${name}' entries must be literal strings (got ${JSON.stringify(e)})`);
|
|
91
|
-
return e;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
/** Read a `{name, sql}` CTE map port (V0 R4). Both fields must be literal strings in the IR. */
|
|
95
|
-
function mapPort(ports, name) {
|
|
96
|
-
const v = ports[name];
|
|
97
|
-
if (v === undefined)
|
|
98
|
-
return undefined;
|
|
99
|
-
if (typeof v === 'object' && v !== null && 'obj' in v && typeof v.obj === 'object') {
|
|
100
|
-
const obj = v.obj;
|
|
101
|
-
if (typeof obj.name !== 'string' || typeof obj.sql !== 'string') {
|
|
102
|
-
throw new Error(`static-bundle: port '${name}' must be a {name, sql} literal map (got ${JSON.stringify(v)})`);
|
|
103
|
-
}
|
|
104
|
-
return { name: obj.name, sql: obj.sql };
|
|
105
|
-
}
|
|
106
|
-
throw new Error(`static-bundle: port '${name}' must be an {obj:{name, sql}} literal (got ${JSON.stringify(v)})`);
|
|
107
|
-
}
|
|
108
|
-
/** Read an `{arr:[…]}` list of DEFERRED value-specs (bc Expression IR), e.g. cte/join params. */
|
|
109
|
-
function exprArrayPort(ports, name) {
|
|
110
|
-
const arr = arrPort(ports, name);
|
|
111
|
-
if (arr === undefined)
|
|
112
|
-
return undefined;
|
|
113
|
-
return arr;
|
|
114
|
-
}
|
|
115
|
-
function opKey(node) {
|
|
116
|
-
if (node === null || typeof node !== 'object' || Array.isArray(node))
|
|
117
|
-
return undefined;
|
|
118
|
-
const keys = Object.keys(node);
|
|
119
|
-
return keys.length === 1 ? keys[0] : undefined;
|
|
120
|
-
}
|
|
121
|
-
function columnOf(node, ctx) {
|
|
122
|
-
const op = opKey(node);
|
|
123
|
-
if (op !== 'ref' && op !== 'refOpt')
|
|
124
|
-
throw new Error(`static-bundle: ${ctx}: the column operand must be a {ref:[…]} / {refOpt:[…]} path`);
|
|
125
|
-
const path = node[op];
|
|
126
|
-
if (!Array.isArray(path) || path.length === 0 || typeof path[path.length - 1] !== 'string') {
|
|
127
|
-
throw new Error(`static-bundle: ${ctx}: column ref path must be a non-empty string path`);
|
|
128
|
-
}
|
|
129
|
-
return path[path.length - 1];
|
|
130
|
-
}
|
|
131
|
-
function inSentinelColumn(node) {
|
|
132
|
-
const op = opKey(node);
|
|
133
|
-
if (op !== 'ref' && op !== 'refOpt')
|
|
134
|
-
return undefined;
|
|
135
|
-
const path = node[op];
|
|
136
|
-
if (Array.isArray(path) && path.length >= 2 && path[0] === tx_1.IN_SENTINEL && typeof path[path.length - 1] === 'string') {
|
|
137
|
-
return path[path.length - 1];
|
|
138
|
-
}
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
function binOperands(node, op, at) {
|
|
142
|
-
const args = node[op];
|
|
143
|
-
if (!Array.isArray(args) || args.length !== 2)
|
|
144
|
-
throw new Error(`static-bundle: ${at}: '${op}' expects exactly 2 operands`);
|
|
145
|
-
return [args[0], args[1]];
|
|
146
|
-
}
|
|
147
|
-
// ── Additive where-primitive decode (V0 R2/R3) ─────────────────────────────────
|
|
148
|
-
//
|
|
149
|
-
// The authoring layer (`authoring-sql.ts`) encodes each additive where-primitive as
|
|
150
|
-
// `eq(<sentinel ref>, <value>)` (the SAME closed-set trick `whereIn` uses). Here we detect the
|
|
151
|
-
// sentinel ref-path head, rebuild the EXACT v1 `ConditionObject` (or `DBSubquery`/`DBExists`) it
|
|
152
|
-
// stands for, drive the ORIGINAL `DBConditions.compile()` for byte-true text, and defer the value
|
|
153
|
-
// operands as value-specs 1:1 with the produced `?` placeholders — never a hand-rolled string.
|
|
154
|
-
/** The sentinel ref-path segments (`[head, seg0, …]`) if `node` is a sentinel column ref. */
|
|
155
|
-
function sentinelPath(node) {
|
|
156
|
-
const op = opKey(node);
|
|
157
|
-
if (op !== 'ref' && op !== 'refOpt')
|
|
158
|
-
return undefined;
|
|
159
|
-
const path = node[op];
|
|
160
|
-
if (!Array.isArray(path) || path.length === 0)
|
|
161
|
-
return undefined;
|
|
162
|
-
return path.every((s) => typeof s === 'string') ? path : undefined;
|
|
163
|
-
}
|
|
164
|
-
/** Extract a literal `{arr:[…]}` value-spec list from a value operand (else undefined). */
|
|
165
|
-
function litArr(node) {
|
|
166
|
-
if (node !== null && typeof node === 'object' && !Array.isArray(node) && 'arr' in node) {
|
|
167
|
-
const a = node.arr;
|
|
168
|
-
if (Array.isArray(a))
|
|
169
|
-
return a;
|
|
170
|
-
}
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
/** Decode the nested-makeSQL Fragment param operand of a subquery/EXISTS primitive. */
|
|
174
|
-
function nestedSub(node, at) {
|
|
175
|
-
if (node === null || typeof node !== 'object' || Array.isArray(node) || !('obj' in node)) {
|
|
176
|
-
throw new Error(`static-bundle: ${at}: subquery operand must be a nested makeSQL Fragment {obj:{sql, params?}}`);
|
|
177
|
-
}
|
|
178
|
-
const obj = node.obj;
|
|
179
|
-
const sqlNode = obj.sql;
|
|
180
|
-
if (typeof sqlNode !== 'string')
|
|
181
|
-
throw new Error(`static-bundle: ${at}: nested subquery 'sql' must be a literal string`);
|
|
182
|
-
const params = obj.params === undefined ? [] : (litArr(obj.params) ?? undefinedThrow(at));
|
|
183
|
-
return { sql: sqlNode, params };
|
|
184
|
-
}
|
|
185
|
-
function undefinedThrow(at) {
|
|
186
|
-
throw new Error(`static-bundle: ${at}: nested subquery 'params' must be an {arr:[…]} literal`);
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Decode a sentinel-encoded additive where-primitive into its v1-sourced fragment. Returns
|
|
190
|
-
* undefined when the LHS is NOT one of the additive sentinels (the caller falls through to the
|
|
191
|
-
* eq/cmp/IN handling). The TEXT is always produced by the ORIGINAL builders (`DBConditions` /
|
|
192
|
-
* `dbCast` / `dbDynamic` / `dbImmediate` / `dbTupleIn` / `DBSubquery` / `DBExists`).
|
|
193
|
-
*/
|
|
194
|
-
function decodeSentinel(col, val, dialect, at) {
|
|
195
|
-
const path = sentinelPath(col);
|
|
196
|
-
if (path === undefined)
|
|
197
|
-
return undefined;
|
|
198
|
-
const head = path[0];
|
|
199
|
-
if (head === authoring_sql_1.BETWEEN_SENTINEL) {
|
|
200
|
-
const [column] = [path[1]];
|
|
201
|
-
const bounds = litArr(val);
|
|
202
|
-
if (bounds === undefined || bounds.length !== 2)
|
|
203
|
-
throw new Error(`static-bundle: ${at}: BETWEEN expects a 2-element bounds array`);
|
|
204
|
-
// v1 custom-op key `col BETWEEN ? AND ?` → 2 placeholders, both deferred.
|
|
205
|
-
return { sql: v1ConditionText({ [`${column} BETWEEN ? AND ?`]: [PROBE, PROBE] }, dialect), params: bounds };
|
|
206
|
-
}
|
|
207
|
-
if (head === authoring_sql_1.LIKE_SENTINEL) {
|
|
208
|
-
const [column, keyword] = [path[1], path[2]];
|
|
209
|
-
// v1 custom-op key `col LIKE ?` / `col ILIKE ?` → 1 placeholder, deferred.
|
|
210
|
-
return { sql: v1ConditionText({ [`${column} ${keyword} ?`]: PROBE }, dialect), params: [val] };
|
|
211
|
-
}
|
|
212
|
-
if (head === authoring_sql_1.CAST_SENTINEL) {
|
|
213
|
-
const [column, sqlType, cmpOp] = [path[1], path[2], path[3]];
|
|
214
|
-
// Drive the ORIGINAL `dbCast(value, type, op)` so the PG `::type` cast (and MySQL/SQLite
|
|
215
|
-
// no-cast) is byte-true and dialect-gated by the formatter `v1ConditionText` passes.
|
|
216
|
-
return { sql: v1ConditionText({ [column]: (0, DBValues_1.dbCast)(PROBE, sqlType, cmpOp) }, dialect), params: [val] };
|
|
217
|
-
}
|
|
218
|
-
if (head === authoring_sql_1.DYNAMIC_SENTINEL) {
|
|
219
|
-
const [column, template] = [path[1], path[2]];
|
|
220
|
-
const values = litArr(val);
|
|
221
|
-
// `dbDynamic(template, values)` → `col = <template>` with one `?` per template placeholder.
|
|
222
|
-
const nPlaceholders = (template.match(/\?/g) ?? []).length;
|
|
223
|
-
const probeVals = Array.from({ length: nPlaceholders }, () => PROBE);
|
|
224
|
-
const deferred = values ?? [val];
|
|
225
|
-
if (deferred.length !== nPlaceholders) {
|
|
226
|
-
throw new Error(`static-bundle: ${at}: dbDynamic template has ${nPlaceholders} '?' but ${deferred.length} value-specs`);
|
|
227
|
-
}
|
|
228
|
-
return { sql: v1ConditionText({ [column]: (0, DBValues_1.dbDynamic)(template, probeVals) }, dialect), params: deferred };
|
|
229
|
-
}
|
|
230
|
-
if (head === authoring_sql_1.IMMEDIATE_SENTINEL) {
|
|
231
|
-
const [column, sql] = [path[1], path[2]];
|
|
232
|
-
// `dbImmediate(sql)` → `col = <sql>` inline, NO bound param.
|
|
233
|
-
return { sql: v1ConditionText({ [column]: (0, DBValues_1.dbImmediate)(sql) }, dialect), params: [] };
|
|
234
|
-
}
|
|
235
|
-
if (head === authoring_sql_1.TUPLE_SENTINEL) {
|
|
236
|
-
const columns = path.slice(1);
|
|
237
|
-
const tuples = litArr(val);
|
|
238
|
-
if (tuples === undefined)
|
|
239
|
-
throw new Error(`static-bundle: ${at}: tuple-IN expects an array-of-tuples value`);
|
|
240
|
-
// Each tuple is itself a literal `{arr:[…]}` of value-specs. The v1 `dbTupleIn` text is
|
|
241
|
-
// `(a, b) IN ((?, ?), …)` — one `?` per tuple element; defer them in row-major order.
|
|
242
|
-
const rows = tuples.map((t) => litArr(t) ?? throwTuple(at));
|
|
243
|
-
const deferred = [];
|
|
244
|
-
for (const row of rows)
|
|
245
|
-
for (const el of row)
|
|
246
|
-
deferred.push(el);
|
|
247
|
-
// Build the v1 text with PROBE tuples of the same shape (all-int probes → identical text).
|
|
248
|
-
const probeRows = rows.map((r) => r.map(() => PROBE));
|
|
249
|
-
return { sql: tupleInText(columns, probeRows, dialect), params: deferred };
|
|
250
|
-
}
|
|
251
|
-
if (head === authoring_sql_1.SUBQUERY_SENTINEL) {
|
|
252
|
-
const [lhs, keyword] = [path[1], path[2]];
|
|
253
|
-
const sub = nestedSub(val, at);
|
|
254
|
-
// v1 `DBSubquery` renders `<lhs> <IN|NOT IN> (<inner SELECT>)`. The inner SELECT text is
|
|
255
|
-
// v1-sourced by the AUTHOR (dialect-tuned) and spliced verbatim; its bound value-specs follow.
|
|
256
|
-
return { sql: `${lhs} ${keyword} (${sub.sql})`, params: sub.params };
|
|
257
|
-
}
|
|
258
|
-
if (head === authoring_sql_1.EXISTS_SENTINEL) {
|
|
259
|
-
const [keyword] = [path[1]];
|
|
260
|
-
const sub = nestedSub(val, at);
|
|
261
|
-
// v1 `DBExists` renders `<EXISTS|NOT EXISTS> (<inner SELECT 1 …>)`. Inner text v1-sourced.
|
|
262
|
-
return { sql: `${keyword} (${sub.sql})`, params: sub.params };
|
|
263
|
-
}
|
|
264
|
-
return undefined;
|
|
265
|
-
}
|
|
266
|
-
function throwTuple(at) {
|
|
267
|
-
throw new Error(`static-bundle: ${at}: each tuple-IN row must be an {arr:[…]} literal`);
|
|
268
|
-
}
|
|
269
|
-
/** The v1 `dbTupleIn` text via a nested `DBConditions.__tuple__` (byte-true; probe tuples). */
|
|
270
|
-
function tupleInText(columns, probeRows, dialect) {
|
|
271
|
-
// Reuse the ORIGINAL dbTupleIn so the `(cols) IN ((?, …), …)` text is v1-produced, not hand-rolled.
|
|
272
|
-
const cond = { __tuple__: (0, DBValues_1.dbTupleIn)([...columns], probeRows) };
|
|
273
|
-
return v1ConditionText(cond, dialect);
|
|
274
|
-
}
|
|
275
|
-
/** The comparison-operator SQL symbols, keyed by the bc operator name (drives the v1 custom-op key). */
|
|
276
|
-
const CMP_OPS = { lt: '<', le: '<=', gt: '>', ge: '>=', ne: '<>' };
|
|
277
|
-
/** A probe placeholder value fed to the v1 builder so it emits one `?` per bound slot. */
|
|
278
|
-
const PROBE = '__probe__';
|
|
279
|
-
/**
|
|
280
|
-
* Produce a bare condition body's TEXT by driving the ORIGINAL v1 builder
|
|
281
|
-
* `DBConditions.compile()` (through `conditionsFor`, so MySQL/SQLite IN-lists take the
|
|
282
|
-
* single-JSON server-side form and PG stays base-class). The `conditions` object is built to
|
|
283
|
-
* be byte-identical to what the v1 eager path would pass for the same construct; the throwaway
|
|
284
|
-
* `probe` params array absorbs the probe values (the real runtime values are the caller's
|
|
285
|
-
* deferred value-specs). Returns the v1 text — no v2 hand-roll.
|
|
286
|
-
*/
|
|
287
|
-
function v1ConditionText(conditions, dialect) {
|
|
288
|
-
const probe = [];
|
|
289
|
-
// Pass the dialect cast formatter so `dbCast` gates the `::type` cast per-dialect (PG applies it,
|
|
290
|
-
// MySQL/SQLite drop it — v1 parity). Inert for every non-cast construct (the formatter is only
|
|
291
|
-
// consulted by DBCast/DBCastArray), so it stays byte-identical for eq/cmp/BETWEEN/LIKE/etc.
|
|
292
|
-
return (0, json_array_1.conditionsFor)(conditions, dialect).compile(probe, (0, compile_1.formatterFor)(dialect));
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Assemble an AND/OR GROUP's text by driving the ORIGINAL `DBConditions.compile()` — the SAME join
|
|
296
|
-
* + paren-wrapping the v1 eager path performs — NOT a v2 hand-roll (#47 item 5). The already-v1-
|
|
297
|
-
* produced leaf texts are fed as `__raw__` members of a nested `DBConditions` under the group's
|
|
298
|
-
* operator; a parent references that nested group, so v1's own `compileCondition` emits the wrapping
|
|
299
|
-
* `(${nested.compile()})` and `nested.compile()` joins the leaves with ` AND `/` OR ` — i.e. the
|
|
300
|
-
* `(A AND B)` / `(A OR B)` text is byte-produced by v1's builder, not concatenated here.
|
|
301
|
-
*
|
|
302
|
-
* `DBConditions.compile` appends probe values per `__raw__` in encounter order, so the parent probe
|
|
303
|
-
* list length equals the group's total placeholder count; the leaf value-specs (kept 1:1 with those
|
|
304
|
-
* placeholders by the caller) remain the runtime binds — the probe array is throwaway.
|
|
305
|
-
*/
|
|
306
|
-
function v1GroupText(op, leafSqls) {
|
|
307
|
-
const operator = op === 'and' ? 'AND' : 'OR';
|
|
308
|
-
// Each leaf is a v1-produced predicate string → a `__raw__` member. `add` preserves order and the
|
|
309
|
-
// nested group's `compile()` joins them with ` ${operator} ` (v1's exact algorithm).
|
|
310
|
-
const inner = new DBConditions_1.DBConditions({}, operator);
|
|
311
|
-
for (const sql of leafSqls)
|
|
312
|
-
inner.add({ __raw__: sql }, operator);
|
|
313
|
-
// A parent whose sole member is the nested group → v1 wraps it as `(${inner.compile()})`
|
|
314
|
-
// (DBConditions.compileCondition, the `value instanceof DBConditions` branch) — the group parens.
|
|
315
|
-
const parent = new DBConditions_1.DBConditions({});
|
|
316
|
-
parent.add(inner);
|
|
317
|
-
const probe = [];
|
|
318
|
-
return parent.compile(probe);
|
|
319
|
-
}
|
|
320
|
-
/** A deferred value-spec that JSON-encodes an array value-spec at runtime (single-param IN). */
|
|
321
|
-
function jsonArraySpec(dialect, valueSpec) {
|
|
322
|
-
// Marker op the runtime interprets: `{ __jsonArray: <valueSpec>, dialect }`. Postgres keeps
|
|
323
|
-
// the array as-is (bound as a t[] param via node-postgres); mysql/sqlite JSON-encode it.
|
|
324
|
-
return { __jsonArray: valueSpec, dialect };
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* The IN-list membership fragment, driven by the ORIGINAL builders (never hand-rolled):
|
|
328
|
-
* - MySQL/SQLite: `conditionsFor({col:[…]})` → the single-JSON `JsonArrayConditions` form
|
|
329
|
-
* (`JSON_TABLE`/`json_each`) — the SAME text the eager path emits; ONE `?`, one JSON param.
|
|
330
|
-
* - PostgreSQL: the single-array-param form `col = ANY(?)` with NO element-type cast. This
|
|
331
|
-
* authored `whereIn` surface carries NO column type (spec §7 `inColumn`), so there is nothing
|
|
332
|
-
* to cast the array TO at compile time — and value-inference (v1's `inferPgArrayType`) cannot
|
|
333
|
-
* recover it either: `[]` is indistinguishable from any element type, and a uuid value is
|
|
334
|
-
* indistinguishable-from-text by value. Emitting a cast therefore RE-BROKE #46 twice on live
|
|
335
|
-
* PG (empty int → `integer = text`; uuid → `uuid = text`). Instead we cast NOTHING and let
|
|
336
|
-
* PostgreSQL infer the array element type from the column context (`id` int → `int[]`, a uuid
|
|
337
|
-
* column → `uuid[]`, empty → the column's type → zero rows, no error) — proven on live PG16
|
|
338
|
-
* for int/bigint/uuid/bool/numeric/text and all three empty cases. This is v1-RESULT-parity
|
|
339
|
-
* (same rows, incl. empty → zero rows via `1 = 0` in v1): v1's authored surface had no
|
|
340
|
-
* `= ANY` form (it expanded `IN(?,?,…)`), so byte-identity to a v1 `= ANY(?::T[])` is not the
|
|
341
|
-
* requirement here — only that the rows match. The array binds verbatim as ONE param.
|
|
342
|
-
*/
|
|
343
|
-
function inListFragment(dialect, column, valueSpec) {
|
|
344
|
-
if (dialect === 'postgres') {
|
|
345
|
-
// `col = ANY(?)` — no cast. PG infers the array element type from the column, which is correct
|
|
346
|
-
// for every type INCLUDING empty and uuid (a value-inferred cast cannot, and re-broke #46).
|
|
347
|
-
const conditions = { __raw__: [`${column} = ANY(?)`, [PROBE]] };
|
|
348
|
-
return { sql: v1ConditionText(conditions, dialect), params: [jsonArraySpec(dialect, valueSpec)] };
|
|
349
|
-
}
|
|
350
|
-
// MySQL/SQLite: the single-JSON IN-list is the JsonArrayConditions form (v1/epic builder).
|
|
351
|
-
const conditions = { [column]: [PROBE] };
|
|
352
|
-
return { sql: v1ConditionText(conditions, dialect), params: [jsonArraySpec(dialect, valueSpec)] };
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Lower ONE where-member Expression node to a static fragment. The TEXT of every leaf is
|
|
356
|
-
* emitted by the ORIGINAL `DBConditions`; AND/OR groups join the v1-produced leaf texts with
|
|
357
|
-
* the EXACT connector `DBConditions.compile` uses (`parts.join(' AND '|' OR ')` + wrapping
|
|
358
|
-
* parens) — pure text glue, the original's own algorithm (mirrors `compile-pg.compileBaseSelect`).
|
|
359
|
-
*/
|
|
360
|
-
function lowerWhereMember(node, dialect, at) {
|
|
361
|
-
const op = opKey(node);
|
|
362
|
-
if (op === undefined)
|
|
363
|
-
throw new Error(`static-bundle: ${at}: a where member must be a single-operator Expression node`);
|
|
364
|
-
if (op === 'and' || op === 'or') {
|
|
365
|
-
const args = node[op];
|
|
366
|
-
if (!Array.isArray(args) || args.length < 2)
|
|
367
|
-
throw new Error(`static-bundle: ${at}: '${op}' group expects >= 2 members`);
|
|
368
|
-
const parts = [];
|
|
369
|
-
const params = [];
|
|
370
|
-
args.forEach((m, i) => {
|
|
371
|
-
const f = lowerWhereMember(m, dialect, `${at}.${op}[${i}]`);
|
|
372
|
-
parts.push(f.sql);
|
|
373
|
-
params.push(...f.params);
|
|
374
|
-
});
|
|
375
|
-
// Assemble the group text through the ORIGINAL `DBConditions` join + paren-wrap (item 5) — NOT
|
|
376
|
-
// a v2 `parts.join(connector)` hand-roll. The v1-produced leaf texts keep their `?` order, so
|
|
377
|
-
// the deferred value-specs (`params`) still bind 1:1 with the placeholders in the assembled sql.
|
|
378
|
-
return { sql: v1GroupText(op, parts), params };
|
|
379
|
-
}
|
|
380
|
-
if (op === 'eq') {
|
|
381
|
-
const [col, val] = binOperands(node, op, at);
|
|
382
|
-
// Additive where-primitives (V0 R2/R3) — decoded FIRST (an IMMEDIATE/EXISTS primitive carries a
|
|
383
|
-
// `null` RHS that would otherwise be mis-read as IS NULL).
|
|
384
|
-
const inCol = inSentinelColumn(col);
|
|
385
|
-
if (inCol !== undefined)
|
|
386
|
-
return inListFragment(dialect, inCol, val);
|
|
387
|
-
const additive = decodeSentinel(col, val, dialect, at);
|
|
388
|
-
if (additive !== undefined)
|
|
389
|
-
return additive;
|
|
390
|
-
const column = columnOf(col, at);
|
|
391
|
-
if (val === null)
|
|
392
|
-
return { sql: v1ConditionText({ [column]: null }, dialect), params: [] };
|
|
393
|
-
// v1 default equality: `{col: value}` → `col = ?`.
|
|
394
|
-
return { sql: v1ConditionText({ [column]: PROBE }, dialect), params: [val] };
|
|
395
|
-
}
|
|
396
|
-
if (op in CMP_OPS) {
|
|
397
|
-
const [col, val] = binOperands(node, op, at);
|
|
398
|
-
const column = columnOf(col, at);
|
|
399
|
-
if (op === 'ne' && val === null) {
|
|
400
|
-
// v1 `IS NOT NULL`: a boolean-true key that is a SQL expression returns the key verbatim.
|
|
401
|
-
return { sql: v1ConditionText({ [`${column} IS NOT NULL`]: true }, dialect), params: [] };
|
|
402
|
-
}
|
|
403
|
-
// v1 comparison: a custom-operator key (`col <op> ?`) returns the key + pushes the value.
|
|
404
|
-
return { sql: v1ConditionText({ [`${column} ${CMP_OPS[op]} ?`]: PROBE }, dialect), params: [val] };
|
|
405
|
-
}
|
|
406
|
-
throw new Error(`static-bundle: ${at}: unsupported where operator '${op}' (supported: eq/ne/lt/le/gt/ge/and/or; IN via ${tx_1.IN_SENTINEL}; IS NULL via eq(col,null))`);
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Lower the `where` port to a list of `{ sql, params, skip? }` fragment statements. A
|
|
410
|
-
* `{cond:[c, member, null]}` SKIP-optional member lowers its inner member and attaches `c`
|
|
411
|
-
* as the fragment's `skip` presence expression (dropped at runtime when `c` is null/false).
|
|
412
|
-
* The FIRST present fragment carries no leading connector; every later one carries ` AND `.
|
|
413
|
-
*/
|
|
414
|
-
function lowerWherePort(ports, dialect, at) {
|
|
415
|
-
const arr = arrPort(ports, 'where');
|
|
416
|
-
if (arr === undefined || arr.length === 0)
|
|
417
|
-
return [];
|
|
418
|
-
// Each member becomes a fragment statement. The leading ` WHERE `/` AND ` connectors are
|
|
419
|
-
// resolved at runtime from the PRESENT set (a compile-time connector would be wrong when an
|
|
420
|
-
// earlier fragment skips). We therefore mark each fragment and let the assembler join them.
|
|
421
|
-
const fragments = [];
|
|
422
|
-
arr.forEach((m, i) => {
|
|
423
|
-
const memberAt = `${at}.where[${i}]`;
|
|
424
|
-
if (opKey(m) === 'cond') {
|
|
425
|
-
const cargs = m.cond;
|
|
426
|
-
if (!Array.isArray(cargs) || cargs.length !== 3 || cargs[2] !== null) {
|
|
427
|
-
throw new Error(`static-bundle: ${memberAt}: a SKIP-optional condition must be {cond:[c, <member>, null]} (else = null)`);
|
|
428
|
-
}
|
|
429
|
-
const inner = lowerWhereMember(cargs[1], dialect, `${memberAt}.cond.then`);
|
|
430
|
-
fragments.push({ sql: inner.sql, params: inner.params, skip: skipFrom(cargs[0]) });
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
const f = lowerWhereMember(m, dialect, memberAt);
|
|
434
|
-
fragments.push({ sql: f.sql, params: f.params });
|
|
435
|
-
});
|
|
436
|
-
return fragments.map((f) => ({ sql: f.sql, params: f.params, ...(f.skip !== undefined ? { skip: f.skip } : {}) }));
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Turn a SKIP-`cond` guard condition `c` (present iff `c` is a present binding) into a bc
|
|
440
|
-
* `skip` expression: the statement is SKIPPED when the guard is ABSENT, i.e. `skip = not(c)`.
|
|
441
|
-
* `c` is a closed-set bc Expression (e.g. `{ne:[{refOpt:['status']}, null]}`).
|
|
442
|
-
*/
|
|
443
|
-
function skipFrom(guard) {
|
|
444
|
-
return { not: [guard] };
|
|
445
|
-
}
|
|
446
|
-
// ── Node → static statements (SELECT read path) ────────────────────────────────
|
|
447
|
-
/**
|
|
448
|
-
* The ORIGINAL `_buildSelectSQL` GROUP BY / ORDER BY append text for a Select tail fragment.
|
|
449
|
-
* Drives `compileSelect` (a bare `SELECT * FROM <t>` head + the one tail field) and slices off
|
|
450
|
-
* the shared head, so the remaining ` GROUP BY …` / ` ORDER BY …` is v1's exact append — never
|
|
451
|
-
* a v2 hand-roll. The head is always `SELECT * FROM <table>` here (no `select`/conditions given).
|
|
452
|
-
*/
|
|
453
|
-
function selectTail(desc, table) {
|
|
454
|
-
const full = (0, compile_select_1.compileSelect)(desc).sql;
|
|
455
|
-
const head = `SELECT * FROM ${table}`;
|
|
456
|
-
return full.startsWith(head) ? full.slice(head.length) : full;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* The ` LIMIT `/` OFFSET ` KEYWORD text sourced from the ORIGINAL `_buildSelectSQL` (via
|
|
460
|
-
* `compileSelect`) — item 5. v1 INLINES the count as a literal (` LIMIT 10`); the static bundle
|
|
461
|
-
* keeps the INTENTIONAL divergence of binding it as a `?` deferred value-spec (a portable equivalent
|
|
462
|
-
* every language runtime renders identically, evaluated per-input). So we drive v1 with a sentinel
|
|
463
|
-
* count, take v1's exact ` LIMIT <n>`/` OFFSET <n>` append, and swap the sentinel for `?` — the
|
|
464
|
-
* keyword text is v1's, only the literal becomes a placeholder. Documented, all-dialect parity-checked.
|
|
465
|
-
*/
|
|
466
|
-
function limitOffsetTail(kind, dialect, table) {
|
|
467
|
-
const SENTINEL = 987654321; // a distinctive count with no substring clash in the keyword text
|
|
468
|
-
const desc = kind === 'limit' ? { dialect, tableName: table, limit: SENTINEL } : { dialect, tableName: table, offset: SENTINEL };
|
|
469
|
-
const tail = selectTail(desc, table); // v1's ` LIMIT 987654321` / ` OFFSET 987654321`
|
|
470
|
-
return tail.replace(String(SENTINEL), '?'); // keep the bound-param behavior; keyword text is v1's
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Compile ONE authored `Select` node into its static `makeSQL` statement templates:
|
|
474
|
-
* the head `SELECT … FROM t` fragment plus one guarded WHERE fragment per member, then the
|
|
475
|
-
* trailing GROUP BY / ORDER BY / LIMIT / OFFSET tail. The head + GROUP BY + ORDER BY TEXT is
|
|
476
|
-
* produced by the ORIGINAL `compileSelect` (which reproduces `_buildSelectSQL` byte-for-byte),
|
|
477
|
-
* never a v2 hand-roll. LIMIT/OFFSET are INLINE literals in v1; for the static bundle they are
|
|
478
|
-
* deferred value-specs bound as `?` (a portable equivalent a per-language runtime renders
|
|
479
|
-
* identically) — evaluated at runtime.
|
|
480
|
-
*/
|
|
481
|
-
function compileSelectNode(node, dialect) {
|
|
482
|
-
const { component, ports } = nodeRef(node);
|
|
483
|
-
if (component !== 'Select' && component !== 'Count') {
|
|
484
|
-
throw new Error(`static-bundle: compileSelectNode only compiles Select/Count nodes (got '${component}')`);
|
|
485
|
-
}
|
|
486
|
-
const table = stringPort(ports, 'table');
|
|
487
|
-
if (table === undefined)
|
|
488
|
-
throw new Error(`static-bundle: ${component} node requires a literal 'table' port`);
|
|
489
|
-
// A `Count` node projects `COUNT(*) as count` (v1 `DBModel._count`: `SELECT COUNT(*) as count
|
|
490
|
-
// FROM t`), a `Select` its own column list (default `*`). Both heads go through the ORIGINAL
|
|
491
|
-
// `compileSelect`, so the text is v1-sourced (count head byte-identical to `_count`).
|
|
492
|
-
const isCount = component === 'Count';
|
|
493
|
-
const select = stringArrayPort(ports, 'select');
|
|
494
|
-
const cols = isCount ? 'COUNT(*) as count' : select && select.length > 0 ? select.join(', ') : '*';
|
|
495
|
-
const statements = [];
|
|
496
|
-
// Head `[WITH …] SELECT <cols> FROM <t>[ JOIN …]` — driven by the ORIGINAL `_buildSelectSQL` (via
|
|
497
|
-
// compileSelect, no WHERE/tail) so the skeleton text is byte-identical to v1. The optional
|
|
498
|
-
// `cte`/`join` head fields (V0 R4/R5) are v1-sourced from the SAME `SelectDesc`; the `?`
|
|
499
|
-
// placeholders in their raw text bind to the deferred `cteParams`/`joinParams` value-specs (v1
|
|
500
|
-
// param order: CTE → JOIN → WHERE). `select` carries the joined-column projection as authored.
|
|
501
|
-
const cte = mapPort(ports, 'cte');
|
|
502
|
-
const join = stringPort(ports, 'join');
|
|
503
|
-
const cteParams = exprArrayPort(ports, 'cteParams');
|
|
504
|
-
const joinParams = exprArrayPort(ports, 'joinParams');
|
|
505
|
-
const headDesc = { dialect, tableName: table, select: cols };
|
|
506
|
-
if (cte !== undefined)
|
|
507
|
-
headDesc.cte = { name: cte.name, sql: cte.sql, params: [] };
|
|
508
|
-
if (join !== undefined)
|
|
509
|
-
headDesc.join = join;
|
|
510
|
-
const headParams = [...(cteParams ?? []), ...(joinParams ?? [])];
|
|
511
|
-
statements.push({ sql: (0, compile_select_1.compileSelect)(headDesc).sql, params: headParams });
|
|
512
|
-
const whereFrags = lowerWherePort(ports, dialect, component);
|
|
513
|
-
for (const frag of whereFrags) {
|
|
514
|
-
// A bare predicate body flagged `whereFragment`; the runtime resolves the ` WHERE `/` AND `
|
|
515
|
-
// connector from the present set (a compile-time connector would be wrong when an earlier
|
|
516
|
-
// fragment skips).
|
|
517
|
-
statements.push({ sql: frag.sql, params: frag.params, whereFragment: true, ...(frag.skip !== undefined ? { skip: frag.skip } : {}) });
|
|
518
|
-
}
|
|
519
|
-
// A Count carries no projection/order/limit/offset tail (v1 `_count` is `COUNT(*)` + WHERE only).
|
|
520
|
-
if (isCount)
|
|
521
|
-
return statements;
|
|
522
|
-
// GROUP BY / ORDER BY tail — the ORIGINAL `_buildSelectSQL` append text (via compileSelect over
|
|
523
|
-
// a bare table, then slicing off the leading `SELECT * FROM <t>` head it shares).
|
|
524
|
-
const group = stringPort(ports, 'group');
|
|
525
|
-
if (group !== undefined)
|
|
526
|
-
statements.push({ sql: selectTail({ dialect, tableName: table, group }, table), params: [] });
|
|
527
|
-
const order = stringPort(ports, 'order');
|
|
528
|
-
if (order !== undefined)
|
|
529
|
-
statements.push({ sql: selectTail({ dialect, tableName: table, order }, table), params: [] });
|
|
530
|
-
// LIMIT/OFFSET: the ` LIMIT `/` OFFSET ` keyword text is v1-sourced (via compileSelect), the count
|
|
531
|
-
// stays a bound `?` value-spec (the intentional portable divergence from v1's literal inlining).
|
|
532
|
-
if (ports.limit !== undefined)
|
|
533
|
-
statements.push({ sql: limitOffsetTail('limit', dialect, table), params: [ports.limit] });
|
|
534
|
-
if (ports.offset !== undefined)
|
|
535
|
-
statements.push({ sql: limitOffsetTail('offset', dialect, table), params: [ports.offset] });
|
|
536
|
-
// FOR UPDATE / append (V0 R3/R6) — v1 `_buildSelectSQL` tail order is `… FOR UPDATE <append>`.
|
|
537
|
-
// Both are v1-sourced via `selectTail` (a bare-head compileSelect, head sliced off). `append`
|
|
538
|
-
// (e.g. HAVING) is raw trailing text with no bound param on this authored surface.
|
|
539
|
-
const forUpdate = stringPort(ports, 'forUpdate');
|
|
540
|
-
if (forUpdate === 'true')
|
|
541
|
-
statements.push({ sql: selectTail({ dialect, tableName: table, forUpdate: true }, table), params: [] });
|
|
542
|
-
const append = stringPort(ports, 'append');
|
|
543
|
-
if (append !== undefined)
|
|
544
|
-
statements.push({ sql: selectTail({ dialect, tableName: table, append }, table), params: [] });
|
|
545
|
-
return statements;
|
|
546
|
-
}
|
|
547
|
-
// ── Compile a behavior method → static bundle (CRUD primary node) ──────────────
|
|
548
|
-
/**
|
|
549
|
-
* Compile ONE authored primary catalog node into its static `makeSQL` statement templates. A
|
|
550
|
-
* `Select` lowers to the fragment/tail statements ({@link compileSelectNode}); a single
|
|
551
|
-
* `Insert`/`Update`/`Delete` lowers via the SAME symbolic write compile the write-tx spine uses
|
|
552
|
-
* ({@link compileWriteNode} in `./tx` — complete tuned SQL text + deferred Expression-IR params),
|
|
553
|
-
* yielding ONE statement. This is the makeSQL SCP path for a standalone CRUD query (a Command
|
|
554
|
-
* with write-time relations rides the tx-DAG plan, not this single-statement path).
|
|
555
|
-
*/
|
|
556
|
-
function compilePrimaryNode(node, dialect) {
|
|
557
|
-
const component = 'map' in node ? node.map.component : node.component;
|
|
558
|
-
if (component === 'Select' || component === 'Count')
|
|
559
|
-
return compileSelectNode(node, dialect);
|
|
560
|
-
if (component === 'Insert' || component === 'Update' || component === 'Delete') {
|
|
561
|
-
const op = (0, tx_2.compileWriteNode)(node, dialect);
|
|
562
|
-
return [{ sql: op.sql, params: op.params }];
|
|
563
|
-
}
|
|
564
|
-
throw new Error(`static-bundle: catalog component '${component}' has no makeSQL compile (SQL CRUD only: Select/Count/Insert/Update/Delete)`);
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Compile the primary CRUD query of an authored behavior method into a {@link StaticBundle}.
|
|
568
|
-
* Relation `.map` nodes are NOT part of the primary bundle (they are the read-relations concern
|
|
569
|
-
* — step 3, still on the reduced spine). The compile is SYMBOLIC (no concrete input); the runtime
|
|
570
|
-
* ({@link executeStaticBundle} for reads / {@link executeStaticWrite} for writes) evaluates skip +
|
|
571
|
-
* value-specs per-input.
|
|
572
|
-
*/
|
|
573
|
-
function compileStaticBundle(contract, dialect, entry) {
|
|
574
|
-
const component = entry ? contract.components.find((c) => c.name === entry) : contract.components[0];
|
|
575
|
-
if (component === undefined)
|
|
576
|
-
throw new Error(`static-bundle: entry component '${entry ?? '<first>'}' not found in contract`);
|
|
577
|
-
let primary;
|
|
578
|
-
for (const n of component.body) {
|
|
579
|
-
if ('cond' in n)
|
|
580
|
-
continue;
|
|
581
|
-
if (isMap(n))
|
|
582
|
-
continue;
|
|
583
|
-
if (primary === undefined)
|
|
584
|
-
primary = n;
|
|
585
|
-
}
|
|
586
|
-
if (primary === undefined)
|
|
587
|
-
throw new Error(`static-bundle: behavior '${component.name}' has no primary catalog node`);
|
|
588
|
-
const statements = compilePrimaryNode(primary, dialect);
|
|
589
|
-
return {
|
|
590
|
-
dialect,
|
|
591
|
-
name: component.name,
|
|
592
|
-
statements,
|
|
593
|
-
optionalHeads: [...optionalHeadsOf(component, statements)],
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
// ── Optional-head detection (SSoT-driven, mirrors ../runtime.optionalHeadsOf) ──
|
|
597
|
-
/**
|
|
598
|
-
* The OPTIONAL input heads to normalize to present-as-null (absent-key SKIP): the
|
|
599
|
-
* schema-optional ports, the SKIP-`cond`-guarded heads (a guard declares its driving input
|
|
600
|
-
* optional — spec §7), AND every head accessed via `refOpt` in a value-spec or skip expression
|
|
601
|
-
* (the author reached for `opt(…)`/`refOpt` precisely because the binding may be absent — e.g. a
|
|
602
|
-
* `coalesce`-defaulted LIMIT). bc's `evaluateExpression` fail-closes on an absent head even
|
|
603
|
-
* under `refOpt`, so these must be pre-normalized to null. A REQUIRED, non-refOpt head that is
|
|
604
|
-
* missing is left absent so a real wiring bug surfaces loudly. This is SSoT-driven (the
|
|
605
|
-
* schema/guard/refOpt IS the optionality declaration), never an ad-hoc code default.
|
|
606
|
-
*/
|
|
607
|
-
function optionalHeadsOf(component, statements) {
|
|
608
|
-
const optional = new Set();
|
|
609
|
-
for (const [port, schema] of Object.entries(component.inputPorts)) {
|
|
610
|
-
if (schema.required !== true)
|
|
611
|
-
optional.add(port);
|
|
612
|
-
}
|
|
613
|
-
for (const n of component.body)
|
|
614
|
-
skipGuardHeads(n, optional);
|
|
615
|
-
for (const stmt of statements) {
|
|
616
|
-
for (const p of stmt.params)
|
|
617
|
-
collectRefOptHeads(p, optional);
|
|
618
|
-
if (stmt.skip !== undefined)
|
|
619
|
-
collectRefOptHeads(stmt.skip, optional);
|
|
620
|
-
}
|
|
621
|
-
return optional;
|
|
622
|
-
}
|
|
623
|
-
/** Collect every path HEAD accessed via `refOpt` in an Expression IR node. */
|
|
624
|
-
function collectRefOptHeads(node, into) {
|
|
625
|
-
if (node === null || typeof node !== 'object')
|
|
626
|
-
return;
|
|
627
|
-
if (Array.isArray(node)) {
|
|
628
|
-
for (const e of node)
|
|
629
|
-
collectRefOptHeads(e, into);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
const keys = Object.keys(node);
|
|
633
|
-
if (keys.length === 1 && keys[0] === 'refOpt') {
|
|
634
|
-
const path = node.refOpt;
|
|
635
|
-
if (Array.isArray(path) && path.length > 0 && typeof path[0] === 'string')
|
|
636
|
-
into.add(path[0]);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
for (const v of Object.values(node))
|
|
640
|
-
collectRefOptHeads(v, into);
|
|
641
|
-
}
|
|
642
|
-
function skipGuardHeads(n, into) {
|
|
643
|
-
if ('cond' in n)
|
|
644
|
-
return;
|
|
645
|
-
const ports = isMap(n) ? n.map.ports : n.ports;
|
|
646
|
-
const where = ports.where;
|
|
647
|
-
if (typeof where !== 'object' || where === null || !('arr' in where))
|
|
648
|
-
return;
|
|
649
|
-
const arr = where.arr;
|
|
650
|
-
if (!Array.isArray(arr))
|
|
651
|
-
return;
|
|
652
|
-
for (const m of arr) {
|
|
653
|
-
if (opKey(m) === 'cond') {
|
|
654
|
-
const cond = m.cond;
|
|
655
|
-
if (Array.isArray(cond) && cond.length === 3)
|
|
656
|
-
collectRefHeads(cond[0], into);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
function collectRefHeads(node, into) {
|
|
661
|
-
if (node === null || typeof node !== 'object')
|
|
662
|
-
return;
|
|
663
|
-
if (Array.isArray(node)) {
|
|
664
|
-
for (const e of node)
|
|
665
|
-
collectRefHeads(e, into);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
const op = opKey(node);
|
|
669
|
-
if (op === 'ref' || op === 'refOpt') {
|
|
670
|
-
const path = node[op];
|
|
671
|
-
if (Array.isArray(path) && path.length > 0 && typeof path[0] === 'string')
|
|
672
|
-
into.add(path[0]);
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
for (const v of Object.values(node))
|
|
676
|
-
collectRefHeads(v, into);
|
|
677
|
-
}
|
|
678
|
-
// ── FIND_FILTER leak guard: authored WHERE column keys (fail-closed model-scope enforcement) ──
|
|
679
|
-
/**
|
|
680
|
-
* Collect the top-level COLUMN names an authored `where` port constrains — the LHS column of each
|
|
681
|
-
* eq/ne/cmp member, the IN-sentinel column, and (recursively) the columns of `and`/`or` groups and
|
|
682
|
-
* SKIP-`cond` members. This is exactly the key set v1 compares against when it folds a model's
|
|
683
|
-
* `FIND_FILTER` into the WHERE (`condsToRecord(FIND_FILTER)` keys vs the compiled predicate), so the
|
|
684
|
-
* {@link assertFindFilterFolded} guard can fail-closed when a FIND_FILTER model is SCP-compiled
|
|
685
|
-
* without its scope predicates folded in. Additive sentinel primitives (immediate/dynamic/exists/…)
|
|
686
|
-
* carry no plain column key and are skipped — a scope key must be an ordinary eq/cmp/in the author
|
|
687
|
-
* expressed, matching how a FIND_FILTER predicate manifests.
|
|
688
|
-
*/
|
|
689
|
-
function whereMemberColumns(node, into) {
|
|
690
|
-
const op = opKey(node);
|
|
691
|
-
if (op === undefined)
|
|
692
|
-
return;
|
|
693
|
-
if (op === 'and' || op === 'or') {
|
|
694
|
-
const args = node[op];
|
|
695
|
-
if (Array.isArray(args))
|
|
696
|
-
for (const m of args)
|
|
697
|
-
whereMemberColumns(m, into);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
if (op === 'cond') {
|
|
701
|
-
const cargs = node.cond;
|
|
702
|
-
if (Array.isArray(cargs) && cargs.length === 3)
|
|
703
|
-
whereMemberColumns(cargs[1], into);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (op === 'eq' || op in CMP_OPS) {
|
|
707
|
-
const args = node[op];
|
|
708
|
-
if (!Array.isArray(args) || args.length !== 2)
|
|
709
|
-
return;
|
|
710
|
-
const inCol = inSentinelColumn(args[0]);
|
|
711
|
-
if (inCol !== undefined) {
|
|
712
|
-
into.add(inCol);
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
// A plain column ref LHS (a bare eq/cmp). An additive sentinel LHS (immediate/dynamic/exists/…)
|
|
716
|
-
// is NOT a plain column — its column key would be encoded in the primitive, so it is skipped
|
|
717
|
-
// (a FIND_FILTER scope key is always an ordinary eq/cmp/IS NULL the author must fold explicitly).
|
|
718
|
-
const lhsOp = opKey(args[0]);
|
|
719
|
-
if (lhsOp === 'ref' || lhsOp === 'refOpt') {
|
|
720
|
-
const path = args[0][lhsOp];
|
|
721
|
-
if (Array.isArray(path) && path.length > 0 && typeof path[path.length - 1] === 'string' && path[0] !== tx_1.IN_SENTINEL) {
|
|
722
|
-
into.add(path[path.length - 1]);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
/** The authored WHERE column-key set of a read node's `where` port (for the FIND_FILTER guard). */
|
|
728
|
-
function authoredWhereKeysOf(ports) {
|
|
729
|
-
const keys = new Set();
|
|
730
|
-
const arr = arrPort(ports, 'where');
|
|
731
|
-
if (arr === undefined)
|
|
732
|
-
return keys;
|
|
733
|
-
for (const m of arr)
|
|
734
|
-
whereMemberColumns(m, keys);
|
|
735
|
-
return keys;
|
|
736
|
-
}
|
|
737
|
-
// ── Runtime: evaluate value-specs + skip per-input, assemble, render, execute ──
|
|
738
|
-
/** bc evaluates ints to bigint; convert a rendered param to a driver-bindable value. */
|
|
739
|
-
function toDriverParam(v) {
|
|
740
|
-
if (typeof v === 'bigint') {
|
|
741
|
-
if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER))
|
|
742
|
-
return Number(v);
|
|
743
|
-
return v;
|
|
744
|
-
}
|
|
745
|
-
return v;
|
|
746
|
-
}
|
|
747
|
-
/** Evaluate one deferred value-spec against the input scope (handling the JSON-array marker). */
|
|
748
|
-
function evalSpec(spec, scope) {
|
|
749
|
-
if (spec !== null && typeof spec === 'object' && !Array.isArray(spec) && '__jsonArray' in spec) {
|
|
750
|
-
const marker = spec;
|
|
751
|
-
const arr = (0, behavior_contracts_1.evaluateExpression)(marker.__jsonArray, scope);
|
|
752
|
-
if (!Array.isArray(arr))
|
|
753
|
-
throw new Error('static-bundle: IN-list value-spec did not evaluate to an array');
|
|
754
|
-
if (marker.dialect === 'postgres')
|
|
755
|
-
return arr.map((e) => toDriverParam(e));
|
|
756
|
-
// MySQL/SQLite single-JSON IN-list param. A BOOLEAN element is encoded as `1`/`0` for MySQL
|
|
757
|
-
// (NOT JSON `true`/`false`): MySQL's `JSON_UNQUOTE(v)` yields the STRING `'true'`, which coerces
|
|
758
|
-
// to `0` against a TINYINT(1) — a silent mismatch. `1`/`0` is exactly what v1's `col IN (?)`
|
|
759
|
-
// bound (mysql2 sends a JS bool as `1`/`0`), preserving v1 RESULT parity. SQLite's `json_each`
|
|
760
|
-
// coerces JSON booleans natively, so it keeps the plain form.
|
|
761
|
-
const encodeElem = (e) => marker.dialect === 'mysql' && typeof e === 'boolean' ? (e ? 1 : 0) : toDriverParam(e);
|
|
762
|
-
return JSON.stringify(arr.map(encodeElem));
|
|
763
|
-
}
|
|
764
|
-
return toDriverParam((0, behavior_contracts_1.evaluateExpression)(spec, scope));
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Evaluate a list of statement templates against a scope: drop skipped statements (skip
|
|
768
|
-
* expression truthy), resolve each surviving statement's WHERE-role connector from the present
|
|
769
|
-
* set, build concrete `makeSQL` nodes, assemble + render to the dialect placeholder form.
|
|
770
|
-
*/
|
|
771
|
-
function renderStatements(statements, dialect, scope) {
|
|
772
|
-
const nodes = [];
|
|
773
|
-
let whereSeen = false;
|
|
774
|
-
for (const stmt of statements) {
|
|
775
|
-
if (stmt.skip !== undefined) {
|
|
776
|
-
const drop = (0, behavior_contracts_1.evaluateExpression)(stmt.skip, scope);
|
|
777
|
-
if (drop !== null && drop !== false)
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
let sql = stmt.sql;
|
|
781
|
-
if (stmt.whereFragment === true) {
|
|
782
|
-
sql = (whereSeen ? ' AND ' : ' WHERE ') + stmt.sql;
|
|
783
|
-
whereSeen = true;
|
|
784
|
-
}
|
|
785
|
-
const params = stmt.params.map((p) => evalSpec(p, scope));
|
|
786
|
-
// The authored PG IN-list emits `col = ANY(?)` with NO cast token (#46 — PG infers the array
|
|
787
|
-
// element type from the column, correct for empty/uuid where value-inference cannot). No
|
|
788
|
-
// render-time cast resolution is needed here; the relation-batch path resolves its own v1
|
|
789
|
-
// `::T[]` cast in `runRelationOp`, not on this authored-statement surface.
|
|
790
|
-
nodes.push({ sql, params });
|
|
791
|
-
}
|
|
792
|
-
const assembled = (0, makesql_1.composeMakeSQL)(nodes);
|
|
793
|
-
return { sql: (0, handler_1.renderPlaceholders)(assembled.sql, dialect), params: assembled.params };
|
|
794
|
-
}
|
|
795
|
-
/** Evaluate a whole bundle's statements against the input scope (convenience over the list). */
|
|
796
|
-
function renderBundle(bundle, scope) {
|
|
797
|
-
return renderStatements(bundle.statements, bundle.dialect, scope);
|
|
798
|
-
}
|
|
799
|
-
/** Normalize omitted OPTIONAL heads to present-as-null (absent-key SKIP; SSoT-driven). */
|
|
800
|
-
function normalizeInput(bundle, input) {
|
|
801
|
-
const out = { ...input };
|
|
802
|
-
for (const head of bundle.optionalHeads) {
|
|
803
|
-
if (!(head in out))
|
|
804
|
-
out[head] = null;
|
|
805
|
-
}
|
|
806
|
-
return out;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Execute a {@link StaticBundle} read query end-to-end against real SQLite: normalize optional
|
|
810
|
-
* heads, evaluate skip + value-specs per-input, assemble present fragments, render, and run the
|
|
811
|
-
* SELECT. Returns the row list.
|
|
812
|
-
*/
|
|
813
|
-
function executeStaticBundle(bundle, input, db) {
|
|
814
|
-
const scope = normalizeInput(bundle, input);
|
|
815
|
-
const { sql, params } = renderBundle(bundle, scope);
|
|
816
|
-
let stmt;
|
|
817
|
-
try {
|
|
818
|
-
stmt = db.prepare(sql);
|
|
819
|
-
return stmt.all(...params);
|
|
820
|
-
}
|
|
821
|
-
catch (e) {
|
|
822
|
-
throw (0, errors_1.mapSqliteError)(e);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Execute a single-statement WRITE {@link StaticBundle} (Insert/Update/Delete) against real
|
|
827
|
-
* SQLite: evaluate the deferred value-specs per-input, render, and run the statement. A statement
|
|
828
|
-
* with a RETURNING tail returns its rows; a non-returning write returns a single-row summary
|
|
829
|
-
* `[{ changes, lastInsertRowid }]`. A driver error maps to a {@link SqlFailure} at the boundary.
|
|
830
|
-
* (A Command with write-time relations rides the tx-DAG plan of `./tx`, not this path.)
|
|
831
|
-
*/
|
|
832
|
-
function executeStaticWrite(bundle, input, db) {
|
|
833
|
-
const scope = normalizeInput(bundle, input);
|
|
834
|
-
const { sql, params } = renderBundle(bundle, scope);
|
|
835
|
-
const hasReturn = /\breturning\b/i.test(sql);
|
|
836
|
-
try {
|
|
837
|
-
const stmt = db.prepare(sql);
|
|
838
|
-
if (hasReturn)
|
|
839
|
-
return stmt.all(...params);
|
|
840
|
-
const info = stmt.run(...params);
|
|
841
|
-
return [{ changes: info.changes, lastInsertRowid: toDriverParam(BigInt(info.lastInsertRowid)) }];
|
|
842
|
-
}
|
|
843
|
-
catch (e) {
|
|
844
|
-
throw (0, errors_1.mapSqliteError)(e);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
// ============================================================================
|
|
848
|
-
// Step 0 — the bc-runBehavior read executor: bc composes (map / Φ-merge / wiring),
|
|
849
|
-
// makeSQL executes. A read behavior method whose output is a Φ-merge of SQL nodes
|
|
850
|
-
// (`{obj:{posts:ref[n0], authors:map(n0)}}`) is executed by rewriting each SQL node
|
|
851
|
-
// to a `makeSQL` node carrying a synthetic `__scope` port and running bc `runBehavior`
|
|
852
|
-
// with a handler that renders that node's static statements against the evaluated scope.
|
|
853
|
-
// This gives map iteration + Φ output + wiring FOR FREE (bc owns orchestration).
|
|
854
|
-
// ============================================================================
|
|
855
|
-
/** The synthetic port that carries a SQL node's render scope (bc evaluates it in-scope). */
|
|
856
|
-
const SCOPE_PORT = '__scope';
|
|
857
|
-
/** The makeSQL catalog leaf name every rewritten SQL node references. */
|
|
858
|
-
const NODE_COMPONENT = '__makeSqlNode';
|
|
859
|
-
/** Collect every ref/refOpt path HEAD used inside an Expression IR node. */
|
|
860
|
-
function collectAllHeads(node, heads) {
|
|
861
|
-
if (node === null || typeof node !== 'object')
|
|
862
|
-
return;
|
|
863
|
-
if (Array.isArray(node)) {
|
|
864
|
-
for (const el of node)
|
|
865
|
-
collectAllHeads(el, heads);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
const keys = Object.keys(node);
|
|
869
|
-
if (keys.length === 1 && (keys[0] === 'ref' || keys[0] === 'refOpt')) {
|
|
870
|
-
const path = node[keys[0]];
|
|
871
|
-
if (Array.isArray(path) && typeof path[0] === 'string')
|
|
872
|
-
heads.add(path[0]);
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
for (const k of keys)
|
|
876
|
-
collectAllHeads(node[k], heads);
|
|
877
|
-
}
|
|
878
|
-
/** Every binding head a node's compiled statements reference (params + skip). */
|
|
879
|
-
function statementHeads(statements) {
|
|
880
|
-
const heads = new Set();
|
|
881
|
-
for (const stmt of statements) {
|
|
882
|
-
for (const p of stmt.params)
|
|
883
|
-
collectAllHeads(p, heads);
|
|
884
|
-
if (stmt.skip !== undefined)
|
|
885
|
-
collectAllHeads(stmt.skip, heads);
|
|
886
|
-
}
|
|
887
|
-
return heads;
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Build the surrogate `__scope` port for a node: a bc `{obj:{…}}` re-exporting each referenced
|
|
891
|
-
* head as `{ref:[head]}` (excluding the IN-sentinel column marker). bc evaluates this in ITS
|
|
892
|
-
* scope (so `input`, sibling wire results and the `map` element `as` all resolve), then the
|
|
893
|
-
* handler renders the node's static statements against the resulting plain scope.
|
|
894
|
-
*/
|
|
895
|
-
function scopePort(statements) {
|
|
896
|
-
const obj = {};
|
|
897
|
-
for (const head of statementHeads(statements)) {
|
|
898
|
-
if (head === tx_1.IN_SENTINEL)
|
|
899
|
-
continue;
|
|
900
|
-
obj[head] = { ref: [head] };
|
|
901
|
-
}
|
|
902
|
-
return { obj };
|
|
903
|
-
}
|
|
904
|
-
/** Compile ONE authored SQL node (Select or CRUD write) into its static statements. */
|
|
905
|
-
function compileNodeStatements(node, dialect) {
|
|
906
|
-
const component = 'map' in node ? node.map.component : node.component;
|
|
907
|
-
if (component === 'Select' || component === 'Count')
|
|
908
|
-
return compileSelectNode(node, dialect);
|
|
909
|
-
const op = (0, tx_2.compileWriteNode)(node, dialect);
|
|
910
|
-
return [{ sql: op.sql, params: op.params }];
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Compile a READ behavior method into a portable {@link ReadGraph}: rewrite each authored SQL
|
|
914
|
-
* body node to a `makeSQL` node carrying a synthetic `__scope` port (bc evaluates it in-scope),
|
|
915
|
-
* and compile each node's static statements ONCE. SYMBOLIC — no concrete input. bc will own map
|
|
916
|
-
* iteration / wire binding / Φ output at execute time; the handler renders each node's statements.
|
|
917
|
-
*/
|
|
918
|
-
function compileReadGraph(contract, dialect, entry, findFilterModel, resolveColumnType) {
|
|
919
|
-
const component = entry ? contract.components.find((c) => c.name === entry) : contract.components[0];
|
|
920
|
-
if (component === undefined)
|
|
921
|
-
throw new Error(`static-bundle: entry component '${entry ?? '<first>'}' not found in contract`);
|
|
922
|
-
// FIND_FILTER fail-closed guard (M2): when a `DBModel`-shaped source that DECLARES a `FIND_FILTER`
|
|
923
|
-
// (soft-delete / tenant scope) is routed through the SCP read compile, its scope predicates MUST be
|
|
924
|
-
// folded into the authored `where` of every read node — the SCP compile has no model context to
|
|
925
|
-
// auto-apply them (see find-filter-guard.ts). If any read node's authored WHERE omits a scope key,
|
|
926
|
-
// `assertFindFilterFolded` throws (never a silent cross-tenant / soft-deleted leak). No model
|
|
927
|
-
// supplied ⇒ nothing to enforce (the guard is a no-op for a model with no FIND_FILTER either way).
|
|
928
|
-
if (findFilterModel !== undefined) {
|
|
929
|
-
for (const n of component.body) {
|
|
930
|
-
if ('cond' in n)
|
|
931
|
-
continue;
|
|
932
|
-
const ref = 'map' in n ? n.map : n;
|
|
933
|
-
if (ref.component !== 'Select' && ref.component !== 'Count')
|
|
934
|
-
continue;
|
|
935
|
-
(0, find_filter_guard_1.assertFindFilterFolded)(findFilterModel, authoredWhereKeysOf(ref.ports));
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
const statementsById = {};
|
|
939
|
-
for (const n of component.body) {
|
|
940
|
-
if ('cond' in n)
|
|
941
|
-
continue;
|
|
942
|
-
statementsById[n.id] = compileNodeStatements(n, dialect);
|
|
943
|
-
}
|
|
944
|
-
// Typed-codegen `outType`/`outputType` annotations (spec §4.1; #58/#56). Derived from the ORIGINAL
|
|
945
|
-
// body (real `table`/`select` ports) so bc's typed-raw de-box emitters can materialize concrete
|
|
946
|
-
// row structs instead of hard-failing ("nothing to de-box"). Only when a column-type resolver (the
|
|
947
|
-
// schema/DDL SoT) is supplied — without it the read graph stays un-annotated (interpret path only;
|
|
948
|
-
// typed codegen then fail-closes downstream, never silently boxes). Any ambiguity throws here.
|
|
949
|
-
const outTypes = resolveColumnType !== undefined ? (0, outtype_1.deriveReadOutTypes)(component, resolveColumnType) : undefined;
|
|
950
|
-
const surrogateBody = component.body.map((n) => {
|
|
951
|
-
if ('cond' in n)
|
|
952
|
-
return n;
|
|
953
|
-
const stmts = statementsById[n.id];
|
|
954
|
-
const outType = outTypes?.byNode.get(n.id);
|
|
955
|
-
const outAnn = outType !== undefined ? { outType } : {};
|
|
956
|
-
if ('map' in n) {
|
|
957
|
-
return { ...n, ...outAnn, map: { ...n.map, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } } };
|
|
958
|
-
}
|
|
959
|
-
return { ...n, ...outAnn, component: NODE_COMPONENT, ports: { [SCOPE_PORT]: scopePort(stmts) } };
|
|
960
|
-
});
|
|
961
|
-
const surrogate = {
|
|
962
|
-
...component,
|
|
963
|
-
body: surrogateBody,
|
|
964
|
-
...(outTypes !== undefined ? { outputType: outTypes.outputType } : {}),
|
|
965
|
-
};
|
|
966
|
-
const ir = { irVersion: 1, exprVersion: 2, components: [surrogate] };
|
|
967
|
-
return {
|
|
968
|
-
dialect,
|
|
969
|
-
name: component.name,
|
|
970
|
-
ir,
|
|
971
|
-
statementsById,
|
|
972
|
-
optionalHeads: [...optionalHeadsOfComponent(component, dialect)],
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Execute a compiled {@link ReadGraph} via bc `runBehavior` + a makeSQL handler: bc drives map
|
|
977
|
-
* iteration / wire binding / Φ output; the handler renders each node's static statements against
|
|
978
|
-
* the evaluated `__scope` and runs REAL SQLite. Returns the component's Φ output. This is the
|
|
979
|
-
* design's "bc composes, makeSQL executes" — the SAME path a per-language runtime follows.
|
|
980
|
-
*/
|
|
981
|
-
function executeReadGraph(graph, input, db) {
|
|
982
|
-
const handle = (ports, ctx) => {
|
|
983
|
-
const stmts = graph.statementsById[ctx.nodeId];
|
|
984
|
-
if (stmts === undefined)
|
|
985
|
-
return { error: `static-bundle: no statements for node '${ctx.nodeId}'` };
|
|
986
|
-
const scope = ports[SCOPE_PORT];
|
|
987
|
-
if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
|
|
988
|
-
return { error: `static-bundle: node '${ctx.nodeId}' surrogate scope did not evaluate to an object` };
|
|
989
|
-
}
|
|
990
|
-
const { sql, params } = renderStatements(stmts, graph.dialect, scope);
|
|
991
|
-
try {
|
|
992
|
-
const rows = db.prepare(sql).all(...params);
|
|
993
|
-
return { ok: rows };
|
|
994
|
-
}
|
|
995
|
-
catch (e) {
|
|
996
|
-
return { error: (0, errors_1.mapSqliteError)(e).message };
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
const handlers = { [NODE_COMPONENT]: handle };
|
|
1000
|
-
const normalized = { ...input };
|
|
1001
|
-
for (const head of graph.optionalHeads)
|
|
1002
|
-
if (!(head in normalized))
|
|
1003
|
-
normalized[head] = null;
|
|
1004
|
-
try {
|
|
1005
|
-
return (0, behavior_contracts_1.runBehavior)(graph.ir, handlers, normalized, graph.name);
|
|
1006
|
-
}
|
|
1007
|
-
catch (e) {
|
|
1008
|
-
throw reErrorToSqlFailure(e);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Async twin of {@link executeReadGraph} — the PG / MySQL execution model (#40).
|
|
1013
|
-
*
|
|
1014
|
-
* Byte-identical composition (SAME IR, SAME per-node SQL text + params, SAME Φ output), but bc's
|
|
1015
|
-
* `runBehaviorAsync` drives it: the makeSQL handler returns a Promise, and bc dispatches the
|
|
1016
|
-
* INDEPENDENT sibling nodes of a plan stage on `runPlanAsync`'s bounded-parallel path (bc#23),
|
|
1017
|
-
* bounded by the plan's `concurrency` (default 16). Against a pooled async driver (`pg` / `mysql2`,
|
|
1018
|
-
* each `exec` call resolving on its own pooled connection), that becomes REAL parallel read-relation
|
|
1019
|
-
* DB I/O — the plan's declared concurrency finally cashes out as concurrent dispatch. The result is
|
|
1020
|
-
* deterministic (bc commits stage outcomes in declaration order), so it equals the serial
|
|
1021
|
-
* `executeReadGraph` output exactly; only the wall-clock changes.
|
|
1022
|
-
*
|
|
1023
|
-
* The conformance bar stays on the SYNC in-proc better-sqlite3 path (`executeReadGraph`); this async
|
|
1024
|
-
* path is the live PG/MySQL execution model and is proven by the latency-injecting concurrency test.
|
|
1025
|
-
*/
|
|
1026
|
-
async function executeReadGraphAsync(graph, input, exec) {
|
|
1027
|
-
const handle = async (ports, ctx) => {
|
|
1028
|
-
const stmts = graph.statementsById[ctx.nodeId];
|
|
1029
|
-
if (stmts === undefined)
|
|
1030
|
-
return { error: `static-bundle: no statements for node '${ctx.nodeId}'` };
|
|
1031
|
-
const scope = ports[SCOPE_PORT];
|
|
1032
|
-
if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
|
|
1033
|
-
return { error: `static-bundle: node '${ctx.nodeId}' surrogate scope did not evaluate to an object` };
|
|
1034
|
-
}
|
|
1035
|
-
const { sql, params } = renderStatements(stmts, graph.dialect, scope);
|
|
1036
|
-
try {
|
|
1037
|
-
const rows = await exec(sql, params);
|
|
1038
|
-
return { ok: rows };
|
|
1039
|
-
}
|
|
1040
|
-
catch (e) {
|
|
1041
|
-
return { error: (0, errors_1.mapSqliteError)(e).message };
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
const handlers = { [NODE_COMPONENT]: handle };
|
|
1045
|
-
const normalized = { ...input };
|
|
1046
|
-
for (const head of graph.optionalHeads)
|
|
1047
|
-
if (!(head in normalized))
|
|
1048
|
-
normalized[head] = null;
|
|
1049
|
-
try {
|
|
1050
|
-
return await (0, behavior_contracts_1.runBehaviorAsync)(graph.ir, handlers, normalized, graph.name);
|
|
1051
|
-
}
|
|
1052
|
-
catch (e) {
|
|
1053
|
-
throw reErrorToSqlFailure(e);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
/**
|
|
1057
|
-
* Convenience: compile + execute a READ behavior method whose output may be a Φ-merge of SQL
|
|
1058
|
-
* nodes (including relation `.map` nodes). Equivalent to `executeReadGraph(compileReadGraph(...))`.
|
|
1059
|
-
*/
|
|
1060
|
-
function executeReadBehavior(contract, input, dialect, db, entry) {
|
|
1061
|
-
return executeReadGraph(compileReadGraph(contract, dialect, entry), input, db);
|
|
1062
|
-
}
|
|
1063
|
-
/**
|
|
1064
|
-
* Render the PRIMARY read node's statements of a {@link ReadGraph} against an input scope to its
|
|
1065
|
-
* dialect SQL text + bound params (the render axis for conformance golden). The primary node is
|
|
1066
|
-
* the first non-map body node (the root SELECT the relations map over). Optional heads are
|
|
1067
|
-
* normalized to present-as-null first (absent-key SKIP), so an omitted optional head renders the
|
|
1068
|
-
* SAME text a runtime would produce.
|
|
1069
|
-
*/
|
|
1070
|
-
function renderReadPrimary(graph, input) {
|
|
1071
|
-
const ids = Object.keys(graph.statementsById);
|
|
1072
|
-
// The primary node is the first body node in the surrogate IR order (map nodes reference it).
|
|
1073
|
-
const bodyIds = graph.ir.components[0].body.map((n) => n.id).filter((id) => ids.includes(id));
|
|
1074
|
-
const primaryId = bodyIds[0];
|
|
1075
|
-
if (primaryId === undefined)
|
|
1076
|
-
throw new Error('static-bundle: read graph has no primary node to render');
|
|
1077
|
-
const scope = { ...input };
|
|
1078
|
-
for (const head of graph.optionalHeads)
|
|
1079
|
-
if (!(head in scope))
|
|
1080
|
-
scope[head] = null;
|
|
1081
|
-
return renderStatements(graph.statementsById[primaryId], graph.dialect, scope);
|
|
1082
|
-
}
|
|
1083
|
-
/** Optional heads across ALL SQL nodes of a component (schema + SKIP-guard + refOpt). */
|
|
1084
|
-
function optionalHeadsOfComponent(component, dialect) {
|
|
1085
|
-
const optional = new Set();
|
|
1086
|
-
for (const [port, schema] of Object.entries(component.inputPorts)) {
|
|
1087
|
-
if (schema.required !== true)
|
|
1088
|
-
optional.add(port);
|
|
1089
|
-
}
|
|
1090
|
-
for (const n of component.body)
|
|
1091
|
-
skipGuardHeads(n, optional);
|
|
1092
|
-
for (const n of component.body) {
|
|
1093
|
-
if ('cond' in n)
|
|
1094
|
-
continue;
|
|
1095
|
-
const stmts = compileNodeStatements(n, dialect);
|
|
1096
|
-
for (const stmt of stmts) {
|
|
1097
|
-
for (const p of stmt.params)
|
|
1098
|
-
collectRefOptHeads(p, optional);
|
|
1099
|
-
if (stmt.skip !== undefined)
|
|
1100
|
-
collectRefOptHeads(stmt.skip, optional);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
return optional;
|
|
1104
|
-
}
|
|
1105
|
-
/** Re-surface a mapped SqlFailure from a runBehavior OP_FAILED message (SQLITE_* code embedded). */
|
|
1106
|
-
function reErrorToSqlFailure(e) {
|
|
1107
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
1108
|
-
const m = /(SQLITE_[A-Z_]+)/.exec(message);
|
|
1109
|
-
if (m)
|
|
1110
|
-
return (0, errors_1.mapSqliteError)({ code: m[1], message });
|
|
1111
|
-
return e;
|
|
1112
|
-
}
|
|
1113
|
-
//# sourceMappingURL=static-bundle.js.map
|