@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/src/sequence.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The deferred, immutable `Sequence`. Every operator returns a
|
|
4
|
+
* NEW sequence; nothing runs until a terminal operation; a sequence may
|
|
5
|
+
* be enumerated repeatedly and each enumeration re-reads its source —
|
|
6
|
+
* the C# contract, including the part that surprises people
|
|
7
|
+
* (LINQ-FORMAT.md §5 has the worked example).
|
|
8
|
+
*
|
|
9
|
+
* The chain is data: `toDocument()` emits one Jaren query document, and
|
|
10
|
+
* a terminal either compiles it in memory (the reference semantics) or
|
|
11
|
+
* hands it WHOLE to a provider (`execute(document, options)`, D2) —
|
|
12
|
+
* which is what makes a query loggable, storable, diffable and
|
|
13
|
+
* authorable by a constrained decoder.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { captureExpression, toExpression } from './expression.js';
|
|
17
|
+
import { emitDocument, wrapTerminal } from './document.js';
|
|
18
|
+
import { classifySource, compileDocument, executeInMemory } from './provider.js';
|
|
19
|
+
import { asyncFromSequence } from './async.js';
|
|
20
|
+
import { LinqBuildError, LinqRuntimeError } from './errors.js';
|
|
21
|
+
|
|
22
|
+
/** Binding names the emitted documents own; parameters may not shadow
|
|
23
|
+
* them (LINQ-FORMAT.md §7). */
|
|
24
|
+
const RESERVED_NAMES = new Set(['it', 'it2', 'acc', 'g']);
|
|
25
|
+
|
|
26
|
+
const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A deep, independent copy of an emitted query document. Plain data
|
|
30
|
+
* only, which is exactly what a document is — every captured expression
|
|
31
|
+
* has already passed the JSON-domain boundary in `expression.js`, so
|
|
32
|
+
* there is nothing here a structural copy would lose.
|
|
33
|
+
* @param {any} node
|
|
34
|
+
* @returns {any}
|
|
35
|
+
*/
|
|
36
|
+
function snapshot(node) {
|
|
37
|
+
if (node === null || typeof node !== 'object') return node;
|
|
38
|
+
if (Array.isArray(node)) return node.map(snapshot);
|
|
39
|
+
/** @type {Record<string, any>} */
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const key of Object.keys(node)) defineOwn(out, key, snapshot(node[key]));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Assign an OWN property, so a `__proto__` member stays a member instead
|
|
47
|
+
* of silently replacing the object's prototype and vanishing.
|
|
48
|
+
* @param {Record<string, any>} target
|
|
49
|
+
* @param {string} key
|
|
50
|
+
* @param {any} value
|
|
51
|
+
*/
|
|
52
|
+
function defineOwn(target, key, value) {
|
|
53
|
+
Object.defineProperty(target, key,
|
|
54
|
+
{ value, writable: true, enumerable: true, configurable: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {number} value @param {string} what */
|
|
58
|
+
function requireIndex(value, what) {
|
|
59
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
60
|
+
throw new LinqBuildError('JL0005', `${what} takes a non-negative integer, got ${value}`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The immutable query sequence. Construct via `from`/`fromDocument`. */
|
|
66
|
+
export class Sequence {
|
|
67
|
+
#source;
|
|
68
|
+
#sourceKind;
|
|
69
|
+
#root;
|
|
70
|
+
#stages;
|
|
71
|
+
#params;
|
|
72
|
+
#options;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {any} source
|
|
76
|
+
* @param {'provider' | 'iterable'} sourceKind
|
|
77
|
+
* @param {any} root - the root expression items come from
|
|
78
|
+
* @param {readonly any[]} stages
|
|
79
|
+
* @param {ReadonlyMap<string, any>} params
|
|
80
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
81
|
+
* pathFunctions?: any, limits?: any, registry?: object }} options
|
|
82
|
+
*/
|
|
83
|
+
constructor(source, sourceKind, root, stages, params, options) {
|
|
84
|
+
this.#source = source;
|
|
85
|
+
this.#sourceKind = sourceKind;
|
|
86
|
+
this.#root = root;
|
|
87
|
+
this.#stages = stages;
|
|
88
|
+
this.#params = params;
|
|
89
|
+
this.#options = options;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {any} stage */
|
|
93
|
+
#with(stage) {
|
|
94
|
+
return new Sequence(this.#source, this.#sourceKind, this.#root,
|
|
95
|
+
[...this.#stages, stage], this.#params, this.#options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#declared() {
|
|
99
|
+
return new Set(this.#params.keys());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {(...roots: any[]) => any} fn @param {readonly any[]} roots */
|
|
103
|
+
#capture(fn, roots = ['it']) {
|
|
104
|
+
if (typeof fn !== 'function') {
|
|
105
|
+
throw new LinqBuildError('JL0005', 'this operator takes a callback function');
|
|
106
|
+
}
|
|
107
|
+
return captureExpression(fn, roots, this.#declared());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#region operators (each returns a new immutable Sequence)
|
|
111
|
+
|
|
112
|
+
/** Filter: `.where(it => it.age.gt(21))` → FLWOR `$where`. */
|
|
113
|
+
where(predicate) {
|
|
114
|
+
return this.#with({ kind: 'where', predicate: this.#capture(predicate) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Project: `.select(it => ({ id: it.id }))` → `$return`. */
|
|
118
|
+
select(projection) {
|
|
119
|
+
return this.#with({ kind: 'select', projection: this.#capture(projection) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Project-and-flatten: a multi-item projection concatenates (the
|
|
123
|
+
* FLWOR `$return` already flattens per tuple). */
|
|
124
|
+
selectMany(selector) {
|
|
125
|
+
return this.#with({ kind: 'select', projection: this.#capture(selector) });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {any} key @param {boolean} desc @param {any} [options] */
|
|
129
|
+
#orderStage(kind, key, desc, options) {
|
|
130
|
+
const spec = { $key: this.#capture(key) };
|
|
131
|
+
if (desc) spec.$dir = 'desc';
|
|
132
|
+
if (options !== undefined) {
|
|
133
|
+
if (options.empty !== undefined) spec.$empty = options.empty;
|
|
134
|
+
if (options.collation !== undefined) spec.$collation = options.collation;
|
|
135
|
+
}
|
|
136
|
+
return this.#with({ kind, spec });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Sort ascending → an `$orderby` key spec (`$dir`/`$empty`/
|
|
140
|
+
* `$collation` exposed through `options`).
|
|
141
|
+
* @param {(...roots: any[]) => any} key
|
|
142
|
+
* @param {{ empty?: string, collation?: string }} [options] */
|
|
143
|
+
orderBy(key, options) {
|
|
144
|
+
return this.#orderStage('orderBy', key, false, options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {(...roots: any[]) => any} key
|
|
148
|
+
* @param {{ empty?: string, collation?: string }} [options] */
|
|
149
|
+
orderByDescending(key, options) {
|
|
150
|
+
return this.#orderStage('orderBy', key, true, options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Secondary sort key; must directly follow `orderBy*` (JL0005).
|
|
154
|
+
* @param {(...roots: any[]) => any} key
|
|
155
|
+
* @param {{ empty?: string, collation?: string }} [options] */
|
|
156
|
+
thenBy(key, options) {
|
|
157
|
+
return this.#orderStage('thenBy', key, false, options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @param {(...roots: any[]) => any} key
|
|
161
|
+
* @param {{ empty?: string, collation?: string }} [options] */
|
|
162
|
+
thenByDescending(key, options) {
|
|
163
|
+
return this.#orderStage('thenBy', key, true, options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Group → `$groupby`; downstream items are `{ key, items }`. */
|
|
167
|
+
groupBy(key) {
|
|
168
|
+
return this.#with({ kind: 'groupBy', key: this.#capture(key) });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Both sides read ONE input document in 0.1 — a query document has
|
|
172
|
+
* one root. Cross-source composition arrives with the relational order.
|
|
173
|
+
* @param {Sequence} inner @param {string} what */
|
|
174
|
+
#requireSameSource(inner, what) {
|
|
175
|
+
if (!(inner instanceof Sequence)) {
|
|
176
|
+
throw new LinqBuildError('JL0005', `${what} takes another sequence as its inner side`);
|
|
177
|
+
}
|
|
178
|
+
if (inner.#source !== this.#source) {
|
|
179
|
+
throw new LinqBuildError('JL0005',
|
|
180
|
+
`${what}'s other side must derive from the same source in 0.1 — `
|
|
181
|
+
+ 'a query document reads one input; load both collections under one root '
|
|
182
|
+
+ '(the relational order lifts this)');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Equi-join → nested `$for` + `$where` equality (the engine rewrites
|
|
187
|
+
* this shape to a hash join; that is why it is fast). */
|
|
188
|
+
join(inner, outerKey, innerKey, result) {
|
|
189
|
+
this.#requireSameSource(inner, 'join');
|
|
190
|
+
return this.#with({
|
|
191
|
+
kind: 'join',
|
|
192
|
+
inner: inner.toDocument(),
|
|
193
|
+
on: {
|
|
194
|
+
$eq: [this.#capture(outerKey), this.#capture(innerKey, ['it2'])],
|
|
195
|
+
},
|
|
196
|
+
result: this.#capture(result, ['it', 'it2']),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Group-join: the result selector receives the outer item and the
|
|
201
|
+
* MATCHING inner group as an expression (`(u, g) => ({ n: g.count() })`). */
|
|
202
|
+
groupJoin(inner, outerKey, innerKey, result) {
|
|
203
|
+
this.#requireSameSource(inner, 'groupJoin');
|
|
204
|
+
const group = {
|
|
205
|
+
$for: { it2: inner.toDocument() },
|
|
206
|
+
$where: { $eq: [this.#capture(outerKey), this.#capture(innerKey, ['it2'])] },
|
|
207
|
+
$return: '$it2',
|
|
208
|
+
};
|
|
209
|
+
return this.#with({
|
|
210
|
+
kind: 'select',
|
|
211
|
+
projection: this.#capture(result, ['it', { doc: group, pathable: false }]),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Seeded fold → `$fold` (the accumulator clause). Only the seeded
|
|
216
|
+
* form exists: JSON has no way to spell an unseeded lambda's implicit
|
|
217
|
+
* first element without one. */
|
|
218
|
+
aggregate(seed, step) {
|
|
219
|
+
return this.#with({
|
|
220
|
+
kind: 'aggregate',
|
|
221
|
+
seed: toExpression(seed),
|
|
222
|
+
step: this.#capture(step, ['acc', 'it']),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
skip(count) {
|
|
227
|
+
return this.#with({ kind: 'skip', count: requireIndex(count, 'skip') });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
take(count) {
|
|
231
|
+
return this.#with({ kind: 'take', count: requireIndex(count, 'take') });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
distinct() {
|
|
235
|
+
return this.#with({ kind: 'distinct' });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
reverse() {
|
|
239
|
+
return this.#with({ kind: 'reverse' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Concatenate another sequence over the SAME source, or a constant
|
|
243
|
+
* array (embedded verbatim; its elements join the stream).
|
|
244
|
+
*
|
|
245
|
+
* The same-source check is not a formality. A query document reads ONE
|
|
246
|
+
* input, so the other sequence contributes its expression, not its
|
|
247
|
+
* data — and a sequence built over a different source would have its
|
|
248
|
+
* expression evaluated against THIS source, quietly reading the wrong
|
|
249
|
+
* rows twice instead of concatenating two inputs. */
|
|
250
|
+
concat(other) {
|
|
251
|
+
let expr;
|
|
252
|
+
if (other instanceof Sequence) {
|
|
253
|
+
this.#requireSameSource(other, 'concat');
|
|
254
|
+
expr = other.toDocument();
|
|
255
|
+
}
|
|
256
|
+
else if (Array.isArray(other)) {
|
|
257
|
+
expr = { $for: { it: { $const: other } }, $return: '$it' };
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
throw new LinqBuildError('JL0005', 'concat takes a sequence or a constant array');
|
|
261
|
+
}
|
|
262
|
+
return this.#with({ kind: 'concat', other: expr });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** `$default`: the sequence, or the fallback when it is empty. */
|
|
266
|
+
defaultIfEmpty(fallback = null) {
|
|
267
|
+
return this.#with({ kind: 'defaultIfEmpty', fallback: toExpression(fallback) });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Keep only items matching the JSON Schema (`$valid` filter). */
|
|
271
|
+
ofType(schema) {
|
|
272
|
+
return this.#with({ kind: 'ofType', schema });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Assert every item against the JSON Schema (`$assert`). */
|
|
276
|
+
cast(schema) {
|
|
277
|
+
return this.#with({ kind: 'cast', schema });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Cross into the async surface: everything BEFORE this call is the
|
|
281
|
+
* prefix — compiled in memory, or pushed WHOLE to the provider — and
|
|
282
|
+
* `mapAsync` plus everything after runs locally over its rows.
|
|
283
|
+
* `explain()` on the result reports the split (LINQ-FORMAT.md §11).
|
|
284
|
+
* @param {(item: any, signal: AbortSignal) => any} fn
|
|
285
|
+
* @param {{ concurrency: number, mode?: string, ordered?: boolean }} options */
|
|
286
|
+
mapAsync(fn, options) {
|
|
287
|
+
return asyncFromSequence({
|
|
288
|
+
runPrefix: () => this.toArray(),
|
|
289
|
+
prefixDocument: () => this.toDocument(),
|
|
290
|
+
params: this.#params,
|
|
291
|
+
options: this.#options,
|
|
292
|
+
}, fn, options);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Recorded `unsupported` (LINQ-FORMAT.md §4): the grammar has no
|
|
296
|
+
* positional co-iteration. */
|
|
297
|
+
zip() {
|
|
298
|
+
throw new LinqBuildError('JL0006',
|
|
299
|
+
'zip is unsupported: the query grammar has no positional co-iteration (see LINQ-FORMAT.md §4)');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Declare (and bind) external parameters: `.params({ tenantId })`.
|
|
303
|
+
* Callbacks read them through their last argument (`(it, p) =>
|
|
304
|
+
* it.tenant.eq(p.tenantId)`); the emitted document carries them as
|
|
305
|
+
* externals — the seam that becomes bound SQL parameters. */
|
|
306
|
+
params(bindings) {
|
|
307
|
+
if (bindings === null || typeof bindings !== 'object' || Array.isArray(bindings)) {
|
|
308
|
+
throw new LinqBuildError('JL0004', 'params takes an object of name → value bindings');
|
|
309
|
+
}
|
|
310
|
+
const merged = new Map(this.#params);
|
|
311
|
+
for (const name of Object.keys(bindings)) {
|
|
312
|
+
if (!VAR_NAME_RE.test(name)) {
|
|
313
|
+
throw new LinqBuildError('JL0004', `'${name}' is not a valid parameter name`);
|
|
314
|
+
}
|
|
315
|
+
if (RESERVED_NAMES.has(name)) {
|
|
316
|
+
throw new LinqBuildError('JL0004',
|
|
317
|
+
`'${name}' is reserved (the emitted document's own binding names: it, it2, acc, g)`);
|
|
318
|
+
}
|
|
319
|
+
merged.set(name, bindings[name]);
|
|
320
|
+
}
|
|
321
|
+
return new Sequence(this.#source, this.#sourceKind, this.#root,
|
|
322
|
+
this.#stages, merged, this.#options);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//#endregion
|
|
326
|
+
|
|
327
|
+
//#region documents and execution
|
|
328
|
+
|
|
329
|
+
/** The chain as ONE query document — public API, not a debug toy:
|
|
330
|
+
* loggable, cacheable, storable, diffable, transportable, and
|
|
331
|
+
* compilable by a bare `compileJsonQuery` with no linq involvement.
|
|
332
|
+
*
|
|
333
|
+
* A DEEP, independent snapshot. The emitted tree embeds the captured
|
|
334
|
+
* expressions a stage holds, so handing them out by reference made this
|
|
335
|
+
* a live window into a sequence documented as immutable: writing into
|
|
336
|
+
* the returned document rewrote the predicate, and the next
|
|
337
|
+
* enumeration answered differently. A snapshot cannot do that. */
|
|
338
|
+
toDocument() {
|
|
339
|
+
return snapshot(emitDocument(this.#root, this.#stages));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The compiled view of the chain: the document, its externals and
|
|
343
|
+
* its dependency sets.
|
|
344
|
+
*
|
|
345
|
+
* This always explains the IN-MEMORY compilation — it is the reference
|
|
346
|
+
* semantics, and it is not the provider's plan. It cannot report SQL
|
|
347
|
+
* pushdown, index use, residual execution or a strict refusal, and it
|
|
348
|
+
* will fail on an operator or collation only the provider can compile.
|
|
349
|
+
* For a provider's real plan, emit `toDocument()` and call that
|
|
350
|
+
* provider's own explanation. */
|
|
351
|
+
explain() {
|
|
352
|
+
const document = this.toDocument();
|
|
353
|
+
const compiled = compileDocument(document, {
|
|
354
|
+
...this.#options,
|
|
355
|
+
externals: [...this.#params.keys()],
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
document,
|
|
359
|
+
externals: [...compiled.externals],
|
|
360
|
+
dependencies: compiled.dependencies,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** @param {string} terminal @param {readonly any[]} [args] */
|
|
365
|
+
#execute(terminal, args) {
|
|
366
|
+
const document = wrapTerminal(this.toDocument(), terminal, args);
|
|
367
|
+
const externalNames = [...this.#params.keys()];
|
|
368
|
+
const externals = Object.fromEntries(this.#params);
|
|
369
|
+
if (this.#sourceKind === 'provider') {
|
|
370
|
+
const result = this.#source.execute(document, { externals });
|
|
371
|
+
// A `Sequence` terminal is a VALUE — `toArray(): T[]`,
|
|
372
|
+
// `count(): number`. A provider whose `execute` answers a promise
|
|
373
|
+
// (the wasm/OPFS drivers do) cannot satisfy that, and the old seam
|
|
374
|
+
// let the promise through under the value's type: `count()` handed
|
|
375
|
+
// back a `Promise` typed `number`, and `first()` indexed the promise
|
|
376
|
+
// and returned `undefined` — a wrong answer with no error anywhere.
|
|
377
|
+
// Refuse at the seam instead, and name the surface that does work.
|
|
378
|
+
if (result !== null && typeof result === 'object'
|
|
379
|
+
&& typeof (/** @type {any} */ (result).then) === 'function') {
|
|
380
|
+
throw new LinqRuntimeError('JL2004',
|
|
381
|
+
`this provider's execute() answered a promise, and a Sequence terminal is a `
|
|
382
|
+
+ 'value — an asynchronous provider cannot back the synchronous surface. '
|
|
383
|
+
+ 'Emit the document with toDocument() and await the provider directly, or '
|
|
384
|
+
+ 'use a synchronous provider.');
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
return executeInMemory(this.#source, document, {
|
|
389
|
+
...this.#options,
|
|
390
|
+
externalNames, externals,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @param {string} terminal @param {readonly any[]} [args] */
|
|
395
|
+
#window(terminal, args) {
|
|
396
|
+
// element terminals emit `[window]`, so the result is always one
|
|
397
|
+
// array item and element extraction is unambiguous
|
|
398
|
+
return /** @type {any[]} */ (this.#execute(terminal, args));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
toArray() {
|
|
402
|
+
return this.#window('toArray');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
* [Symbol.iterator]() {
|
|
406
|
+
yield* this.toArray();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
first() {
|
|
410
|
+
const w = this.#window('first');
|
|
411
|
+
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'first() found no element');
|
|
412
|
+
return w[0];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** @param {any} [defaultValue] */
|
|
416
|
+
firstOrDefault(defaultValue) {
|
|
417
|
+
const w = this.#window('first');
|
|
418
|
+
return w.length === 0 ? defaultValue : w[0];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
single() {
|
|
422
|
+
const w = this.#window('single');
|
|
423
|
+
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'single() found no element');
|
|
424
|
+
if (w.length > 1) throw new LinqRuntimeError('JL2002', 'single() found more than one element');
|
|
425
|
+
return w[0];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** @param {any} [defaultValue] */
|
|
429
|
+
singleOrDefault(defaultValue) {
|
|
430
|
+
const w = this.#window('single');
|
|
431
|
+
if (w.length > 1) throw new LinqRuntimeError('JL2002', 'singleOrDefault() found more than one element');
|
|
432
|
+
return w.length === 0 ? defaultValue : w[0];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
last() {
|
|
436
|
+
const w = this.#window('last');
|
|
437
|
+
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'last() found no element');
|
|
438
|
+
return w[0];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** @param {any} [defaultValue] */
|
|
442
|
+
lastOrDefault(defaultValue) {
|
|
443
|
+
const w = this.#window('last');
|
|
444
|
+
return w.length === 0 ? defaultValue : w[0];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
elementAt(index) {
|
|
448
|
+
requireIndex(index, 'elementAt');
|
|
449
|
+
const w = this.#window('elementAt', [index]);
|
|
450
|
+
if (w.length === 0) throw new LinqRuntimeError('JL2003', `elementAt(${index}) is out of range`);
|
|
451
|
+
return w[0];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** @param {number} index @param {any} [defaultValue] */
|
|
455
|
+
elementAtOrDefault(index, defaultValue) {
|
|
456
|
+
requireIndex(index, 'elementAtOrDefault');
|
|
457
|
+
const w = this.#window('elementAt', [index]);
|
|
458
|
+
return w.length === 0 ? defaultValue : w[0];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
count() {
|
|
462
|
+
return this.#execute('count');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
sum() {
|
|
466
|
+
return this.#execute('sum');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** C#: `Average()` over an empty sequence throws. */
|
|
470
|
+
average() {
|
|
471
|
+
const v = this.#execute('average');
|
|
472
|
+
if (v === undefined) throw new LinqRuntimeError('JL2001', 'average() of an empty sequence');
|
|
473
|
+
return v;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
min() {
|
|
477
|
+
const v = this.#execute('min');
|
|
478
|
+
if (v === undefined) throw new LinqRuntimeError('JL2001', 'min() of an empty sequence');
|
|
479
|
+
return v;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
max() {
|
|
483
|
+
const v = this.#execute('max');
|
|
484
|
+
if (v === undefined) throw new LinqRuntimeError('JL2001', 'max() of an empty sequence');
|
|
485
|
+
return v;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** `any()` is existence; `any(pred)` is the `$some` quantifier.
|
|
489
|
+
* @param {(...roots: any[]) => any} [predicate] */
|
|
490
|
+
any(predicate) {
|
|
491
|
+
if (predicate === undefined) return this.#execute('exists');
|
|
492
|
+
return this.#execute('some', [this.#capture(predicate)]);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The `$every` quantifier (vacuously true over the empty sequence). */
|
|
496
|
+
all(predicate) {
|
|
497
|
+
return this.#execute('every', [this.#capture(predicate)]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
//#endregion
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Build a sequence over an iterable or a provider (D2). Dispatch
|
|
505
|
+
* happens ONCE, here: an `execute` duck is a provider and is never
|
|
506
|
+
* enumerated locally; any iterable gets the in-memory reference
|
|
507
|
+
* semantics; anything else is `JL0001` now, not at enumeration time.
|
|
508
|
+
* @param {any} source
|
|
509
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
510
|
+
* pathFunctions?: any, limits?: any, registry?: object }} [options] -
|
|
511
|
+
* the engine registries this sequence compiles against, under the
|
|
512
|
+
* engine's own option names: `compileTypeTest` enables `ofType`/`cast`,
|
|
513
|
+
* `collations` makes `orderBy(..., {collation})` executable in memory,
|
|
514
|
+
* `functions`/`pathFunctions` make `$call` and custom path functions
|
|
515
|
+
* resolvable, and `limits` bounds step and result counts. Pass
|
|
516
|
+
* `registry` when the hooks are rebuilt per call, so compiled documents
|
|
517
|
+
* still share a cache partition.
|
|
518
|
+
* @returns {Sequence}
|
|
519
|
+
*/
|
|
520
|
+
export function from(source, options = {}) {
|
|
521
|
+
return new Sequence(source, classifySource(source), '$[*]', [], new Map(), options);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Attach a hand-written (or stored) query document to a source. The
|
|
526
|
+
* document's result is the item sequence; further operators chain over
|
|
527
|
+
* it. A version envelope is unwrapped so the expression embeds.
|
|
528
|
+
* @param {any} source - iterable or provider, as `from`
|
|
529
|
+
* @param {any} document - a Jaren query document
|
|
530
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
531
|
+
* pathFunctions?: any, limits?: any, registry?: object }} [options] -
|
|
532
|
+
* as {@link from}. A SAVED document is the case `limits` exists for:
|
|
533
|
+
* bound its steps and results before running it.
|
|
534
|
+
* @returns {Sequence}
|
|
535
|
+
*/
|
|
536
|
+
export function fromDocument(source, document, options = {}) {
|
|
537
|
+
let root = document;
|
|
538
|
+
if (root !== null && typeof root === 'object' && !Array.isArray(root)
|
|
539
|
+
&& Object.hasOwn(root, '$expr')) {
|
|
540
|
+
root = root.$expr;
|
|
541
|
+
}
|
|
542
|
+
return new Sequence(source, classifySource(source), root, [], new Map(), options);
|
|
543
|
+
}
|
package/src/sources.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Async source adapters (LINQ-FORMAT.md §12): everything
|
|
4
|
+
* `fromAsync` accepts normalizes to "a factory of async iterators" —
|
|
5
|
+
* a fresh iterator per enumeration, so the deferred re-enumeration
|
|
6
|
+
* contract carries over exactly (a one-shot generator object simply
|
|
7
|
+
* exhausts, the same way it does under sync `from`).
|
|
8
|
+
*
|
|
9
|
+
* Shipped shapes: any `AsyncIterable`, any sync iterable (wrapped), a
|
|
10
|
+
* CURSOR (`{ next(): Promise<{done, value}>, return?() }` — the shape
|
|
11
|
+
* the SQL provider's row iterator implements later), and a push-queue
|
|
12
|
+
* for feed/end-style readers that have no pull protocol of their own.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { LinqBuildError } from './errors.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize an async source into an iterator factory, or throw
|
|
19
|
+
* `JL0001` — at `fromAsync()` time, never at enumeration time.
|
|
20
|
+
* @param {any} source
|
|
21
|
+
* @returns {() => AsyncIterator<any>}
|
|
22
|
+
*/
|
|
23
|
+
export function adaptAsyncSource(source) {
|
|
24
|
+
if (source != null) {
|
|
25
|
+
if (typeof source[Symbol.asyncIterator] === 'function') {
|
|
26
|
+
return () => source[Symbol.asyncIterator]();
|
|
27
|
+
}
|
|
28
|
+
if (typeof source[Symbol.iterator] === 'function' && typeof source !== 'string') {
|
|
29
|
+
return () => (async function* () { yield* source; })();
|
|
30
|
+
}
|
|
31
|
+
if (typeof source.next === 'function') {
|
|
32
|
+
// the cursor shape: already an (async) iterator
|
|
33
|
+
return () => /** @type {AsyncIterator<any>} */ (source);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new LinqBuildError('JL0001',
|
|
37
|
+
'fromAsync() needs an async iterable, an iterable, a cursor ({ next, return? }) or a push queue');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A push→pull adapter for feed/end readers (josl's push parsers have
|
|
42
|
+
* deliberately no backpressure protocol, so the queue is the boundary
|
|
43
|
+
* where one appears): `feed(value)` enqueues and returns `false` once
|
|
44
|
+
* the queue holds more than `highWaterMark` items — a HINT to pause,
|
|
45
|
+
* never a hard stop — and `end(error?)` closes the stream. The queue
|
|
46
|
+
* itself is the async-iterable to hand to `fromAsync`.
|
|
47
|
+
* @param {{ highWaterMark?: number }} [options]
|
|
48
|
+
* @returns {{ feed: (value: any) => boolean, end: (error?: unknown) => void,
|
|
49
|
+
* [Symbol.asyncIterator]: () => AsyncIterator<any> }}
|
|
50
|
+
*/
|
|
51
|
+
export function createPushQueue(options = {}) {
|
|
52
|
+
const highWaterMark = options.highWaterMark ?? 1024;
|
|
53
|
+
if (!Number.isInteger(highWaterMark) || highWaterMark < 1) {
|
|
54
|
+
throw new LinqBuildError('JL0005', 'highWaterMark must be a positive integer');
|
|
55
|
+
}
|
|
56
|
+
/** @type {any[]} */
|
|
57
|
+
const buffer = [];
|
|
58
|
+
/** @type {(() => void) | null} */
|
|
59
|
+
let wake = null;
|
|
60
|
+
let ended = false;
|
|
61
|
+
/** @type {unknown} */
|
|
62
|
+
let failure = null;
|
|
63
|
+
|
|
64
|
+
function signal() {
|
|
65
|
+
if (wake !== null) {
|
|
66
|
+
const w = wake;
|
|
67
|
+
wake = null;
|
|
68
|
+
w();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
feed(value) {
|
|
74
|
+
if (ended) throw new LinqBuildError('JL0005', 'feed() after end()');
|
|
75
|
+
buffer.push(value);
|
|
76
|
+
signal();
|
|
77
|
+
return buffer.length <= highWaterMark;
|
|
78
|
+
},
|
|
79
|
+
end(error) {
|
|
80
|
+
ended = true;
|
|
81
|
+
failure = error;
|
|
82
|
+
signal();
|
|
83
|
+
},
|
|
84
|
+
async* [Symbol.asyncIterator]() {
|
|
85
|
+
for (;;) {
|
|
86
|
+
if (buffer.length > 0) {
|
|
87
|
+
yield buffer.shift();
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ended) {
|
|
91
|
+
if (failure !== undefined && failure !== null) throw failure;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await new Promise((resolve) => { wake = () => resolve(undefined); });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|