litedbmodel 1.2.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +6 -0
- package/dist/DBModel.js.map +1 -1
- package/dist/scp/authoring-sql.d.ts +56 -0
- package/dist/scp/authoring-sql.d.ts.map +1 -0
- package/dist/scp/authoring-sql.js +83 -0
- package/dist/scp/authoring-sql.js.map +1 -0
- package/dist/scp/authoring.d.ts +155 -0
- package/dist/scp/authoring.d.ts.map +1 -0
- package/dist/scp/authoring.js +162 -0
- package/dist/scp/authoring.js.map +1 -0
- package/dist/scp/bridge.d.ts +64 -0
- package/dist/scp/bridge.d.ts.map +1 -0
- package/dist/scp/bridge.js +329 -0
- package/dist/scp/bridge.js.map +1 -0
- package/dist/scp/catalog.d.ts +91 -0
- package/dist/scp/catalog.d.ts.map +1 -0
- package/dist/scp/catalog.js +203 -0
- package/dist/scp/catalog.js.map +1 -0
- package/dist/scp/codegen.d.ts +131 -0
- package/dist/scp/codegen.d.ts.map +1 -0
- package/dist/scp/codegen.js +137 -0
- package/dist/scp/codegen.js.map +1 -0
- package/dist/scp/compile-dialect.d.ts +39 -0
- package/dist/scp/compile-dialect.d.ts.map +1 -0
- package/dist/scp/compile-dialect.js +62 -0
- package/dist/scp/compile-dialect.js.map +1 -0
- package/dist/scp/compile-sqlite.d.ts +114 -0
- package/dist/scp/compile-sqlite.d.ts.map +1 -0
- package/dist/scp/compile-sqlite.js +149 -0
- package/dist/scp/compile-sqlite.js.map +1 -0
- package/dist/scp/dialect.d.ts +141 -0
- package/dist/scp/dialect.d.ts.map +1 -0
- package/dist/scp/dialect.js +194 -0
- package/dist/scp/dialect.js.map +1 -0
- package/dist/scp/errors.d.ts +36 -0
- package/dist/scp/errors.d.ts.map +1 -0
- package/dist/scp/errors.js +72 -0
- package/dist/scp/errors.js.map +1 -0
- package/dist/scp/guard.d.ts +47 -0
- package/dist/scp/guard.d.ts.map +1 -0
- package/dist/scp/guard.js +137 -0
- package/dist/scp/guard.js.map +1 -0
- package/dist/scp/index.cjs +4744 -0
- package/dist/scp/index.d.ts +49 -0
- package/dist/scp/index.d.ts.map +1 -0
- package/dist/scp/index.js +164 -0
- package/dist/scp/index.js.map +1 -0
- package/dist/scp/index.mjs +2000 -0
- package/dist/scp/ir.d.ts +92 -0
- package/dist/scp/ir.d.ts.map +1 -0
- package/dist/scp/ir.js +28 -0
- package/dist/scp/ir.js.map +1 -0
- package/dist/scp/relation.d.ts +136 -0
- package/dist/scp/relation.d.ts.map +1 -0
- package/dist/scp/relation.js +191 -0
- package/dist/scp/relation.js.map +1 -0
- package/dist/scp/render.d.ts +40 -0
- package/dist/scp/render.d.ts.map +1 -0
- package/dist/scp/render.js +170 -0
- package/dist/scp/render.js.map +1 -0
- package/dist/scp/runtime.d.ts +295 -0
- package/dist/scp/runtime.d.ts.map +1 -0
- package/dist/scp/runtime.js +665 -0
- package/dist/scp/runtime.js.map +1 -0
- package/dist/scp/typed-object.d.ts +88 -0
- package/dist/scp/typed-object.d.ts.map +1 -0
- package/dist/scp/typed-object.js +163 -0
- package/dist/scp/typed-object.js.map +1 -0
- package/dist/scp/write-plan.d.ts +161 -0
- package/dist/scp/write-plan.d.ts.map +1 -0
- package/dist/scp/write-plan.js +528 -0
- package/dist/scp/write-plan.js.map +1 -0
- package/dist/scp/write-runtime.d.ts +80 -0
- package/dist/scp/write-runtime.d.ts.map +1 -0
- package/dist/scp/write-runtime.js +167 -0
- package/dist/scp/write-runtime.js.map +1 -0
- package/dist/scp/writes.d.ts +251 -0
- package/dist/scp/writes.d.ts.map +1 -0
- package/dist/scp/writes.js +176 -0
- package/dist/scp/writes.js.map +1 -0
- package/docs/proposal/contracts-architecture-feasibility.md +328 -0
- package/docs/proposal/contracts-architecture.md +579 -0
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +382 -0
- package/docs/proposal/sql-dynamic-expansion-spec.md +175 -0
- package/package.json +27 -5
- package/docs/README_REVIEW.md +0 -23
package/dist/scp/ir.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* litedbmodel v2 SCP — Backend Compile IR shapes (WS1, #21).
|
|
3
|
+
*
|
|
4
|
+
* These are the litedbmodel-specific SQL IR structures a Component-graph CRUD node
|
|
5
|
+
* carries AFTER Backend Compile (spec §8). They are the input a thin multi-language
|
|
6
|
+
* runtime reads (WS3) to produce byte-identical SQL. The normative rules for how a
|
|
7
|
+
* runtime expands them are `docs/proposal/sql-dynamic-expansion-spec.md`.
|
|
8
|
+
*
|
|
9
|
+
* ## Param slots (spec §8) — the closed Expression IR set only
|
|
10
|
+
*
|
|
11
|
+
* Every element of a `params` array is 1:1 with a `?` placeholder and is one of (using
|
|
12
|
+
* bc's FLAT scope — input ports and node results are top-level scope roots, matching
|
|
13
|
+
* `runBehavior`'s `{...input, ...results}`):
|
|
14
|
+
* - an **input reference** `{ ref: ["authorId"] }`
|
|
15
|
+
* - a **wire reference** `{ ref: ["<nodeId>", "field"] }`
|
|
16
|
+
* - an **Expression IR operator node** `{ coalesce: [{ refOpt: ["limit"] }, 20] }`
|
|
17
|
+
*
|
|
18
|
+
* All three are Expression IR nodes from bc's CLOSED operator set (`PORTABLE_EXPR_OPERATORS`
|
|
19
|
+
* — `ref`/`refOpt`/`coalesce`/`add`/`eq`/… ~23 ops). NO litedbmodel-local opcodes are ever
|
|
20
|
+
* emitted; an un-lowerable SQL construct is the Raw SQL escape hatch's concern (spec §13),
|
|
21
|
+
* not a fake opcode. This is enforced by {@link assertParamsPortable}.
|
|
22
|
+
*/
|
|
23
|
+
/** An Expression IR node (bc closed set). Opaque here; validated by the guard. */
|
|
24
|
+
export type ExprNode = unknown;
|
|
25
|
+
/**
|
|
26
|
+
* A dynamic WHERE/SET fragment with an existence rule (spec §8).
|
|
27
|
+
*
|
|
28
|
+
* - `always: true` — the fragment is unconditionally present.
|
|
29
|
+
* - `when: <ExprNode>` — the fragment is present iff the Expression evaluates to a
|
|
30
|
+
* truthy (present) binding. A `cond ? [...] : SKIP` in authoring lowers to a fragment
|
|
31
|
+
* whose `when` tests the presence of the driving input (§8 / feasibility §4). SKIP is
|
|
32
|
+
* NOT an Expression opcode — it is the ABSENCE of the fragment when `when` is false.
|
|
33
|
+
*
|
|
34
|
+
* `sql` is the fragment's literal SQL text (leading connector included, e.g. `" AND "`),
|
|
35
|
+
* with `?` placeholders. `params` are the fragment's param slots (1:1 with its `?`),
|
|
36
|
+
* appended to the assembled params array in fragment order only when the fragment is
|
|
37
|
+
* present. `expand` marks an IN-list slot whose single `?` expands to `(?, ?, …)` per the
|
|
38
|
+
* bound array's length (dynamic-expansion spec §5).
|
|
39
|
+
*/
|
|
40
|
+
export interface Fragment {
|
|
41
|
+
/** Unconditionally present. Exactly one of `always` / `when` is set. */
|
|
42
|
+
always?: true;
|
|
43
|
+
/** Presence guard (Expression IR). Present iff it evaluates truthy. */
|
|
44
|
+
when?: ExprNode;
|
|
45
|
+
/** Literal SQL text (includes the leading connector, e.g. `" AND status = ?"`). */
|
|
46
|
+
sql: string;
|
|
47
|
+
/** Param slots (Expression IR nodes), 1:1 with this fragment's `?`. */
|
|
48
|
+
params: ExprNode[];
|
|
49
|
+
/**
|
|
50
|
+
* IN-list expansion: index into `params` whose bound value is an array; its single `?`
|
|
51
|
+
* in `sql` (written `(?)`) expands to `(?, ?, …)` (dynamic-expansion spec §5).
|
|
52
|
+
*/
|
|
53
|
+
expand?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A compiled fragment tree (spec §8): ordered fragments combined by a single connector
|
|
57
|
+
* (`AND` / `OR`). Nested groups parenthesize (dynamic-expansion spec §4). A `null` tree
|
|
58
|
+
* means "no WHERE" (empty-WHERE degeneration, §3 of the spec).
|
|
59
|
+
*/
|
|
60
|
+
export interface FragmentTree {
|
|
61
|
+
connector: 'AND' | 'OR';
|
|
62
|
+
fragments: (Fragment | FragmentTree)[];
|
|
63
|
+
}
|
|
64
|
+
/** The assembly spec (row → logical model). WS1 emits the shape only. */
|
|
65
|
+
export interface AssemblySpec {
|
|
66
|
+
shape: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The Backend Compile output of one CRUD node (spec §8). `sql` is the fully flattened
|
|
70
|
+
* static SQL with a single `{where}` splice point where the fragment tree renders (or no
|
|
71
|
+
* splice point at all for writes whose WHERE is static). `params` are the STATIC param
|
|
72
|
+
* slots (those outside the fragment tree — e.g. Insert values, LIMIT); fragment params
|
|
73
|
+
* are interleaved at render time per the dynamic-expansion spec.
|
|
74
|
+
*/
|
|
75
|
+
export interface CompiledOperation {
|
|
76
|
+
/** Catalog component name (`Select` / `Insert` / `Update` / `Delete`). */
|
|
77
|
+
component: string;
|
|
78
|
+
/** SQLite dialect SQL text with a `{where}` splice marker if a fragment tree applies. */
|
|
79
|
+
sql: string;
|
|
80
|
+
/** The dynamic WHERE/SET fragment tree, or null when the WHERE is fully static/absent. */
|
|
81
|
+
where: FragmentTree | null;
|
|
82
|
+
/**
|
|
83
|
+
* Static param slots outside the fragment tree, in SQL position order. Each is an
|
|
84
|
+
* Expression IR node (input-ref / wire-ref / operator) from the closed set.
|
|
85
|
+
*/
|
|
86
|
+
params: ExprNode[];
|
|
87
|
+
/** Row → logical model assembly (shape only in WS1). */
|
|
88
|
+
assembly: AssemblySpec;
|
|
89
|
+
}
|
|
90
|
+
/** The literal `{where}` splice marker inside `CompiledOperation.sql`. */
|
|
91
|
+
export declare const WHERE_SLOT = "{where}";
|
|
92
|
+
//# sourceMappingURL=ir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../../src/scp/ir.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC;AAE/B;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,uEAAuE;IACvE,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,mFAAmF;IACnF,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,KAAK,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,CAAC,QAAQ,GAAG,YAAY,CAAC,EAAE,CAAC;CACxC;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,0FAA0F;IAC1F,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B;;;OAGG;IACH,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,wDAAwD;IACxD,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,0EAA0E;AAC1E,eAAO,MAAM,UAAU,YAAY,CAAC"}
|
package/dist/scp/ir.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* litedbmodel v2 SCP — Backend Compile IR shapes (WS1, #21).
|
|
4
|
+
*
|
|
5
|
+
* These are the litedbmodel-specific SQL IR structures a Component-graph CRUD node
|
|
6
|
+
* carries AFTER Backend Compile (spec §8). They are the input a thin multi-language
|
|
7
|
+
* runtime reads (WS3) to produce byte-identical SQL. The normative rules for how a
|
|
8
|
+
* runtime expands them are `docs/proposal/sql-dynamic-expansion-spec.md`.
|
|
9
|
+
*
|
|
10
|
+
* ## Param slots (spec §8) — the closed Expression IR set only
|
|
11
|
+
*
|
|
12
|
+
* Every element of a `params` array is 1:1 with a `?` placeholder and is one of (using
|
|
13
|
+
* bc's FLAT scope — input ports and node results are top-level scope roots, matching
|
|
14
|
+
* `runBehavior`'s `{...input, ...results}`):
|
|
15
|
+
* - an **input reference** `{ ref: ["authorId"] }`
|
|
16
|
+
* - a **wire reference** `{ ref: ["<nodeId>", "field"] }`
|
|
17
|
+
* - an **Expression IR operator node** `{ coalesce: [{ refOpt: ["limit"] }, 20] }`
|
|
18
|
+
*
|
|
19
|
+
* All three are Expression IR nodes from bc's CLOSED operator set (`PORTABLE_EXPR_OPERATORS`
|
|
20
|
+
* — `ref`/`refOpt`/`coalesce`/`add`/`eq`/… ~23 ops). NO litedbmodel-local opcodes are ever
|
|
21
|
+
* emitted; an un-lowerable SQL construct is the Raw SQL escape hatch's concern (spec §13),
|
|
22
|
+
* not a fake opcode. This is enforced by {@link assertParamsPortable}.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.WHERE_SLOT = void 0;
|
|
26
|
+
/** The literal `{where}` splice marker inside `CompiledOperation.sql`. */
|
|
27
|
+
exports.WHERE_SLOT = '{where}';
|
|
28
|
+
//# sourceMappingURL=ir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir.js","sourceRoot":"","sources":["../../src/scp/ir.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AA0EH,0EAA0E;AAC7D,QAAA,UAAU,GAAG,SAAS,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* litedbmodel v2 SCP — Relation ops (Read) + staged batch resolution (WS4, #24).
|
|
3
|
+
*
|
|
4
|
+
* Relations are NOT SQL JOINs by default (spec §5). They are the same
|
|
5
|
+
* staged-batch query-composition + object-assembly shape graphddb and v1 `LazyRelation`
|
|
6
|
+
* use: collect the parent key set of a result page, run ONE batched child SELECT
|
|
7
|
+
* (`fk IN (…)` / per-parent-limited `ROW_NUMBER()` window), then distribute the child rows
|
|
8
|
+
* back to their parents. This makes a relation load structurally bounded — one batched
|
|
9
|
+
* query per relation edge, NEVER one-per-parent (no N+1).
|
|
10
|
+
*
|
|
11
|
+
* ## The relation op is a pre-compiled artifact (spec §8)
|
|
12
|
+
*
|
|
13
|
+
* A {@link RelationOp} is compiled ONCE from a model {@link RelationDecl}, dialect-aware
|
|
14
|
+
* (SQLite for α), into the SAME {@link CompiledOperation} shape WS1 already emits for a
|
|
15
|
+
* Select — so it renders through the SAME normative {@link renderOperation} (fragment tree
|
|
16
|
+
* + IN-list `(?, ?, …)` expansion, spec §5). It carries NO functions and lands in the §8
|
|
17
|
+
* bundle (`SqlBundle.relations`) as pure JSON, so a thin per-language runtime (WS7) gets
|
|
18
|
+
* the relation batch SQL for free. The v1 `LazyRelation` batch-SQL assets (IN-list, the
|
|
19
|
+
* `ROW_NUMBER() OVER (PARTITION BY … )` per-parent limit) are reused here as the compiled
|
|
20
|
+
* content, not re-invented.
|
|
21
|
+
*
|
|
22
|
+
* ## ONE relation op, TWO surfaces (spec §5 / feasibility §9)
|
|
23
|
+
*
|
|
24
|
+
* Both read surfaces trigger the IDENTICAL compiled {@link RelationOp} via
|
|
25
|
+
* {@link runRelationOp} — there is exactly one batch-execution code path:
|
|
26
|
+
*
|
|
27
|
+
* - **Declarative select** (`with: { author: true }`): the planner resolves sibling
|
|
28
|
+
* relations eagerly, dedups the parent key set, prefetch-optimized (staged batch).
|
|
29
|
+
* - **Lazy** (`await post.author`): a prototype getter + a non-enumerable Symbol batch
|
|
30
|
+
* context (graphddb's `GRAPHDDB_KEY` technique) fires the SAME op at access time over
|
|
31
|
+
* the whole sibling set (batch, so still no N+1 — it just can't prefetch).
|
|
32
|
+
*
|
|
33
|
+
* `runRelationOp` renders `op.query` once for the deduped parent key set and returns the
|
|
34
|
+
* child rows grouped by parent key. Both surfaces call it; a test asserts they resolve via
|
|
35
|
+
* the same compiled op (same SQL text), not two parallel code paths.
|
|
36
|
+
*/
|
|
37
|
+
import type { CompiledOperation } from './ir';
|
|
38
|
+
/** A read relation cardinality (v1 parity). `belongsTo`/`hasOne` are single; `hasMany` many. */
|
|
39
|
+
export type RelationKind = 'belongsTo' | 'hasMany' | 'hasOne';
|
|
40
|
+
/**
|
|
41
|
+
* A model's read-relation declaration (the authored/decorated relation metadata, spec §4).
|
|
42
|
+
* This is the litedbmodel-consumer input the relation compiler lowers to a {@link RelationOp}.
|
|
43
|
+
* It mirrors v1's `@belongsTo`/`@hasMany`/`@hasOne` config (source/target key + optional
|
|
44
|
+
* per-parent order/limit), reduced to the fields the batch SQL needs.
|
|
45
|
+
*/
|
|
46
|
+
export interface RelationDecl {
|
|
47
|
+
/** Relation name (the property attached on the parent object). */
|
|
48
|
+
readonly name: string;
|
|
49
|
+
/** Cardinality. */
|
|
50
|
+
readonly kind: RelationKind;
|
|
51
|
+
/** The child (target) table name. */
|
|
52
|
+
readonly targetTable: string;
|
|
53
|
+
/** Child columns to project (the child typed-object own props). */
|
|
54
|
+
readonly select: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* The PARENT column whose value is the batch key (e.g. `belongsTo` → the parent FK
|
|
57
|
+
* `author_id`; `hasMany`/`hasOne` → the parent PK `id`).
|
|
58
|
+
*/
|
|
59
|
+
readonly parentKey: string;
|
|
60
|
+
/**
|
|
61
|
+
* The CHILD column matched against the parent key (e.g. `belongsTo` → child PK `id`;
|
|
62
|
+
* `hasMany`/`hasOne` → child FK `post_id`).
|
|
63
|
+
*/
|
|
64
|
+
readonly targetKey: string;
|
|
65
|
+
/** Optional per-parent ORDER BY body (dialect-neutral text, e.g. `"created_at DESC"`). */
|
|
66
|
+
readonly order?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Optional per-parent row limit (`hasMany` only). When set, the batch query uses a
|
|
69
|
+
* `ROW_NUMBER() OVER (PARTITION BY <targetKey> ORDER BY <order>)` window so each parent
|
|
70
|
+
* gets at most `limit` children in ONE query (v1's SQLite `batchLoadWithRowNumber`).
|
|
71
|
+
*/
|
|
72
|
+
readonly limit?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A pre-compiled relation batch op (spec §8 `relation ops`). Pure JSON — it carries the
|
|
76
|
+
* compiled batch SELECT ({@link CompiledOperation}, rendered by {@link renderOperation})
|
|
77
|
+
* plus the grouping metadata the runtime needs to distribute child rows to parents. No
|
|
78
|
+
* functions, so it round-trips through `JSON.stringify` inside the bundle.
|
|
79
|
+
*/
|
|
80
|
+
export interface RelationOp {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly kind: RelationKind;
|
|
83
|
+
/** Parent column supplying the batch key values (dedup key). */
|
|
84
|
+
readonly parentKey: string;
|
|
85
|
+
/** Child column the batch groups rows by (matches the parent key). */
|
|
86
|
+
readonly targetKey: string;
|
|
87
|
+
/**
|
|
88
|
+
* The batched child SELECT as a {@link CompiledOperation}. Its WHERE is a single IN-list
|
|
89
|
+
* fragment `targetKey IN (?)` whose one slot binds the deduped parent-key array and
|
|
90
|
+
* expands to `(?, ?, …)` at render time (spec §5). A `limit` decl compiles to a
|
|
91
|
+
* `ROW_NUMBER()` window CTE instead (per-parent limit).
|
|
92
|
+
*/
|
|
93
|
+
readonly query: CompiledOperation;
|
|
94
|
+
}
|
|
95
|
+
/** The reserved input head the relation batch query binds its deduped key array to. */
|
|
96
|
+
export declare const RELATION_KEYS_HEAD = "__keys";
|
|
97
|
+
/**
|
|
98
|
+
* Compile ONE {@link RelationDecl} into a {@link RelationOp} for SQLite (α dialect). The
|
|
99
|
+
* batch query selects the child rows whose {@link RelationDecl.targetKey} is in the deduped
|
|
100
|
+
* parent-key set. Reuses the existing IN-list fragment + `renderOperation` expansion (no new
|
|
101
|
+
* SQL machinery). A `hasMany` with `limit` compiles to the v1 `ROW_NUMBER()` window form so
|
|
102
|
+
* the per-parent cap holds in a single batched statement.
|
|
103
|
+
*/
|
|
104
|
+
export declare function compileRelationOp(decl: RelationDecl): RelationOp;
|
|
105
|
+
/** A minimal read-only driver surface (`prepare(sql).all(...params)`), the SQLite `Database`. */
|
|
106
|
+
export interface RelationDriver {
|
|
107
|
+
prepare(sql: string): {
|
|
108
|
+
all(...params: unknown[]): unknown[];
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The child rows grouped for a batch: parent-key value (stringified) → child rows. */
|
|
112
|
+
export type RelationBatch = Map<string, Record<string, unknown>[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Run ONE {@link RelationOp} for a set of parent rows: dedup the parent keys, render + execute
|
|
115
|
+
* the batched child SELECT ONCE (real SQLite), and group the child rows by their target key.
|
|
116
|
+
* This is the single batch primitive BOTH the declarative-select and the lazy surface invoke —
|
|
117
|
+
* proving the "same relation op" invariant structurally (there is no second path).
|
|
118
|
+
*
|
|
119
|
+
* Returns `{ sql, keys, batch }`: the rendered SQL (so a caller can assert both surfaces
|
|
120
|
+
* produced the same text), the deduped keys bound, and the grouping. When there are no
|
|
121
|
+
* non-null parent keys the query is NOT issued (an empty batch, matching v1) — this is not a
|
|
122
|
+
* fallback default, it is the correct empty-set behavior (an IN over no keys selects nothing).
|
|
123
|
+
*/
|
|
124
|
+
export declare function runRelationOp(op: RelationOp, parents: readonly Record<string, unknown>[], db: RelationDriver): {
|
|
125
|
+
sql: string;
|
|
126
|
+
keys: unknown[];
|
|
127
|
+
batch: RelationBatch;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Distribute a resolved {@link RelationBatch} onto ONE parent per the relation cardinality:
|
|
131
|
+
* `hasMany` → the child list (`[]` when none); `belongsTo`/`hasOne` → the single child (or
|
|
132
|
+
* `null`). `null`/`undefined` is the correct absence value (an unresolved single relation),
|
|
133
|
+
* NOT an ad-hoc default — it is the declared cardinality's empty representation.
|
|
134
|
+
*/
|
|
135
|
+
export declare function distributeToParent(op: RelationOp, parent: Record<string, unknown>, batch: RelationBatch): Record<string, unknown>[] | Record<string, unknown> | null;
|
|
136
|
+
//# sourceMappingURL=relation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation.d.ts","sourceRoot":"","sources":["../../src/scp/relation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAoC,MAAM,MAAM,CAAC;AAGhF,gGAAgG;AAChG,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mBAAmB;IACnB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC;AAED,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU,CAYhE;AAuED,iGAAiG;AACjG,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;KAAE,CAAC;CAChE;AAED,uFAAuF;AACvF,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3C,EAAE,EAAE,cAAc,GACjB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,EAAE,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAmBxD;AAiBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,aAAa,GACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAK5D"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* litedbmodel v2 SCP — Relation ops (Read) + staged batch resolution (WS4, #24).
|
|
4
|
+
*
|
|
5
|
+
* Relations are NOT SQL JOINs by default (spec §5). They are the same
|
|
6
|
+
* staged-batch query-composition + object-assembly shape graphddb and v1 `LazyRelation`
|
|
7
|
+
* use: collect the parent key set of a result page, run ONE batched child SELECT
|
|
8
|
+
* (`fk IN (…)` / per-parent-limited `ROW_NUMBER()` window), then distribute the child rows
|
|
9
|
+
* back to their parents. This makes a relation load structurally bounded — one batched
|
|
10
|
+
* query per relation edge, NEVER one-per-parent (no N+1).
|
|
11
|
+
*
|
|
12
|
+
* ## The relation op is a pre-compiled artifact (spec §8)
|
|
13
|
+
*
|
|
14
|
+
* A {@link RelationOp} is compiled ONCE from a model {@link RelationDecl}, dialect-aware
|
|
15
|
+
* (SQLite for α), into the SAME {@link CompiledOperation} shape WS1 already emits for a
|
|
16
|
+
* Select — so it renders through the SAME normative {@link renderOperation} (fragment tree
|
|
17
|
+
* + IN-list `(?, ?, …)` expansion, spec §5). It carries NO functions and lands in the §8
|
|
18
|
+
* bundle (`SqlBundle.relations`) as pure JSON, so a thin per-language runtime (WS7) gets
|
|
19
|
+
* the relation batch SQL for free. The v1 `LazyRelation` batch-SQL assets (IN-list, the
|
|
20
|
+
* `ROW_NUMBER() OVER (PARTITION BY … )` per-parent limit) are reused here as the compiled
|
|
21
|
+
* content, not re-invented.
|
|
22
|
+
*
|
|
23
|
+
* ## ONE relation op, TWO surfaces (spec §5 / feasibility §9)
|
|
24
|
+
*
|
|
25
|
+
* Both read surfaces trigger the IDENTICAL compiled {@link RelationOp} via
|
|
26
|
+
* {@link runRelationOp} — there is exactly one batch-execution code path:
|
|
27
|
+
*
|
|
28
|
+
* - **Declarative select** (`with: { author: true }`): the planner resolves sibling
|
|
29
|
+
* relations eagerly, dedups the parent key set, prefetch-optimized (staged batch).
|
|
30
|
+
* - **Lazy** (`await post.author`): a prototype getter + a non-enumerable Symbol batch
|
|
31
|
+
* context (graphddb's `GRAPHDDB_KEY` technique) fires the SAME op at access time over
|
|
32
|
+
* the whole sibling set (batch, so still no N+1 — it just can't prefetch).
|
|
33
|
+
*
|
|
34
|
+
* `runRelationOp` renders `op.query` once for the deduped parent key set and returns the
|
|
35
|
+
* child rows grouped by parent key. Both surfaces call it; a test asserts they resolve via
|
|
36
|
+
* the same compiled op (same SQL text), not two parallel code paths.
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.RELATION_KEYS_HEAD = void 0;
|
|
40
|
+
exports.compileRelationOp = compileRelationOp;
|
|
41
|
+
exports.runRelationOp = runRelationOp;
|
|
42
|
+
exports.distributeToParent = distributeToParent;
|
|
43
|
+
const render_1 = require("./render");
|
|
44
|
+
const ir_1 = require("./ir");
|
|
45
|
+
/** The reserved input head the relation batch query binds its deduped key array to. */
|
|
46
|
+
exports.RELATION_KEYS_HEAD = '__keys';
|
|
47
|
+
/**
|
|
48
|
+
* Compile ONE {@link RelationDecl} into a {@link RelationOp} for SQLite (α dialect). The
|
|
49
|
+
* batch query selects the child rows whose {@link RelationDecl.targetKey} is in the deduped
|
|
50
|
+
* parent-key set. Reuses the existing IN-list fragment + `renderOperation` expansion (no new
|
|
51
|
+
* SQL machinery). A `hasMany` with `limit` compiles to the v1 `ROW_NUMBER()` window form so
|
|
52
|
+
* the per-parent cap holds in a single batched statement.
|
|
53
|
+
*/
|
|
54
|
+
function compileRelationOp(decl) {
|
|
55
|
+
if (decl.kind !== 'hasMany' && decl.limit !== undefined) {
|
|
56
|
+
throw new Error(`relation '${decl.name}': a per-parent 'limit' is only valid for hasMany (got ${decl.kind})`);
|
|
57
|
+
}
|
|
58
|
+
const query = decl.limit !== undefined ? compileLimitedBatch(decl) : compilePlainBatch(decl);
|
|
59
|
+
return {
|
|
60
|
+
name: decl.name,
|
|
61
|
+
kind: decl.kind,
|
|
62
|
+
parentKey: decl.parentKey,
|
|
63
|
+
targetKey: decl.targetKey,
|
|
64
|
+
query,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The IN-list membership slot binding the deduped key array (`{ref:[RELATION_KEYS_HEAD]}`). */
|
|
68
|
+
function keysRef() {
|
|
69
|
+
return { ref: [exports.RELATION_KEYS_HEAD] };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Plain batch (`belongsTo`/`hasOne`/unlimited `hasMany`):
|
|
73
|
+
* `SELECT <cols> FROM <target>{where}[ ORDER BY <o>]` with `{where}` = `targetKey IN (?)`.
|
|
74
|
+
* The `(?)` expands to one `?` per deduped key at render time (dynamic-expansion spec §5).
|
|
75
|
+
*/
|
|
76
|
+
function compilePlainBatch(decl) {
|
|
77
|
+
const cols = decl.select.join(', ');
|
|
78
|
+
const inFragment = {
|
|
79
|
+
always: true,
|
|
80
|
+
sql: `${decl.targetKey} IN (?)`,
|
|
81
|
+
params: [keysRef()],
|
|
82
|
+
expand: 0,
|
|
83
|
+
};
|
|
84
|
+
const where = { connector: 'AND', fragments: [inFragment] };
|
|
85
|
+
let sql = `SELECT ${cols} FROM ${decl.targetTable}${ir_1.WHERE_SLOT}`;
|
|
86
|
+
if (decl.order !== undefined)
|
|
87
|
+
sql += ` ORDER BY ${decl.order}`;
|
|
88
|
+
return { component: 'Select', sql, where, params: [], assembly: { shape: 'items' } };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Per-parent-limited `hasMany` batch — v1's SQLite `ROW_NUMBER()` window form:
|
|
92
|
+
*
|
|
93
|
+
* WITH ranked AS (
|
|
94
|
+
* SELECT <cols>, ROW_NUMBER() OVER (PARTITION BY <targetKey> ORDER BY <order>) AS _rn
|
|
95
|
+
* FROM <target>{where} -- {where} = targetKey IN (?)
|
|
96
|
+
* )
|
|
97
|
+
* SELECT <cols> FROM ranked WHERE _rn <= <limit>
|
|
98
|
+
*
|
|
99
|
+
* The `{where}` splice keeps the IN-list expansion (spec §5). `_rn` is a synthetic window
|
|
100
|
+
* column dropped from the final projection, so the child typed-object own props stay exactly
|
|
101
|
+
* `select`. `limit` is inlined as a literal integer (a compiled per-parent cap from the decl,
|
|
102
|
+
* NOT a runtime-bound value — the decl is the SSoT), mirroring v1's `_rn <= ${limit}`.
|
|
103
|
+
*/
|
|
104
|
+
function compileLimitedBatch(decl) {
|
|
105
|
+
const cols = decl.select.join(', ');
|
|
106
|
+
const limit = decl.limit;
|
|
107
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
108
|
+
throw new Error(`relation '${decl.name}': per-parent limit must be a non-negative integer (got ${String(limit)})`);
|
|
109
|
+
}
|
|
110
|
+
// A per-parent limit REQUIRES a deterministic ORDER BY (the window decides WHICH `limit`
|
|
111
|
+
// rows each parent keeps). Absent order = fail-closed authoring error, NOT a silent default
|
|
112
|
+
// — the relation declaration is the SSoT for the per-parent ordering.
|
|
113
|
+
if (decl.order === undefined) {
|
|
114
|
+
throw new Error(`relation '${decl.name}': a per-parent 'limit' requires an explicit 'order' (the ROW_NUMBER() window needs a deterministic ordering to decide which ${limit} rows each parent keeps)`);
|
|
115
|
+
}
|
|
116
|
+
const order = decl.order;
|
|
117
|
+
const inFragment = {
|
|
118
|
+
always: true,
|
|
119
|
+
sql: `${decl.targetKey} IN (?)`,
|
|
120
|
+
params: [keysRef()],
|
|
121
|
+
expand: 0,
|
|
122
|
+
};
|
|
123
|
+
const where = { connector: 'AND', fragments: [inFragment] };
|
|
124
|
+
const sql = `WITH ranked AS (SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY ${decl.targetKey} ORDER BY ${order}) AS _rn ` +
|
|
125
|
+
`FROM ${decl.targetTable}${ir_1.WHERE_SLOT}) ` +
|
|
126
|
+
`SELECT ${cols} FROM ranked WHERE _rn <= ${limit}`;
|
|
127
|
+
return { component: 'Select', sql, where, params: [], assembly: { shape: 'items' } };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run ONE {@link RelationOp} for a set of parent rows: dedup the parent keys, render + execute
|
|
131
|
+
* the batched child SELECT ONCE (real SQLite), and group the child rows by their target key.
|
|
132
|
+
* This is the single batch primitive BOTH the declarative-select and the lazy surface invoke —
|
|
133
|
+
* proving the "same relation op" invariant structurally (there is no second path).
|
|
134
|
+
*
|
|
135
|
+
* Returns `{ sql, keys, batch }`: the rendered SQL (so a caller can assert both surfaces
|
|
136
|
+
* produced the same text), the deduped keys bound, and the grouping. When there are no
|
|
137
|
+
* non-null parent keys the query is NOT issued (an empty batch, matching v1) — this is not a
|
|
138
|
+
* fallback default, it is the correct empty-set behavior (an IN over no keys selects nothing).
|
|
139
|
+
*/
|
|
140
|
+
function runRelationOp(op, parents, db) {
|
|
141
|
+
const keys = dedupeKeys(parents, op.parentKey);
|
|
142
|
+
const batch = new Map();
|
|
143
|
+
if (keys.length === 0) {
|
|
144
|
+
// No parent keys → no batched query issued, empty grouping. Still render the SQL (for
|
|
145
|
+
// the same-op assertion) against the empty key set so the IN-list `1 = 0` degeneration
|
|
146
|
+
// is observable, but do not touch the driver.
|
|
147
|
+
const rendered = (0, render_1.renderOperation)(op.query, { [exports.RELATION_KEYS_HEAD]: [] });
|
|
148
|
+
return { sql: rendered.sql, keys, batch };
|
|
149
|
+
}
|
|
150
|
+
const rendered = (0, render_1.renderOperation)(op.query, { [exports.RELATION_KEYS_HEAD]: keys });
|
|
151
|
+
const rows = db.prepare(rendered.sql).all(...rendered.params);
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
const k = String(row[op.targetKey]);
|
|
154
|
+
const list = batch.get(k);
|
|
155
|
+
if (list === undefined)
|
|
156
|
+
batch.set(k, [row]);
|
|
157
|
+
else
|
|
158
|
+
list.push(row);
|
|
159
|
+
}
|
|
160
|
+
return { sql: rendered.sql, keys, batch };
|
|
161
|
+
}
|
|
162
|
+
/** The deduped, non-null parent-key values (insertion order preserved — deterministic). */
|
|
163
|
+
function dedupeKeys(parents, parentKey) {
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const p of parents) {
|
|
167
|
+
const v = p[parentKey];
|
|
168
|
+
if (v === undefined || v === null)
|
|
169
|
+
continue;
|
|
170
|
+
const s = String(v);
|
|
171
|
+
if (seen.has(s))
|
|
172
|
+
continue;
|
|
173
|
+
seen.add(s);
|
|
174
|
+
out.push(v);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Distribute a resolved {@link RelationBatch} onto ONE parent per the relation cardinality:
|
|
180
|
+
* `hasMany` → the child list (`[]` when none); `belongsTo`/`hasOne` → the single child (or
|
|
181
|
+
* `null`). `null`/`undefined` is the correct absence value (an unresolved single relation),
|
|
182
|
+
* NOT an ad-hoc default — it is the declared cardinality's empty representation.
|
|
183
|
+
*/
|
|
184
|
+
function distributeToParent(op, parent, batch) {
|
|
185
|
+
const key = parent[op.parentKey];
|
|
186
|
+
const rows = key === undefined || key === null ? undefined : batch.get(String(key));
|
|
187
|
+
if (op.kind === 'hasMany')
|
|
188
|
+
return rows ?? [];
|
|
189
|
+
return rows !== undefined && rows.length > 0 ? rows[0] : null;
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=relation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation.js","sourceRoot":"","sources":["../../src/scp/relation.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;;;AA4EH,8CAYC;AA0FD,sCAuBC;AAuBD,gDASC;AAvOD,qCAA6D;AAE7D,6BAAkC;AA8DlC,uFAAuF;AAC1E,QAAA,kBAAkB,GAAG,QAAQ,CAAC;AAE3C;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,IAAkB;IAClD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,aAAa,IAAI,CAAC,IAAI,0DAA0D,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAChH,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC7F,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,KAAK;KACN,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,SAAS,OAAO;IACd,OAAO,EAAE,GAAG,EAAE,CAAC,0BAAkB,CAAC,EAAc,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAkB;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,UAAU,GAAa;QAC3B,MAAM,EAAE,IAAI;QACZ,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,SAAS;QAC/B,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,EAAE,CAAC;KACV,CAAC;IACF,MAAM,KAAK,GAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;IAC1E,IAAI,GAAG,GAAG,UAAU,IAAI,SAAS,IAAI,CAAC,WAAW,GAAG,eAAU,EAAE,CAAC;IACjE,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,GAAG,IAAI,aAAa,IAAI,CAAC,KAAK,EAAE,CAAC;IAC/D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;AACvF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,mBAAmB,CAAC,IAAkB;IAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAM,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,aAAa,IAAI,CAAC,IAAI,2DAA2D,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrH,CAAC;IACD,yFAAyF;IACzF,4FAA4F;IAC5F,sEAAsE;IACtE,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,aAAa,IAAI,CAAC,IAAI,gIAAgI,KAAK,0BAA0B,CACtL,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,UAAU,GAAa;QAC3B,MAAM,EAAE,IAAI;QACZ,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,SAAS;QAC/B,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,EAAE,CAAC;KACV,CAAC;IACF,MAAM,KAAK,GAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;IAC1E,MAAM,GAAG,GACP,0BAA0B,IAAI,qCAAqC,IAAI,CAAC,SAAS,aAAa,KAAK,WAAW;QAC9G,QAAQ,IAAI,CAAC,WAAW,GAAG,eAAU,IAAI;QACzC,UAAU,IAAI,6BAA6B,KAAK,EAAE,CAAC;IACrD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;AACvF,CAAC;AAYD;;;;;;;;;;GAUG;AACH,SAAgB,aAAa,CAC3B,EAAc,EACd,OAA2C,EAC3C,EAAkB;IAElB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAkB,IAAI,GAAG,EAAE,CAAC;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,sFAAsF;QACtF,uFAAuF;QACvF,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,IAAA,wBAAe,EAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,0BAAkB,CAAC,EAAE,EAAE,EAAW,CAAC,CAAC;QAClF,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC5C,CAAC;IACD,MAAM,QAAQ,GAAgB,IAAA,wBAAe,EAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,0BAAkB,CAAC,EAAE,IAAI,EAAW,CAAC,CAAC;IACjG,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAA8B,CAAC;IAC3F,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;;YACvC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC5C,CAAC;AAED,2FAA2F;AAC3F,SAAS,UAAU,CAAC,OAA2C,EAAE,SAAiB;IAChF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS;QAC5C,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAS;QAC1B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACZ,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAChC,EAAc,EACd,MAA+B,EAC/B,KAAoB;IAEpB,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACpF,IAAI,EAAE,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,IAAI,EAAE,CAAC;IAC7C,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* litedbmodel v2 SCP — fragment-tree render + param assembly (WS1, #21).
|
|
3
|
+
*
|
|
4
|
+
* The NORMATIVE reference implementation of `docs/proposal/sql-dynamic-expansion-spec.md`.
|
|
5
|
+
* Given a compiled operation and a bound input scope, it deterministically produces the
|
|
6
|
+
* final SQLite SQL text (`?` placeholders) and the flat params array. Every multi-language
|
|
7
|
+
* runtime (WS3) must reproduce this byte-for-byte; this TS implementation is the golden
|
|
8
|
+
* reference the conformance harness pins.
|
|
9
|
+
*
|
|
10
|
+
* The three moving parts (dynamic-expansion spec):
|
|
11
|
+
* §2 SKIP → fragment existence: a fragment with `when` is present iff `when` evaluates
|
|
12
|
+
* to a present (non-null / true) binding; an absent fragment contributes NO SQL and
|
|
13
|
+
* NO params.
|
|
14
|
+
* §3 empty-WHERE degeneration: if no fragment is present the whole `{where}` splice
|
|
15
|
+
* (including the ` WHERE ` keyword) collapses to the empty string.
|
|
16
|
+
* §4 AND/OR structure + parenthesization: a nested tree renders `(… <connector> …)`.
|
|
17
|
+
* §5 IN-list array expansion: a fragment slot marked `expand` turns its `(?)` into
|
|
18
|
+
* `(?, ?, …)` for each element of the bound array (0 elements → the `1 = 0`
|
|
19
|
+
* always-false degeneration, byte-identical to litedbmodel v1's empty-IN handling).
|
|
20
|
+
*/
|
|
21
|
+
import { type Scope, type Value } from 'behavior-contracts';
|
|
22
|
+
import type { CompiledOperation } from './ir';
|
|
23
|
+
import { type Dialect } from './dialect';
|
|
24
|
+
/** The result of rendering: final SQL text + flat params (1:1 with `?`). */
|
|
25
|
+
export interface RenderedSql {
|
|
26
|
+
sql: string;
|
|
27
|
+
params: Value[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Render a compiled operation to final SQLite SQL + params for a bound input scope.
|
|
31
|
+
*
|
|
32
|
+
* Param order (matches SQL text order, dynamic-expansion spec §6): the compiled `sql`
|
|
33
|
+
* splices the WHERE tree at `{where}`; static params before the marker are emitted first,
|
|
34
|
+
* then the fragment params in tree order, then static params after the marker. WS1's CRUD
|
|
35
|
+
* shapes place all pre-WHERE statics (Insert values, Update SET) before `{where}` and all
|
|
36
|
+
* post-WHERE statics (LIMIT / OFFSET) after — so a single left-to-right walk of the
|
|
37
|
+
* spliced SQL yields the canonical `?` order.
|
|
38
|
+
*/
|
|
39
|
+
export declare function renderOperation(op: CompiledOperation, input: Scope, dialect?: Dialect): RenderedSql;
|
|
40
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/scp/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAsB,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAA0B,MAAM,MAAM,CAAC;AAEtE,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAEjD,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAqFD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAE,OAAgB,GAAG,WAAW,CA6B3G"}
|