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
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* litedbmodel v2 SCP — the decorator/metadata → SCP-restricted TS LOWERING EMITTER.
|
|
4
|
+
*
|
|
5
|
+
* This is the middle of the settled model (CLAUDE.md §1):
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* ORM user: decorated models + DECLARED endpoints (no SQL) ← src/decorators.ts + ./endpoint
|
|
9
|
+
* ↓ emitBehaviorModule — THIS module
|
|
10
|
+
* SCP-restricted TS: `@behavior static` methods calling the ONE `@leaf` catalog (`Db`), with the
|
|
11
|
+
* dialect SQL already baked in by `makesql`
|
|
12
|
+
* ↓ bc generate --from
|
|
13
|
+
* go / rust / py / php / ts native modules — bc wires the leaves automatically
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## It compiles nothing of its own
|
|
17
|
+
*
|
|
18
|
+
* Every artifact the emitter puts in the emitted source comes from an EXISTING aggregation point —
|
|
19
|
+
* the emitter only RENDERS them as TypeScript:
|
|
20
|
+
*
|
|
21
|
+
* | emitted thing | comes from |
|
|
22
|
+
* |------------------------------|-----------------------------------------------------------------|
|
|
23
|
+
* | read SQL + param order | `makesql/compile-select.compileSelect` |
|
|
24
|
+
* | WHERE text (whole / fragment)| `makesql/compile.compileWhere` → `DBConditions` |
|
|
25
|
+
* | static IN-list membership | `makesql/json-array.inListPredicate` (PG `= ANY(?)`, #46) |
|
|
26
|
+
* | write SQL + param order | `makesql/tx.compileWriteNode` (INSERT/UPDATE/DELETE + batch) |
|
|
27
|
+
* | relation batch SQL + keys | `relation.compileRelationOp` (+ `parentKeyCols`/`targetKeyCols`) |
|
|
28
|
+
* | relation hard-limit guard | `relation.relationGuard` (the compiled op's resolved cap) |
|
|
29
|
+
* | row types | `makesql/outtype.deriveReadRow` over the model's `@column` SoT |
|
|
30
|
+
* | column SQL types | `decorator-adapter.deriveModelColumns` / `modelColumnResolver` |
|
|
31
|
+
* | relation declarations | `decorator-adapter.relationDeclOf` |
|
|
32
|
+
* | find hard-limit cap | `limit-config.resolveFindHardLimit` |
|
|
33
|
+
* | the leaf catalog | `leaf-transport.Db` (imported by the emitted module) |
|
|
34
|
+
*
|
|
35
|
+
* ## How a parameter reaches the SQL
|
|
36
|
+
*
|
|
37
|
+
* The builders bind VALUES: they push whatever value a condition/port carried into `params` in the
|
|
38
|
+
* exact order the `?`s appear. The emitter therefore hands them a {@link ParamRef} SENTINEL instead
|
|
39
|
+
* of a value and reads the sentinels back out of `params` — so the emitted `params` array is the
|
|
40
|
+
* builders' own binding order, with each slot rendered as the method parameter it stands for. A slot
|
|
41
|
+
* that came back as a plain value (a QUERY view's own fragment params) renders as a literal.
|
|
42
|
+
*
|
|
43
|
+
* ## SKIP / dynamic WHERE (CLAUDE.md §2 — settled)
|
|
44
|
+
*
|
|
45
|
+
* A predicate declared `optional` is present-or-absent PER CALL, so the final SQL can only be
|
|
46
|
+
* determined at execution time. The emitter therefore does NOT bake it: it passes the WHERE as
|
|
47
|
+
* FRAGMENTS — SQL text + params, and a skipped fragment is simply omitted — and the `executeSQL`
|
|
48
|
+
* leaf assembles the survivors when it runs ({@link import('../leaves').assembleDynamicWhere}); the
|
|
49
|
+
* `?`→`$N` render happens after that, on the final SQL. There is no intermediate IR vocabulary: a
|
|
50
|
+
* fragment is `{sql, params}` and nothing else, and a skipped one evaluates to `null` (bc's `cond` is
|
|
51
|
+
* lazy, so a dropped fragment's params are never evaluated).
|
|
52
|
+
*
|
|
53
|
+
* A statement with NO optional predicate carries no plan at all — its WHERE is lowered into the
|
|
54
|
+
* static `sql` at emit time (native-clean). One path, chosen per statement.
|
|
55
|
+
*/
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.emitBehaviorModule = emitBehaviorModule;
|
|
58
|
+
const DBValues_1 = require("../../DBValues");
|
|
59
|
+
const compile_select_1 = require("../makesql/compile-select");
|
|
60
|
+
const compile_1 = require("../makesql/compile");
|
|
61
|
+
const json_array_1 = require("../makesql/json-array");
|
|
62
|
+
const tx_1 = require("../makesql/tx");
|
|
63
|
+
const outtype_1 = require("../makesql/outtype");
|
|
64
|
+
const coltype_1 = require("../coltype");
|
|
65
|
+
const compile_relation_1 = require("../makesql/compile-relation");
|
|
66
|
+
const relation_1 = require("../relation");
|
|
67
|
+
const limit_config_1 = require("../limit-config");
|
|
68
|
+
const decorator_adapter_1 = require("../decorator-adapter");
|
|
69
|
+
const decorators_1 = require("../../decorators");
|
|
70
|
+
// ── parameter sentinels (what the builders bind instead of a value) ───────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* A stand-in for a bound VALUE that renders as a TS expression in the emitted `params` array. The
|
|
73
|
+
* builders treat it as an opaque scalar (it is not `null`, not an array, not a `DBToken`, not a
|
|
74
|
+
* `DBConditions`), so every builder emits its ordinary single-`?` form and pushes this object into
|
|
75
|
+
* `params` at exactly the slot the `?` occupies.
|
|
76
|
+
*/
|
|
77
|
+
class ParamRef {
|
|
78
|
+
expr;
|
|
79
|
+
constructor(expr) {
|
|
80
|
+
this.expr = expr;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── entry point ───────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Lower a set of declared endpoints to SCP-restricted TypeScript. The result's `source` is ordinary
|
|
86
|
+
* `tsc --strict` TypeScript (that is bc's authoring requirement) whose only imports are bc's
|
|
87
|
+
* authoring markers and the library's ONE leaf catalog.
|
|
88
|
+
*/
|
|
89
|
+
function emitBehaviorModule(spec) {
|
|
90
|
+
const names = Object.keys(spec.endpoints);
|
|
91
|
+
if (names.length === 0)
|
|
92
|
+
throw new Error(`emit: behavior '${spec.behavior}' declares no endpoints`);
|
|
93
|
+
const ctx = new EmitContext(spec);
|
|
94
|
+
const methods = names.map((name) => ctx.method(name, spec.endpoints[name]));
|
|
95
|
+
return { source: ctx.render(methods), endpoints: ctx.contracts };
|
|
96
|
+
}
|
|
97
|
+
class EmitContext {
|
|
98
|
+
spec;
|
|
99
|
+
/** Emitted row interfaces, in declaration order (`name → body text`). */
|
|
100
|
+
interfaces = new Map();
|
|
101
|
+
/** The per-endpoint call contracts, in emission order. */
|
|
102
|
+
contracts = [];
|
|
103
|
+
constructor(spec) {
|
|
104
|
+
this.spec = spec;
|
|
105
|
+
}
|
|
106
|
+
// ── model metadata (all of it from the decorator SoT) ───────────────────────────────────────
|
|
107
|
+
resolver(model) {
|
|
108
|
+
const r = (0, decorator_adapter_1.modelColumnResolver)(model, this.spec.columnOptions);
|
|
109
|
+
if (r === undefined) {
|
|
110
|
+
throw new Error(`emit: model '${model.name}' declares no @column metadata — the emitter types every projection ` +
|
|
111
|
+
`and every bound parameter from the column SoT (no-assume, no-fallback).`);
|
|
112
|
+
}
|
|
113
|
+
return r;
|
|
114
|
+
}
|
|
115
|
+
/** The model's declared columns (`column → SQL type`) — the ONE type SoT. */
|
|
116
|
+
columnsOf(model) {
|
|
117
|
+
const table = (0, decorator_adapter_1.tableNameOf)(model);
|
|
118
|
+
const cols = (0, decorator_adapter_1.deriveModelColumns)(model, this.spec.columnOptions)[table];
|
|
119
|
+
if (cols === undefined) {
|
|
120
|
+
throw new Error(`emit: model '${model.name}' declares no @column metadata (table '${table}')`);
|
|
121
|
+
}
|
|
122
|
+
return cols;
|
|
123
|
+
}
|
|
124
|
+
sqlTypeOf(model, column) {
|
|
125
|
+
const t = this.columnsOf(model)[column];
|
|
126
|
+
if (t === undefined) {
|
|
127
|
+
throw new Error(`emit: model '${model.name}' has no @column '${column}' — a declared endpoint may only name ` +
|
|
128
|
+
`columns the model declares (no-assume, no-fallback).`);
|
|
129
|
+
}
|
|
130
|
+
return t;
|
|
131
|
+
}
|
|
132
|
+
/** The TS type a BOUND parameter carries for a column (the bc scalar of its declared SQL type). */
|
|
133
|
+
bindTypeOf(model, column) {
|
|
134
|
+
return tsScalar((0, coltype_1.sqlTypeToBcScalar)(this.sqlTypeOf(model, column)));
|
|
135
|
+
}
|
|
136
|
+
// ── methods ────────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
method(name, endpoint) {
|
|
138
|
+
assertIdentifier(name, `endpoint name '${name}'`);
|
|
139
|
+
const m = this.lower(name, endpoint);
|
|
140
|
+
const cap = endpoint.kind === 'read' ? this.bakedFindCap(endpoint) : null;
|
|
141
|
+
this.contracts.push({
|
|
142
|
+
name,
|
|
143
|
+
params: m.params.map((p) => ({ name: p.name, type: p.type })),
|
|
144
|
+
returnType: m.returnType,
|
|
145
|
+
...(cap !== null ? { findHardLimit: cap } : {}),
|
|
146
|
+
});
|
|
147
|
+
return m;
|
|
148
|
+
}
|
|
149
|
+
lower(name, endpoint) {
|
|
150
|
+
switch (endpoint.kind) {
|
|
151
|
+
case 'read':
|
|
152
|
+
return this.read(name, endpoint);
|
|
153
|
+
case 'create':
|
|
154
|
+
case 'update':
|
|
155
|
+
case 'delete':
|
|
156
|
+
return this.write(name, endpoint);
|
|
157
|
+
case 'createMany':
|
|
158
|
+
case 'updateMany':
|
|
159
|
+
case 'deleteMany':
|
|
160
|
+
return this.batch(name, endpoint);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── READ ───────────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* The `findHardLimit` cap this read bakes: the configured cap when the endpoint declares NO explicit
|
|
166
|
+
* limit (an authored LIMIT governs — v1's skip rule; a BOUND limit is authored too, the author
|
|
167
|
+
* declared that this endpoint is paged), else `null`. Read from the config SSoT
|
|
168
|
+
* ({@link resolveFindHardLimit}) at EMIT time, exactly as the retired compile-time bake did.
|
|
169
|
+
*/
|
|
170
|
+
bakedFindCap(endpoint) {
|
|
171
|
+
if (endpoint.limit !== undefined)
|
|
172
|
+
return null;
|
|
173
|
+
return (0, limit_config_1.resolveFindHardLimit)();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Lower ONE declared page position to the `compileSelect` tail. A static count passes straight
|
|
177
|
+
* through and inlines as the literal it always was; a `{ param }` position declares an `Int` method
|
|
178
|
+
* parameter and binds through the SAME tail as a {@link BoundCount} sentinel, so the paged and the
|
|
179
|
+
* bounded read are one statement builder and one param list.
|
|
180
|
+
*
|
|
181
|
+
* A count is an `Int` by SQL's own definition of `LIMIT` / `OFFSET` — it names no model column, so
|
|
182
|
+
* there is no column type to resolve it from.
|
|
183
|
+
*/
|
|
184
|
+
page(p, params) {
|
|
185
|
+
if (p === undefined || typeof p === 'number')
|
|
186
|
+
return p;
|
|
187
|
+
assertIdentifier(p.param, `parameter '${p.param}'`);
|
|
188
|
+
params.push({ name: p.param, type: tsScalar('int') });
|
|
189
|
+
return { bind: new ParamRef(p.param) };
|
|
190
|
+
}
|
|
191
|
+
read(name, endpoint) {
|
|
192
|
+
const model = endpoint.model;
|
|
193
|
+
const resolve = this.resolver(model);
|
|
194
|
+
const baseTable = (0, decorator_adapter_1.tableNameOf)(model);
|
|
195
|
+
// A QUERY view-model reads from the derived CTE alias; the projection types resolve against the
|
|
196
|
+
// model's declared columns either way (the CTE projects the model's own columns).
|
|
197
|
+
const alias = endpoint.view?.alias ?? 'derived';
|
|
198
|
+
const table = endpoint.view !== undefined ? alias : baseTable;
|
|
199
|
+
const projection = endpoint.select ?? Object.keys(this.columnsOf(model));
|
|
200
|
+
const readResolve = endpoint.view !== undefined ? (t, c) => resolve(t === alias ? baseTable : t, c) : resolve;
|
|
201
|
+
const cap = this.bakedFindCap(endpoint);
|
|
202
|
+
const where = endpoint.where ?? [];
|
|
203
|
+
const dynamic = where.some(isOptional);
|
|
204
|
+
if (dynamic && endpoint.view !== undefined) {
|
|
205
|
+
throw new Error(`emit: endpoint '${name}': a QUERY view-model binds its own CTE params BEFORE the WHERE, and a ` +
|
|
206
|
+
`dynamic (SKIP) WHERE is assembled at execution time — the two cannot share one param order. ` +
|
|
207
|
+
`Declare the view's own predicate inside the view query, or drop the optional predicate.`);
|
|
208
|
+
}
|
|
209
|
+
const params = [];
|
|
210
|
+
for (const p of where)
|
|
211
|
+
this.declareWhereParams(model, p, params);
|
|
212
|
+
// The page tail binds AFTER the WHERE — the order its `?`s occupy in the finished statement, and
|
|
213
|
+
// therefore the order the parameters are declared in.
|
|
214
|
+
const limit = this.page(endpoint.limit ?? (cap !== null ? cap + 1 : undefined), params);
|
|
215
|
+
const offset = this.page(endpoint.offset, params);
|
|
216
|
+
// A read with ANY optional predicate carries its WHOLE WHERE as fragments (the leaf assembles the
|
|
217
|
+
// survivors at execution time); a fully-bounded read lowers its WHERE into the static `sql` here.
|
|
218
|
+
const compiled = (0, compile_select_1.compileSelect)({
|
|
219
|
+
dialect: this.spec.dialect,
|
|
220
|
+
tableName: table,
|
|
221
|
+
select: projection.join(', '),
|
|
222
|
+
...(dynamic ? {} : { conditions: this.conditions(model, where) }),
|
|
223
|
+
...(endpoint.view !== undefined ? { cte: cteOf(endpoint.view) } : {}),
|
|
224
|
+
...(endpoint.order !== undefined ? { order: endpoint.order } : {}),
|
|
225
|
+
...(limit !== undefined ? { limit } : {}),
|
|
226
|
+
...(offset !== undefined ? { offset } : {}),
|
|
227
|
+
...(endpoint.lock === 'update' ? { forUpdate: true } : endpoint.lock === 'share' ? { forShare: true } : {}),
|
|
228
|
+
});
|
|
229
|
+
const bigint = endpoint.bigint === true;
|
|
230
|
+
const call = `${quote(compiled.sql)}, [${compiled.params.map(renderSlot).join(', ')}], false, false, ${bigint}${dynamic ? `, ${this.dynamicWherePlan(model, where)}` : ''}`;
|
|
231
|
+
const rowObj = objOf((0, outtype_1.deriveReadRow)(table, projection, readResolve, `endpoint '${name}'`).outType, name);
|
|
232
|
+
const selections = normalizeSelections(endpoint.with ?? []);
|
|
233
|
+
const fresh = freshNames(params.map((p) => p.name));
|
|
234
|
+
if (selections.length === 0) {
|
|
235
|
+
const rowType = this.declareInterface(`${cap1(name)}Row`, rowObj);
|
|
236
|
+
const out = fresh('rows');
|
|
237
|
+
return {
|
|
238
|
+
name,
|
|
239
|
+
params,
|
|
240
|
+
returnType: `${rowType}[]`,
|
|
241
|
+
lines: [`const ${out}: ${rowType}[] = Db.executeSQL(${call}) as ${rowType}[];`, `return ${out};`],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Relation graph: parents → (pluck → child executeSQL → group)*, ONE query per level (N+1-free).
|
|
245
|
+
const lines = [];
|
|
246
|
+
const parentVar = fresh('rows');
|
|
247
|
+
lines.push(`const ${parentVar}: WireValue[] = Db.executeSQL(${call});`);
|
|
248
|
+
const graph = this.relations(model, parentVar, rowObj, selections, lines, fresh, bigint, `endpoint '${name}'`);
|
|
249
|
+
const rowType = this.declareInterface(`${cap1(name)}Row`, graph.obj);
|
|
250
|
+
// The terminal binding is the SINGLE de-box point: the annotation is what bc reads, the one
|
|
251
|
+
// assertion is what narrows the opaque wire value for tsc.
|
|
252
|
+
lines[lines.length - 1] = `const ${graph.terminalVar}: ${rowType}[] = ${graph.terminalCall} as ${rowType}[];`;
|
|
253
|
+
lines.push(`return ${graph.terminalVar};`);
|
|
254
|
+
return { name, params, returnType: `${rowType}[]`, lines };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The DYNAMIC WHERE plan expression: one fragment per declared predicate — `{sql, params}` from the
|
|
258
|
+
* SAME `compileWhere` that produces a bounded WHERE — with an optional one wrapped in a conditional
|
|
259
|
+
* that yields `null` when its parameter is absent. That is the whole fragment vocabulary.
|
|
260
|
+
*/
|
|
261
|
+
dynamicWherePlan(model, where) {
|
|
262
|
+
const frags = where.map((p) => {
|
|
263
|
+
const w = (0, compile_1.compileWhere)(this.conditions(model, [p]), this.spec.dialect);
|
|
264
|
+
const frag = `{ sql: ${quote(w.sql)}, params: [${w.params.map(renderSlot).join(', ')}] }`;
|
|
265
|
+
return isParameterised(p) && p.optional === true ? `${p.param} !== null ? ${frag} : null` : frag;
|
|
266
|
+
});
|
|
267
|
+
return `{ frags: [${frags.join(', ')}] }`;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Emit ONE relation LEVEL: for each selected relation, pluck the parent key set, run the ONE batched
|
|
271
|
+
* child SELECT (`compileRelationOp`'s static SQL) and nest the children under the parent. A nested
|
|
272
|
+
* selection recurses FIRST, so a grandchild level is grouped into its own parent before that parent
|
|
273
|
+
* is grouped into ours (one query per depth — never one per parent).
|
|
274
|
+
*/
|
|
275
|
+
relations(model, parentVar, parentObj, selections, lines, fresh, bigint, at) {
|
|
276
|
+
let acc = parentVar;
|
|
277
|
+
const obj = { ...parentObj };
|
|
278
|
+
let terminalCall = '';
|
|
279
|
+
let terminalVar = '';
|
|
280
|
+
for (const sel of selections) {
|
|
281
|
+
const { decl, targetModel } = this.relationDecl(model, sel.name, at);
|
|
282
|
+
const op = (0, relation_1.compileRelationOp)(decl, this.resolver(targetModel));
|
|
283
|
+
const keysVar = fresh(`${sel.name}Keys`);
|
|
284
|
+
lines.push(`const ${keysVar}: WireValue[] = Db.pluck(${acc}, ${jsonArray((0, relation_1.parentKeyCols)(op))});`);
|
|
285
|
+
const childVar = fresh(sel.name);
|
|
286
|
+
// The runaway guard the compiled op resolved (`hasManyHardLimit` / the per-relation override):
|
|
287
|
+
// baked onto the child fetch so the `executeSQL` transport asserts the RAW child row count —
|
|
288
|
+
// the only point they are visible, since `group` receives an already-nested graph. A relation
|
|
289
|
+
// batch never carries a dynamic WHERE (its SQL is fully static), so the `whereDynamic` slot is
|
|
290
|
+
// an honest `null`. Uncapped ⇒ the ports stop at `bigint` and the call is byte-unchanged.
|
|
291
|
+
const guard = (0, relation_1.relationGuard)(op);
|
|
292
|
+
const guardPorts = guard !== null ? `, null, ${JSON.stringify(guard)}` : '';
|
|
293
|
+
lines.push(`const ${childVar}: WireValue[] = Db.executeSQL(${quote(op.sql)}, [${keysVar}], false, false, ${bigint}${guardPorts});`);
|
|
294
|
+
let childRows = childVar;
|
|
295
|
+
let childObj = objOf((0, outtype_1.deriveReadRow)(decl.targetTable, decl.select, this.resolver(targetModel), `${at} relation '${sel.name}'`).outType, `${at} relation '${sel.name}'`);
|
|
296
|
+
if (sel.with.length > 0) {
|
|
297
|
+
const nested = this.relations(targetModel, childVar, childObj, normalizeSelections(sel.with), lines, fresh, bigint, `${at}.${sel.name}`);
|
|
298
|
+
childObj = nested.obj;
|
|
299
|
+
childRows = nested.terminalVar;
|
|
300
|
+
}
|
|
301
|
+
const single = decl.kind !== 'hasMany';
|
|
302
|
+
obj[sel.name] = single ? { opt: { obj: childObj } } : { arr: { obj: childObj } };
|
|
303
|
+
terminalVar = fresh(`${sel.name}Graph`);
|
|
304
|
+
terminalCall = `Db.group(${acc}, ${childRows}, ${jsonArray((0, relation_1.parentKeyCols)(op))}, ${jsonArray((0, relation_1.targetKeyCols)(op))}, ${quote(sel.name)}, ${single})`;
|
|
305
|
+
lines.push(`const ${terminalVar}: WireValue[] = ${terminalCall};`);
|
|
306
|
+
acc = terminalVar;
|
|
307
|
+
}
|
|
308
|
+
return { obj, terminalCall, terminalVar };
|
|
309
|
+
}
|
|
310
|
+
/** The model's `@hasMany`/`@belongsTo`/`@hasOne` declaration for `name` (the decorator SoT). */
|
|
311
|
+
relationDecl(model, name, at) {
|
|
312
|
+
const rel = (0, decorators_1.getRelationMeta)(model).find((r) => r.propertyKey === name);
|
|
313
|
+
if (rel === undefined) {
|
|
314
|
+
throw new Error(`emit: ${at}: model '${model.name}' declares no relation '${name}' (@hasMany/@belongsTo/@hasOne)`);
|
|
315
|
+
}
|
|
316
|
+
const models = this.spec.models;
|
|
317
|
+
if (models === undefined) {
|
|
318
|
+
throw new Error(`emit: ${at}: relation '${name}' needs the model registry (EmitSpec.models) to resolve its target model`);
|
|
319
|
+
}
|
|
320
|
+
return (0, decorator_adapter_1.relationDeclOf)(rel, models, this.spec.dialect);
|
|
321
|
+
}
|
|
322
|
+
// ── WRITE (single-row INSERT / UPDATE / DELETE) ─────────────────────────────────────────────
|
|
323
|
+
write(name, endpoint) {
|
|
324
|
+
const model = endpoint.model;
|
|
325
|
+
const table = (0, decorator_adapter_1.tableNameOf)(model);
|
|
326
|
+
const params = [];
|
|
327
|
+
const returning = endpoint.returning;
|
|
328
|
+
const ports = { table };
|
|
329
|
+
this.returningPorts(model, returning, ports);
|
|
330
|
+
let component;
|
|
331
|
+
if (endpoint.kind === 'create') {
|
|
332
|
+
component = 'Insert';
|
|
333
|
+
this.valuePorts(model, endpoint.values, 'values', ports, params);
|
|
334
|
+
if (endpoint.onConflict !== undefined) {
|
|
335
|
+
ports.onConflict = endpoint.onConflict.join(', ');
|
|
336
|
+
ports.onConflictAction = endpoint.onConflictAction ?? 'update';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (endpoint.kind === 'update') {
|
|
340
|
+
component = 'Update';
|
|
341
|
+
this.valuePorts(model, endpoint.set, 'set', ports, params);
|
|
342
|
+
ports.where = this.wherePort(model, endpoint.where, params, `endpoint '${name}'`);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
component = 'Delete';
|
|
346
|
+
ports.where = this.wherePort(model, endpoint.where, params, `endpoint '${name}'`);
|
|
347
|
+
}
|
|
348
|
+
const op = (0, tx_1.compileWriteNode)({ component, ports }, this.spec.dialect, this.resolver(model));
|
|
349
|
+
const rowType = this.writeRowType(name, model, returning);
|
|
350
|
+
const out = freshNames(params.map((p) => p.name))('rows');
|
|
351
|
+
return {
|
|
352
|
+
name,
|
|
353
|
+
params,
|
|
354
|
+
returnType: `${rowType}[]`,
|
|
355
|
+
lines: [
|
|
356
|
+
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(', ')}], true, ${returning !== undefined}, false) as ${rowType}[];`,
|
|
357
|
+
`return ${out};`,
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Declare the `<family>.<column>` ports for a VALUES / SET list, binding each to a method parameter. */
|
|
362
|
+
valuePorts(model, bindings, family, ports, params) {
|
|
363
|
+
if (bindings.length === 0)
|
|
364
|
+
throw new Error(`emit: a write needs at least one '${family}' binding`);
|
|
365
|
+
for (const b of bindings) {
|
|
366
|
+
assertIdentifier(b.param, `parameter '${b.param}'`);
|
|
367
|
+
params.push({ name: b.param, type: this.bindTypeOf(model, b.column) });
|
|
368
|
+
ports[`${family}.${b.column}`] = new ParamRef(b.param);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The write path's `where` port — the `{arr:[…]}` Expression-IR member list `compileWriteNode`
|
|
373
|
+
* lowers (`lowerWherePort`). Only that compiler's declared vocabulary is lowerable there; anything
|
|
374
|
+
* else is a loud reject rather than a second WHERE compiler.
|
|
375
|
+
*/
|
|
376
|
+
wherePort(model, where, params, at) {
|
|
377
|
+
if (where.length === 0)
|
|
378
|
+
throw new Error(`emit: ${at}: an UPDATE / DELETE endpoint must declare a WHERE`);
|
|
379
|
+
const members = where.map((p) => {
|
|
380
|
+
if (p.kind === 'isNull')
|
|
381
|
+
return { eq: [{ ref: [p.column] }, null] };
|
|
382
|
+
if (p.kind === 'isNotNull')
|
|
383
|
+
return { ne: [{ ref: [p.column] }, null] };
|
|
384
|
+
if (p.kind !== undefined && p.kind !== 'compare') {
|
|
385
|
+
throw new Error(`emit: ${at}: the write compiler's WHERE vocabulary is eq/ne/lt/le/gt/ge and IS [NOT] NULL ` +
|
|
386
|
+
`(compileWriteNode.lowerWhereMember) — '${p.kind}' is not lowerable on a write.`);
|
|
387
|
+
}
|
|
388
|
+
const cmp = p;
|
|
389
|
+
if (cmp.optional === true) {
|
|
390
|
+
throw new Error(`emit: ${at}: a SKIP (optional) predicate is a READ construct — a write's WHERE is fixed.`);
|
|
391
|
+
}
|
|
392
|
+
if (cmp.op === 'like')
|
|
393
|
+
throw new Error(`emit: ${at}: LIKE is not part of the write compiler's WHERE vocabulary`);
|
|
394
|
+
assertIdentifier(cmp.param, `parameter '${cmp.param}'`);
|
|
395
|
+
params.push({ name: cmp.param, type: this.bindTypeOf(model, cmp.column) });
|
|
396
|
+
return { [cmp.op]: [{ ref: [cmp.column] }, new ParamRef(cmp.param)] };
|
|
397
|
+
});
|
|
398
|
+
return { arr: members };
|
|
399
|
+
}
|
|
400
|
+
// ── BATCH writes ───────────────────────────────────────────────────────────────────────────
|
|
401
|
+
batch(name, endpoint) {
|
|
402
|
+
if (endpoint.kind === 'deleteMany')
|
|
403
|
+
return this.deleteMany(name, endpoint);
|
|
404
|
+
const model = endpoint.model;
|
|
405
|
+
assertIdentifier(endpoint.param, `parameter '${endpoint.param}'`);
|
|
406
|
+
const pg = this.spec.dialect === 'postgres';
|
|
407
|
+
const params = [];
|
|
408
|
+
const ports = { table: (0, decorator_adapter_1.tableNameOf)(model), batch: 'true' };
|
|
409
|
+
this.returningPorts(model, endpoint.returning, ports);
|
|
410
|
+
// The bind SHAPE is the builder's own: PG's `UNNEST(?::t[], …)` binds ONE ARRAY PER COLUMN, so each
|
|
411
|
+
// column gets its own array parameter; MySQL/SQLite expand ONE record-array JSON param server-side.
|
|
412
|
+
const columnParam = (column) => {
|
|
413
|
+
if (!pg)
|
|
414
|
+
return new ParamRef(endpoint.param);
|
|
415
|
+
const p = `${endpoint.param}_${column}`;
|
|
416
|
+
params.push({ name: p, type: `${this.bindTypeOf(model, column)}[]` });
|
|
417
|
+
return new ParamRef(p);
|
|
418
|
+
};
|
|
419
|
+
const columns = endpoint.kind === 'createMany' ? endpoint.columns : [...endpoint.keyColumns, ...endpoint.columns];
|
|
420
|
+
if (endpoint.kind === 'createMany') {
|
|
421
|
+
for (const c of endpoint.columns)
|
|
422
|
+
ports[`values.${c}`] = columnParam(c);
|
|
423
|
+
if (endpoint.onConflict !== undefined) {
|
|
424
|
+
ports.onConflict = endpoint.onConflict.join(', ');
|
|
425
|
+
ports.onConflictAction = endpoint.onConflictAction ?? 'update';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
for (const c of endpoint.keyColumns)
|
|
430
|
+
ports[`key.${c}`] = columnParam(c);
|
|
431
|
+
for (const c of endpoint.columns)
|
|
432
|
+
ports[`set.${c}`] = columnParam(c);
|
|
433
|
+
}
|
|
434
|
+
if (!pg)
|
|
435
|
+
params.push({ name: endpoint.param, type: `${this.recordType(name, model, columns)}[]` });
|
|
436
|
+
const component = endpoint.kind === 'createMany' ? 'Insert' : 'Update';
|
|
437
|
+
const op = (0, tx_1.compileWriteNode)({ component, ports }, this.spec.dialect, this.resolver(model));
|
|
438
|
+
const rowType = this.writeRowType(name, model, endpoint.returning);
|
|
439
|
+
const slots = op.params.map((p) => (isBatchRowsMarker(p) ? endpoint.param : renderSlot(p)));
|
|
440
|
+
const out = freshNames(params.map((p) => p.name))('rows');
|
|
441
|
+
return {
|
|
442
|
+
name,
|
|
443
|
+
params,
|
|
444
|
+
returnType: `${rowType}[]`,
|
|
445
|
+
lines: [
|
|
446
|
+
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${slots.join(', ')}], true, ${endpoint.returning !== undefined}, false) as ${rowType}[];`,
|
|
447
|
+
`return ${out};`,
|
|
448
|
+
],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* `deleteMany` — a key-set DELETE. Its WHERE is the `in` member of the write compiler's own WHERE
|
|
453
|
+
* vocabulary, which lowers to the SAME static single-param membership predicate a read IN-list uses
|
|
454
|
+
* (`json-array.inListPredicate`) — so the statement text, the RETURNING tail and the mysql pk hint
|
|
455
|
+
* all come from `compileWriteNode`, not from here.
|
|
456
|
+
*/
|
|
457
|
+
deleteMany(name, endpoint) {
|
|
458
|
+
const model = endpoint.model;
|
|
459
|
+
assertIdentifier(endpoint.param, `parameter '${endpoint.param}'`);
|
|
460
|
+
const params = [{ name: endpoint.param, type: `${this.bindTypeOf(model, endpoint.keyColumn)}[]` }];
|
|
461
|
+
const ports = {
|
|
462
|
+
table: (0, decorator_adapter_1.tableNameOf)(model),
|
|
463
|
+
where: { arr: [{ in: [{ ref: [endpoint.keyColumn] }, new ParamRef(endpoint.param)] }] },
|
|
464
|
+
};
|
|
465
|
+
this.returningPorts(model, endpoint.returning, ports);
|
|
466
|
+
const op = (0, tx_1.compileWriteNode)({ component: 'Delete', ports }, this.spec.dialect, this.resolver(model));
|
|
467
|
+
const rowType = this.writeRowType(name, model, endpoint.returning);
|
|
468
|
+
const out = freshNames(params.map((p) => p.name))('rows');
|
|
469
|
+
return {
|
|
470
|
+
name,
|
|
471
|
+
params,
|
|
472
|
+
returnType: `${rowType}[]`,
|
|
473
|
+
lines: [
|
|
474
|
+
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(', ')}], true, ${endpoint.returning !== undefined}, false) as ${rowType}[];`,
|
|
475
|
+
`return ${out};`,
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Declare the `returning` + `pk` ports a RETURNING write needs. The key is the model's DECLARATION
|
|
481
|
+
* (`@column({ primaryKey, autoIncrement })`) — a dialect with no native RETURNING recovers the
|
|
482
|
+
* written rows from it. ONE place, shared by the single-row and batch write paths.
|
|
483
|
+
*/
|
|
484
|
+
returningPorts(model, returning, ports) {
|
|
485
|
+
if (returning === undefined)
|
|
486
|
+
return;
|
|
487
|
+
ports.returning = returning.join(', ');
|
|
488
|
+
const pkey = (0, decorators_1.getPrimaryKey)(model);
|
|
489
|
+
ports.pk = pkey.columns.join(',');
|
|
490
|
+
if (pkey.autoInc !== null)
|
|
491
|
+
ports.autoInc = pkey.autoInc;
|
|
492
|
+
}
|
|
493
|
+
/** A write's row type: the uniform transport summary, or the declared RETURNING projection. */
|
|
494
|
+
writeRowType(name, model, returning) {
|
|
495
|
+
if (returning === undefined)
|
|
496
|
+
return this.writeSummaryType();
|
|
497
|
+
return this.declareInterface(`${cap1(name)}Row`, objOf((0, outtype_1.deriveReadRow)((0, decorator_adapter_1.tableNameOf)(model), returning, this.resolver(model), `endpoint '${name}' RETURNING`).outType, name));
|
|
498
|
+
}
|
|
499
|
+
/** The record type a MySQL/SQLite batch write's JSON param carries (one field per bound column). */
|
|
500
|
+
recordType(name, model, columns) {
|
|
501
|
+
const obj = {};
|
|
502
|
+
for (const c of columns)
|
|
503
|
+
obj[c] = (0, coltype_1.sqlTypeToBcScalar)(this.sqlTypeOf(model, c));
|
|
504
|
+
return this.declareInterface(`${cap1(name)}Record`, obj);
|
|
505
|
+
}
|
|
506
|
+
// ── WHERE → the builders' ConditionObject ──────────────────────────────────────────────────
|
|
507
|
+
/** Declare the method parameters ONE predicate contributes (in declaration order). */
|
|
508
|
+
declareWhereParams(model, p, params) {
|
|
509
|
+
if (p.kind === 'exists' || p.kind === 'subquery') {
|
|
510
|
+
for (const m of p.match) {
|
|
511
|
+
if (m.param === undefined)
|
|
512
|
+
continue;
|
|
513
|
+
assertIdentifier(m.param, `parameter '${m.param}'`);
|
|
514
|
+
params.push({ name: m.param, type: this.bindTypeOf(p.model, m.column) });
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (p.kind === 'tupleIn') {
|
|
519
|
+
params.push(...this.tupleInParams(model, p));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (!isParameterised(p))
|
|
523
|
+
return; // IS [NOT] NULL binds nothing
|
|
524
|
+
assertIdentifier(p.param, `parameter '${p.param}'`);
|
|
525
|
+
const base = this.bindTypeOf(model, p.column);
|
|
526
|
+
const type = p.kind === 'in' ? `${base}[]` : base;
|
|
527
|
+
params.push({ name: p.param, type: p.optional === true ? `${type} | null` : type });
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Build the `ConditionObject` the surviving compilers consume. Every member is v1 condition
|
|
531
|
+
* vocabulary — a bare column key (equality / IS NULL), a custom-operator key carrying its own `?`
|
|
532
|
+
* (the comparison + membership forms), or a `DBExists` / `DBSubquery` token — so the WHERE text is
|
|
533
|
+
* produced by `DBConditions` exactly as the imperative path produces it.
|
|
534
|
+
*/
|
|
535
|
+
conditions(model, where) {
|
|
536
|
+
const out = {};
|
|
537
|
+
let n = 0;
|
|
538
|
+
const put = (key, value) => {
|
|
539
|
+
// The condition object is keyed BY THE SQL FRAGMENT, so two identical fragments would collapse
|
|
540
|
+
// into one. A repeat gets trailing spaces, which `DBConditions` emits verbatim (SQL-insignificant).
|
|
541
|
+
let k = key;
|
|
542
|
+
while (k in out)
|
|
543
|
+
k = `${key}${' '.repeat(++n)}`;
|
|
544
|
+
out[k] = value;
|
|
545
|
+
};
|
|
546
|
+
for (const p of where) {
|
|
547
|
+
if (p.kind === 'isNull') {
|
|
548
|
+
put(p.column, null);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (p.kind === 'isNotNull') {
|
|
552
|
+
put(`${p.column} IS NOT NULL`, true);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (p.kind === 'exists') {
|
|
556
|
+
put('__exists__', new DBValues_1.DBExists((0, decorator_adapter_1.tableNameOf)(p.model), this.correlation(p, model), p.not === true));
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (p.kind === 'subquery') {
|
|
560
|
+
const parentTable = (0, decorator_adapter_1.tableNameOf)(model);
|
|
561
|
+
const sub = new DBValues_1.DBSubquery(p.columns.map((c) => ({ tableName: parentTable, columnName: c })), (0, decorator_adapter_1.tableNameOf)(p.model), p.select.map((c) => ({ tableName: (0, decorator_adapter_1.tableNameOf)(p.model), columnName: c })), this.correlation(p, model), p.not === true ? 'NOT IN' : 'IN');
|
|
562
|
+
put('__subquery__', sub);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (p.kind === 'in') {
|
|
566
|
+
put((0, json_array_1.inListPredicate)(this.spec.dialect, p.column), new ParamRef(p.param));
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (p.kind === 'tupleIn') {
|
|
570
|
+
// The predicate carries its own `?`s (one per array param on PG, one JSON param elsewhere), so
|
|
571
|
+
// it rides `DBConditions`' custom-operator route: the value list fills them positionally.
|
|
572
|
+
put((0, json_array_1.tupleInPredicate)(this.spec.dialect, (0, decorator_adapter_1.tableNameOf)(model), p.columns, this.pgElementTypes(model, p.columns)), this.tupleInParams(model, p).map((d) => new ParamRef(d.name)));
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const cmp = p;
|
|
576
|
+
const ref = new ParamRef(cmp.param);
|
|
577
|
+
if (cmp.op === 'eq')
|
|
578
|
+
put(cmp.column, ref);
|
|
579
|
+
else
|
|
580
|
+
put(`${cmp.column} ${SQL_OPS[cmp.op]} ?`, ref);
|
|
581
|
+
}
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* The PG `UNNEST` element type of each key column, derived from the model's DECLARED column type via
|
|
586
|
+
* the SAME schema specimen the batch writes use (`pgTypeSpecimen` → `inferPgArrayType`) — never from
|
|
587
|
+
* the values, which are unknown at emit time and empty at call time often enough to matter.
|
|
588
|
+
*/
|
|
589
|
+
pgElementTypes(model, columns) {
|
|
590
|
+
if (this.spec.dialect !== 'postgres')
|
|
591
|
+
return undefined;
|
|
592
|
+
return columns.map((c) => (0, compile_relation_1.inferPgArrayType)([(0, tx_1.pgTypeSpecimen)(this.sqlTypeOf(model, c))]));
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* A composite `tupleIn`'s parameters, in bind order — the ONE derivation both the signature and the
|
|
596
|
+
* condition value consume. PostgreSQL's `UNNEST(?::T1, ?::T2)` binds ONE ARRAY PER KEY COLUMN;
|
|
597
|
+
* MySQL / SQLite bind ONE JSON array of tuples.
|
|
598
|
+
*/
|
|
599
|
+
tupleInParams(model, p) {
|
|
600
|
+
assertIdentifier(p.param, `parameter '${p.param}'`);
|
|
601
|
+
if (p.columns.length < 2) {
|
|
602
|
+
throw new Error(`emit: a 'tupleIn' predicate needs at least two key columns (got ${p.columns.length}) — use 'in' for a single column`);
|
|
603
|
+
}
|
|
604
|
+
if (this.spec.dialect === 'postgres') {
|
|
605
|
+
return p.columns.map((c) => ({ name: `${p.param}_${c}`, type: `${this.bindTypeOf(model, c)}[]` }));
|
|
606
|
+
}
|
|
607
|
+
const elements = [...new Set(p.columns.map((c) => this.bindTypeOf(model, c)))];
|
|
608
|
+
const element = elements.length === 1 ? elements[0] : `(${elements.join(' | ')})`;
|
|
609
|
+
return [{ name: p.param, type: `${element}[][]` }];
|
|
610
|
+
}
|
|
611
|
+
/** The subquery / EXISTS correlation terms: a parent-column ref (`parentRef` sugar) or a bound param. */
|
|
612
|
+
correlation(p, parent) {
|
|
613
|
+
const subTable = (0, decorator_adapter_1.tableNameOf)(p.model);
|
|
614
|
+
const parentTable = (0, decorator_adapter_1.tableNameOf)(parent);
|
|
615
|
+
return p.match.map((m) => {
|
|
616
|
+
const column = { tableName: subTable, columnName: m.column };
|
|
617
|
+
// Fail closed on an unknown column on either side (the correlation must name declared columns).
|
|
618
|
+
this.sqlTypeOf(p.model, m.column);
|
|
619
|
+
if (m.parentColumn !== undefined) {
|
|
620
|
+
this.sqlTypeOf(parent, m.parentColumn);
|
|
621
|
+
return { column, value: new DBValues_1.DBParentRef({ tableName: parentTable, columnName: m.parentColumn }) };
|
|
622
|
+
}
|
|
623
|
+
return { column, value: new ParamRef(m.param) };
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// ── type declarations ──────────────────────────────────────────────────────────────────────
|
|
627
|
+
/** The uniform non-RETURNING write summary the `executeSQL` transport returns. */
|
|
628
|
+
writeSummaryType() {
|
|
629
|
+
return this.declareInterface('WriteSummary', { changes: 'int', lastInsertRowid: 'int' });
|
|
630
|
+
}
|
|
631
|
+
/** Declare (or reuse) a row interface. Two declarations of the same name must be structurally equal. */
|
|
632
|
+
declareInterface(name, obj) {
|
|
633
|
+
const body = Object.entries(obj)
|
|
634
|
+
.map(([k, t]) => ` ${propName(k)}: ${this.tsType(t, `${name}_${k}`)};`)
|
|
635
|
+
.join('\n');
|
|
636
|
+
const existing = this.interfaces.get(name);
|
|
637
|
+
if (existing !== undefined && existing !== body) {
|
|
638
|
+
throw new Error(`emit: two different row shapes both want the interface name '${name}'`);
|
|
639
|
+
}
|
|
640
|
+
this.interfaces.set(name, body);
|
|
641
|
+
return name;
|
|
642
|
+
}
|
|
643
|
+
/** A `PortableType` → its TS type text, declaring a nested interface for an embedded object. */
|
|
644
|
+
tsType(t, nameHint) {
|
|
645
|
+
if (typeof t === 'string')
|
|
646
|
+
return tsScalar(t);
|
|
647
|
+
if ('opt' in t)
|
|
648
|
+
return `${this.tsType(t.opt, nameHint)} | null`;
|
|
649
|
+
if ('arr' in t)
|
|
650
|
+
return `${this.tsType(t.arr, nameHint)}[]`;
|
|
651
|
+
if ('obj' in t)
|
|
652
|
+
return this.declareInterface(cap1(nameHint), t.obj);
|
|
653
|
+
throw new Error(`emit: portable type ${JSON.stringify(t)} has no TS rendering`);
|
|
654
|
+
}
|
|
655
|
+
// ── module rendering ───────────────────────────────────────────────────────────────────────
|
|
656
|
+
render(methods) {
|
|
657
|
+
const text = [...this.interfaces.values(), ...methods.flatMap((m) => [...m.lines, ...m.params.map((p) => p.type)])].join('\n');
|
|
658
|
+
const typeImports = ['Int', 'Float', 'WireValue'].filter((s) => new RegExp(`\\b${s}\\b`).test(text));
|
|
659
|
+
const head = [
|
|
660
|
+
`// GENERATED by litedbmodel \`emitBehaviorModule\` — the decorator → SCP lowering. DO NOT EDIT.`,
|
|
661
|
+
`//`,
|
|
662
|
+
`// Read by \`bc generate --from\`: each \`@behavior static\` method is one declared endpoint, its`,
|
|
663
|
+
`// parameters are the input ports and its return type is the output contract. The SQL is the`,
|
|
664
|
+
`// ${this.spec.dialect} text the litedbmodel \`makesql\` builders produced; the transports are the`,
|
|
665
|
+
`// library's ONE \`@leaf\` catalog (\`Db\`).`,
|
|
666
|
+
`import { behavior${typeImports.length > 0 ? `, type ${typeImports.join(', type ')}` : ''} } from 'behavior-contracts';`,
|
|
667
|
+
`import { Db } from ${quote(this.spec.leafImport)};`,
|
|
668
|
+
``,
|
|
669
|
+
];
|
|
670
|
+
const decls = [...this.interfaces].map(([name, body]) => `interface ${name} {\n${body}\n}`);
|
|
671
|
+
const body = methods.map((m) => {
|
|
672
|
+
const sig = m.params.map((p) => `${p.name}: ${p.type}`).join(', ');
|
|
673
|
+
return ` @behavior static ${m.name}(${sig}): ${m.returnType} {\n${m.lines.map((l) => ` ${l}`).join('\n')}\n }`;
|
|
674
|
+
});
|
|
675
|
+
return `${head.join('\n')}${decls.join('\n\n')}\n\nexport class ${this.spec.behavior} {\n${body.join('\n\n')}\n}\n`;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// ── helpers ───────────────────────────────────────────────────────────────────────────────────
|
|
679
|
+
/** The SQL comparison text of each declared comparison operator (the v1 custom-operator key form). */
|
|
680
|
+
const SQL_OPS = {
|
|
681
|
+
ne: '!=',
|
|
682
|
+
lt: '<',
|
|
683
|
+
le: '<=',
|
|
684
|
+
gt: '>',
|
|
685
|
+
ge: '>=',
|
|
686
|
+
like: 'LIKE',
|
|
687
|
+
};
|
|
688
|
+
/** A predicate that BINDS a parameter of its own (so it can be a SKIP member). */
|
|
689
|
+
function isParameterised(p) {
|
|
690
|
+
return p.kind === undefined || p.kind === 'compare' || p.kind === 'in';
|
|
691
|
+
}
|
|
692
|
+
/** A predicate is a SKIP member when it declares `optional` (only a parameterised member can be one). */
|
|
693
|
+
function isOptional(p) {
|
|
694
|
+
return isParameterised(p) && p.optional === true;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Render ONE bound slot: a parameter sentinel becomes its identifier, anything else a TS literal.
|
|
698
|
+
*
|
|
699
|
+
* A batch write's slot is one of `compileWriteNode`'s BATCH MARKERS rather than a bare sentinel: the
|
|
700
|
+
* PostgreSQL `UNNEST` form binds one `{__batchArray:{column, ref}}` per column (the sentinel is its
|
|
701
|
+
* `ref`), the MySQL/SQLite JSON form binds the SAME `{__batchRows:{…}}` to every `?` (see
|
|
702
|
+
* {@link batchRowsSlot}). Both are unwrapped to the parameter they stand for.
|
|
703
|
+
*/
|
|
704
|
+
function renderSlot(slot) {
|
|
705
|
+
if (slot instanceof ParamRef)
|
|
706
|
+
return slot.expr;
|
|
707
|
+
const ref = slot?.__batchArray?.ref;
|
|
708
|
+
if (ref instanceof ParamRef)
|
|
709
|
+
return ref.expr;
|
|
710
|
+
return JSON.stringify(slot);
|
|
711
|
+
}
|
|
712
|
+
/** The MySQL/SQLite batch marker `compileWriteNode` binds to every `?` of a batch statement. */
|
|
713
|
+
function isBatchRowsMarker(p) {
|
|
714
|
+
return typeof p === 'object' && p !== null && '__batchRows' in p;
|
|
715
|
+
}
|
|
716
|
+
/** A QUERY view-model's CTE (v1 `WITH <alias> AS (<query>)`, its own params bound FIRST). */
|
|
717
|
+
function cteOf(view) {
|
|
718
|
+
const alias = view.alias ?? 'derived';
|
|
719
|
+
const q = view.query;
|
|
720
|
+
return typeof q === 'string' ? { name: alias, sql: q, params: [] } : { name: alias, sql: q.sql, params: [...q.params] };
|
|
721
|
+
}
|
|
722
|
+
/** The row `obj` of a `{arr:{obj:…}}` read row type. */
|
|
723
|
+
function objOf(t, at) {
|
|
724
|
+
const arr = typeof t === 'object' && t !== null && 'arr' in t ? t.arr : undefined;
|
|
725
|
+
const obj = arr !== undefined && typeof arr === 'object' && arr !== null && 'obj' in arr ? arr.obj : undefined;
|
|
726
|
+
if (obj === undefined)
|
|
727
|
+
throw new Error(`emit: ${at}: expected a row type {arr:{obj:…}}, got ${JSON.stringify(t)}`);
|
|
728
|
+
if (Object.keys(obj).length === 0) {
|
|
729
|
+
throw new Error(`emit: ${at}: the projection resolved to an EMPTY row type — every projected column must be a declared @column`);
|
|
730
|
+
}
|
|
731
|
+
return obj;
|
|
732
|
+
}
|
|
733
|
+
/** bc scalar → TS type text (`Int`/`Float` are bc's branded numeric declarations). */
|
|
734
|
+
function tsScalar(s) {
|
|
735
|
+
switch (s) {
|
|
736
|
+
case 'int':
|
|
737
|
+
return 'Int';
|
|
738
|
+
case 'float':
|
|
739
|
+
return 'Float';
|
|
740
|
+
case 'bool':
|
|
741
|
+
return 'boolean';
|
|
742
|
+
case 'null':
|
|
743
|
+
return 'null';
|
|
744
|
+
case 'string':
|
|
745
|
+
return 'string';
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function normalizeSelections(sels) {
|
|
749
|
+
return sels.map((s) => (typeof s === 'string' ? { name: s, with: [] } : { name: s.name, with: s.with ?? [] }));
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* A body-binding name allocator. `reserved` holds the method's PARAMETER names: a `const` that shadowed
|
|
753
|
+
* a parameter would be a TDZ error the moment the parameter is also bound (`const rows = f(rows)`), so
|
|
754
|
+
* the allocator never hands one out.
|
|
755
|
+
*/
|
|
756
|
+
function freshNames(reserved = []) {
|
|
757
|
+
const used = new Map(reserved.map((r) => [r, 1]));
|
|
758
|
+
return (base) => {
|
|
759
|
+
const n = (used.get(base) ?? 0) + 1;
|
|
760
|
+
used.set(base, n);
|
|
761
|
+
return n === 1 ? base : `${base}${n}`;
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function quote(s) {
|
|
765
|
+
return JSON.stringify(s);
|
|
766
|
+
}
|
|
767
|
+
function jsonArray(items) {
|
|
768
|
+
return `[${items.map(quote).join(', ')}]`;
|
|
769
|
+
}
|
|
770
|
+
function cap1(s) {
|
|
771
|
+
return s.charAt(0).toUpperCase() + s.slice(1).replace(/[^A-Za-z0-9_]/g, '_');
|
|
772
|
+
}
|
|
773
|
+
/** An object-literal property name: bare when it is an identifier, quoted otherwise. */
|
|
774
|
+
function propName(k) {
|
|
775
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(k) ? k : quote(k);
|
|
776
|
+
}
|
|
777
|
+
function assertIdentifier(name, at) {
|
|
778
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
779
|
+
throw new Error(`emit: ${at} is not a valid TypeScript identifier`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
//# sourceMappingURL=emitter.js.map
|