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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/dist/DBModel.d.ts.map +1 -1
  3. package/dist/DBModel.js +6 -0
  4. package/dist/DBModel.js.map +1 -1
  5. package/dist/scp/authoring-sql.d.ts +56 -0
  6. package/dist/scp/authoring-sql.d.ts.map +1 -0
  7. package/dist/scp/authoring-sql.js +83 -0
  8. package/dist/scp/authoring-sql.js.map +1 -0
  9. package/dist/scp/authoring.d.ts +155 -0
  10. package/dist/scp/authoring.d.ts.map +1 -0
  11. package/dist/scp/authoring.js +162 -0
  12. package/dist/scp/authoring.js.map +1 -0
  13. package/dist/scp/bridge.d.ts +64 -0
  14. package/dist/scp/bridge.d.ts.map +1 -0
  15. package/dist/scp/bridge.js +329 -0
  16. package/dist/scp/bridge.js.map +1 -0
  17. package/dist/scp/catalog.d.ts +91 -0
  18. package/dist/scp/catalog.d.ts.map +1 -0
  19. package/dist/scp/catalog.js +203 -0
  20. package/dist/scp/catalog.js.map +1 -0
  21. package/dist/scp/codegen.d.ts +131 -0
  22. package/dist/scp/codegen.d.ts.map +1 -0
  23. package/dist/scp/codegen.js +137 -0
  24. package/dist/scp/codegen.js.map +1 -0
  25. package/dist/scp/compile-dialect.d.ts +39 -0
  26. package/dist/scp/compile-dialect.d.ts.map +1 -0
  27. package/dist/scp/compile-dialect.js +62 -0
  28. package/dist/scp/compile-dialect.js.map +1 -0
  29. package/dist/scp/compile-sqlite.d.ts +114 -0
  30. package/dist/scp/compile-sqlite.d.ts.map +1 -0
  31. package/dist/scp/compile-sqlite.js +149 -0
  32. package/dist/scp/compile-sqlite.js.map +1 -0
  33. package/dist/scp/dialect.d.ts +141 -0
  34. package/dist/scp/dialect.d.ts.map +1 -0
  35. package/dist/scp/dialect.js +194 -0
  36. package/dist/scp/dialect.js.map +1 -0
  37. package/dist/scp/errors.d.ts +36 -0
  38. package/dist/scp/errors.d.ts.map +1 -0
  39. package/dist/scp/errors.js +72 -0
  40. package/dist/scp/errors.js.map +1 -0
  41. package/dist/scp/guard.d.ts +47 -0
  42. package/dist/scp/guard.d.ts.map +1 -0
  43. package/dist/scp/guard.js +137 -0
  44. package/dist/scp/guard.js.map +1 -0
  45. package/dist/scp/index.cjs +4744 -0
  46. package/dist/scp/index.d.ts +49 -0
  47. package/dist/scp/index.d.ts.map +1 -0
  48. package/dist/scp/index.js +164 -0
  49. package/dist/scp/index.js.map +1 -0
  50. package/dist/scp/index.mjs +2000 -0
  51. package/dist/scp/ir.d.ts +92 -0
  52. package/dist/scp/ir.d.ts.map +1 -0
  53. package/dist/scp/ir.js +28 -0
  54. package/dist/scp/ir.js.map +1 -0
  55. package/dist/scp/relation.d.ts +136 -0
  56. package/dist/scp/relation.d.ts.map +1 -0
  57. package/dist/scp/relation.js +191 -0
  58. package/dist/scp/relation.js.map +1 -0
  59. package/dist/scp/render.d.ts +40 -0
  60. package/dist/scp/render.d.ts.map +1 -0
  61. package/dist/scp/render.js +170 -0
  62. package/dist/scp/render.js.map +1 -0
  63. package/dist/scp/runtime.d.ts +295 -0
  64. package/dist/scp/runtime.d.ts.map +1 -0
  65. package/dist/scp/runtime.js +665 -0
  66. package/dist/scp/runtime.js.map +1 -0
  67. package/dist/scp/typed-object.d.ts +88 -0
  68. package/dist/scp/typed-object.d.ts.map +1 -0
  69. package/dist/scp/typed-object.js +163 -0
  70. package/dist/scp/typed-object.js.map +1 -0
  71. package/dist/scp/write-plan.d.ts +161 -0
  72. package/dist/scp/write-plan.d.ts.map +1 -0
  73. package/dist/scp/write-plan.js +528 -0
  74. package/dist/scp/write-plan.js.map +1 -0
  75. package/dist/scp/write-runtime.d.ts +80 -0
  76. package/dist/scp/write-runtime.d.ts.map +1 -0
  77. package/dist/scp/write-runtime.js +167 -0
  78. package/dist/scp/write-runtime.js.map +1 -0
  79. package/dist/scp/writes.d.ts +251 -0
  80. package/dist/scp/writes.d.ts.map +1 -0
  81. package/dist/scp/writes.js +176 -0
  82. package/dist/scp/writes.js.map +1 -0
  83. package/docs/proposal/contracts-architecture-feasibility.md +328 -0
  84. package/docs/proposal/contracts-architecture.md +579 -0
  85. package/docs/proposal/litedbmodel-v2-scp-architecture.md +382 -0
  86. package/docs/proposal/sql-dynamic-expansion-spec.md +175 -0
  87. package/package.json +27 -5
  88. package/docs/README_REVIEW.md +0 -23
@@ -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"}