@jarenjs/linq 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/docs/LINQ-FORMAT.md +382 -0
- package/package.json +53 -0
- package/src/async.js +599 -0
- package/src/concurrency.js +213 -0
- package/src/document.js +212 -0
- package/src/errors.js +94 -0
- package/src/expression.js +320 -0
- package/src/index.js +14 -0
- package/src/provider.js +166 -0
- package/src/sequence.js +543 -0
- package/src/sources.js +98 -0
- package/types/index.d.ts +430 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @jarenjs/linq
|
|
2
|
+
|
|
3
|
+
A C#-familiar fluent query surface whose output is a **plain JSON
|
|
4
|
+
query document**. You write `from(users).where(u =>
|
|
5
|
+
u.age.gt(21)).orderBy(u => u.name)`; what exists afterwards is data —
|
|
6
|
+
inspectable, serializable, executable by the `@jarenjs/json` engine in
|
|
7
|
+
memory, streamed over a cursor, or pushed into a database by any
|
|
8
|
+
provider. The chain is the pen; the document is the deliverable.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { from } from '@jarenjs/linq';
|
|
12
|
+
|
|
13
|
+
const adults = from(users)
|
|
14
|
+
.where((u) => u.age.gt(21))
|
|
15
|
+
.orderBy((u) => u.name)
|
|
16
|
+
.select((u) => ({ id: u.id, name: u.name }));
|
|
17
|
+
|
|
18
|
+
adults.toArray(); // runs in memory, deferred until now
|
|
19
|
+
adults.toDocument(); // { $for: { it: '$[*]' }, $where: { $gt: ['$it.age', 21] }, … }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**The C# comparison, stated honestly.** The operator names, deferred
|
|
23
|
+
execution and the query-as-data idea are LINQ's. What differs: capture
|
|
24
|
+
is a recording proxy, never source-text inspection, so callbacks must
|
|
25
|
+
use the expression surface (`u.age.gt(21)`, not `u.age > 21` — a
|
|
26
|
+
JavaScript proxy cannot overload `>`); the operator vocabulary is the
|
|
27
|
+
query engine's, closed and documented in the
|
|
28
|
+
[mapping table](docs/LINQ-FORMAT.md); and `IQueryable`'s role is
|
|
29
|
+
played by the provider seam below.
|
|
30
|
+
|
|
31
|
+
- **Deferred and immutable.** A `Sequence` holds a stage list; nothing
|
|
32
|
+
runs until a terminal (`toArray`, `first`, `count`, …). Every
|
|
33
|
+
operator returns a new sequence.
|
|
34
|
+
- **Typed where it counts.** Hand-authored declarations type the
|
|
35
|
+
common path precisely and degrade to honest `unknown` — never a
|
|
36
|
+
wrong type — with runtime twins pinning every claim.
|
|
37
|
+
- **The async story (the obvious objection, answered).** `fromAsync`
|
|
38
|
+
runs the SAME operator set over cursors and streams — async is a
|
|
39
|
+
boundary, not a colour (the same chain emits a byte-identical
|
|
40
|
+
document through both drivers, test-pinned). Element-wise async
|
|
41
|
+
work happens in exactly one place, `mapAsync`, with a REQUIRED
|
|
42
|
+
concurrency bound and the `parallel`/`concat`/`switch`/`exhaust`
|
|
43
|
+
vocabulary; barrier operators buffer and run through the one engine
|
|
44
|
+
so streaming answers equal in-memory answers by construction.
|
|
45
|
+
- **The provider contract.** Any object with
|
|
46
|
+
`execute(queryDocument, { externals })` is a provider.
|
|
47
|
+
`@jarenjs/db` implements it — a chain over a SQLite-backed
|
|
48
|
+
collection pushes to SQL with no import edge in either direction.
|
|
49
|
+
`mapAsync` splits a provider chain into a pushed prefix and a local
|
|
50
|
+
residual, and `explain()` shows the split.
|
|
51
|
+
|
|
52
|
+
## What this is not
|
|
53
|
+
|
|
54
|
+
Not an ORM — entities, storage and migrations live in `@jarenjs/db`.
|
|
55
|
+
Not expression trees over arbitrary methods — the vocabulary is the
|
|
56
|
+
query engine's, and a construct outside it fails loudly at build time
|
|
57
|
+
with a coded error (`JL0001`–`JL0006`) rather than guessing. Not a
|
|
58
|
+
general lazy-iterable library — if you don't want a query document,
|
|
59
|
+
you don't want this package.
|
|
60
|
+
|
|
61
|
+
The normative mapping — every operator, its emitted phrase, and the
|
|
62
|
+
deliberate deviations — is [docs/LINQ-FORMAT.md](docs/LINQ-FORMAT.md);
|
|
63
|
+
internals are in [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# The Jaren LINQ surface (normative)
|
|
2
|
+
|
|
3
|
+
Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
|
|
4
|
+
interpreted as described in RFC 2119.
|
|
5
|
+
|
|
6
|
+
## 1. Scope
|
|
7
|
+
|
|
8
|
+
`@jarenjs/linq` is a fluent front-end to the Jaren JSON Query language
|
|
9
|
+
([QUERY-FORMAT.md](../../json/docs/QUERY-FORMAT.md)): a C#-familiar
|
|
10
|
+
method chain whose expressions are CAPTURED as plain query documents
|
|
11
|
+
and executed deferred — over any iterable in memory, or by any
|
|
12
|
+
**provider** exposing `execute(document, options)` (§8). The builder
|
|
13
|
+
emits the query language and nothing else; there is no second grammar,
|
|
14
|
+
no private protocol, and no `Function.prototype.toString` anywhere.
|
|
15
|
+
|
|
16
|
+
What this package is NOT: it is not an ORM (storage is `@jarenjs/db`'s
|
|
17
|
+
job), it does not evaluate JavaScript callbacks per element (callbacks
|
|
18
|
+
run ONCE, at build time, against recording proxies), and it promises
|
|
19
|
+
nothing the query grammar cannot express — §4 records every such gap
|
|
20
|
+
as `unsupported`, by name.
|
|
21
|
+
|
|
22
|
+
## 2. The surface
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { from } from '@jarenjs/linq';
|
|
26
|
+
|
|
27
|
+
const adults = from(users)
|
|
28
|
+
.where((u) => u.age.gt(21))
|
|
29
|
+
.orderBy((u) => u.name)
|
|
30
|
+
.select((u) => ({ id: u.id, name: u.name }));
|
|
31
|
+
|
|
32
|
+
adults.toArray(); // executes in memory
|
|
33
|
+
adults.toDocument(); // the SAME query, as one JSON document:
|
|
34
|
+
// { "$for": { "it": "$[*]" },
|
|
35
|
+
// "$where": { "$gt": ["$it.age", 21] },
|
|
36
|
+
// "$orderby": { "$key": "$it.name" },
|
|
37
|
+
// "$return": { "id": "$it.id", "name": "$it.name" } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `from(source, options?)` — `source` is any iterable (arrays,
|
|
41
|
+
strings, generators, Sets…) or a provider (§8); anything else is
|
|
42
|
+
`JL0001` at `from()` time, never at enumeration time.
|
|
43
|
+
`options.compileTypeTest` enables the schema operators behind
|
|
44
|
+
`ofType`/`cast` (§4); absent, those two are `JL0003` with the fix in
|
|
45
|
+
the message.
|
|
46
|
+
- `fromDocument(source, document, options?)` — attach a hand-written
|
|
47
|
+
or stored query document; its result is the item sequence and every
|
|
48
|
+
operator chains over it (a `{$query, $expr}` envelope is unwrapped).
|
|
49
|
+
- Every operator returns a NEW immutable sequence (§5); terminal
|
|
50
|
+
operations execute (§6).
|
|
51
|
+
|
|
52
|
+
## 3. Expression capture
|
|
53
|
+
|
|
54
|
+
A predicate or projection callback receives a **recording proxy** per
|
|
55
|
+
binding (and the parameters proxy last, §7). Member access records a
|
|
56
|
+
path segment; a method call records an operator; the callback's return
|
|
57
|
+
value becomes the expression:
|
|
58
|
+
|
|
59
|
+
- `u.a.b` records the path `$it.a.b`; `u.list.at(0)` records
|
|
60
|
+
`$it.list[0]` (negative integers count from the end);
|
|
61
|
+
`u.list.all()` records `$it.list[*]`; a key that is not an
|
|
62
|
+
identifier — or one that collides with a method name — goes through
|
|
63
|
+
`u.get('odd key')`.
|
|
64
|
+
- Method names SHADOW member access: `u.eq` is the operator, never
|
|
65
|
+
the member. `u.get('eq')` reaches the member.
|
|
66
|
+
- Returned object literals become constructors: plain-keyed objects
|
|
67
|
+
are Rule 1 map constructors, arrays are Rule 3 array constructors,
|
|
68
|
+
and a data object with `$`-prefixed keys embeds through `$map`.
|
|
69
|
+
Literal strings embed with the `$$` escape when they start with
|
|
70
|
+
`$`; plain data trees embed as `$const`.
|
|
71
|
+
- A proxy belongs to exactly ONE capture. Storing one and replaying
|
|
72
|
+
it into a later operator is `JL0002` — the emitted document would
|
|
73
|
+
silently reference the wrong binding, so the build fails instead.
|
|
74
|
+
(`===` between proxies is untrappable and therefore undetectable;
|
|
75
|
+
do not compare proxies.)
|
|
76
|
+
- The item binding is always named `it` in the emitted document
|
|
77
|
+
(nested phrases shadow it legally), so captured expressions read
|
|
78
|
+
`$it.…` at every depth and the document stays hand-readable.
|
|
79
|
+
|
|
80
|
+
## 4. The mapping table
|
|
81
|
+
|
|
82
|
+
Status vocabulary: **native** (emits the named construct), **emulated**
|
|
83
|
+
(emits a composition with identical semantics), **unsupported** (throws
|
|
84
|
+
a coded error naming the reason — an honest row beats a silently wrong
|
|
85
|
+
emission). The *typing* column is the intended TypeScript signature
|
|
86
|
+
shipped by the typed surface order; an operator whose signature cannot
|
|
87
|
+
be written is an operator whose runtime shape is wrong, so the column
|
|
88
|
+
is part of THIS design.
|
|
89
|
+
|
|
90
|
+
| C# / LINQ | Emission | Status | Typing (element `T`) |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `Where` | FLWOR `$where` | native | `(e: Expr<T>) => Expr<boolean>` → `Seq<T>` |
|
|
93
|
+
| `Select` | `$return` constructor | native | `(e: Expr<T>) => Expr<R>` → `Seq<R>` |
|
|
94
|
+
| `SelectMany` | `$return` (a multi-item projection flattens per tuple) | native | `(e: Expr<T>) => Expr<R[]>` → `Seq<R>` |
|
|
95
|
+
| `OrderBy` / `OrderByDescending` | `$orderby` key spec (`$dir`; `$empty`/`$collation` via `options`) | native | `(e: Expr<T>) => Expr<K>` → `Seq<T>` |
|
|
96
|
+
| `ThenBy` / `ThenByDescending` | appended `$orderby` spec; must directly follow `orderBy*` (`JL0005`) | native | as `OrderBy` |
|
|
97
|
+
| `GroupBy` | `$groupby`; downstream items are `{ key, items }` | native | `(e: Expr<T>) => Expr<K>` → `Seq<{key: K, items: T[]}>` |
|
|
98
|
+
| `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. Both sides MUST derive from the same source in 0.1 (`JL0005`): a query document reads one input; the relational order lifts this | native | `(inner: Seq<U>, ok, ik, (o: Expr<T>, i: Expr<U>) => Expr<R>)` → `Seq<R>` |
|
|
99
|
+
| `GroupJoin` | projection over a correlated inner phrase (the matching group as an expression: `(u, g) => ({ n: g.count() })`); same-source rule as `Join` | emulated | `(inner: Seq<U>, ok, ik, (o: Expr<T>, g: Expr<U[]>) => Expr<R>)` → `Seq<R>` |
|
|
100
|
+
| `Skip` / `Take` | `$subsequence` | native | `(n: number)` → `Seq<T>` |
|
|
101
|
+
| `Distinct` | `$distinct` (deep structural equality — the grouping relation) | native | `()` → `Seq<T>` |
|
|
102
|
+
| `Reverse` | `$reverse` | native | `()` → `Seq<T>` |
|
|
103
|
+
| `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) |
|
|
104
|
+
| `Any()` | `$exists` | native | `(): boolean` |
|
|
105
|
+
| `Any(pred)` / `All(pred)` | `$some` / `$every` quantifier phrase | native | `(pred): boolean` (`all` vacuously true on empty) |
|
|
106
|
+
| `Aggregate(seed, fn)` | `$fold` — the accumulator clause | native | `(seed: A, (acc: Expr<A>, e: Expr<T>) => Expr<A>): A` |
|
|
107
|
+
| `Aggregate(fn)` (unseeded) | — JSON cannot spell "the implicit first element" as a lambda seed | unsupported (`JL0005`) | — |
|
|
108
|
+
| `First` / `FirstOrDefault` | `[ $subsequence [expr, 0, 1] ]` window | native | `(): T` (`JL2001` on empty) / `(d?): T \| D` |
|
|
109
|
+
| `Single` / `SingleOrDefault` | `[ $subsequence [expr, 0, 2] ]` window | native | `(): T` (`JL2001`/`JL2002`) / `(d?): T \| D` (`JL2002` on 2+) |
|
|
110
|
+
| `Last` / `LastOrDefault` | `[ $subsequence [$reverse expr, 0, 1] ]` | native | as `First` |
|
|
111
|
+
| `ElementAt` / `ElementAtOrDefault` | `[ $subsequence [expr, i, 1] ]` | native | `(i): T` (`JL2003` out of range) / `(i, d?)` |
|
|
112
|
+
| `Concat` | `$seq` (a constant array's elements join the stream) | native | `(other: Seq<T> \| T[])` → `Seq<T>` |
|
|
113
|
+
| `DefaultIfEmpty` | `$default` | native | `(fallback?: T)` → `Seq<T>` |
|
|
114
|
+
| `OfType<S>` | `$valid` filter with a JSON Schema literal | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
|
|
115
|
+
| `Cast<S>` | `$assert` per item | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
|
|
116
|
+
| `Zip` | — no positional co-iteration in the grammar | unsupported (`JL0006`) | — |
|
|
117
|
+
| 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); `year month day epoch` → §8.13; `exists isEmpty`; `at all get` | native | on `Expr<…>`, per the typed-surface order |
|
|
118
|
+
| spatial family (§8.14) | — surface deferred to the relational order; hand-write the document (`fromDocument`) today | unsupported (`JL0006`-adjacent: no methods exist yet) | — |
|
|
119
|
+
|
|
120
|
+
## 5. Deferred execution and re-enumeration
|
|
121
|
+
|
|
122
|
+
Every operator returns a new immutable `Sequence`; NOTHING runs until a
|
|
123
|
+
terminal operation. A sequence may be enumerated repeatedly and **each
|
|
124
|
+
enumeration re-reads the source** — the C# contract, and the one that
|
|
125
|
+
surprises people:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const rows = [1, 2, 3];
|
|
129
|
+
const q = from(rows).where((n) => n.gt(1));
|
|
130
|
+
q.toArray(); // [2, 3]
|
|
131
|
+
rows.push(4);
|
|
132
|
+
q.toArray(); // [2, 3, 4] — the source was read AGAIN
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The compiled query is shared through a bounded cache keyed by the
|
|
136
|
+
document's COMPLETE structural identity (`semanticKey`), so
|
|
137
|
+
re-enumeration is cheap without pretending the results are frozen.
|
|
138
|
+
A 32-bit fingerprint would not do here: it collides after tens of
|
|
139
|
+
thousands of documents, and a collision means one query runs another
|
|
140
|
+
query's compiled program — wrong rows, cache hit reported, nothing said.
|
|
141
|
+
`for…of` a sequence iterates `toArray()`'s result (one enumeration per
|
|
142
|
+
loop).
|
|
143
|
+
|
|
144
|
+
**`toDocument()` is a deep snapshot.** A sequence is immutable, so the
|
|
145
|
+
document it hands out is an independent tree: writing into a returned
|
|
146
|
+
document cannot change what a later enumeration answers.
|
|
147
|
+
|
|
148
|
+
**A `null` a callback returns is a VALUE, not an absent clause.**
|
|
149
|
+
`where(() => null)` filters everything out (null is not true),
|
|
150
|
+
`select(() => null)` projects nulls, `groupBy(() => null)` is one
|
|
151
|
+
null-keyed group, and a null seed still folds. The emitted document
|
|
152
|
+
carries the clause with its null in place.
|
|
153
|
+
|
|
154
|
+
**A captured constant crosses a real JSON boundary.** The query data
|
|
155
|
+
model is JSON, so a `Date`, `Map`, `Set`, `RegExp` or class instance is
|
|
156
|
+
refused (`JL0005`) rather than embedded — `Object.keys` reports nothing
|
|
157
|
+
for them, so they would embed as `{}` and the query would compare against
|
|
158
|
+
an empty object. `NaN` and `±Infinity` are refused for the same reason
|
|
159
|
+
(JSON has neither, and lenient serialization folds them into `null`), and
|
|
160
|
+
so is `-0`, which shares its JSON text with `0` while dividing to the
|
|
161
|
+
opposite infinity. Convert first — a `Date` to its ISO string or epoch
|
|
162
|
+
number — or bind through `params()`.
|
|
163
|
+
|
|
164
|
+
## 6. Terminal semantics
|
|
165
|
+
|
|
166
|
+
The real C# semantics, because getting these wrong is how a
|
|
167
|
+
"LINQ-like" library becomes lodash with different names:
|
|
168
|
+
|
|
169
|
+
- `first()` on empty throws `JL2001`; `firstOrDefault(d)` returns `d`
|
|
170
|
+
(or `undefined` when omitted).
|
|
171
|
+
- `single()` on empty throws `JL2001`; on two-or-more throws `JL2002`;
|
|
172
|
+
`singleOrDefault(d)` throws on two-or-more and returns `d` on empty.
|
|
173
|
+
- `last()`/`lastOrDefault(d)` mirror `first` over the reversed window.
|
|
174
|
+
- `elementAt(i)` out of range throws `JL2003`;
|
|
175
|
+
`elementAtOrDefault(i, d)` returns `d`.
|
|
176
|
+
- `average()`, `min()` and `max()` over an empty sequence throw
|
|
177
|
+
`JL2001` (C# `InvalidOperationException`); `sum()` of nothing is `0`;
|
|
178
|
+
`count()` of nothing is `0`.
|
|
179
|
+
- `any()` is existence; `all(pred)` is vacuously true over the empty
|
|
180
|
+
sequence.
|
|
181
|
+
|
|
182
|
+
Element terminals emit their window inside an ARRAY constructor
|
|
183
|
+
(`[ … ]`), so the engine's result mapping (`undefined | item | items`)
|
|
184
|
+
can never confuse "one array-valued item" with "several items" — the
|
|
185
|
+
window array is always the single result and its elements are read
|
|
186
|
+
positionally.
|
|
187
|
+
|
|
188
|
+
## 7. Parameters
|
|
189
|
+
|
|
190
|
+
`.params({ tenantId })` declares AND binds externals; a callback reads
|
|
191
|
+
them through its last argument:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
from(rows)
|
|
195
|
+
.params({ tenantId: 'a7' })
|
|
196
|
+
.where((r, p) => r.tenant.eq(p.tenantId))
|
|
197
|
+
.toDocument();
|
|
198
|
+
// { "$for": { "it": "$[*]" }, "$where": { "$eq": ["$it.tenant", "$tenantId"] }, "$return": "$it" }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The emitted document carries `$tenantId` as an external parameter
|
|
202
|
+
(QUERY-FORMAT §9) — the seam that later becomes a bound SQL parameter.
|
|
203
|
+
Undeclared use is `JL0004` at BUILD time with the fix in the message
|
|
204
|
+
(the engine would say JQ0005 at compile time; earlier and clearer
|
|
205
|
+
wins). The names `it`, `it2`, `acc` and `g` are RESERVED — they are the
|
|
206
|
+
emitted document's own binding names — and declaring them is `JL0004`.
|
|
207
|
+
|
|
208
|
+
## 8. The provider contract
|
|
209
|
+
|
|
210
|
+
A **provider** is any object exposing:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
execute(queryDocument, options) -> undefined | item | items[]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `queryDocument` arrives WHOLE — a terminal hands over the full
|
|
217
|
+
emitted document (including the terminal's own wrapper, §6);
|
|
218
|
+
nothing is enumerated locally, ever.
|
|
219
|
+
- `options.externals` is the `{ name: value }` record of bound
|
|
220
|
+
parameters (§7).
|
|
221
|
+
- The return value uses the ENGINE's result mapping
|
|
222
|
+
(`undefined` = empty, a single item as itself, several items as an
|
|
223
|
+
array) — the in-memory runner is the reference semantics every
|
|
224
|
+
provider MUST match, and it implements this same interface.
|
|
225
|
+
- **`execute` is SYNCHRONOUS.** A `Sequence` terminal is a value —
|
|
226
|
+
`toArray(): T[]`, `count(): number` — so a promise cannot be returned
|
|
227
|
+
under that type. A provider that answers one is refused with `JL2004`
|
|
228
|
+
at the seam, because the alternative is not a slow answer but a wrong
|
|
229
|
+
one: the promise came back typed as the value, `count()` handed a
|
|
230
|
+
`Promise` to arithmetic, and `first()` indexed the promise and returned
|
|
231
|
+
`undefined`. An asynchronous provider (a wasm/OPFS driver) is reached
|
|
232
|
+
by emitting `toDocument()` and awaiting the provider directly.
|
|
233
|
+
|
|
234
|
+
`@jarenjs/db` implements this contract without either package
|
|
235
|
+
importing the other; a test double proves the document arrives whole.
|
|
236
|
+
|
|
237
|
+
### 8.1 Compilation registries
|
|
238
|
+
|
|
239
|
+
`from(source, options)` and `fromDocument(source, doc, options)` take the
|
|
240
|
+
engine's own compile options, so a document that is expressible is also
|
|
241
|
+
executable in memory:
|
|
242
|
+
|
|
243
|
+
| option | what it enables |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `compileTypeTest` | `ofType`/`cast` (the schema operators) |
|
|
246
|
+
| `collations` | `orderBy(…, { collation })` — a `nl` sort is `JQ0010` without it |
|
|
247
|
+
| `functions` | `$call` in a hand-written or saved document |
|
|
248
|
+
| `pathFunctions` | custom RFC 9535 path function extensions |
|
|
249
|
+
| `limits` | step, sequence and result bounds — the reason a SAVED document can be run at all |
|
|
250
|
+
| `registry` | an explicit cache-partition key, when the hooks above are rebuilt per call |
|
|
251
|
+
|
|
252
|
+
Compiled documents are cached per registry COMBINATION, not per document
|
|
253
|
+
alone: the same document compiles to different code with and without a
|
|
254
|
+
collation registry, so sharing one partition would answer a caller who
|
|
255
|
+
passed no collations with the compiled-with version.
|
|
256
|
+
|
|
257
|
+
## 9. Error codes
|
|
258
|
+
|
|
259
|
+
Build errors (`LinqBuildError`; `docPath` where a document position
|
|
260
|
+
exists):
|
|
261
|
+
|
|
262
|
+
| Code | Condition |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `JL0001` | `from()` received neither an iterable nor a provider |
|
|
265
|
+
| `JL0002` | an expression proxy escaped its capture callback |
|
|
266
|
+
| `JL0003` | `ofType`/`cast` need an injected `compileTypeTest` |
|
|
267
|
+
| `JL0004` | an undeclared or reserved parameter name was used |
|
|
268
|
+
| `JL0005` | an operator was used invalidly at build time |
|
|
269
|
+
| `JL0006` | an unsupported operator was invoked |
|
|
270
|
+
|
|
271
|
+
Runtime errors (`LinqRuntimeError`):
|
|
272
|
+
|
|
273
|
+
| Code | Condition |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `JL2001` | `first`/`single` found no element |
|
|
276
|
+
| `JL2002` | `single` found more than one element |
|
|
277
|
+
| `JL2003` | `elementAt` is out of range |
|
|
278
|
+
| `JL2004` | an asynchronous provider cannot back the synchronous surface |
|
|
279
|
+
|
|
280
|
+
Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
|
|
281
|
+
through unwrapped — they already carry their own code and `docPath`.
|
|
282
|
+
|
|
283
|
+
## 10. The asynchronous surface: streaming and barriers
|
|
284
|
+
|
|
285
|
+
`fromAsync(source, options?)` gives the SAME operator surface over
|
|
286
|
+
async sources, emitting the SAME query documents — the same chain
|
|
287
|
+
through `from` and `fromAsync` MUST emit byte-identical documents (the
|
|
288
|
+
one-operator-set proof) — with terminals returning promises. The rule:
|
|
289
|
+
**the pipeline is synchronous, the boundaries are async.** A compiled
|
|
290
|
+
query never awaits; what is asynchronous is where rows come from and
|
|
291
|
+
where element-wise host work happens (§11).
|
|
292
|
+
|
|
293
|
+
Per operator, whether it STREAMS (per-item evaluation, flat memory) or
|
|
294
|
+
is a BARRIER (materialises the stream so far and runs the maximal run
|
|
295
|
+
of document stages through the engine over the buffer — inherent,
|
|
296
|
+
because the engine itself materialises for `$orderby`/`$groupby`):
|
|
297
|
+
|
|
298
|
+
| Operator | Async behaviour |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `where`, `select`, `selectMany`, `ofType`, `cast` | stream (per-item compiled evaluators — the engine, one item at a time) |
|
|
301
|
+
| `skip`, `take` | stream; `take` CLOSES the source when satisfied |
|
|
302
|
+
| `distinct` | stream, with a running key set (the grouping relation: `NaN` groups with `NaN`) |
|
|
303
|
+
| `defaultIfEmpty` | stream (an emptiness flag) |
|
|
304
|
+
| `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 |
|
|
305
|
+
| | 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 |
|
|
306
|
+
| `orderBy`/`thenBy`, `groupBy`, `join`, `aggregate`, `reverse` | BARRIER, named by `explain()` with the reason |
|
|
307
|
+
| `count`, `any`, `all`, `first`, `single`, `elementAt` | stream with early exit where semantics allow |
|
|
308
|
+
| `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 |
|
|
309
|
+
|
|
310
|
+
**Early termination MUST close the source**: `first()`, `any()`,
|
|
311
|
+
`take(n)`, and an exception mid-chain all call `.return()` on the
|
|
312
|
+
iterator — a generator left suspended holds a file handle or a read
|
|
313
|
+
transaction open. `explain()` reports `{ barriers: [{ operator,
|
|
314
|
+
reason }], document }` — or, when a `mapAsync` sits in the chain,
|
|
315
|
+
`{ split: { pushed, residual } }` instead of `document`
|
|
316
|
+
(`toDocument()` refuses with `JL0005`: a host callback has no document
|
|
317
|
+
form). No silent caps, no silent buffering: if a chain materialises,
|
|
318
|
+
the report says which operator forced it.
|
|
319
|
+
|
|
320
|
+
Re-enumeration follows the sync contract: each enumeration calls the
|
|
321
|
+
source's iterator method again. A one-shot generator object simply
|
|
322
|
+
exhausts — the same way it does under `from`.
|
|
323
|
+
|
|
324
|
+
## 11. The concurrency boundary
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
await fromAsync(rows)
|
|
328
|
+
.mapAsync(async (row, signal) => fetchScore(row.id, signal),
|
|
329
|
+
{ concurrency: 8, mode: 'parallel', ordered: true })
|
|
330
|
+
.where((r) => r.score.gt(0.5))
|
|
331
|
+
.toArray();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`mapAsync` is the ONE explicit boundary for element-wise asynchronous
|
|
335
|
+
host work. There is no parallel universe of `selectAwait`-shaped
|
|
336
|
+
operators; a per-element async *predicate* is `mapAsync` then `where`.
|
|
337
|
+
|
|
338
|
+
- `concurrency` is REQUIRED and MUST be a positive integer (`JL0005`)
|
|
339
|
+
— the unbounded default is how libraries like this take down a
|
|
340
|
+
downstream service.
|
|
341
|
+
- `mode` reuses the `createTaskEffect` vocabulary (`@jarenjs/app` §9),
|
|
342
|
+
deliberately, so a reader who knows one knows the other:
|
|
343
|
+
`parallel` (a sliding window of N), `concat` (strictly sequential),
|
|
344
|
+
`switch` (a newer item supersedes and ABORTS the in-flight task),
|
|
345
|
+
`exhaust` (items arriving while busy are dropped). The source is
|
|
346
|
+
pulled eagerly under `switch`/`exhaust` — that race IS the mode.
|
|
347
|
+
- `ordered: true` (default) preserves source order and buffers at most
|
|
348
|
+
`concurrency` results — the stated cost; `ordered: false` yields on
|
|
349
|
+
completion.
|
|
350
|
+
- An `AbortSignal` is threaded to every callback and aborted on early
|
|
351
|
+
termination and on failure. A rejected callback FAILS CLOSED: the
|
|
352
|
+
first failure wins, every in-flight sibling aborts, the source
|
|
353
|
+
closes (the `compileDag` discipline).
|
|
354
|
+
- `mapAsync` is NOT translatable to a provider. A provider-backed
|
|
355
|
+
chain that reaches it SPLITS: everything before is pushed to the
|
|
356
|
+
provider whole, everything after runs locally, and `explain()`
|
|
357
|
+
reports `{ split: { pushed, residual } }` — the same residual
|
|
358
|
+
honesty the SQL pushdown owes (D8), applied to the async boundary.
|
|
359
|
+
|
|
360
|
+
## 12. The cursor contract and the source adapters
|
|
361
|
+
|
|
362
|
+
`fromAsync` accepts, in order of preference:
|
|
363
|
+
|
|
364
|
+
- any **`AsyncIterable`** (async generators, `ReadableStream` — every
|
|
365
|
+
target exposes `Symbol.asyncIterator` on it, josl's
|
|
366
|
+
`iterateCsvStream` output);
|
|
367
|
+
- any sync iterable (wrapped);
|
|
368
|
+
- a **cursor**: `{ next(): Promise<{done, value}>, return?() }` — the
|
|
369
|
+
shape the SQL provider's row iterator implements later, adopted
|
|
370
|
+
as-is;
|
|
371
|
+
- a **push queue** (`createPushQueue({ highWaterMark = 1024 })`) for
|
|
372
|
+
feed/end-style readers with no pull protocol of their own (josl's
|
|
373
|
+
push parsers deliberately have no backpressure protocol; the queue
|
|
374
|
+
is where one appears): `feed(value)` returns `false` once the queue
|
|
375
|
+
exceeds the mark — a pause HINT, never a hard stop — and
|
|
376
|
+
`end(error?)` closes (or fails) the stream. Anything else is
|
|
377
|
+
`JL0001` at `fromAsync()` time.
|
|
378
|
+
|
|
379
|
+
What this surface does NOT do, by design: it does not make the query
|
|
380
|
+
engine async (`packages/json` is untouched and strictly synchronous),
|
|
381
|
+
it does not add a second operator table, and it does not add
|
|
382
|
+
`selectAwait`/`whereAwait` variants.
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jarenjs/linq",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.34.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./types/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"types/",
|
|
18
|
+
"src/",
|
|
19
|
+
"docs/"
|
|
20
|
+
],
|
|
21
|
+
"description": "A C#-familiar fluent LINQ surface for the Jaren suite: expression capture into plain query documents, deferred immutable sequences, and a provider seam that runs the same document in memory or anywhere else",
|
|
22
|
+
"author": "joham",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/jklarenbeek/jarenjs.git",
|
|
26
|
+
"directory": "packages/linq"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"jaren",
|
|
38
|
+
"json",
|
|
39
|
+
"linq",
|
|
40
|
+
"query",
|
|
41
|
+
"fluent",
|
|
42
|
+
"expression-tree"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "npm run build:types",
|
|
46
|
+
"build:types": "tsc -p tsconfig.json",
|
|
47
|
+
"prepack": "npm run build:types"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@jarenjs/core": "^0.34.0",
|
|
51
|
+
"@jarenjs/json": "^0.34.0"
|
|
52
|
+
}
|
|
53
|
+
}
|