@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.
- package/ARCHITECTURE.md +217 -0
- package/README.md +559 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1217 -0
- package/docs/DB-CLIENT.md +814 -0
- package/docs/FLOW-PEN.md +1026 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +771 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1083 -0
- package/docs/QUERY-PEN.md +1636 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +255 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +260 -0
- package/src/app/index.js +20 -0
- package/src/app/patch.js +277 -0
- package/src/app/sub.js +106 -0
- package/src/async.js +329 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +9 -4
- package/src/contract/define.js +269 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +342 -0
- package/src/db/handle.js +86 -0
- package/src/db/include.js +316 -0
- package/src/db/index.js +19 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +82 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +69 -6
- package/src/expression.js +437 -36
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +302 -0
- package/src/flow/fsm.js +328 -0
- package/src/flow/index.js +22 -0
- package/src/forms/index.js +43 -0
- package/src/forms/rules.js +170 -0
- package/src/forms/submit.js +177 -0
- package/src/index.js +4 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +207 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +323 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +248 -0
- package/src/model/collection.js +171 -0
- package/src/model/define.js +125 -0
- package/src/model/entity.js +307 -0
- package/src/model/index.js +47 -0
- package/src/model/relation.js +85 -0
- package/src/provider.js +137 -20
- package/src/schema/brand.js +31 -0
- package/src/schema/builders.js +526 -0
- package/src/schema/check.js +29 -0
- package/src/schema/emit.js +394 -0
- package/src/schema/factories.js +239 -0
- package/src/schema/index.js +37 -0
- package/src/schema-of.js +24 -0
- package/src/sequence.js +233 -103
- package/src/sources.js +10 -3
- package/types/app.d.ts +293 -0
- package/types/contract.d.ts +371 -0
- package/types/db.d.ts +188 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +231 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +493 -0
- 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.
|