@jarenjs/linq 0.49.2 → 0.56.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 (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
@@ -0,0 +1,1636 @@
1
+ # The Jaren query pen
2
+
3
+ > the chain, `.` — query documents (`jaren-query`) and the provider
4
+ > seam. **Read it when** you are querying data, or implementing a
5
+ > provider that answers a query document
6
+
7
+ Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
8
+ interpreted as described in RFC 2119. This document is a **guide** — read
9
+ it in order and you can write the format — whose normative section is
10
+ [§4 The mapping table](#4-the-mapping-table); the rules every pen keeps,
11
+ the shared refusal table, the index of the other pens and every pen's
12
+ mapping table collected in one place are the normative reference,
13
+ [LINQ-FORMAT.md](LINQ-FORMAT.md).
14
+
15
+ ## 1. Scope
16
+
17
+ You have data — an array in memory, a store's rows, a stream — and a
18
+ question to ask it, and you would like to write that question the way you
19
+ write code: filter, order, project, join, group. What you actually need
20
+ to hand the engine is a JSON document. So you either write the document,
21
+ in a grammar your editor knows nothing about, or you write JavaScript
22
+ and lose the ability to send it anywhere. This is the third option: a
23
+ method chain that RECORDS what you wrote and hands you the document.
24
+
25
+ `@jarenjs/linq` is a fluent front-end to the Jaren JSON Query language
26
+ ([QUERY-FORMAT.md](../../json/docs/QUERY-FORMAT.md)): a C#-familiar
27
+ method chain whose expressions are CAPTURED as plain query documents
28
+ and executed deferred — over any iterable in memory, or by any
29
+ **provider** exposing `execute(document, options)` (§8). The builder
30
+ emits the query language and nothing else; there is no second grammar,
31
+ no private protocol, and no `Function.prototype.toString` anywhere.
32
+
33
+ **Scope.** This document is normative for the CHAIN — the `.` entry, the
34
+ query documents it emits, and the provider seam. The package's other
35
+ subpaths are **pens**: the same idea applied to the suite's other
36
+ formats, each writing exactly the published document its engine already
37
+ takes. They have one document each, indexed by the binder,
38
+ [LINQ-FORMAT.md](LINQ-FORMAT.md), which covers the rules every pen keeps
39
+ (§1) and the `JL01xx` refusal table this document's §9 mirrors;
40
+ `@jarenjs/linq/db` — the store's typed front door and the package's one
41
+ runtime edge — is [DB-CLIENT.md](DB-CLIENT.md).
42
+
43
+ What this package is NOT: it is not a storage engine — the store, its
44
+ tables, its planner, its unit of work and its migrations are
45
+ `@jarenjs/db`'s, and the client subpath is that store's front door
46
+ rather than a second engine; it does not evaluate JavaScript callbacks
47
+ per element (callbacks run ONCE, at build time, against recording
48
+ proxies); it infers nothing from a JSON literal (a document stays a
49
+ document — `from(json)` is `unknown` until the caller says otherwise,
50
+ and the pens are the only inference route); and it promises nothing the
51
+ query grammar cannot express — §4 records every such gap as
52
+ `unsupported`, by name.
53
+
54
+ **Why this one is the longest.** The other ten documents target 600–1000
55
+ lines; this one is half again as long, and it stays one document. Its
56
+ §1–§12 are cited by section number from more than seventy places, so the
57
+ numbering is fixed and the sections cannot be split or moved. It also
58
+ covers three surfaces no pen has — the chain, the asynchronous surface
59
+ and the provider contract — each of which a different reader arrives for.
60
+ A reader who wants only one of the three should use the section list: §1
61
+ to §7 are the chain, §8 and §12 the provider seam, §10 and §11 the
62
+ asynchronous surface, and §13 to §17 the same seven sections every pen
63
+ document carries.
64
+
65
+ **The running example.** §13's eight fences are one question asked eight
66
+ ways over one small blog's data — the users, the posts they wrote and the
67
+ orders placed against them — and §15 reads the types back off the same
68
+ chains. Two of the eight need a PROVIDER rather than an array, and they
69
+ are that same blog seen as a store's entity sets.
70
+
71
+ **How to read this document.** The ten pen documents this one is indexed
72
+ beside share a fixed seven-section shape, and a reader who has learned
73
+ one of them arrives here expecting it. This document keeps its own twelve
74
+ sections instead — 72 citations across the repository point at them by
75
+ number, and moving one would break them silently — so the same seven
76
+ questions are answered where they already were, and the five that had no
77
+ home were appended rather than inserted:
78
+
79
+ | What you came for | Read |
80
+ |---|---|
81
+ | what it writes, and the one-import example | §1 Scope, §2 The surface |
82
+ | the mapping table: every operator and what it emits | §4 |
83
+ | worked examples, executed by the docs gate | §13 |
84
+ | refusals: the spelling that trips each code, and the one that works | §14 (§9 is the normative code table) |
85
+ | the types | §15 |
86
+ | what it cannot spell | §16 |
87
+ | cost | §17 |
88
+
89
+ The eight sections the table does not name have no counterpart in a pen
90
+ document at all, because they are the chain's own subject: §3 expression
91
+ capture, §5 deferred execution and re-enumeration, §6 terminal
92
+ semantics, §7 parameters, §8 the provider contract, and §§10–12 the
93
+ asynchronous surface, its concurrency boundary and its source adapters.
94
+ A reader following the suite from the binder,
95
+ [LINQ-FORMAT.md](LINQ-FORMAT.md), can skip to the row they need; a
96
+ reader learning the chain should read §3, §5 and §6 in order first,
97
+ because everything else assumes them.
98
+
99
+ ## 2. The surface
100
+
101
+ ```js
102
+ import { from } from '@jarenjs/linq';
103
+
104
+ const adults = from(users)
105
+ .where((u) => u.age.gt(21))
106
+ .orderBy((u) => u.name)
107
+ .select((u) => ({ id: u.id, name: u.name }));
108
+
109
+ adults.toArray(); // executes in memory
110
+ adults.toDocument(); // the SAME query, as one JSON document:
111
+ // { "$for": { "it": ["$[*]"] },
112
+ // "$where": { "$gt": ["$it.age", 21] },
113
+ // "$orderby": { "$key": "$it.name" },
114
+ // "$return": { "id": "$it.id", "name": "$it.name" } }
115
+ ```
116
+
117
+ (The source is bound through an array constructor, `["$[*]"]`, so that
118
+ an array-valued row stays one item — §5 says why; a provider's own root
119
+ is bound bare.)
120
+
121
+ - `from(source, options?)` — `source` is any iterable (arrays,
122
+ strings, generators, Sets…) or a provider (§8); anything else is
123
+ `JL0001` at `from()` time, never at enumeration time. A provider's
124
+ items are bound through ITS root (`root` — `'$.Post[*]'` for a store's
125
+ entity set; the emitted `$for` iterates that root, bare); a provider
126
+ that serves several roots and none of its own (a store with entities,
127
+ `roots`) is `JL0007` at `from()` time, naming the roots to chain over;
128
+ a provider carrying a relation table (`relations` — a store's entity
129
+ set does) lets a relation member NAVIGATE (§3): `p.author.email`
130
+ lowers to the correlated phrase the engine and the store run, and the
131
+ document never carries the relation's name.
132
+ `options.compileTypeTest` enables the schema operators behind
133
+ `ofType`/`cast` (§4); absent, those two are `JL0003` with the fix in
134
+ the message.
135
+ - `fromDocument(source, document, options?)` — attach a hand-written
136
+ or stored query document; its result is the item sequence and every
137
+ operator chains over it. Exactly the `{ "$query": "0.1", "$expr": … }`
138
+ envelope is unwrapped; any other envelope — an unknown version, a
139
+ stray member, a missing half — is compiled first so the ENGINE's
140
+ verdict (`JQ0006`, `JQ0001`, `JQ0003`) is what surfaces, never a
141
+ silent run under this version.
142
+ - Every operator returns a NEW immutable sequence (§5); terminal
143
+ operations execute (§6).
144
+
145
+ ## 3. Expression capture
146
+
147
+ A predicate or projection callback receives a **recording proxy** per
148
+ binding (and the parameters proxy last, §7). Member access records a
149
+ path segment; a method call records an operator; the callback's return
150
+ value becomes the expression:
151
+
152
+ - `u.a.b` records the path `$it.a.b`; `u.list.at(0)` records
153
+ `$it.list[0]` (negative integers count from the end);
154
+ `u.list.all()` records `$it.list[*]`; a key that is not an
155
+ identifier — or one that collides with a method name — goes through
156
+ `u.get('odd key')`.
157
+ - Method names SHADOW member access: `u.eq` is the operator, never
158
+ the member. `u.get('eq')` reaches the member.
159
+ - Returned object literals become constructors: plain-keyed objects
160
+ are Rule 1 map constructors, arrays are Rule 3 array constructors,
161
+ and a data object with `$`-prefixed keys embeds through `$map`.
162
+ Literal strings embed with the `$$` escape when they start with
163
+ `$`; plain data trees embed as `$const`.
164
+ - A proxy belongs to exactly ONE capture. Storing one and replaying
165
+ it into a later operator is `JL0002` — the emitted document would
166
+ silently reference the wrong binding, so the build fails instead.
167
+ Captures NEST: a chain built and run inside a callback is ordinary
168
+ (`select((u) => ({ id: u.id, n: from(rows).count() }))`); an
169
+ enclosing capture's proxy used inside the nested one is `JL0002` by
170
+ name — the inner document rebinds `$it`, so a correlated subquery
171
+ cannot be spelled this way. (`===` between proxies is untrappable and
172
+ therefore undetectable; do not compare proxies.)
173
+ - **JavaScript's own operators are not trappable, and they do not
174
+ fail loudly.** A proxy is an object, so `&&`, `||`, `!`, `?:`, `in`,
175
+ `typeof`, `Object.keys` and `===` evaluate against the PROXY and
176
+ yield a silently wrong document: `u.age.gt(21) && u.name.eq('x')`
177
+ captures only the right operand, `!u.deleted` is the constant
178
+ `false`, `u.deleted ? 'a' : 'b'` is always `'a'`. Use the expression
179
+ surface — `.and()`, `.or()`, `.not()` — for logic. Arithmetic and
180
+ comparison operators (`u.age > 21`, `u.age + 1`, `${u.name}`) throw a
181
+ plain `TypeError` (a proxy cannot be converted to a primitive): loud,
182
+ but not coded.
183
+ - The item binding is always named `it` in the emitted document
184
+ (nested phrases shadow it legally), so captured expressions read
185
+ `$it.…` at every depth and the document stays hand-readable.
186
+ - **A relation name hops.** When the items are the rows of an entity
187
+ whose provider carries a relation table (§8: `relations`, a store's
188
+ entity set), a member access naming a declared relation records a
189
+ HOP rather than a path segment — `p.author` is the related row,
190
+ `u.posts` the array of related rows — and is lowered, at capture, to
191
+ the correlated phrase §4's "relation navigation" rows spell; the
192
+ emitted document carries the phrase, never the member's name. The
193
+ hop's target is the target entity's row, with ITS relation table, so
194
+ hops chain (`p.author.posts`); a trailing path continues on the target
195
+ (`p.author.email`); `all()` on a to-many hop fans the related rows,
196
+ and the aggregates and `exists()`/`isEmpty()` range over them. A
197
+ relation name reached through `get()` hops too (the escape for a
198
+ relation that collides with a method name). The rows stop being rows
199
+ at a projection — after `select`, `selectMany`, `groupBy`, `join`,
200
+ `groupJoin`, `aggregate` (and a `mapAsync`) a relation name is an
201
+ ordinary member again — and a `fromDocument` chain never hops: there
202
+ the document decides what the items are. A hop that cannot lower is
203
+ `JL0105` at build time (a many-to-many member: its join table is not a
204
+ queryable root in this version; a composite key); a member read off
205
+ the to-many ARRAY before `all()` is `JL0005` with the fix named, where
206
+ the same read off a stored array would answer nothing.
207
+
208
+ ## 4. The mapping table
209
+
210
+ Status vocabulary: **native** (emits the named construct), **emulated**
211
+ (emits a composition with identical semantics), **unsupported** (throws
212
+ a coded error naming the reason — an honest row beats a silently wrong
213
+ emission). The *typing* column is the intended TypeScript signature
214
+ shipped by the typed surface order; an operator whose signature cannot
215
+ be written is an operator whose runtime shape is wrong, so the column
216
+ is part of THIS design.
217
+
218
+ | C# / LINQ | Emission | Status | Typing (element `T`) |
219
+ |---|---|---|---|
220
+ | `Where` | FLWOR `$where` | native | `(e: Expr<T>) => Expr<boolean>` → `Seq<T>` |
221
+ | `Select` | `$return` constructor | native | `(e: Expr<T>) => Expr<R>` → `Seq<R>` |
222
+ | `SelectMany` | `$return` of a `$for` phrase over the projection — the projected value is iterated ONE level (an array member's elements, a constructed array's members; a scalar is itself), and the FLWOR `$return` concatenates per tuple. Emitted as `{ "$for": { "it": <projection> }, "$return": "$it" }` (the nested phrase rebinds `it` legally) | native | `(e: Expr<T>) => Expr<R[]>` → `Seq<R>` |
223
+ | `OrderBy` / `OrderByDescending` | `$orderby` key spec (`$dir`; `$empty`/`$collation` via `options`) | native | `(e: Expr<T>) => Expr<K>` → `Seq<T>` |
224
+ | `ThenBy` / `ThenByDescending` | appended `$orderby` spec; must directly follow `orderBy*` (`JL0005`) | native | as `OrderBy` |
225
+ | `GroupBy` | `$groupby`; downstream items are `{ key, items }` | native | `(e: Expr<T>) => Expr<K>` → `Seq<{key: K, items: T[]}>` |
226
+ | `Join` | nested `$for` + `$where` `$eq` — the engine rewrites this shape to a HASH JOIN (compile-time, QUERY-FORMAT §6), which is why it is fast **when both keys are plain member paths** (`o => o.pid`, `i => i.id`); a key with an operator in it (`o => o.name.lower()`, `o => o.p.add(0)`) is not a probe key and the join runs as a nested loop. Both sides MUST derive from the same source, or from two providers sharing one `scope` (§8 — two entity sets of one store are two roots of ONE multi-entity input, and the store answers the equijoin in one statement); anything else is `JL0005`: a query document reads one input. On the async surface the join exists only over a provider, pushed whole (§10). The inner side's declared parameters ride along (§7) | native | `(inner: Seq<U>, ok, ik, (o: Expr<T>, i: Expr<U>) => Expr<R>)` → `Seq<R>` |
227
+ | `GroupJoin` | the matching group bound as an ARRAY value — `$let: { g: [ <correlated inner phrase> ] }` — so the result selector can index it (`g.at(0)`), fan it (`g.all()`), place it in a member (`{ matches: g }`) and aggregate over its members (`(u, g) => ({ n: g.count() })` counts the matches, `g.exists()` is whether there are any); same-source rule and parameter merge as `Join` | emulated | `(inner: Seq<U>, ok, ik, (o: Expr<T>, g: ArrayExpr<U> & AggregatableExpr) => Expr<R>)` → `Seq<R>` |
228
+ | `Skip` / `Take` | `$subsequence` | native | `(n: number)` → `Seq<T>` |
229
+ | `Distinct` | `$distinct` (deep structural equality — the grouping relation) | native | `()` → `Seq<T>` |
230
+ | `Reverse` | `$reverse` | native | `()` → `Seq<T>` |
231
+ | `Count` / `Sum` / `Average` / `Min` / `Max` | §8.8 aggregates (`Average` → `$avg`) | native | `count(): number`; `sum(): number`; `average/min/max(): number` (throw `JL2001` on empty; `min`/`max` follow the operand family) |
232
+ | `Any()` | `$exists` | native | `(): boolean` |
233
+ | `Any(pred)` / `All(pred)` | `$some` / `$every` quantifier phrase | native | `(pred): boolean` (`all` vacuously true on empty) |
234
+ | `Aggregate(seed, fn)` | `$fold` — the accumulator clause; the result is a sequence of exactly ONE accumulated value (`.first()` reads it) | native | `(seed: A, (acc: Expr<A>, e: Expr<T>) => Expr<A>)` → `Seq<A>` |
235
+ | `Aggregate(fn)` (unseeded) | — JSON cannot spell "the implicit first element" as a lambda seed | unsupported (`JL0006`) | — |
236
+ | `First` / `FirstOrDefault` | `[ $subsequence [expr, 0, 1] ]` window | native | `(): T` (`JL2001` on empty) / `(d?): T \| D` |
237
+ | `Single` / `SingleOrDefault` | `[ $subsequence [expr, 0, 2] ]` window | native | `(): T` (`JL2001`/`JL2002`) / `(d?): T \| D` (`JL2002` on 2+) |
238
+ | `Last` / `LastOrDefault` | `[ $subsequence [$reverse expr, 0, 1] ]` | native | as `First` |
239
+ | `ElementAt` / `ElementAtOrDefault` | `[ $subsequence [expr, i, 1] ]` | native | `(i): T` (`JL2003` out of range) / `(i, d?)` |
240
+ | `Concat` | `$seq` (a constant array's elements join the stream) | native | `(other: Seq<T> \| T[])` → `Seq<T>` |
241
+ | `DefaultIfEmpty` | `$default` | native | `(fallback?: T)` → `Seq<T>` |
242
+ | `OfType<S>` | `$valid` filter with a JSON Schema literal | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
243
+ | `Cast<S>` | `$assert` per item | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
244
+ | `Zip` | — no positional co-iteration in the grammar | unsupported (`JL0006`) | — |
245
+ | expression methods | `eq ne lt le gt ge` → `$eq…$ge`; `and or not`; `add sub mul div idiv mod neg`; `startsWith endsWith contains matches upper lower length concat substring replace` → §8.7; `count sum avg min max` → §8.8 (aggregates as expressions, e.g. over a group); `exists isEmpty`; `at all get` | native | on `Expr<…>`, per the typed-surface order |
246
+ | date family (§8.13) | the whole family, one method per operator. Components `year month day hours minutes seconds offset week weekYear quarter weekday`; instants `epoch datetime`; predicates `isDate isTime isDatetime isDuration`; arithmetic `startOf(unit) endOf(unit) dateAdd(duration \| amount, unit?) dateSub(…) dateDiff(to, unit) dateFormat(pattern)`. `dateAdd`/`dateSub`/`dateFormat` carry the prefix because `add`, `sub` and `format` are taken or ambiguous on this surface — the same reason §8.14 spells `geoArea`. There is no `now()`: §8.13 has no clock, and a fluent surface does not get to add one | native | on `DateTimeExpr` (the `DateTime` brand) and on `UnknownExpr` |
247
+ | series family (§8.16) | `overlaps(other)` → `$overlaps`; `timeBucket(every, origin?, context?)` → `$time-bucket`; `resample(spec)`, `rolling(spec)` and `asof(right, spec?)` → the three sequence operators. A **spec is a literal** and is embedded verbatim — it is read once when the query compiles, so a spec built from the row is `JL0005`, and every rule about what it may *say* stays in the compiler (`JQ0003`). Note that a member literally named `at` is read with `get('at')`: `at(index)` is path navigation on this surface | native | on `ArrayExpr`/fanned paths for the three sequence operators, on `Expr<…>` for the two scalar ones |
248
+ | spatial family (§8.14) | `bbox geoArea geoLength centroid` → `$bbox $area $length $centroid`; `distance within bboxIntersects` → `$distance $within $bbox-intersects`; `geohash(precision?)` → `$geohash` (optional arity, like `substring`); `geoParse geoText geohashBounds geohashNeighbours` → the conversion family; `geoSimplify(tolerance)` → `$geo-simplify`. A plain JSON polygon embeds as a literal (`p.at.within(poly)`); `.params({ region })` makes it an external instead | native | on `Expr<…>`, per the typed-surface order |
249
+ | vector family (§8.15) | `similarity(other)` → `$similarity`. The other operand is an array of numbers: a captured one embeds as a literal, `.params({ query })` binds it at call time. There is no `knn` method — k-nearest is `orderByDescending(...).take(k)`, which is the composition the emitted document already is | native | on `Expr<…>`, per the typed-surface order |
250
+ | relation navigation — to-one hop (`p.author`, `p.author.email`) | over a provider with a relation table (§3, §8): `{ "$for": { "r1": "$.User[*]" }, "$where": { "$eq": ["$r1.<targetKey>", "$it.<via>"] }, "$return": "$r1.email" }` — the target's key against the row's foreign key (`kind: "oneToOne"`, the key on the declaring entity). Zero or one item: an object member's one value (absent when there is none), an operand elsewhere (empty compares false; `exists()`/`isEmpty()` say which), and under `$orderby` a key that may be empty (`$empty` applies). The binding is `r1`, `r2`, … per capture | native by desugaring — the document is the phrase; a store runs it as a named residual (`explain()`, MODEL-FORMAT §10.6) | `Expr<Post>['author']` is `ObjectExpr<User>` — emit's optional relation member, nothing new |
251
+ | relation navigation — to-many hop (`u.posts`, `u.posts.all()`) | `{ "$for": { "r1": "$.Post[*]" }, "$where": { "$eq": ["$r1.<via>", "$it.<targetKey>"] }, "$return": "$r1" }` — the target's foreign key against the row's key (`kind: "oneToMany"`, the key on the target). As a VALUE the phrase is packed, `[ <phrase> ]`, the array of related rows a member holds (`{ posts: u.posts }`; `u.posts.at(0)` indexes it); fanned, `u.posts.all()` is the bare phrase, a sequence: `.all().count()` → `{ "$count": <phrase> }`, `.all().exists()` → `{ "$exists": <phrase> }`, `.all().title` returns `"$r1.title"` per row (`[u.posts.all().title]` packs the titles). `count()`/`exists()` on the value range over the rows too, as a group-join's group's do | native by desugaring, as above | `ArrayExpr<Post>`; `all()` is `FannedExpr<Post>` |
252
+ | relation navigation — chained, and from every row binding | hops nest: `p.author.posts.all().count()` is `{ "$count": { "$for": { "r1": "$.User[*]" }, "$where": …, "$return": { "$for": { "r2": "$.Post[*]" }, "$where": { "$eq": ["$r2.authorId", "$r1.id"] }, "$return": "$r2" } } }` — the inner phrase correlates with the outer binding; a hop off a fanned to-many (`u.posts.all().author`) is a sequence, one target per row; a join's `it2` hops from the inner row; a group-join's fanned group (`g.all().author`) binds each row first (`{ "$for": { "r1": "$g[*]" }, "$return": <hop over $r1> }`); the group itself is an array, not a row | native by desugaring, as above | as the target's `Expr<…>` |
253
+ | relation navigation — many-to-many (`u.labels`) | — the join table is not a queryable root in this version, so no phrase exists to lower to; `load({ include: { labels: true } })` reads the memberships | unsupported (`JL0105`, naming the join table) | — |
254
+
255
+ Two spatial names are deliberately not the obvious ones, and the reason
256
+ is the same one that made §8.14's `$length` and §8.7's `$string-length`
257
+ two operators: **`length` on this surface is already `$string-length`**,
258
+ and §8.14's `$length` is a geodesic line measurement. One method name
259
+ cannot carry both, and renaming the shipped string method for symmetry
260
+ would break a published surface for a cosmetic gain — so the spatial one
261
+ is **`geoLength`**, and **`geoArea`** joins it, because a bare `area()`
262
+ on an arbitrary expression reads as arithmetic to a C# eye. The prefix
263
+ names the family the way `geoParse`/`geoText` already do.
264
+
265
+ Every method name shadows a data member of the same name — that is what
266
+ the null prototype on the method table is for, and what `get(name)`
267
+ escapes. A position stored as `at` is the case that bites: `p.at` is the
268
+ index method, so it reads `p.get('at').within(region)`. A stored score
269
+ named `similarity` is the same bite with a worse error — `r.similarity`
270
+ is the *method*, so calling it as a member yields a `TypeError` about a
271
+ function rather than a coded build error, because the surface never sees
272
+ a member access at all. `r.get('similarity')` reads the data.
273
+
274
+ **k-nearest is a chain, not a method.** `similarity()` is one operator
275
+ and the ordering and the window are stages that already exist, so the
276
+ top k reads as what it is:
277
+
278
+ ```js
279
+ from(memories)
280
+ .params({ query })
281
+ .orderByDescending((m, p) => m.embedding.similarity(p.query), { empty: 'least' })
282
+ .thenBy((m) => m.id)
283
+ .take(10)
284
+ .select((m) => m.text)
285
+ ```
286
+
287
+ `{ empty: 'least' }` under a descending sort puts the rows whose key is
288
+ empty — no vector, or one of the wrong width — **last**, and `thenBy` on
289
+ the identity breaks ties, so the chain answers the same rows in the same
290
+ order every time it runs. `.params({ query })` rather than a captured
291
+ array is what makes the emitted document one query for every question,
292
+ which is the shape a provider can push down.
293
+
294
+ ## 5. Deferred execution and re-enumeration
295
+
296
+ Every operator returns a new immutable `Sequence`; NOTHING runs until a
297
+ terminal operation. A sequence may be enumerated repeatedly and **each
298
+ enumeration re-reads the source** — the C# contract, and the one that
299
+ surprises people:
300
+
301
+ ```js
302
+ const rows = [1, 2, 3];
303
+ const q = from(rows).where((n) => n.gt(1));
304
+ q.toArray(); // [2, 3]
305
+ rows.push(4);
306
+ q.toArray(); // [2, 3, 4] — the source was read AGAIN
307
+ ```
308
+
309
+ The compiled query is shared through a bounded cache keyed by the
310
+ document's exact JSON text — COLLISION-FREE and ORDER-SENSITIVE — so
311
+ re-enumeration is cheap without pretending the results are frozen.
312
+ A 32-bit fingerprint would not do here: it collides after tens of
313
+ thousands of documents, and a collision means one query runs another
314
+ query's compiled program — wrong rows, cache hit reported, nothing said.
315
+ Nor would an order-insensitive identity: a constructor's member order is
316
+ part of a document's meaning, and `{ id, name }` and `{ name, id }` must
317
+ each answer in their own order however the cache is warmed.
318
+ `for…of` a sequence iterates `toArray()`'s result (one enumeration per
319
+ loop).
320
+
321
+ **`toDocument()` is a deep snapshot**, on both surfaces. A sequence is
322
+ immutable, so the document it hands out is an independent tree: writing
323
+ into a returned document (or into `explain()`'s) cannot change what a
324
+ later enumeration answers.
325
+
326
+ **An item is an item.** The engine's `$for` unpacks an item that is an
327
+ array into its members, one level (QUERY-FORMAT §6.2, D4) — right for a
328
+ path like `$.tags`, wrong for a chain, where an array-valued ROW (a CSV
329
+ record, a pair) is one item that `where`, `select` and `count` never
330
+ split. So the emitter binds every source a phrase iterates through an
331
+ array constructor — `{ "$for": { "it": ["$[*]"] } }` — whose one array
332
+ item is unpacked exactly once, into the rows as they are; a reseated
333
+ phrase and a join side are packed the same way. `from([[1, 2], [3]])
334
+ .where(() => true).count()` is `2` on both surfaces, and the streaming
335
+ async surface agrees by construction. The one source left bare is a
336
+ PROVIDER's own root (`$.Post[*]`): a stored document is an object, so D4
337
+ never applies there, and the bare root is the shape the provider's
338
+ planner pushes.
339
+
340
+ **Constants come back frozen and shared on the sync surface.** A
341
+ literal object or array in a projection, a `defaultIfEmpty` fallback or
342
+ a `concat` array is engine data: every row that yields it yields the
343
+ SAME frozen value (`rows[0] === rows[1]`, and writing into it throws).
344
+ The async surface hands out a fresh copy per enumeration instead —
345
+ same values, no shared identity — because a streamed row is yours.
346
+
347
+ **A `null` a callback returns is a VALUE, not an absent clause.**
348
+ `where(() => null)` filters everything out (null is not true),
349
+ `select(() => null)` projects nulls, `groupBy(() => null)` is one
350
+ null-keyed group, and a null seed still folds. The emitted document
351
+ carries the clause with its null in place.
352
+
353
+ **A captured constant crosses a real JSON boundary.** The query data
354
+ model is JSON, so a `Date`, `Map`, `Set`, `RegExp` or class instance is
355
+ refused (`JL0005`) rather than embedded — `Object.keys` reports nothing
356
+ for them, so they would embed as `{}` and the query would compare against
357
+ an empty object. `NaN` and `±Infinity` are refused for the same reason
358
+ (JSON has neither, and lenient serialization folds them into `null`), and
359
+ so is `-0`, which shares its JSON text with `0` while dividing to the
360
+ opposite infinity. Convert first — a `Date` to its ISO string or epoch
361
+ number. The boundary is the same for a `concat` array and for a
362
+ `params()` binding (`JL0004`): a parameter becomes an external and, on a
363
+ provider, a bound SQL parameter, so a `Date` there would compare against
364
+ nothing and answer `[]` with no error anywhere.
365
+
366
+ ## 6. Terminal semantics
367
+
368
+ The real C# semantics, because getting these wrong is how a
369
+ "LINQ-like" library becomes lodash with different names:
370
+
371
+ - `first()` on empty throws `JL2001`; `firstOrDefault(d)` returns `d`
372
+ (or `undefined` when omitted).
373
+ - `single()` on empty throws `JL2001`; on two-or-more throws `JL2002`;
374
+ `singleOrDefault(d)` throws on two-or-more and returns `d` on empty.
375
+ - `last()`/`lastOrDefault(d)` mirror `first` over the reversed window.
376
+ - `elementAt(i)` out of range throws `JL2003`;
377
+ `elementAtOrDefault(i, d)` returns `d`.
378
+ - `average()`, `min()` and `max()` over an empty sequence throw
379
+ `JL2001` (C# `InvalidOperationException`); `sum()` of nothing is `0`;
380
+ `count()` of nothing is `0`.
381
+ - `any()` is existence; `all(pred)` is vacuously true over the empty
382
+ sequence.
383
+
384
+ Element terminals emit their window inside an ARRAY constructor
385
+ (`[ … ]`), so the engine's result mapping (`undefined | item | items`)
386
+ can never confuse "one array-valued item" with "several items" — the
387
+ window array is always the single result and its elements are read
388
+ positionally.
389
+
390
+ ## 7. Parameters
391
+
392
+ `.params({ tenantId })` declares AND binds externals; a callback reads
393
+ them through its last argument:
394
+
395
+ ```js
396
+ from(rows)
397
+ .params({ tenantId: 'a7' })
398
+ .where((r, p) => r.tenant.eq(p.tenantId))
399
+ .toDocument();
400
+ // { "$for": { "it": ["$[*]"] },
401
+ // "$where": { "$eq": ["$it.tenant", "$tenantId"] },
402
+ // "$return": "$it" }
403
+ ```
404
+
405
+ The emitted document carries `$tenantId` as an external parameter
406
+ (QUERY-FORMAT §9) — the seam that later becomes a bound SQL parameter.
407
+ Undeclared use is `JL0004` at BUILD time with the fix in the message
408
+ (the engine would say JQ0005 at compile time; earlier and clearer
409
+ wins). The names `it`, `it2`, `acc` and `g` are RESERVED — they are the
410
+ emitted document's own binding names — and so are `r1`, `r2`, … (`r`
411
+ followed by a positive integer): the bindings a relation hop allocates,
412
+ numbered per capture (§3, §4 "relation navigation"). Declaring any of
413
+ them is `JL0004`. A binding must be query data (§5): a `Date`, `Map`,
414
+ `NaN` or `-0` is `JL0004` with the conversion named.
415
+
416
+ The inner side of a `join`, `groupJoin` or `concat` contributes its
417
+ document WHOLE, so its declared parameters ride along into the new
418
+ sequence (`explain().externals` lists the union, `explain().bindings`
419
+ the values bound so far — what a provider receives as `externals`, and
420
+ what a host handed the chain, such as a live registration, forwards
421
+ without a second spelling); a name both sides
422
+ bind to different values is `JL0004` — one document carries one binding
423
+ per name. Rebinding a name later (`.params({ k: 3 })`) re-runs the
424
+ whole document under the new value, on the async surface too: a
425
+ `params()` after a `mapAsync` rebinds the pushed prefix as well as the
426
+ residual.
427
+
428
+ ## 8. The provider contract
429
+
430
+ A **provider** is any object exposing:
431
+
432
+ ```
433
+ execute(queryDocument, options) -> undefined | item | items[]
434
+ ```
435
+
436
+ - `queryDocument` arrives WHOLE — a terminal hands over the full
437
+ emitted document (including the terminal's own wrapper, §6);
438
+ nothing is enumerated locally, ever.
439
+ - `options.externals` is the `{ name: value }` record of bound
440
+ parameters (§7).
441
+ - The return value uses the ENGINE's result mapping
442
+ (`undefined` = empty, a single item as itself, several items as an
443
+ array) — the in-memory runner is the reference semantics every
444
+ provider MUST match, and it implements this same interface.
445
+ - **`execute` is SYNCHRONOUS on this surface.** A `Sequence` terminal
446
+ is a value — `toArray(): T[]`, `count(): number` — so a promise cannot
447
+ be returned under that type. A provider that answers one is refused
448
+ with `JL2004` at the seam, because the alternative is not a slow
449
+ answer but a wrong one: the promise came back typed as the value,
450
+ `count()` handed a `Promise` to arithmetic, and `first()` indexed the
451
+ promise and returned `undefined`. An asynchronous provider (a
452
+ wasm/OPFS driver, a store's asynchronous entity set) is `fromAsync`'s
453
+ source (§12): the same document arrives whole, and `execute` MAY
454
+ answer a promise there.
455
+
456
+ A provider MAY carry three more members, read at `from()`/`fromAsync()`
457
+ time:
458
+
459
+ - `root` — the path expression its items are bound through
460
+ (`'$.Post[*]'` for a store's entity set); absent means the whole
461
+ input, `'$[*]'`. The emitted document iterates the root BARE (§5): a
462
+ stored document is an object, so an item is never an array there.
463
+ - `roots` — the entity roots a STORE-LEVEL provider serves when it has
464
+ no root of its own (`['User', 'Post']`). Such a provider is refused by
465
+ `from()`/`fromAsync()` with `JL0007`, naming them: `$[*]` over the
466
+ entity map would answer every entity's rows mixed, or count the sets.
467
+ `fromDocument` keeps its own rule — there the document IS the root.
468
+ - `scope` — an identity two providers share when their documents may be
469
+ joined. One store's entity sets carry one `scope`, so
470
+ `from(posts).join(from(users), (p) => p.authorId, (u) => u.id, (p) => p)`
471
+ emits `{ "$for": { "it": "$.Post[*]", "it2": "$.User[*]" }, "$where":
472
+ { "$eq": ["$it.authorId", "$it2.id"] }, "$return": "$it" }` — the shape
473
+ the store's translator answers in ONE statement when the result is a
474
+ bare binding (MODEL-FORMAT §10.2), and the declared residual over both
475
+ fetched roots when it is a projection (§10.6). `concat` stays
476
+ same-source even within a scope: one input per document. A scope MAY
477
+ carry `relations` — the relation tables of every root of the scope,
478
+ keyed by root name (a store's does) — which is where a chained hop
479
+ finds its target's table; without it the first hop lowers and the
480
+ target's members are plain paths.
481
+ - `relations` — the relation table of the rows the provider serves
482
+ (MODEL-FORMAT §10.1; a store's entity set carries its entity's): a
483
+ plain record, one entry per declared relation member, `{ to, kind,
484
+ via?, fkEntity?, fkTargets?, joinTable?, targetKey }`. With it, a
485
+ relation name on a callback's row hops (§3) and is lowered to the
486
+ phrase §4's "relation navigation" rows spell; `kind` decides the
487
+ equality's sides (`oneToOne`: the key on the declaring entity;
488
+ `oneToMany`: on the target), `to` the root the hop binds (`$.<to>[*]`),
489
+ `via` and `targetKey` its two columns. A `manyToMany` entry is
490
+ `JL0105`. Absent, a relation name is an ordinary member.
491
+
492
+ An element terminal hands over the one-item WINDOW `[<phrase>]` (§6). A
493
+ provider that plans documents reads through that window — the store
494
+ plans the phrase inside as if it were bare and answers its rows as the
495
+ one array the constructor yields (`[]` for none, `[row]` for one) — so
496
+ `toArray()` and `first()` push exactly as `count()` does.
497
+
498
+ `@jarenjs/db` implements this contract without either package
499
+ importing the other: its collections and its entity sets are providers
500
+ (the sets carry `root`, `scope` and `relations`; the store carries
501
+ `roots` and `relations`), and a test double proves the document arrives
502
+ whole. A lowered hop is what a store receives as any other document: it
503
+ runs the correlated phrase in its residual over the fetched roots and
504
+ `explain()` names the §10.6 reason — no lowered shape pushes natively in
505
+ this version, and the store's `strict` refuses them all (`JD0010`).
506
+
507
+ ### 8.1 Compilation registries
508
+
509
+ `from(source, options)` and `fromDocument(source, doc, options)` take the
510
+ engine's own compile options, so a document that is expressible is also
511
+ executable in memory:
512
+
513
+ | option | what it enables |
514
+ |---|---|
515
+ | `compileTypeTest` | `ofType`/`cast` (the schema operators) |
516
+ | `collations` | `orderBy(…, { collation })` — a `nl` sort is `JQ0010` without it |
517
+ | `functions` | `$call` in a hand-written or saved document |
518
+ | `pathFunctions` | custom RFC 9535 path function extensions |
519
+ | `limits` | step, depth and sequence bounds — the reason a SAVED document can be run at all. `resultItems` does not bind a chain: a terminal reads ONE packed window (§6), so the bound that guards a chain's size is `sequenceItems` on its phrases; on the async surface only `steps`/`depth` and a barrier phase's `sequenceItems` apply, because streaming stages evaluate one item at a time |
520
+ | `registry` | an explicit cache-partition key, when the hooks above are rebuilt per call |
521
+
522
+ Compiled documents are cached per registry COMBINATION, not per document
523
+ alone: the same document compiles to different code with and without a
524
+ collation registry, so sharing one partition would answer a caller who
525
+ passed no collations with the compiled-with version.
526
+
527
+ ## 9. Error codes
528
+
529
+ Build errors (`LinqBuildError`; `docPath` where a document position
530
+ exists):
531
+
532
+ | Code | Condition |
533
+ |---|---|
534
+ | `JL0001` | `from()` received neither an iterable nor a provider |
535
+ | `JL0002` | an expression proxy escaped its capture callback |
536
+ | `JL0003` | `ofType`/`cast` need an injected `compileTypeTest` |
537
+ | `JL0004` | an undeclared or reserved parameter name was used |
538
+ | `JL0005` | an operator was used invalidly at build time |
539
+ | `JL0006` | an unsupported operator was invoked |
540
+ | `JL0007` | a provider serves several entity roots (`roots`) and has no root of its own — chain over `store.entity(name)` |
541
+
542
+ Pen build errors (`LinqBuildError`, raised by `@jarenjs/linq/schema`,
543
+ `/model`, `/jslt` and the pens that follow them; LINQ-FORMAT.md §1.3 is
544
+ the normative home, this table mirrors it):
545
+
546
+ | Code | Condition |
547
+ |---|---|
548
+ | `JL0101` | a pen received a value it cannot spell: not JSON, not what the keyword takes, or a name → value map whose prototype a `__proto__:` literal replaced |
549
+ | `JL0102` | a pen was asked for a construct the format cannot carry |
550
+ | `JL0103` | a `$defs` name collision, a dangling ref, or an unnamed recursion |
551
+ | `JL0104` | a pen-owned keyword through `meta()`, or an external a captured rule did not declare |
552
+ | `JL0105` | a relation hop on the chain cannot lower: a many-to-many member (its join table is not a queryable root), a composite or undeclared key, or a malformed relation entry (§3, §4 "relation navigation") |
553
+ | `JL0106` | a migration step names a table the target model does not declare, or a draft it cannot match |
554
+ | `JL0107` | the client (`@jarenjs/linq/db`, [DB-CLIENT.md](DB-CLIENT.md)) named a member that is not the relation kind the operation needs: `include()` over a member that is not a declared relation, `link()`/`unlink()` over a relation that is not many-to-many |
555
+
556
+ Runtime errors (`LinqRuntimeError`):
557
+
558
+ | Code | Condition |
559
+ |---|---|
560
+ | `JL2001` | `first`/`single` found no element |
561
+ | `JL2002` | `single` found more than one element |
562
+ | `JL2003` | `elementAt` is out of range |
563
+ | `JL2004` | an asynchronous provider cannot back the synchronous surface |
564
+ | `JL2005` | a push queue was fed after it ended |
565
+ | `JL2006` | a provider answered an element terminal with something other than one array |
566
+
567
+ Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
568
+ through unwrapped — they already carry their own code and `docPath` —
569
+ with one exception: `JQ0008` (a schema operator with no type-test
570
+ compiler) is reported as `JL0003`, because the fix is the same
571
+ `compileTypeTest` hook whether `ofType`/`cast` or the document spelled
572
+ the operator.
573
+
574
+ ## 10. The asynchronous surface: streaming and barriers
575
+
576
+ `fromAsync(source, options?)` gives the same operator surface over
577
+ async sources — joins only over a provider, see the table — emitting the SAME query
578
+ documents: the same chain through `from` and `fromAsync` MUST emit
579
+ byte-identical documents (the one-operator-set proof), with terminals
580
+ returning promises. The rule: **the pipeline is synchronous, the
581
+ boundaries are async.** A compiled query never awaits; what is
582
+ asynchronous is where rows come from and where element-wise host work
583
+ happens (§11).
584
+
585
+ Per operator, whether it STREAMS (per-item evaluation, flat memory) or
586
+ is a BARRIER (materialises the stream so far and runs the maximal run
587
+ of document stages through the engine over the buffer — inherent,
588
+ because the engine itself materialises for `$orderby`/`$groupby`):
589
+
590
+ | Operator | Async behaviour |
591
+ |---|---|
592
+ | `where`, `select`, `selectMany`, `ofType`, `cast` | stream (per-item compiled evaluators — the engine, one item at a time) |
593
+ | `skip`, `take` | stream; `take` CLOSES the source when satisfied |
594
+ | `distinct` | stream, with a running key set (the grouping relation: `NaN` groups with `NaN`) |
595
+ | `defaultIfEmpty` | stream (an emptiness flag) |
596
+ | `concat` | stream for a CONSTANT array; another sequence is refused (`JL0005`) — an async source is single-pass and cannot be re-iterated for a second chain |
597
+ | | on the SYNC surface, `concat` also requires the same source: a query document reads one input, so the other sequence contributes its EXPRESSION, and a foreign sequence would have that expression evaluated against THIS source — reading the wrong rows twice instead of concatenating two inputs |
598
+ | `orderBy`/`thenBy`, `groupBy`, `aggregate`, `reverse` | BARRIER, named by `explain()` with the reason (a `thenBy` is part of the `$orderby` barrier it extends) |
599
+ | `join`, `groupJoin` | over a PROVIDER origin, before any `mapAsync`: pushed WHOLE inside the one document, with an async sequence over the same provider (or one sharing its `scope`) as the inner side — the store answers a two-root equijoin in one statement; over an iterable, a cursor or a push queue `JL0005`: a join's inner side re-reads the source, and a single-pass source cannot be read twice (join on the sync surface, or collect the stream first) |
600
+ | `count`, `any`, `all`, `first`, `single`, `elementAt` | stream with early exit where semantics allow |
601
+ | `sum`, `average`, `min`, `max`, `last` | consume the stream; the aggregate itself runs through the ENGINE over the collected items, so its semantics (type errors included) are identical to the sync surface |
602
+
603
+ **Early termination MUST close the source**: `first()`, `any()`,
604
+ `take(n)`, and an exception mid-chain all call `.return()` on the
605
+ iterator — a generator left suspended holds a file handle or a read
606
+ transaction open. `explain()` reports `{ barriers: [{ operator,
607
+ reason }], hops, document }` — `hops` the relation hops the callbacks
608
+ navigated, as on the sync surface (§4) — or, when a `mapAsync` sits in
609
+ the chain, `{ split: { pushed, residual } }` instead of `document`
610
+ (`toDocument()` refuses with `JL0005`: a host callback has no document
611
+ form). No silent caps, no silent buffering: if a chain materialises,
612
+ the report says which operator forced it.
613
+
614
+ Re-enumeration follows the sync contract: each enumeration calls the
615
+ source's iterator method again. A one-shot generator object simply
616
+ exhausts — the same way it does under `from`. Streamed constants
617
+ (`concat`, `defaultIfEmpty`) are handed out as a fresh copy per
618
+ enumeration (§5).
619
+
620
+ ## 11. The concurrency boundary
621
+
622
+ ```js
623
+ await fromAsync(rows)
624
+ .mapAsync(async (row, signal) => fetchScore(row.id, signal),
625
+ { concurrency: 8, mode: 'parallel', ordered: true })
626
+ .where((r) => r.score.gt(0.5))
627
+ .toArray();
628
+ ```
629
+
630
+ `mapAsync` is the ONE explicit boundary for element-wise asynchronous
631
+ host work. There is no parallel universe of `selectAwait`-shaped
632
+ operators; a per-element async *predicate* is `mapAsync` then `where`.
633
+
634
+ - `concurrency` is REQUIRED and MUST be a positive integer (`JL0005`)
635
+ — the unbounded default is how libraries like this take down a
636
+ downstream service.
637
+ - `mode` reuses the `createTaskEffect` vocabulary (`@jarenjs/app` §9),
638
+ deliberately, so a reader who knows one knows the other:
639
+ `parallel` (a sliding window of N), `concat` (strictly sequential),
640
+ `switch` (a newer item supersedes and ABORTS the in-flight task),
641
+ `exhaust` (items arriving while busy are dropped). The source is
642
+ pulled eagerly under `switch`/`exhaust` — that race IS the mode.
643
+ - `ordered: true` (default) preserves source order and buffers at most
644
+ `concurrency` results — the stated cost; `ordered: false` yields on
645
+ completion.
646
+ - An `AbortSignal` is threaded to every callback and aborted on early
647
+ termination and on failure. A rejected callback FAILS CLOSED: the
648
+ first failure wins, every in-flight sibling aborts, the source
649
+ closes (the `compileDag` discipline).
650
+ - `mapAsync` is NOT translatable to a provider. A provider-backed
651
+ chain that reaches it SPLITS: everything before is pushed to the
652
+ provider whole, everything after runs locally, and `explain()`
653
+ reports `{ split: { pushed, residual } }` — the same residual
654
+ honesty the SQL pushdown owes (D8), applied to the async boundary.
655
+
656
+ ## 12. The cursor contract and the source adapters
657
+
658
+ `fromAsync` accepts, in order of preference:
659
+
660
+ - a **provider** (§8) — asked for BEFORE the shapes below, so an
661
+ `execute` duck that also happens to be iterable is a provider. The
662
+ whole chain up to the first `mapAsync` — the terminal's wrapper
663
+ included — is ONE document `execute` receives, once, with the bound
664
+ externals, and `execute` MAY answer a promise here (D8: the contract
665
+ mirrors §8's; the awaiting is this surface's). The residual after a
666
+ `mapAsync` streams locally over the pushed rows, and `explain()`
667
+ reports `{ split: { pushed, residual } }` exactly as for a
668
+ synchronous prefix, with `barriers` naming only the residual's own.
669
+ The same chain through `from(store.sync.entity('X'))` and
670
+ `fromAsync(store.entity('X'))` MUST emit byte-identical documents —
671
+ the one-operator-set proof, extended to roots;
672
+ - any **`AsyncIterable`** (async generators, `ReadableStream` — every
673
+ target exposes `Symbol.asyncIterator` on it, josl's
674
+ `iterateCsvStream` output);
675
+ - any sync iterable (wrapped);
676
+ - a **cursor**: `{ next(): Promise<{done, value}>, return?() }` — the
677
+ shape the SQL provider's row iterator implements later, adopted
678
+ as-is;
679
+ - a **push queue** (`createPushQueue({ highWaterMark = 1024 })`) for
680
+ feed/end-style readers with no pull protocol of their own (josl's
681
+ push parsers deliberately have no backpressure protocol; the queue
682
+ is where one appears): `feed(value)` returns `false` once the queue
683
+ exceeds the mark — a pause HINT, never a hard stop — and
684
+ `end(error?)` closes (or fails) the stream. Anything else is
685
+ `JL0001` at `fromAsync()` time.
686
+
687
+ What this surface does NOT do, by design: it does not make the query
688
+ engine async (`packages/json` is untouched and strictly synchronous),
689
+ it does not add a second operator table, and it does not add
690
+ `selectAwait`/`whereAwait` variants.
691
+
692
+ ## 13. Worked examples
693
+
694
+ Every `js` fence below is EXECUTED. `test/linq/pen-docs.test.js` writes
695
+ it as a module beside the workspace's `node_modules` — so `@jarenjs/linq`
696
+ resolves exactly as it does for a consumer — imports it, and asserts that
697
+ the single export's `toDocument()` equals the `json` fence beside it. A
698
+ fence that drifts from the emitter fails the suite; nothing here is a
699
+ sketch.
700
+
701
+ The eight are chosen to teach the CAPTURE MODEL rather than to cover the
702
+ operator table (§4 is the table). Read them in order: the first shows
703
+ what a chain is, and each one after it adds one thing the emitted
704
+ document does that the source does not obviously say.
705
+
706
+ They are also one question, asked eight ways, over one small blog's data
707
+ — the users, the posts they wrote and the orders placed against them.
708
+ Nothing is shared between the fences at run time (each is a whole module,
709
+ and that is what the gate runs), but the shapes are the same throughout,
710
+ so a member you meet in §13.1 means the same thing in §13.8, and the two
711
+ fences that need a PROVIDER rather than an array — the join and the hop —
712
+ are that same blog seen as a store's entity sets.
713
+
714
+ ### 13.1 The chain, whole
715
+
716
+ The opening example of the README and of §2, executed. `where` becomes
717
+ the FLWOR `$where`, `orderBy` an `$orderby` key spec
718
+ (`orderByDescending`, `thenBy` and `thenByDescending` extend the same
719
+ clause), and `select` the `$return` constructor — one phrase, in the
720
+ order a reader writes it.
721
+
722
+ ```js
723
+ import { from } from '@jarenjs/linq';
724
+
725
+ const users = [
726
+ { id: 1, name: 'Ada', age: 36 },
727
+ { id: 2, name: 'Bo', age: 19 },
728
+ ];
729
+
730
+ export const adults = from(users)
731
+ .where((u) => u.age.gt(21))
732
+ .orderBy((u) => u.name)
733
+ .select((u) => ({ id: u.id, name: u.name }));
734
+ ```
735
+
736
+ ```json
737
+ {
738
+ "$for": { "it": ["$[*]"] },
739
+ "$where": { "$gt": ["$it.age", 21] },
740
+ "$orderby": { "$key": "$it.name" },
741
+ "$return": { "id": "$it.id", "name": "$it.name" }
742
+ }
743
+ ```
744
+
745
+ `adults.toArray()` answers `[{ "id": 1, "name": "Ada" }]`. The source is
746
+ bound through an array constructor, `["$[*]"]`, and §5 says why: the
747
+ engine unpacks an item that is an array one level, which is right for a
748
+ path and wrong for a row.
749
+
750
+ ### 13.2 A join is a nested `$for` and an equality
751
+
752
+ Both sides read ONE input, so a join's other side derives from the same
753
+ source — or, as here, from a second provider sharing its `scope`: two
754
+ entity sets of one store are two roots of one multi-entity document, and
755
+ the store answers the equijoin in a single statement (§8).
756
+
757
+ ```js
758
+ import { from } from '@jarenjs/linq';
759
+
760
+ // two entity sets of ONE store: two roots of one multi-entity input,
761
+ // which is what a shared `scope` declares (§8)
762
+ const scope = {};
763
+ const postSet = { execute: () => [], root: '$.Post[*]', scope };
764
+ const userSet = { execute: () => [], root: '$.User[*]', scope };
765
+
766
+ export const bylines = from(postSet).join(
767
+ from(userSet),
768
+ (p) => p.authorId,
769
+ (u) => u.id,
770
+ (p, u) => ({ title: p.title, author: u.name }),
771
+ );
772
+ ```
773
+
774
+ ```json
775
+ {
776
+ "$for": { "it": "$.Post[*]", "it2": "$.User[*]" },
777
+ "$where": { "$eq": ["$it.authorId", "$it2.id"] },
778
+ "$return": { "title": "$it.title", "author": "$it2.name" }
779
+ }
780
+ ```
781
+
782
+ There is no `$join` operator in the emitted document and there is no need
783
+ for one: the engine recognises this shape at COMPILE time and runs a hash
784
+ join (QUERY-FORMAT §6). It recognises it **only when both key
785
+ expressions are plain member paths** — `(p) => p.authorId` and
786
+ `(u) => u.id` are; `(p) => p.title.lower()` is not, and that join runs as
787
+ a nested loop with the same answer and a different cost. A provider's
788
+ roots stay bare (`"$.Post[*]"`, not `["$.Post[*]"]`): a stored document
789
+ is an object, so the unpacking rule §5 guards against cannot arise.
790
+
791
+ ### 13.3 A group is a phrase, and its items are an array
792
+
793
+ `groupBy` emits `$groupby` and reseats: the downstream items are
794
+ `{ key, items }` objects, and every operator after it reads THOSE. The
795
+ `key` carries a `$default` to `null` because a group whose key expression
796
+ yielded nothing still has rows.
797
+
798
+ ```js
799
+ import { from } from '@jarenjs/linq';
800
+
801
+ const orders = [
802
+ { id: 1, city: 'Delft', total: 12 },
803
+ { id: 2, city: 'Delft', total: 30 },
804
+ { id: 3, city: 'Gouda', total: 7 },
805
+ ];
806
+
807
+ export const perCity = from(orders)
808
+ .groupBy((o) => o.city)
809
+ .select((g) => ({ city: g.key, orders: g.items.count(), total: g.items.all().total.sum() }));
810
+ ```
811
+
812
+ ```json
813
+ {
814
+ "$for": {
815
+ "it": [
816
+ {
817
+ "$for": { "it": ["$[*]"] },
818
+ "$groupby": { "g": "$it.city" },
819
+ "$return": { "key": { "$default": ["$g", null] }, "items": ["$it"] }
820
+ }
821
+ ]
822
+ },
823
+ "$return": {
824
+ "city": "$it.key",
825
+ "orders": { "$count": "$it.items[*]" },
826
+ "total": { "$sum": "$it.items[*].total" }
827
+ }
828
+ }
829
+ ```
830
+
831
+ `perCity.toArray()` answers `[{ city: 'Delft', orders: 2, total: 42 },
832
+ { city: 'Gouda', orders: 1, total: 7 }]`.
833
+
834
+ **A group aggregates as its ROWS.** `g.items.count()` is the number of
835
+ rows in the group: the chain knows `items` holds a group and emits
836
+ `{ "$count": "$it.items[*]" }` — the fan — rather than `$count` over the
837
+ one array value, which would answer `1` for every group. This is the
838
+ same rule a group-JOIN's group has always kept (`g.count()` there is the
839
+ number of matches, §4), and the two group shapes now spell it the same
840
+ way.
841
+
842
+ `g.items` itself is still the array, and everything an array can do it
843
+ still does: a member takes it whole (`{ rows: g.items }` emits
844
+ `"$it.items"`), `at(0)` indexes it, and `all()` fans it explicitly —
845
+ which is what `g.items.all().total.sum()` above needs, because summing a
846
+ MEMBER of each row means fanning the rows first and then reading the
847
+ member (`"$it.items[*].total"`). Only the aggregates changed, and only
848
+ for the member the emitter writes the group into.
849
+
850
+ An array a CALLER stored is a different thing and keeps the older rule:
851
+ `u.tags.count()` is `1`. At capture time an array member and a scalar
852
+ member are the same path — the chain has no type to tell them apart, and
853
+ inventing one would be a guess — so `u.tags.all().count()` is how the
854
+ elements are counted, and `u.tags.exists()` is what the un-fanned form
855
+ was really answering.
856
+
857
+ ### 13.4 A relation name hops, and the document never carries it
858
+
859
+ When the items are the rows of an entity whose provider carries a
860
+ relation table, a member access naming a declared relation records a HOP
861
+ and is lowered, at capture, into the correlated phrase §4's "relation
862
+ navigation" rows spell. What the reader writes is `p.author.name`; what
863
+ the store receives has no member called `author` anywhere in it.
864
+
865
+ ```js
866
+ import { from } from '@jarenjs/linq';
867
+
868
+ // a store's entity set: its rows carry the entity's relation table
869
+ // (MODEL-FORMAT §10.1), which is what makes a relation name hop
870
+ const postSet = {
871
+ execute: () => [],
872
+ root: '$.Post[*]',
873
+ scope: {
874
+ relations: {
875
+ Post: { author: { to: 'User', kind: 'oneToOne', via: 'authorId', targetKey: 'id' } },
876
+ User: { posts: { to: 'Post', kind: 'oneToMany', via: 'authorId', targetKey: 'id' } },
877
+ },
878
+ },
879
+ relations: { author: { to: 'User', kind: 'oneToOne', via: 'authorId', targetKey: 'id' } },
880
+ };
881
+
882
+ export const bylines = from(postSet)
883
+ .select((p) => ({ title: p.title, author: p.author.name, siblings: p.author.posts.count() }));
884
+ ```
885
+
886
+ ```json
887
+ {
888
+ "$for": { "it": "$.Post[*]" },
889
+ "$return": {
890
+ "title": "$it.title",
891
+ "author": {
892
+ "$for": { "r1": "$.User[*]" },
893
+ "$where": { "$eq": ["$r1.id", "$it.authorId"] },
894
+ "$return": "$r1.name"
895
+ },
896
+ "siblings": {
897
+ "$count": {
898
+ "$for": { "r2": "$.User[*]" },
899
+ "$where": { "$eq": ["$r2.id", "$it.authorId"] },
900
+ "$return": {
901
+ "$for": { "r3": "$.Post[*]" },
902
+ "$where": { "$eq": ["$r3.authorId", "$r2.id"] },
903
+ "$return": "$r3"
904
+ }
905
+ }
906
+ }
907
+ }
908
+ }
909
+ ```
910
+
911
+ Three things this document shows that the source does not:
912
+
913
+ - **The hop bindings are numbered per CAPTURE, not per hop site.** Both
914
+ members are captured by one `select` callback, so the first hop takes
915
+ `r1` and the chained one takes `r2` and `r3`. A second callback — a
916
+ `where` before this `select` — would start again at `r1` in its own
917
+ phrase.
918
+ - **`kind` decides which side of the equality carries the key.** The
919
+ to-one hop compares the TARGET's key with the row's foreign key
920
+ (`$r1.id` against `$it.authorId`); the to-many hop inside it compares
921
+ the target's foreign key with the row's key (`$r3.authorId` against
922
+ `$r2.id`).
923
+ - **A chained hop re-binds its source.** `p.author.posts` is not one
924
+ phrase with two roots; it is a phrase inside a phrase, the inner one
925
+ correlated with the outer's binding. The scope's `relations` is what
926
+ lets the second link find `User`'s table — without it the first hop
927
+ lowers and `posts` would be an ordinary member of the target.
928
+
929
+ `p.author` and `p.author.posts` are declarations of intent, not
930
+ instructions: `explain().hops` lists what the callbacks navigated, and
931
+ no lowered shape pushes natively in this version — a store runs the
932
+ phrase as a named residual and says so (§8).
933
+
934
+ ### 13.5 A parameter is a seam, not a value
935
+
936
+ `.params()` DECLARES and BINDS in one call. The declaration is what the
937
+ document carries — `$tenantId`, an external (QUERY-FORMAT §9) — and the
938
+ binding is what the runner is handed beside it. The same document serves
939
+ every tenant, which is what makes it cacheable, loggable and pushable to
940
+ a provider as a prepared statement.
941
+
942
+ ```js
943
+ import { from } from '@jarenjs/linq';
944
+
945
+ const orders = [{ id: 1, tenant: 'a7', total: 12 }];
946
+
947
+ export const ours = from(orders)
948
+ .params({ tenantId: 'a7' })
949
+ .where((r, p) => r.tenant.eq(p.tenantId));
950
+ ```
951
+
952
+ ```json
953
+ {
954
+ "$for": { "it": ["$[*]"] },
955
+ "$where": { "$eq": ["$it.tenant", "$tenantId"] },
956
+ "$return": "$it"
957
+ }
958
+ ```
959
+
960
+ `ours.explain().externals` is `['tenantId']` and `explain().bindings` is
961
+ `{ tenantId: 'a7' }` — the two halves the seam keeps apart. Reading an
962
+ undeclared name is `JL0004` at build time rather than `JQ0005` at compile
963
+ time (§14), and `.params({ tenantId: 'b3' })` on the result re-runs the
964
+ same document under the new value.
965
+
966
+ ### 13.6 `selectMany` unpacks exactly one level
967
+
968
+ The projected value is iterated once — an array member's elements, a
969
+ constructed array's members, a scalar as itself — and the FLWOR `$return`
970
+ concatenates per tuple. That is a nested `$for` whose binding legally
971
+ shadows the outer `it`.
972
+
973
+ ```js
974
+ import { from } from '@jarenjs/linq';
975
+
976
+ const posts = [{ id: 1, tags: ['linq', 'json'] }, { id: 2, tags: [] }];
977
+
978
+ export const tags = from(posts).selectMany((p) => p.tags);
979
+ ```
980
+
981
+ ```json
982
+ {
983
+ "$for": { "it": ["$[*]"] },
984
+ "$return": {
985
+ "$for": { "it": "$it.tags" },
986
+ "$return": "$it"
987
+ }
988
+ }
989
+ ```
990
+
991
+ `tags.toArray()` answers `["linq", "json"]`: the second post contributes
992
+ nothing, and an array of arrays would come back as an array of arrays —
993
+ one level, never a deep flatten.
994
+
995
+ ### 13.7 `ofType` is a `$valid` filter, and it needs a compiler
996
+
997
+ `ofType` emits a `$valid` over a JSON Schema literal and `cast` an
998
+ `$assert` per item. Both are SCHEMA operators, and the query engine
999
+ compiles a schema operator only when a type-test compiler is injected —
1000
+ so the option travels with the source, not with the operator.
1001
+
1002
+ ```js
1003
+ import { from } from '@jarenjs/linq';
1004
+ import { createTypeTestCompiler } from '@jarenjs/validate/query';
1005
+
1006
+ const users = [{ id: 1, email: 'ada@example.com' }, { id: 2 }];
1007
+
1008
+ export const reachable = from(users, { compileTypeTest: createTypeTestCompiler() })
1009
+ .ofType({ type: 'object', required: ['email'] });
1010
+ ```
1011
+
1012
+ ```json
1013
+ {
1014
+ "$for": { "it": ["$[*]"] },
1015
+ "$where": { "$valid": ["$it", { "type": "object", "required": ["email"] }] },
1016
+ "$return": "$it"
1017
+ }
1018
+ ```
1019
+
1020
+ The document is the same with or without the hook — emission never needs
1021
+ it. What needs it is running: `from(users).ofType(…).toArray()` is
1022
+ `JL0003` with the fix in the message (§14), and a schema-pen builder may
1023
+ stand in for the literal (`s.object({ email: s.string() })`), whose
1024
+ document is taken.
1025
+
1026
+ ### 13.8 A hand-written document is a source of items
1027
+
1028
+ `fromDocument` attaches a stored or hand-written query document; its
1029
+ result is the item sequence, and every operator chains over it. Only the
1030
+ envelope this version knows is unwrapped — anything else is handed to the
1031
+ engine so ITS verdict is what surfaces (§14).
1032
+
1033
+ ```js
1034
+ import { fromDocument } from '@jarenjs/linq';
1035
+
1036
+ const users = [{ id: 1, name: 'Ada', age: 36 }];
1037
+ const saved = {
1038
+ $query: '0.1',
1039
+ $expr: { $for: { it: ['$[*]'] }, $where: { $gt: ['$it.age', 21] }, $return: '$it' },
1040
+ };
1041
+
1042
+ export const names = fromDocument(users, saved).select((u) => u.name);
1043
+ ```
1044
+
1045
+ ```json
1046
+ {
1047
+ "$for": {
1048
+ "it": [
1049
+ {
1050
+ "$for": { "it": ["$[*]"] },
1051
+ "$where": { "$gt": ["$it.age", 21] },
1052
+ "$return": "$it"
1053
+ }
1054
+ ]
1055
+ },
1056
+ "$return": "$it.name"
1057
+ }
1058
+ ```
1059
+
1060
+ `names.toArray()` answers `["Ada"]`. The saved document became the source
1061
+ of a new phrase rather than being merged into one — which is what keeps a
1062
+ document a reader did not write from being reinterpreted. A
1063
+ `fromDocument` chain never hops (§3): there the document decides what the
1064
+ items are, so a relation table has nothing to attach to.
1065
+
1066
+ ## 14. Refusals
1067
+
1068
+ §9 is the normative code table: every `JL` code this package can raise,
1069
+ held equal to the runtime's `LINQ_CODES` by
1070
+ `test/errors/code-tables.test.js`. This section is the other half a
1071
+ reader needs — the SPELLING that trips each one and the spelling that
1072
+ works.
1073
+
1074
+ The chain raises fourteen of the twenty: `JL0001`–`JL0007` at build
1075
+ time, `JL2001`–`JL2006` while a terminal runs, and `JL0105`, which sits
1076
+ in the `JL01xx` block because a relation hop is a pen-shaped refusal but
1077
+ is raised by the chain's own expression capture. The other six —
1078
+ `JL0101`–`JL0104`, `JL0106` and `JL0107` — are the PENS' and the
1079
+ CLIENT's. Their per-code conditions are the binder's,
1080
+ [LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3, and the spelling that trips each
1081
+ one is in §4 of the document of the pen that raises it:
1082
+ [SCHEMA-PEN.md](SCHEMA-PEN.md#4-refusals),
1083
+ [MODEL-PEN.md](MODEL-PEN.md#4-refusals),
1084
+ [JSLT-PEN.md](JSLT-PEN.md#4-refusals),
1085
+ [MIGRATION-PEN.md](MIGRATION-PEN.md#4-refusals),
1086
+ [CONTRACT-PEN.md](CONTRACT-PEN.md#4-refusals),
1087
+ [FLOW-PEN.md](FLOW-PEN.md#4-refusals),
1088
+ [APP-PEN.md](APP-PEN.md#4-refusals),
1089
+ [FORMS-PEN.md](FORMS-PEN.md#4-refusals), and
1090
+ [DB-CLIENT.md](DB-CLIENT.md#4-refusals) for the client.
1091
+
1092
+ `test/linq/pen-docs.test.js` holds the list below equal, in both
1093
+ directions, to the codes thrown by the chain's own modules —
1094
+ `packages/linq/src/*.js` less `json-boundary.js` and `capture-root.js`,
1095
+ which are the pens' shared doors and raise only `JL01xx` (the gate
1096
+ proves that too, so the exclusion cannot hide a chain refusal).
1097
+
1098
+ | Code | What the chain raises it for |
1099
+ |---|---|
1100
+ | `JL0001` | `from()`/`fromAsync()` received a source that is neither a supported shape nor a provider |
1101
+ | `JL0002` | an expression proxy was used outside the capture it belongs to |
1102
+ | `JL0003` | `ofType`/`cast` compiled a schema operator with no type-test compiler injected |
1103
+ | `JL0004` | a parameter was read undeclared, declared under a reserved or invalid name, bound to a non-JSON value, or bound to two values by one join |
1104
+ | `JL0005` | an operator was used invalidly at build time: a value the document cannot carry, a stage in the wrong place, a bad argument, an async-surface rule |
1105
+ | `JL0006` | an operator §4 records as `unsupported` was invoked |
1106
+ | `JL0007` | a provider serves several entity roots and has none of its own |
1107
+ | `JL0105` | a relation hop cannot lower to a phrase |
1108
+ | `JL2001` | `first`/`single`/`last`, or `average`/`min`/`max`, over an empty sequence |
1109
+ | `JL2002` | `single`/`singleOrDefault` over two or more elements |
1110
+ | `JL2003` | `elementAt` out of range |
1111
+ | `JL2004` | a provider's `execute()` answered a promise on the synchronous surface |
1112
+ | `JL2005` | a push queue was fed after `end()` |
1113
+ | `JL2006` | a provider answered an element terminal with something other than one array |
1114
+
1115
+ Every message below is the one the chain raised when the spelling beside
1116
+ it was run, with the code prefix (`JL0005: `) removed. Where a refusal
1117
+ carries a `docPath`, it is appended to the message text as well
1118
+ (`… at /0/$where/$valid`).
1119
+
1120
+ ### 14.1 `JL0001` — the source
1121
+
1122
+ Dispatch happens ONCE, at `from()`/`fromAsync()` time, never at
1123
+ enumeration time: an `execute` duck is a provider and is never
1124
+ enumerated locally, any iterable gets the in-memory reference semantics,
1125
+ and anything else is refused before a single row is read.
1126
+
1127
+ | The spelling that trips it | The message | The spelling that works |
1128
+ |---|---|---|
1129
+ | `from(42)` | `from() needs an iterable or a provider exposing execute(document, options)` | an array, a string, a `Set`, a generator, or a provider |
1130
+ | `fromAsync(42)` | `fromAsync() needs an async iterable, an iterable, a cursor ({ next, return? }) or a push queue` | one of the five shapes §12 lists |
1131
+ | `fromAsync('abc')` | the same message | a string is a CHUNK on the async surface, not a character stream — feed it through `createPushQueue()`. `from('abc')` iterates characters, and the twins differ here by design (§12) |
1132
+
1133
+ ### 14.2 `JL0002` — the proxy left its capture
1134
+
1135
+ A recording proxy belongs to exactly ONE capture. The two conditions read
1136
+ alike and mean different things, so they carry different messages.
1137
+
1138
+ | The spelling that trips it | The message | The spelling that works |
1139
+ |---|---|---|
1140
+ | `let saved; from(rows).where((u) => { saved = u; return u.id.gt(0); }); from(rows).where(() => saved.id.gt(0))` | `an expression proxy escaped its capture callback; expressions cannot be stored and replayed across operators` | capture in the callback that uses it — the document would otherwise reference a binding this phrase does not have |
1141
+ | `from(rows).select((u) => ({ n: from(rows).where((v) => v.id.eq(u.id)).count() }))` | `an expression proxy of an enclosing capture was used inside a nested capture — a correlated subquery cannot be spelled this way (the inner document rebinds the item); compute the inner query first and use its result` | compute the inner query first, or — over a provider with a relation table — write the hop (§13.4), which is what a correlated phrase is |
1142
+
1143
+ Captures NEST legally: a chain built and run inside a callback
1144
+ (`select((u) => ({ n: from(other).count() }))`) is ordinary, because it
1145
+ touches none of the enclosing proxies. `===` between proxies is
1146
+ untrappable and therefore undetectable; do not compare proxies.
1147
+
1148
+ ### 14.3 `JL0003` — the schema operators need a compiler
1149
+
1150
+ `ofType` and `cast` emit `$valid` and `$assert`, and the query engine
1151
+ compiles those only against an injected type-test compiler. The refusal
1152
+ arrives when the document COMPILES, not when it is emitted (§13.7), and
1153
+ it carries the `docPath` of the operator that needed it.
1154
+
1155
+ | The spelling that trips it | The message | The spelling that works |
1156
+ |---|---|---|
1157
+ | `from(rows).ofType({ type: 'object' }).toArray()` | `ofType/cast compile schema operators, which need a type-test compiler — pass options.compileTypeTest to from()/fromDocument() (e.g. createTypeTestCompiler() from @jarenjs/validate/query)` — `docPath` `/0/$where/$valid` | `from(rows, { compileTypeTest: createTypeTestCompiler() })` |
1158
+ | `from(rows).cast({ type: 'object' }).toArray()` | the same message — `docPath` `/0/$return/$assert` | the same option |
1159
+
1160
+ The fix is one option on the source, and it is the same fix whether the
1161
+ operator came from `ofType`/`cast` or from a hand-written document: the
1162
+ engine's own `JQ0008` is re-reported under this code for that reason
1163
+ (§9).
1164
+
1165
+ ### 14.4 `JL0004` — the parameters
1166
+
1167
+ `.params({ … })` declares AND binds. Everything that can go wrong with a
1168
+ name or a value is one code, because the reader's next action is the same
1169
+ in every case: fix the `params()` call.
1170
+
1171
+ | The spelling that trips it | The message | The spelling that works |
1172
+ |---|---|---|
1173
+ | `from(rows).where((r, p) => r.tenant.eq(p.tenantId))` | `parameter 'tenantId' is not declared — declare it first: .params({ tenantId: value })` | declare it, as the message spells |
1174
+ | `from(rows).params(42)` | `params takes an object of name → value bindings` | an object literal |
1175
+ | `from(rows).params({ 'a-b': 1 })` | `'a-b' is not a valid parameter name` | an identifier: letters, digits and `_`, not starting with a digit |
1176
+ | `from(rows).params({ it: 1 })` | `'it' is reserved (the emitted document's own binding names: it, it2, acc, g, and r1, r2, … for relation hops)` | any other name |
1177
+ | `from(rows).params({ r1: 1 })` | the same message | `r`-plus-digits is reserved for the bindings a hop allocates (§13.4) |
1178
+ | `from(rows).params({ d: new Date() })` | `parameter 'd' is bound to a Date instance, which is not query data — convert it first (a Date to its ISO string or epoch number, a Map to an object, NaN or -0 to a number)` | `d: date.toISOString()` |
1179
+ | `from(rows).params({ z: -0 })` | `parameter 'z' is bound to -0, which is not query data — …` (the same tail) | `0`, or negate at query time — and note the message names `-0`, not the `0` its JSON text would suggest |
1180
+ | `a.params({ k: 1 }).join(b.params({ k: 2 }), …)` | `parameter 'k' is bound to different values by the two sides of join — one document carries one binding per name; bind it once, or rename one side` | bind it once on the outer side, or rename one |
1181
+
1182
+ A binding is not a captured constant: it becomes an external, and later a
1183
+ bound SQL parameter. A `Date` there would compare against nothing and
1184
+ answer `[]` with no error anywhere — which is why the check is at
1185
+ `params()` time and not at the boundary.
1186
+
1187
+ ### 14.5 `JL0005` — the build-time catch-all
1188
+
1189
+ The widest code the chain has, and deliberately one code: every condition
1190
+ under it is a defect in the chain as WRITTEN, found before anything runs.
1191
+ They group into four families.
1192
+
1193
+ **A value the document cannot carry.** The query data model is JSON
1194
+ (§5).
1195
+
1196
+ | The spelling that trips it | The message | The spelling that works |
1197
+ |---|---|---|
1198
+ | `select(() => NaN)` | `a captured expression cannot embed NaN — the query data model is JSON, which has no NaN or Infinity, and lenient serialization would fold it into null` | a finite number |
1199
+ | `select(() => Infinity)` | the same message, naming `Infinity` | a finite number, or a bound |
1200
+ | `select(() => -0)` | `a captured expression cannot embed -0 — it shares its JSON text with 0 while dividing to the opposite infinity, so a document holding it cannot be keyed, stored or compared faithfully; use 0, or negate at query time` | `0` |
1201
+ | `where((u) => u.at.eq(new Date()))` | `a captured expression cannot embed a Date instance — it carries no own enumerable members, so it would embed as {}. Convert it to query data first (a Date to its ISO string or epoch number, a Map to an object), or bind it through params().` | the ISO string, or `.params({ when })` |
1202
+ | `select(() => new Map())` | the same message, naming `Map` | a plain object |
1203
+ | `select(() => MyArray.from([1]))` | `a captured expression cannot embed an Array subclass instance — its behaviour is not expressible as query data` | a plain array |
1204
+ | `select(() => undefined)` | `a captured expression cannot embed an undefined value` | `null`, which IS a value (§5) — a callback that forgot its `return` is the usual cause |
1205
+
1206
+ `-0` is the one nobody guesses, and it is worth the sentence. It is
1207
+ JSON-representable by TEXT and not by value: `JSON.stringify(-0)` is
1208
+ `"0"`, so a document holding it round-trips to a different number, while
1209
+ `1 / -0` is `-Infinity` and `1 / 0` is `+Infinity`. A key built from it
1210
+ would not match itself, a stored document would not compare equal to the
1211
+ one that was written, and a cached compilation keyed by the document's
1212
+ text would serve the `0` query for the `-0` one. There is no spelling
1213
+ that preserves it, so there is no spelling that is allowed to.
1214
+
1215
+ **A stage in the wrong place, or an argument that is not one.**
1216
+
1217
+ | The spelling that trips it | The message | The spelling that works |
1218
+ |---|---|---|
1219
+ | `from(rows).thenBy((u) => u.id)` | `thenBy/thenByDescending must directly follow orderBy/orderByDescending` | an `orderBy` first — `thenBy` extends that clause, it does not open one |
1220
+ | `select((u) => u.age.add(1).all())` | `all() fans out a PATH ('$it.tags[*]'); it cannot follow an operator result` | `all()` on the path, then the operator |
1221
+ | `select((u) => u.posts.title)` (a to-many hop) | `.title is read off a to-many relation, which holds an array of related rows — fan them first (.all().title), index one (.at(0)), or aggregate the array` | `u.posts.all().title`, `u.posts.at(0).title` |
1222
+ | `from(rows).skip(-1)` | `skip takes a non-negative integer, got -1` | a non-negative integer |
1223
+ | `from(rows).take(1.5)` | `take takes a non-negative integer, got 1.5` | an integer |
1224
+ | `from(rows).where(42)` | `this operator takes a callback function` | a callback |
1225
+ | `from(rows).join([], …)` | `join takes another sequence as its inner side` | `from(sameSource)` |
1226
+ | `from(a).join(from(b), …)` | `join's other side must derive from the same source, or from two providers sharing one scope (one store's entity sets) — a query document reads one input; load both collections under one root, or join two entity sets of one store` | one source, or one store's two entity sets (§13.2) |
1227
+ | `from(rows).concat(42)` | `concat takes a sequence or a constant array` | a sequence over the same source, or an array |
1228
+ | `select((r) => r.v.all().rolling(spec))` where `spec` reads the row | `rolling() takes a plain literal spec object; it is read once when the query compiles, so it cannot be an expression or carry a captured value` | a literal spec |
1229
+
1230
+ **A provider or a document that is not shaped as the contract says.**
1231
+
1232
+ | The spelling that trips it | The message | The spelling that works |
1233
+ |---|---|---|
1234
+ | `from({ execute, root: 7 })` | `a provider's root is a path expression string ('$.Post[*]'), got number` | a path expression, or no `root` at all |
1235
+ | `createPushQueue({ highWaterMark: 0 })` | `highWaterMark must be a positive integer` | a positive integer (1024 by default) |
1236
+
1237
+ A malformed VERSION envelope is not this code: `fromDocument(rows,
1238
+ { $query: '0.2', $expr })` is compiled by the engine first, so the
1239
+ engine's own verdict is what surfaces — `JQ0006: unknown query format
1240
+ version "0.2"` — and a future document is never silently run as a 0.1
1241
+ one. Only a spelling the engine accepts and this version does not
1242
+ reaches `JL0005` (`a version envelope is exactly { $query: '0.1',
1243
+ $expr: … } (QUERY-FORMAT §4.1)`).
1244
+
1245
+ **The async surface's own rules** (§§10–12).
1246
+
1247
+ | The spelling that trips it | The message | The spelling that works |
1248
+ |---|---|---|
1249
+ | `fromAsync(rows).mapAsync(fn, {})` | `mapAsync requires { concurrency: <positive integer> } — an unbounded default is a denial of service waiting for a slow downstream` | `{ concurrency: 8 }` |
1250
+ | `mapAsync(fn, { concurrency: 2, mode: 'x' })` | `mapAsync mode must be one of parallel\|concat\|switch\|exhaust, got 'x'` | one of the four |
1251
+ | `mapAsync(42, { concurrency: 1 })` | `mapAsync takes an async callback` | a callback |
1252
+ | `fromAsync(rows).concat(fromAsync(rows))` | `concat on an async sequence takes a constant array — an async source cannot be re-iterated for a second sequence` | a constant array, or `concat` on the sync surface |
1253
+ | `fromAsync(rows).join(fromAsync(rows), …)` | `join on the async surface is pushed whole to a provider — it needs a provider source and comes before any mapAsync; over an iterable, a cursor or a push queue there is no join, because a single-pass source cannot be read twice (QUERY-PEN.md §10)` | join over a provider, join on the sync surface, or collect the stream first |
1254
+ | `fromAsync(rows).mapAsync(fn, { concurrency: 1 }).toDocument()` | `toDocument() cannot represent mapAsync (a host callback); explain() reports the split` | `explain()`, which reports `{ split: { pushed, residual } }` |
1255
+
1256
+ ### 14.6 `JL0006` — an operator §4 records as `unsupported`
1257
+
1258
+ Two operators, both refused by NAME rather than emulated wrongly. §16
1259
+ carries the reasons.
1260
+
1261
+ | The spelling that trips it | The message | The spelling that works |
1262
+ |---|---|---|
1263
+ | `from(rows).zip(other)` | `zip is unsupported: the query grammar has no positional co-iteration (see QUERY-PEN.md §4)` | there is none — index both sides and join on the index, in host code |
1264
+ | `from(rows).aggregate((acc, it) => …)` | `aggregate(fn) is unsupported: JSON cannot spell the implicit first element as a lambda seed — pass a seed, aggregate(seed, fn) (see QUERY-PEN.md §4)` | `aggregate(0, (acc, it) => acc.add(it.n))` |
1265
+
1266
+ ### 14.7 `JL0007` — a provider with several roots and none of its own
1267
+
1268
+ A store is a provider that serves many entity roots. `$[*]` over its
1269
+ entity map would answer every entity's rows mixed together, or count the
1270
+ SETS rather than the rows — an answer that looks like an answer. So the
1271
+ chain refuses at `from()` time and names the roots to chain over.
1272
+
1273
+ | The spelling that trips it | The message | The spelling that works |
1274
+ |---|---|---|
1275
+ | `from(store)` where the store serves `User` and `Post` | `this provider serves entity roots User, Post and has no root of its own — chain over one of them: from(store.entity(name)) (QUERY-PEN.md §8)` | `from(store.sync.entity('User'))` |
1276
+ | `fromAsync(store)` | the same message | `fromAsync(store.entity('User'))` |
1277
+
1278
+ `fromDocument` keeps its own rule and is not refused here: there the
1279
+ document IS the root, so there is nothing to choose.
1280
+
1281
+ ### 14.8 `JL0105` — a hop that cannot lower
1282
+
1283
+ A relation hop is lowered at CAPTURE into the correlated phrase §4
1284
+ spells. Four conditions have no phrase to lower to, and each names what
1285
+ is missing.
1286
+
1287
+ | The spelling that trips it | The message | The spelling that works |
1288
+ |---|---|---|
1289
+ | `u.labels` where `labels` is many-to-many | `'labels' is a many-to-many relation: the join table 'UserLabel' is not a queryable root in this version, so the hop has no phrase to lower to — read the memberships with load({ include: { labels: true } })` | `load({ include: { labels: true } })` through the client ([DB-CLIENT.md](DB-CLIENT.md) §2) |
1290
+ | a relation entry that is not a relation record | `the relation table names 'labels' but its entry is not a relation record ({ to, kind, via, fkEntity, fkTargets, targetKey } — MODEL-FORMAT §10.1)` | a provider whose `relations` is the store's own table |
1291
+ | a relation whose `kind` is neither of the two | `'labels' has relation kind 'oneToNone', which is not one this surface lowers (oneToOne, oneToMany)` | `oneToOne` or `oneToMany` |
1292
+ | a relation over a composite or undeclared key | `'labels' cannot lower: its foreign key or the key it references is composite or undeclared, and the hop's equality would need a tuple the vocabulary does not spell` | a single-column key, or `load({ include })` |
1293
+
1294
+ The first is the one a reader meets: a many-to-many member is exactly
1295
+ the relation that has no direction to correlate in. The hop would need
1296
+ to bind the JOIN TABLE as a root and correlate twice, and a join table
1297
+ is not a queryable root in this version — so there is no phrase, and an
1298
+ honest refusal that names the join table beats a document that quietly
1299
+ reads the wrong rows. `load({ include })` reads the memberships through
1300
+ the client instead, which is the operation the store already has.
1301
+
1302
+ ### 14.9 The runtime codes
1303
+
1304
+ `JL2001`–`JL2006` are raised while a terminal RUNS. The first three are
1305
+ the C# semantics, exactly (§6); the last three are the seam between a
1306
+ terminal and the provider behind it.
1307
+
1308
+ | The spelling that trips it | The message | The spelling that works |
1309
+ |---|---|---|
1310
+ | `from([]).first()` | `first() found no element` | `firstOrDefault()`, which answers `undefined` |
1311
+ | `from([]).single()` | `single() found no element` | `singleOrDefault(d)` |
1312
+ | `from([]).last()` | `last() found no element` | `lastOrDefault(d)` |
1313
+ | `from([]).average()` | `average() of an empty sequence` | guard with `any()`; `sum()` of nothing is `0` and `count()` of nothing is `0` |
1314
+ | `from([]).min()` | `min() of an empty sequence` | as above |
1315
+ | `from([]).max()` | `max() of an empty sequence` | as above |
1316
+ | `from([1, 2]).single()` | `single() found more than one element` | `first()`, or a narrower `where` |
1317
+ | `from([1, 2]).singleOrDefault(0)` | `singleOrDefault() found more than one element` | the default covers EMPTY, never ambiguity |
1318
+ | `from([1]).elementAt(5)` | `elementAt(5) is out of range` | `elementAtOrDefault(5, d)` |
1319
+ | `from(asyncProvider).toArray()` | `this provider's execute() answered a promise, and a Sequence terminal is a value — an asynchronous provider cannot back the synchronous surface. Emit the document with toDocument() and await the provider directly, or use a synchronous provider.` | `fromAsync(provider)` (§12), or `toDocument()` and await |
1320
+ | `q.end(); q.feed(1)` on a push queue | `feed() after end(): the push queue is closed and takes no more values` | feed before `end()`; `end(error)` fails the stream |
1321
+ | a provider answering `toArray()` with `undefined` | `the provider answered toArray() with undefined — an element terminal emits an array constructor, so a conforming execute() answers exactly one array (QUERY-PEN.md §8)` | answer the one array the window constructor yields (`[]` for none) |
1322
+
1323
+ `JL2004` is worth the sentence its message spends. The old seam let the
1324
+ promise through under the value's type: `count()` handed back a `Promise`
1325
+ typed `number`, and `first()` indexed the promise and returned
1326
+ `undefined`. A wrong answer with no error anywhere is worse than a slow
1327
+ one, so the synchronous surface refuses an asynchronous provider by name.
1328
+
1329
+ `JL2006` is the same argument one layer out: an element terminal emits
1330
+ `[window]` precisely so a single array-valued item cannot be confused
1331
+ with several items (§6), so a provider that answers anything but one
1332
+ array is named rather than indexed into a `TypeError`. `count()` and the
1333
+ other scalar terminals are not windowed and are not checked — a provider
1334
+ answering `3` there is answering correctly.
1335
+
1336
+ ## 15. The types
1337
+
1338
+ The declarations are HAND-AUTHORED, in `packages/linq/types/index.d.ts`
1339
+ — chosen over emitting them from JSDoc, so the implementation stays
1340
+ plain JavaScript and this file is the public type contract. The line it
1341
+ holds, stated in the README and at the top of the file:
1342
+
1343
+ > the common path is precisely typed; the exotic path is honestly
1344
+ > `unknown`; nothing is ever a WRONG type.
1345
+
1346
+ Every claim below has two pins. `test/consumer/types.ts` compiles it as
1347
+ a consumer would (`strict`, `skipLibCheck: false`, NodeNext — the chain's
1348
+ block runs from its `@jarenjs/linq` import to the end of the file), and
1349
+ `test/linq/types.test.js` is its runtime twin: the same spelling, asserted
1350
+ to emit and to answer what the type says it does. A claim with only one
1351
+ of the two is half a claim.
1352
+
1353
+ ### 15.1 The recording proxy is a type, not a shape
1354
+
1355
+ §3 describes what a proxy RECORDS; this is what it is declared as. The
1356
+ callback's first argument is `Expr<T>` — a conditional that picks the
1357
+ expression family from the element type, in an order that matters
1358
+ because the `DateTime` brand is a string subtype and must match first:
1359
+
1360
+ | The element is | The proxy is | It carries |
1361
+ |---|---|---|
1362
+ | a `DateTime`-branded string | `DateTimeExpr` | the whole §8.13 date family |
1363
+ | a `string` | `StringExpr` | comparison, the string operators, the spatial family (a geohash is a string) |
1364
+ | a `number` | `NumberExpr` | comparison and arithmetic |
1365
+ | a `boolean` | `BoolExpr` | `and`, `or`, `not` |
1366
+ | an array | `ArrayExpr<E>` | `all()`, `at()`, `count()`, `similarity()`, the §8.16 sequence operators |
1367
+ | an object | `ObjectExpr<T>` | exactly its members, recursively typed |
1368
+ | anything else | `UnknownExpr` | everything, precisely nothing |
1369
+
1370
+ `UnknownExpr` is the honest top and the whole reason the line above can
1371
+ be kept: where inference ends — a dynamic `get(name)`, a member read
1372
+ after an operator, an element the source never declared — the surface
1373
+ widens rather than guesses. `from(users).select((u) => u.get('odd key'))
1374
+ .first()` is `unknown`, and a caller who knows better narrows it
1375
+ themselves.
1376
+
1377
+ A member's type goes through `MemberExpr<V>`, which has one job: an
1378
+ `unknown` (or `any`) member answers `UnknownExpr` rather than the first
1379
+ arm `Expr<>` would otherwise pick for it. An OPTIONAL member is its
1380
+ non-nullable expression — `u.address.city` on `address?: { city: string }`
1381
+ is a `StringExpr`, and the projection's element type is `string` — because
1382
+ absence is a query-time fact (§4: an empty operand compares false,
1383
+ `exists()`/`isEmpty()` say which), not a type-level one.
1384
+
1385
+ Method names shadow member access on the proxy (§3), and the types say
1386
+ so: `u.count` is the aggregate, and the escape `u.get('count')` is
1387
+ declared to answer `UnknownExpr` because a dynamic key cannot be looked
1388
+ up in `T`.
1389
+
1390
+ ### 15.2 The sequence carries two type parameters
1391
+
1392
+ `Sequence<T, P>` and `AsyncSequence<T, P>`: `T` is the element, `P` the
1393
+ parameters declared so far.
1394
+
1395
+ - `select` re-types through `Unwrap<R>` — an expression by its `__value`
1396
+ phantom, an object or array literal recursively, a literal as itself —
1397
+ so `select((u) => ({ id: u.id, name: u.name }))` is
1398
+ `Sequence<{ id: number, name: string }>` with nothing written down.
1399
+ - `selectMany` unwraps and then takes the ELEMENT, one level, exactly as
1400
+ the runtime does (§13.6).
1401
+ - `groupBy` reseats to `Sequence<{ key: K | null, items: T[] }>` — the
1402
+ `| null` is the `$default` the emitted document carries.
1403
+ - `params<Q>(bindings: Q)` answers `Sequence<T, P & Q>`, and every
1404
+ callback's last argument is `ParamsExpr<P>`: exactly the declared
1405
+ names, each typed from its bound value. Reading an undeclared name is
1406
+ a compile error before it is `JL0004` (§14.4).
1407
+ - `mapAsync<R>` crosses to `AsyncSequence<Awaited<R>, P>`: the element
1408
+ becomes the callback's RESOLVED type, and every terminal becomes a
1409
+ promise.
1410
+ - `min()`/`max()` follow the operand family — a sequence of strings
1411
+ answers a string, everything else a number.
1412
+
1413
+ ### 15.3 A document is a document
1414
+
1415
+ `fromDocument<T = unknown>(source, document, options?)` infers NOTHING
1416
+ from the document it is handed: a query document is data, not a type,
1417
+ and there is no honest way to read an element type out of it. It answers
1418
+ `Sequence<unknown>` until the caller states otherwise
1419
+ (`fromDocument<User>(rows, saved)`), and so does a parsed JSON literal
1420
+ handed to `from()` — `from(JSON.parse(text)).toArray()` is `unknown[]`.
1421
+ The pens are the inference route: `ofType`/`cast` given a schema-pen
1422
+ builder re-type the sequence from the builder's own `Infer<>`
1423
+ (`ofType<S>(schema: SchemaBuilder<S, …>): Sequence<S, P>`), and a
1424
+ hand-written schema literal is caller-asserted with `unknown` as the
1425
+ default, because a JSON Schema is not a TypeScript type.
1426
+
1427
+ The same rule runs through the provider seam: `Provider<T>` carries an
1428
+ `__item` phantom, so a typed entity set infers its rows without a cast
1429
+ and an untyped provider is `unknown`.
1430
+
1431
+ ### 15.4 The exports that are not vocabulary
1432
+
1433
+ Five exports are surface a caller meets without ever calling:
1434
+
1435
+ | Export | Why a caller meets it |
1436
+ |---|---|
1437
+ | `Sequence` | to ANNOTATE (`function page(q: Sequence<User>)`). Its constructor is `private`: a sequence is built by `from`/`fromDocument`, never with `new` |
1438
+ | `AsyncSequence` | the same, for the asynchronous surface (`fromAsync`) |
1439
+ | `LinqBuildError` | `instanceof` on the build-time refusals — `code`, `reason` and `docPath` are declared readonly |
1440
+ | `LinqRuntimeError` | `instanceof` on the terminal-time refusals, same three members |
1441
+ | `LINQ_CODES` | the runtime code table §9 is held equal to; a `Readonly<Record<string, string>>` a host can render |
1442
+
1443
+ `DateTime` is a type-only export and costs nothing at run time: it is
1444
+ `string & { __jarenTag: 'date-time' }`, a marker that turns on the date
1445
+ family for a member without making every string a date.
1446
+
1447
+ **A refusal encoded in the types takes a `never` PARAMETER, not just a
1448
+ `never` return.** `zip(unsupported: never): never` makes both
1449
+ `from(rows).zip()` and `from(rows).zip(other)` compile errors. The
1450
+ return type alone does not: `zip(...args: never[])` refuses an argument
1451
+ and accepts none, so the one spelling a caller would actually write
1452
+ type-checked and failed at run time instead. A JavaScript caller still
1453
+ gets the coded refusal (`JL0006`, §14.6) — the encoding closes the
1454
+ TypeScript half, and `test/consumer/types.ts` pins both spellings with
1455
+ `@ts-expect-error`.
1456
+
1457
+ ## 16. What it cannot spell
1458
+
1459
+ §4's table records three constructs as `unsupported` — a status that
1460
+ means "throws a coded error naming the reason", never "emits something
1461
+ close". This section gathers them with their reasons, and adds the
1462
+ boundaries the package draws on purpose, so a reader can check each one
1463
+ rather than discover it.
1464
+
1465
+ ### 16.1 The three refused operators
1466
+
1467
+ | Construct | Why there is no emission | What it raises |
1468
+ |---|---|---|
1469
+ | `aggregate(fn)`, unseeded | C#'s unseeded overload means "the first element is the seed". A query document is data: there is no clause that says "start from whichever item comes first", and inventing one would make the document mean something the grammar does not define | `JL0006`, naming the seeded form |
1470
+ | `zip()` | positional co-iteration — pair the *n*th of one input with the *n*th of another — has no operator in the grammar, and a FLWOR phrase reads ONE input. Emulating it would mean materialising both sides in the host, which is exactly the "runs somewhere other than the document says" the chain exists to avoid | `JL0006` |
1471
+ | a many-to-many hop (`u.labels`) | the phrase would have to bind the JOIN TABLE as a root and correlate twice, and a join table is not a queryable root in this version | `JL0105`, naming the join table and pointing at `load({ include })` |
1472
+
1473
+ All three are checked in both directions: `test/linq/pen-docs.test.js`
1474
+ holds §14's code list equal to what the chain's modules throw, and the
1475
+ spellings above are the ones §14 shows raising them.
1476
+
1477
+ ### 16.2 The operators that are not on the surface at all
1478
+
1479
+ The C# operator set is larger than the query grammar, and the chain does
1480
+ not carry a method for an operator it cannot lower. `union`,
1481
+ `intersect`, `except`, `skipWhile`, `takeWhile`, `chunk`, `append`,
1482
+ `prepend`, `sequenceEqual`, `toDictionary` and `toLookup` are not
1483
+ declared and not defined — reaching for one is a plain `TypeError`, not a
1484
+ coded refusal, because there is no method to refuse from.
1485
+
1486
+ Two of them are compositions a reader can write today, and they are
1487
+ worth naming because the absence otherwise reads as a gap:
1488
+
1489
+ - **`Union`** is `.concat(other).distinct()` — `$seq` followed by
1490
+ `$distinct`, whose equality is the grammar's deep structural one.
1491
+ - **`Append`** is `.concat([value])`: a constant array's elements join
1492
+ the stream. There is no general `Prepend`, because `concat` appends;
1493
+ starting from the single-element source and concatenating the rest
1494
+ (`from([first]).concat(rest)`) works only when `rest` is a constant
1495
+ array.
1496
+
1497
+ The rest have no composition on this surface. `skipWhile`/`takeWhile`
1498
+ need a predicate-terminated window and `$subsequence` takes positions;
1499
+ `chunk` needs a windowing operator; `sequenceEqual`, `toDictionary` and
1500
+ `toLookup` are host-side shapes rather than query results — read the
1501
+ sequence and build them.
1502
+
1503
+ ### 16.3 The boundaries this package draws on purpose
1504
+
1505
+ Each of these is a design commitment, checkable in the source:
1506
+
1507
+ - **No `Function.prototype.toString`, anywhere.** A callback is executed
1508
+ ONCE against recording proxies; nothing parses its text. `grep -rn
1509
+ 'toString()' packages/linq/src/` finds none — the only `toString` in
1510
+ the package is a radix conversion escaping a control character in a
1511
+ path segment.
1512
+ - **No per-element callback evaluation.** A predicate runs at BUILD
1513
+ time, produces an expression, and the engine evaluates that expression
1514
+ per row. This is why a JavaScript operator inside a callback is a trap
1515
+ rather than a slow path (§3): `&&`, `||`, `!`, `?:`, `in`, `typeof`,
1516
+ `Object.keys` and `===` evaluate against the proxy and yield a
1517
+ silently wrong document, while `>` and `+` throw a plain `TypeError`.
1518
+ Use `.and()`, `.or()`, `.not()` and the comparison methods.
1519
+ - **No second grammar.** The chain emits the published query language
1520
+ and nothing else; `toDocument()` is compilable by a bare
1521
+ `compileJsonQuery` with no linq involvement, which is what makes a
1522
+ query loggable, storable, diffable and authorable by a constrained
1523
+ decoder.
1524
+ - **No inference from a document.** `fromDocument` and a parsed JSON
1525
+ literal answer `unknown` (§15.3). The pens are the inference route.
1526
+ - **No clock.** There is no `now()`: §8.13 has no clock operator, and a
1527
+ fluent surface does not get to add one. Bind the instant with
1528
+ `.params({ now })`.
1529
+ - **No `knn` method.** k-nearest is `orderByDescending(… similarity …)`
1530
+ then `take(k)` — the composition the emitted document already is (§4).
1531
+ - **No async query engine.** `packages/json` is strictly synchronous.
1532
+ `fromAsync` makes the SOURCE and the host boundary asynchronous and
1533
+ emits byte-identical documents (§10); there are no `selectAwait` or
1534
+ `whereAwait` variants, because a per-element async predicate is
1535
+ `mapAsync` then `where` (§11).
1536
+ - **No correlated subquery through a captured proxy.** An enclosing
1537
+ capture's proxy used inside a nested one is `JL0002` (§14.2) — the
1538
+ inner document rebinds `$it`. Over a provider with a relation table
1539
+ the correlated phrase has a spelling: the hop (§13.4).
1540
+ - **No non-JSON constant.** A `Date`, `Map`, `Set`, `RegExp`, class
1541
+ instance, `NaN`, `±Infinity` or `-0` in a captured expression is
1542
+ `JL0005`, and in a `params()` binding `JL0004` (§14.4, §14.5). The
1543
+ query data model is JSON, and a value that cannot survive the
1544
+ round-trip cannot be compared faithfully.
1545
+ - **No document form for a host callback.** A chain carrying `mapAsync`
1546
+ has no `toDocument()`; `explain()` reports `{ split: { pushed,
1547
+ residual } }` instead (§11). The split is stated rather than hidden,
1548
+ which is the same honesty a SQL pushdown owes its residual.
1549
+
1550
+ ### 16.4 When not to reach for the chain
1551
+
1552
+ The chain earns its place when a query has to TRAVEL — to a store, into a
1553
+ saved document, across a version. Where it does not, the honest answers
1554
+ are shorter:
1555
+
1556
+ - **The data is in memory and the query stays there.** `rows.filter()`
1557
+ and `rows.map()` are the language's own, need no import, and any
1558
+ JavaScript reader can follow them. A chain over an array buys one
1559
+ thing: a document you could have sent somewhere. If you are not going
1560
+ to send it, you are paying for a capture you never read.
1561
+ - **The query is one statement of SQL you already know.** A store takes
1562
+ raw statements. A reporting query with three joins and a window
1563
+ function is a statement; expressing it as a chain either does not
1564
+ translate (§4 records every such gap) or translates into something
1565
+ nobody can review against the original.
1566
+ - **The document already exists.** A saved `$query` is run with
1567
+ `fromDocument` (§13.8) or handed to the engine directly. Re-authoring
1568
+ it through the chain to "keep it typed" makes two spellings of one
1569
+ query, and the one that runs in production is whichever the deploy
1570
+ picked.
1571
+ - **The predicate needs JavaScript.** A callback runs ONCE, at build
1572
+ time, against a proxy — so `if`, `&&`, a loop, a call into a library
1573
+ and a closure over a mutable variable all either throw or record
1574
+ something you did not mean (§3). A predicate that genuinely needs the
1575
+ language is `mapAsync`'s host boundary (§11), and a chain that is
1576
+ mostly host boundary is a program with a `where` at the front.
1577
+ - **You want a type, not a query.** `ofType` and `cast` narrow a
1578
+ sequence's element type; neither validates unless a compiler was
1579
+ handed in (§13.7, §14.3). A chain reached for as a type assertion is a
1580
+ cast with extra steps — `from(rows)` already answers
1581
+ `Sequence<unknown>` and `as` is the language's own spelling.
1582
+
1583
+ ## 17. Cost
1584
+
1585
+ A consumer importing `from` from `@jarenjs/linq` and calling one
1586
+ terminal bundles **<!--fact:bundle.chain-->173,080<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
1587
+ `platform: 'neutral'`). The figure is measured by
1588
+ `scripts/check-tree-shaking.js`'s chain probe and compared with this
1589
+ section on every `npm run test:tree-shaking`: it is derived, never typed,
1590
+ and a stale one is red here rather than wrong in a document somebody
1591
+ reads.
1592
+
1593
+ Of that, **<!--fact:bundle.chain.own-->39,025<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
1594
+ `async.js`, `expression.js`, `document.js`, `provider.js`,
1595
+ `concurrency.js`, `errors.js` and `schema-of.js`. The remaining ~134 kB
1596
+ is the query ENGINE and the core it stands on: a chain's document has to
1597
+ run somewhere, and the in-memory runner is the reference semantics every
1598
+ provider is measured against (§8). A consumer that only ever hands
1599
+ `toDocument()` to a provider still pays it today, because the terminal
1600
+ that emits the document is the same terminal that would run it.
1601
+
1602
+ The probe asserts four exclusions, and they are the cost claims worth
1603
+ making:
1604
+
1605
+ - **no schema-pen module** — `ofType`/`cast` reach a builder through a
1606
+ registry symbol looked up by key (`schema-of.js`), so the chain
1607
+ imports nothing from `src/schema/`;
1608
+ - **no client module** — `src/db/` is the package's one runtime edge and
1609
+ is not on this path;
1610
+ - **not one byte of `@jarenjs/db`, `@jarenjs/validate` or
1611
+ `@jarenjs/formats`** — the client's optional peers. A consumer of the
1612
+ chain alone installs nothing new;
1613
+ - **no pen bytes at all**, in either direction: the pens carry no chain
1614
+ module either, which is what keeps a <!--fact:bundle.jslt.kb-->19<!--/fact--> kB JSLT
1615
+ pen <!--fact:bundle.jslt.kb-->19<!--/fact--> kB.
1616
+
1617
+ `docs/CONSUMING.md` states the rounded price of all ten subpaths in one
1618
+ table, each figure held equal to the same measurements. Two of its rows
1619
+ are the ones to read together: the chain at <!--fact:bundle.chain.kb-->173<!--/fact--> kB and
1620
+ `./db` at <!--fact:bundle.db.kb-->478<!--/fact--> kB.
1621
+ The client costs what the store costs, by construction, and the chain
1622
+ costs what running a query costs.
1623
+
1624
+ **Taking a pen as well costs less than the two figures suggest**, and
1625
+ the reason is worth knowing: a bundler counts a shared module once, and
1626
+ the chain and every pen share the expression capture (`expression.js`)
1627
+ and the coded errors under it (`errors.js`, and `@jarenjs/core`'s error
1628
+ and object helpers). A consumer importing the chain AND the schema pen
1629
+ bundles **<!--fact:bundle.chain.withSchemaPen-->194,155<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->11,352<!--/fact--> bytes** less than the sum of the
1630
+ figure above and [SCHEMA-PEN.md](SCHEMA-PEN.md#7-cost) §7's, which is
1631
+ what those shared modules weigh. The probe measures that pair too, so
1632
+ the saving is derived like everything else here. What the chain does NOT
1633
+ share with a pen is the pens' own two shared doors, `capture-root.js`
1634
+ and `json-boundary.js`: no chain callback reaches either, and neither is
1635
+ in the figure above. Every pen document's §7 carries its own
1636
+ subpath's figure; nothing here restates one.