@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
package/src/async.js
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
* - `mapAsync` applies the bounded-concurrency machinery between
|
|
25
25
|
* segments; it is NOT translatable to a document, so `toDocument()`
|
|
26
26
|
* refuses (`JL0005`) and `explain()` reports the split.
|
|
27
|
+
* - a PROVIDER origin (`fromAsync(store.entity('Post'))`) runs nothing
|
|
28
|
+
* here: everything up to the first `mapAsync` is ONE document the
|
|
29
|
+
* provider executes whole — the terminal's wrapper included, exactly
|
|
30
|
+
* as the synchronous surface pushes it — and `execute` may answer a
|
|
31
|
+
* promise (D8); the residual after the split streams locally, and a
|
|
32
|
+
* `join` exists on this surface only inside that pushed document.
|
|
27
33
|
*
|
|
28
34
|
* Early termination CLOSES the source: every consumer is a
|
|
29
35
|
* `for await … break` chain, and async generators propagate `return()`
|
|
@@ -31,20 +37,31 @@
|
|
|
31
37
|
* read transaction open.
|
|
32
38
|
*/
|
|
33
39
|
|
|
34
|
-
import {
|
|
35
|
-
|
|
40
|
+
import {
|
|
41
|
+
compileDocument, isProviderSource, providerRoot, providerRelations, sharesScope,
|
|
42
|
+
} from './provider.js';
|
|
43
|
+
import {
|
|
44
|
+
emitDocument, wrapTerminal, snapshot, fanProjection, isReservedBinding, RESERVED_BINDINGS_TEXT,
|
|
45
|
+
PROJECTING_STAGES,
|
|
46
|
+
} from './document.js';
|
|
36
47
|
import { adaptAsyncSource } from './sources.js';
|
|
37
48
|
import { applyMapAsync, normalizeMapAsyncOptions } from './concurrency.js';
|
|
38
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
captureExpression, toExpression, requireJsonBinding, createHopSink, rowRoot, groupRoot,
|
|
51
|
+
} from './expression.js';
|
|
39
52
|
import { LinqBuildError, LinqRuntimeError } from './errors.js';
|
|
53
|
+
import { schemaOf } from './schema-of.js';
|
|
40
54
|
import { semanticKey } from '@jarenjs/core/object';
|
|
41
55
|
|
|
42
|
-
/** Barrier stage kinds and the reason each materialises.
|
|
56
|
+
/** Barrier stage kinds and the reason each materialises. A `join` is
|
|
57
|
+
* never a barrier: over a provider it rides INSIDE the one pushed
|
|
58
|
+
* document (the only place this surface joins), and over a single-pass
|
|
59
|
+
* source — a cursor, a queue, a stream — it is refused, because the
|
|
60
|
+
* inner side would have to read the source twice (QUERY-PEN.md §10). */
|
|
43
61
|
const BARRIERS = {
|
|
44
62
|
orderBy: '$orderby materialises the tuple stream to sort it',
|
|
45
63
|
thenBy: '$orderby materialises the tuple stream to sort it',
|
|
46
64
|
groupBy: '$groupby materialises the tuple stream to group it',
|
|
47
|
-
join: 'a join needs the whole inner side',
|
|
48
65
|
aggregate: '$fold folds the whole stream into one value',
|
|
49
66
|
reverse: '$reverse needs the last item first',
|
|
50
67
|
};
|
|
@@ -85,24 +102,72 @@ export class AsyncSequence {
|
|
|
85
102
|
#stages;
|
|
86
103
|
#params;
|
|
87
104
|
#options;
|
|
105
|
+
#relations;
|
|
88
106
|
|
|
89
107
|
/**
|
|
90
108
|
* @param {{ kind: 'source', iterate: () => AsyncIterator<any> }
|
|
91
|
-
* | { kind: '
|
|
109
|
+
* | { kind: 'provider', source: any, root: string }
|
|
110
|
+
* | { kind: 'sequence', runPrefix: (params: ReadonlyMap<string, any>) => any[],
|
|
111
|
+
* prefixDocument: () => any }} origin
|
|
92
112
|
* @param {readonly any[]} stages
|
|
93
113
|
* @param {ReadonlyMap<string, any>} params
|
|
94
|
-
* @param {{ compileTypeTest?: any
|
|
114
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
115
|
+
* pathFunctions?: any, limits?: any, registry?: object }} options
|
|
116
|
+
* @param {{ table: any, resolve: (name: string) => any } | null} [relations] -
|
|
117
|
+
* the relation table of the rows the items ARE (a provider's, while
|
|
118
|
+
* no stage has projected them), or null — as on the sync surface
|
|
95
119
|
*/
|
|
96
|
-
constructor(origin, stages, params, options) {
|
|
120
|
+
constructor(origin, stages, params, options, relations = null) {
|
|
97
121
|
this.#origin = origin;
|
|
98
122
|
this.#stages = stages;
|
|
99
123
|
this.#params = params;
|
|
100
124
|
this.#options = options;
|
|
125
|
+
this.#relations = relations;
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
/** @param {any} stage */
|
|
104
|
-
#with(stage) {
|
|
105
|
-
|
|
128
|
+
/** @param {any} stage @param {ReadonlyMap<string, any>} [params] */
|
|
129
|
+
#with(stage, params = this.#params) {
|
|
130
|
+
// the items stop being rows at a projection, and at the host boundary
|
|
131
|
+
const projects = PROJECTING_STAGES.has(stage.kind) || stage.kind === 'mapAsync';
|
|
132
|
+
return new AsyncSequence(this.#origin, [...this.#stages, stage], params, this.#options,
|
|
133
|
+
projects ? null : this.#relations);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Capture one callback over this sequence's items — the expression and
|
|
138
|
+
* the relation hops it navigated — exactly as the synchronous surface
|
|
139
|
+
* does, so the two emit one document.
|
|
140
|
+
* @param {any} fn
|
|
141
|
+
* @param {(sink: ReturnType<typeof createHopSink>) => readonly any[]} [rootsOf]
|
|
142
|
+
* @returns {{ expression: any, hops: readonly any[] }}
|
|
143
|
+
*/
|
|
144
|
+
#capture(fn, rootsOf = (sink) => [this.#rowRoot('it', sink)]) {
|
|
145
|
+
if (typeof fn !== 'function') {
|
|
146
|
+
throw new LinqBuildError('JL0005', 'this operator takes a callback function');
|
|
147
|
+
}
|
|
148
|
+
const sink = createHopSink();
|
|
149
|
+
const expression = captureExpression(fn, rootsOf(sink), new Set(this.#params.keys()));
|
|
150
|
+
return { expression, hops: sink.hops };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Whether this sequence's items are a `groupBy`'s `{ key, items }`,
|
|
154
|
+
* as on the synchronous surface. */
|
|
155
|
+
#grouped() {
|
|
156
|
+
for (let i = this.#stages.length - 1; i >= 0; i--) {
|
|
157
|
+
const kind = this.#stages[i].kind;
|
|
158
|
+
if (PROJECTING_STAGES.has(kind) || kind === 'mapAsync') return kind === 'groupBy';
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @param {string} name @param {ReturnType<typeof createHopSink>} sink */
|
|
164
|
+
#rowRoot(name, sink) {
|
|
165
|
+
return rowRoot(name, this.#relations, sink, this.#grouped());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The relation hops every stage's callbacks navigated, in order. */
|
|
169
|
+
#hops() {
|
|
170
|
+
return this.#stages.flatMap((stage) => stage.hops ?? []);
|
|
106
171
|
}
|
|
107
172
|
|
|
108
173
|
#externals() {
|
|
@@ -112,6 +177,57 @@ export class AsyncSequence {
|
|
|
112
177
|
};
|
|
113
178
|
}
|
|
114
179
|
|
|
180
|
+
/** Whether the chain so far goes to a provider as ONE document: a
|
|
181
|
+
* provider origin with no `mapAsync` yet (D8 — the document arrives
|
|
182
|
+
* whole; a host callback is where it splits). */
|
|
183
|
+
#pushable() {
|
|
184
|
+
return this.#origin.kind === 'provider'
|
|
185
|
+
&& !this.#stages.some((stage) => stage.kind === 'mapAsync');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Whether this sequence is a provider's own root, untouched — the
|
|
189
|
+
* one `$for` source the emitter leaves unpacked (document.js). */
|
|
190
|
+
#isBareRoot() {
|
|
191
|
+
return this.#origin.kind === 'provider' && this.#stages.length === 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The root expression the stages iterate: a provider's own root, the
|
|
195
|
+
* pushed synchronous prefix, or the whole input. */
|
|
196
|
+
#rootExpression() {
|
|
197
|
+
if (this.#origin.kind === 'sequence') return this.#origin.prefixDocument();
|
|
198
|
+
return this.#origin.kind === 'provider' ? this.#origin.root : '$[*]';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The document for a run of stages over this origin's root. */
|
|
202
|
+
#documentOf(stages) {
|
|
203
|
+
return emitDocument(this.#rootExpression(), stages,
|
|
204
|
+
{ bareRoot: this.#origin.kind === 'provider' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Hand one document to the provider, whole, with the bound
|
|
208
|
+
* externals; `execute` may answer a value or a promise here. */
|
|
209
|
+
async #push(document) {
|
|
210
|
+
return this.#origin.source.execute(document, { externals: this.#externals().values });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A pushed element window over `stages`: the provider must answer
|
|
214
|
+
* exactly one array, as on the synchronous surface (`JL2006`). */
|
|
215
|
+
async #pushWindow(terminal, args = undefined, stages = this.#stages) {
|
|
216
|
+
const result = await this.#push(wrapTerminal(this.#documentOf(stages), terminal, args));
|
|
217
|
+
if (!Array.isArray(result)) {
|
|
218
|
+
throw new LinqRuntimeError('JL2006',
|
|
219
|
+
`the provider answered ${terminal}() with ${result === undefined ? 'undefined'
|
|
220
|
+
: `a ${typeof result}`} — an element terminal emits an array constructor, so a `
|
|
221
|
+
+ 'conforming execute() answers exactly one array (QUERY-PEN.md §8)');
|
|
222
|
+
}
|
|
223
|
+
return /** @type {any[]} */ (result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A pushed aggregate or quantifier over the whole chain. */
|
|
227
|
+
#pushTerminal(terminal, args = undefined) {
|
|
228
|
+
return this.#push(wrapTerminal(this.#documentOf(this.#stages), terminal, args));
|
|
229
|
+
}
|
|
230
|
+
|
|
115
231
|
/** Compile one per-item evaluator: the stage expression under
|
|
116
232
|
* `{$let: {it: '$'}}` with the terminal-window discipline. */
|
|
117
233
|
#evaluator(returnExpr) {
|
|
@@ -124,13 +240,21 @@ export class AsyncSequence {
|
|
|
124
240
|
|
|
125
241
|
where(predicate) { return this.#chainCaptured('where', 'predicate', predicate); }
|
|
126
242
|
select(projection) { return this.#chainCaptured('select', 'projection', projection); }
|
|
127
|
-
selectMany(selector) {
|
|
243
|
+
selectMany(selector) {
|
|
244
|
+
const { expression, hops } = this.#capture(selector);
|
|
245
|
+
return this.#with({
|
|
246
|
+
kind: 'select', name: 'selectMany',
|
|
247
|
+
projection: fanProjection(expression),
|
|
248
|
+
hops,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
128
251
|
|
|
129
252
|
/** @param {string} kind @param {string} slot @param {any} fn */
|
|
130
253
|
#chainCaptured(kind, slot, fn) {
|
|
131
254
|
// reuse the sync Sequence's capture through a local import-free
|
|
132
255
|
// seam: capture lives in expression.js and is stage-agnostic
|
|
133
|
-
|
|
256
|
+
const { expression, hops } = this.#capture(fn);
|
|
257
|
+
return this.#with({ kind, [slot]: expression, hops });
|
|
134
258
|
}
|
|
135
259
|
|
|
136
260
|
orderBy(key, options) { return this.#orderStage('orderBy', key, false, options); }
|
|
@@ -139,25 +263,109 @@ export class AsyncSequence {
|
|
|
139
263
|
thenByDescending(key, options) { return this.#orderStage('thenBy', key, true, options); }
|
|
140
264
|
|
|
141
265
|
#orderStage(kind, key, desc, options) {
|
|
142
|
-
const
|
|
266
|
+
const { expression, hops } = this.#capture(key);
|
|
267
|
+
const spec = { $key: expression };
|
|
143
268
|
if (desc) spec.$dir = 'desc';
|
|
144
269
|
if (options !== undefined) {
|
|
145
270
|
if (options.empty !== undefined) spec.$empty = options.empty;
|
|
146
271
|
if (options.collation !== undefined) spec.$collation = options.collation;
|
|
147
272
|
}
|
|
148
|
-
return this.#with({ kind, spec });
|
|
273
|
+
return this.#with({ kind, name: desc ? `${kind}Descending` : kind, spec, hops });
|
|
149
274
|
}
|
|
150
275
|
|
|
151
|
-
groupBy(key) {
|
|
276
|
+
groupBy(key) {
|
|
277
|
+
const { expression, hops } = this.#capture(key);
|
|
278
|
+
return this.#with({ kind: 'groupBy', key: expression, hops });
|
|
279
|
+
}
|
|
152
280
|
|
|
153
281
|
aggregate(seed, step) {
|
|
282
|
+
const { expression, hops } = this.#capture(step, (sink) => ['acc', this.#rowRoot('it', sink)]);
|
|
154
283
|
return this.#with({
|
|
155
284
|
kind: 'aggregate',
|
|
156
285
|
seed: toExpression(seed),
|
|
157
|
-
step:
|
|
286
|
+
step: expression,
|
|
287
|
+
hops,
|
|
158
288
|
});
|
|
159
289
|
}
|
|
160
290
|
|
|
291
|
+
/** The inner side of a join, checked (QUERY-PEN.md §10): a join on
|
|
292
|
+
* this surface rides INSIDE the one document a provider receives, so
|
|
293
|
+
* it needs a provider origin with no `mapAsync` before it, an async
|
|
294
|
+
* sequence over the same provider (or one sharing its scope) as the
|
|
295
|
+
* inner side, and — as on the synchronous surface — one binding per
|
|
296
|
+
* parameter name across the two sides. Over a single-pass source there
|
|
297
|
+
* is no join: the inner side would read the source twice.
|
|
298
|
+
* @param {any} inner @param {string} what
|
|
299
|
+
* @returns {ReadonlyMap<string, any>} the merged parameter bindings */
|
|
300
|
+
#requireJoinable(inner, what) {
|
|
301
|
+
if (!this.#pushable()) {
|
|
302
|
+
throw new LinqBuildError('JL0005',
|
|
303
|
+
`${what} on the async surface is pushed whole to a provider — it needs a provider `
|
|
304
|
+
+ 'source and comes before any mapAsync; over an iterable, a cursor or a push queue '
|
|
305
|
+
+ 'there is no join, because a single-pass source cannot be read twice (QUERY-PEN.md §10)');
|
|
306
|
+
}
|
|
307
|
+
if (!(inner instanceof AsyncSequence) || !inner.#pushable()) {
|
|
308
|
+
throw new LinqBuildError('JL0005',
|
|
309
|
+
`${what} takes another async sequence over a provider (with no mapAsync) as its inner side`);
|
|
310
|
+
}
|
|
311
|
+
if (!sharesScope(this.#origin.source, inner.#origin.source)) {
|
|
312
|
+
throw new LinqBuildError('JL0005',
|
|
313
|
+
`${what}'s other side must derive from the same source, or from two providers sharing `
|
|
314
|
+
+ "one scope (one store's entity sets) — a query document reads one input");
|
|
315
|
+
}
|
|
316
|
+
const merged = new Map(this.#params);
|
|
317
|
+
for (const [name, value] of inner.#params) {
|
|
318
|
+
if (merged.has(name) && merged.get(name) !== value) {
|
|
319
|
+
throw new LinqBuildError('JL0004',
|
|
320
|
+
`parameter '${name}' is bound to different values by the two sides of ${what} — `
|
|
321
|
+
+ 'one document carries one binding per name; bind it once, or rename one side');
|
|
322
|
+
}
|
|
323
|
+
merged.set(name, value);
|
|
324
|
+
}
|
|
325
|
+
return merged;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Equi-join, pushed whole: the nested-`$for` shape the synchronous
|
|
329
|
+
* surface emits (a store answers a two-root join in one statement);
|
|
330
|
+
* the inner rows keep their relation table under `it2`. */
|
|
331
|
+
join(inner, outerKey, innerKey, result) {
|
|
332
|
+
const params = this.#requireJoinable(inner, 'join');
|
|
333
|
+
const outer = this.#capture(outerKey);
|
|
334
|
+
const key = this.#capture(innerKey, (sink) => [inner.#rowRoot('it2', sink)]);
|
|
335
|
+
const projection = this.#capture(result,
|
|
336
|
+
(sink) => [this.#rowRoot('it', sink), inner.#rowRoot('it2', sink)]);
|
|
337
|
+
return this.#with({
|
|
338
|
+
kind: 'join',
|
|
339
|
+
inner: inner.toDocument(),
|
|
340
|
+
innerBare: inner.#isBareRoot(),
|
|
341
|
+
on: { $eq: [outer.expression, key.expression] },
|
|
342
|
+
result: projection.expression,
|
|
343
|
+
hops: [...inner.#hops(), ...outer.hops, ...key.hops, ...projection.hops],
|
|
344
|
+
}, params);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Group-join, pushed whole: the `$let`-bound group of the synchronous
|
|
348
|
+
* surface, under the same rule as `join`. */
|
|
349
|
+
groupJoin(inner, outerKey, innerKey, result) {
|
|
350
|
+
const params = this.#requireJoinable(inner, 'groupJoin');
|
|
351
|
+
const innerDoc = inner.toDocument();
|
|
352
|
+
const outer = this.#capture(outerKey);
|
|
353
|
+
const key = this.#capture(innerKey, (sink) => [inner.#rowRoot('it2', sink)]);
|
|
354
|
+
const group = {
|
|
355
|
+
$for: { it2: inner.#isBareRoot() ? innerDoc : [innerDoc] },
|
|
356
|
+
$where: { $eq: [outer.expression, key.expression] },
|
|
357
|
+
$return: '$it2',
|
|
358
|
+
};
|
|
359
|
+
const projection = this.#capture(result,
|
|
360
|
+
(sink) => [this.#rowRoot('it', sink), groupRoot(inner.#relations, sink)]);
|
|
361
|
+
return this.#with({
|
|
362
|
+
kind: 'groupJoin',
|
|
363
|
+
group,
|
|
364
|
+
projection: projection.expression,
|
|
365
|
+
hops: [...inner.#hops(), ...outer.hops, ...key.hops, ...projection.hops],
|
|
366
|
+
}, params);
|
|
367
|
+
}
|
|
368
|
+
|
|
161
369
|
skip(count) { return this.#with({ kind: 'skip', count: requireIndex(count, 'skip') }); }
|
|
162
370
|
take(count) { return this.#with({ kind: 'take', count: requireIndex(count, 'take') }); }
|
|
163
371
|
distinct() { return this.#with({ kind: 'distinct' }); }
|
|
@@ -170,26 +378,30 @@ export class AsyncSequence {
|
|
|
170
378
|
throw new LinqBuildError('JL0005',
|
|
171
379
|
'concat on an async sequence takes a constant array — an async source cannot be re-iterated for a second sequence');
|
|
172
380
|
}
|
|
381
|
+
// the JSON boundary (§5) and one copy at build time: the stage owns
|
|
382
|
+
// its constants, and hands out a fresh copy per enumeration
|
|
383
|
+
const items = toExpression(other).$const;
|
|
173
384
|
return this.#with({
|
|
174
385
|
kind: 'concat',
|
|
175
|
-
other: { $for: { it: { $const:
|
|
176
|
-
items
|
|
386
|
+
other: { $for: { it: { $const: items } }, $return: '$it' },
|
|
387
|
+
items,
|
|
177
388
|
});
|
|
178
389
|
}
|
|
179
390
|
|
|
180
391
|
defaultIfEmpty(fallback = null) {
|
|
181
|
-
|
|
392
|
+
const expr = toExpression(fallback);
|
|
393
|
+
return this.#with({ kind: 'defaultIfEmpty', fallback: expr, value: expr?.$const ?? fallback });
|
|
182
394
|
}
|
|
183
395
|
|
|
184
|
-
ofType(schema) { return this.#with({ kind: 'ofType', schema }); }
|
|
185
|
-
cast(schema) { return this.#with({ kind: 'cast', schema }); }
|
|
396
|
+
ofType(schema) { return this.#with({ kind: 'ofType', schema: schemaOf(schema) }); }
|
|
397
|
+
cast(schema) { return this.#with({ kind: 'cast', schema: schemaOf(schema) }); }
|
|
186
398
|
|
|
187
399
|
zip() {
|
|
188
400
|
throw new LinqBuildError('JL0006',
|
|
189
|
-
'zip is unsupported: the query grammar has no positional co-iteration (see
|
|
401
|
+
'zip is unsupported: the query grammar has no positional co-iteration (see QUERY-PEN.md §4)');
|
|
190
402
|
}
|
|
191
403
|
|
|
192
|
-
/** The bounded-concurrency boundary (
|
|
404
|
+
/** The bounded-concurrency boundary (QUERY-PEN.md §11). */
|
|
193
405
|
mapAsync(fn, options) {
|
|
194
406
|
if (typeof fn !== 'function') {
|
|
195
407
|
throw new LinqBuildError('JL0005', 'mapAsync takes an async callback');
|
|
@@ -200,8 +412,11 @@ export class AsyncSequence {
|
|
|
200
412
|
params(bindings) {
|
|
201
413
|
validateParams(bindings);
|
|
202
414
|
const merged = new Map(this.#params);
|
|
203
|
-
for (const name of Object.keys(bindings))
|
|
204
|
-
|
|
415
|
+
for (const name of Object.keys(bindings)) {
|
|
416
|
+
requireJsonBinding(name, bindings[name]);
|
|
417
|
+
merged.set(name, bindings[name]);
|
|
418
|
+
}
|
|
419
|
+
return new AsyncSequence(this.#origin, this.#stages, merged, this.#options, this.#relations);
|
|
205
420
|
}
|
|
206
421
|
|
|
207
422
|
//#endregion
|
|
@@ -209,38 +424,45 @@ export class AsyncSequence {
|
|
|
209
424
|
//#region documents and reporting
|
|
210
425
|
|
|
211
426
|
/** The chain as one query document — refuses when a `mapAsync` sits
|
|
212
|
-
* in the chain, because a host callback has no document form.
|
|
427
|
+
* in the chain, because a host callback has no document form. A deep
|
|
428
|
+
* snapshot, as on the sync surface: the emitted tree embeds the
|
|
429
|
+
* stages' captured expressions, and handing those out by reference
|
|
430
|
+
* made the document a live window into an immutable sequence. */
|
|
213
431
|
toDocument() {
|
|
214
432
|
if (this.#stages.some((s) => s.kind === 'mapAsync')) {
|
|
215
433
|
throw new LinqBuildError('JL0005',
|
|
216
434
|
'toDocument() cannot represent mapAsync (a host callback); explain() reports the split');
|
|
217
435
|
}
|
|
218
|
-
|
|
219
|
-
? this.#origin.prefixDocument()
|
|
220
|
-
: '$[*]';
|
|
221
|
-
return emitDocument(root, this.#stages);
|
|
436
|
+
return snapshot(this.#documentOf(this.#stages));
|
|
222
437
|
}
|
|
223
438
|
|
|
224
|
-
/** Barriers, the split,
|
|
439
|
+
/** Barriers, the split, the relation hops the callbacks navigated,
|
|
440
|
+
* and — when representable — the document. Stages are named by the
|
|
441
|
+
* OPERATOR the caller wrote (`selectMany`, `orderByDescending`), and a
|
|
442
|
+
* `thenBy` is part of the `$orderby` barrier it extends, not a barrier
|
|
443
|
+
* of its own. */
|
|
225
444
|
explain() {
|
|
445
|
+
const firstMap = this.#stages.findIndex((s) => s.kind === 'mapAsync');
|
|
446
|
+
// over a provider nothing before the split materialises HERE — the
|
|
447
|
+
// provider runs the pushed document — so only the residual can hold
|
|
448
|
+
// a barrier of this surface's own
|
|
449
|
+
const local = this.#origin.kind === 'provider'
|
|
450
|
+
? (firstMap < 0 ? [] : this.#stages.slice(firstMap))
|
|
451
|
+
: this.#stages;
|
|
226
452
|
const barriers = [];
|
|
227
|
-
for (const stage of
|
|
228
|
-
if (BARRIERS[stage.kind] !== undefined) {
|
|
229
|
-
barriers.push({ operator: stage.kind, reason: BARRIERS[stage.kind] });
|
|
453
|
+
for (const stage of local) {
|
|
454
|
+
if (BARRIERS[stage.kind] !== undefined && stage.kind !== 'thenBy') {
|
|
455
|
+
barriers.push({ operator: stage.name ?? stage.kind, reason: BARRIERS[stage.kind] });
|
|
230
456
|
}
|
|
231
457
|
}
|
|
232
|
-
const
|
|
233
|
-
const out = { barriers };
|
|
458
|
+
const out = { barriers, hops: this.#hops(), bindings: this.#externals().values };
|
|
234
459
|
if (firstMap < 0) {
|
|
235
460
|
out.document = this.toDocument();
|
|
236
461
|
}
|
|
237
462
|
else {
|
|
238
|
-
const prefix = this.#stages.slice(0, firstMap);
|
|
239
463
|
out.split = {
|
|
240
|
-
pushed: this.#
|
|
241
|
-
|
|
242
|
-
: emitDocument('$[*]', prefix),
|
|
243
|
-
residual: this.#stages.slice(firstMap).map((s) => s.kind),
|
|
464
|
+
pushed: snapshot(this.#documentOf(this.#stages.slice(0, firstMap))),
|
|
465
|
+
residual: this.#stages.slice(firstMap).map((s) => s.name ?? s.kind),
|
|
244
466
|
};
|
|
245
467
|
}
|
|
246
468
|
return out;
|
|
@@ -254,12 +476,22 @@ export class AsyncSequence {
|
|
|
254
476
|
* barrier chunks. @returns {AsyncGenerator<any>} */
|
|
255
477
|
async* [Symbol.asyncIterator]() {
|
|
256
478
|
const { names, values } = this.#externals();
|
|
257
|
-
let stream = this.#origin.kind === 'sequence'
|
|
258
|
-
? arrayStream(this.#origin.runPrefix())
|
|
259
|
-
: this.#origin.iterate();
|
|
260
|
-
|
|
261
479
|
const stages = this.#stages;
|
|
480
|
+
let stream;
|
|
262
481
|
let i = 0;
|
|
482
|
+
if (this.#origin.kind === 'sequence') {
|
|
483
|
+
stream = arrayStream(this.#origin.runPrefix(this.#params));
|
|
484
|
+
}
|
|
485
|
+
else if (this.#origin.kind === 'provider') {
|
|
486
|
+
// everything up to the first mapAsync is ONE document the provider
|
|
487
|
+
// runs whole; the residual continues locally over its rows
|
|
488
|
+
const firstMap = stages.findIndex((s) => s.kind === 'mapAsync');
|
|
489
|
+
i = firstMap < 0 ? stages.length : firstMap;
|
|
490
|
+
stream = arrayStream(await this.#pushWindow('toArray', undefined, stages.slice(0, i)));
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
stream = this.#origin.iterate();
|
|
494
|
+
}
|
|
263
495
|
while (i < stages.length) {
|
|
264
496
|
const stage = stages[i];
|
|
265
497
|
if (BARRIERS[stage.kind] !== undefined) {
|
|
@@ -360,9 +592,12 @@ export class AsyncSequence {
|
|
|
360
592
|
})();
|
|
361
593
|
}
|
|
362
594
|
case 'concat': {
|
|
595
|
+
// a fresh copy per enumeration: a consumer that writes into a
|
|
596
|
+
// yielded constant must not rewrite what the next enumeration
|
|
597
|
+
// answers (the sync surface hands out the engine's frozen values)
|
|
363
598
|
return (async function* () {
|
|
364
599
|
yield* iterateAndClose(stream);
|
|
365
|
-
|
|
600
|
+
for (const item of stage.items) yield snapshot(item);
|
|
366
601
|
})();
|
|
367
602
|
}
|
|
368
603
|
default: { // 'defaultIfEmpty'
|
|
@@ -373,7 +608,7 @@ export class AsyncSequence {
|
|
|
373
608
|
any = true;
|
|
374
609
|
yield item;
|
|
375
610
|
}
|
|
376
|
-
if (!any) yield stage.value ?? null;
|
|
611
|
+
if (!any) yield snapshot(stage.value ?? null);
|
|
377
612
|
})();
|
|
378
613
|
}
|
|
379
614
|
}
|
|
@@ -394,19 +629,19 @@ export class AsyncSequence {
|
|
|
394
629
|
}
|
|
395
630
|
|
|
396
631
|
async first() {
|
|
397
|
-
const w = await this.#window(1);
|
|
632
|
+
const w = this.#pushable() ? await this.#pushWindow('first') : await this.#window(1);
|
|
398
633
|
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'first() found no element');
|
|
399
634
|
return w[0];
|
|
400
635
|
}
|
|
401
636
|
|
|
402
637
|
/** @param {any} [defaultValue] */
|
|
403
638
|
async firstOrDefault(defaultValue) {
|
|
404
|
-
const w = await this.#window(1);
|
|
639
|
+
const w = this.#pushable() ? await this.#pushWindow('first') : await this.#window(1);
|
|
405
640
|
return w.length === 0 ? defaultValue : w[0];
|
|
406
641
|
}
|
|
407
642
|
|
|
408
643
|
async single() {
|
|
409
|
-
const w = await this.#window(2);
|
|
644
|
+
const w = this.#pushable() ? await this.#pushWindow('single') : await this.#window(2);
|
|
410
645
|
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'single() found no element');
|
|
411
646
|
if (w.length > 1) throw new LinqRuntimeError('JL2002', 'single() found more than one element');
|
|
412
647
|
return w[0];
|
|
@@ -414,27 +649,34 @@ export class AsyncSequence {
|
|
|
414
649
|
|
|
415
650
|
/** @param {any} [defaultValue] */
|
|
416
651
|
async singleOrDefault(defaultValue) {
|
|
417
|
-
const w = await this.#window(2);
|
|
652
|
+
const w = this.#pushable() ? await this.#pushWindow('single') : await this.#window(2);
|
|
418
653
|
if (w.length > 1) throw new LinqRuntimeError('JL2002', 'singleOrDefault() found more than one element');
|
|
419
654
|
return w.length === 0 ? defaultValue : w[0];
|
|
420
655
|
}
|
|
421
656
|
|
|
422
657
|
async last() {
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
return
|
|
658
|
+
const w = this.#pushable() ? await this.#pushWindow('last') : await this.#lastWindow();
|
|
659
|
+
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'last() found no element');
|
|
660
|
+
return w[0];
|
|
426
661
|
}
|
|
427
662
|
|
|
428
663
|
/** @param {any} [defaultValue] */
|
|
429
664
|
async lastOrDefault(defaultValue) {
|
|
665
|
+
const w = this.#pushable() ? await this.#pushWindow('last') : await this.#lastWindow();
|
|
666
|
+
return w.length === 0 ? defaultValue : w[0];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** The last item as a window, read from the whole stream. */
|
|
670
|
+
async #lastWindow() {
|
|
430
671
|
const all = await this.toArray();
|
|
431
|
-
return all.length === 0 ?
|
|
672
|
+
return all.length === 0 ? [] : [all[all.length - 1]];
|
|
432
673
|
}
|
|
433
674
|
|
|
434
675
|
/** @param {number} index */
|
|
435
676
|
async elementAt(index) {
|
|
436
677
|
requireIndex(index, 'elementAt');
|
|
437
|
-
const w =
|
|
678
|
+
const w = this.#pushable()
|
|
679
|
+
? await this.#pushWindow('elementAt', [index]) : await this.skip(index).#window(1);
|
|
438
680
|
if (w.length === 0) throw new LinqRuntimeError('JL2003', `elementAt(${index}) is out of range`);
|
|
439
681
|
return w[0];
|
|
440
682
|
}
|
|
@@ -442,11 +684,13 @@ export class AsyncSequence {
|
|
|
442
684
|
/** @param {number} index @param {any} [defaultValue] */
|
|
443
685
|
async elementAtOrDefault(index, defaultValue) {
|
|
444
686
|
requireIndex(index, 'elementAtOrDefault');
|
|
445
|
-
const w =
|
|
687
|
+
const w = this.#pushable()
|
|
688
|
+
? await this.#pushWindow('elementAt', [index]) : await this.skip(index).#window(1);
|
|
446
689
|
return w.length === 0 ? defaultValue : w[0];
|
|
447
690
|
}
|
|
448
691
|
|
|
449
692
|
async count() {
|
|
693
|
+
if (this.#pushable()) return this.#pushTerminal('count');
|
|
450
694
|
let n = 0;
|
|
451
695
|
// eslint-disable-next-line no-unused-vars
|
|
452
696
|
for await (const item of this) n++;
|
|
@@ -457,6 +701,7 @@ export class AsyncSequence {
|
|
|
457
701
|
* their semantics (type errors included) match the sync surface
|
|
458
702
|
* exactly. @param {string} terminal */
|
|
459
703
|
async #aggregateTerminal(terminal) {
|
|
704
|
+
if (this.#pushable()) return this.#pushTerminal(terminal);
|
|
460
705
|
const items = await this.toArray();
|
|
461
706
|
const { names, values } = this.#externals();
|
|
462
707
|
const compiled = compileDocument(wrapTerminal('$[*]', terminal),
|
|
@@ -486,6 +731,11 @@ export class AsyncSequence {
|
|
|
486
731
|
|
|
487
732
|
/** @param {any} [predicate] */
|
|
488
733
|
async any(predicate) {
|
|
734
|
+
if (this.#pushable()) {
|
|
735
|
+
return predicate === undefined
|
|
736
|
+
? this.#pushTerminal('exists')
|
|
737
|
+
: this.#pushTerminal('some', [this.#capture(predicate).expression]);
|
|
738
|
+
}
|
|
489
739
|
const seq = predicate === undefined ? this : this.where(predicate);
|
|
490
740
|
for await (const item of seq) {
|
|
491
741
|
void item;
|
|
@@ -496,7 +746,8 @@ export class AsyncSequence {
|
|
|
496
746
|
|
|
497
747
|
/** @param {any} predicate */
|
|
498
748
|
async all(predicate) {
|
|
499
|
-
|
|
749
|
+
if (this.#pushable()) return this.#pushTerminal('every', [this.#capture(predicate).expression]);
|
|
750
|
+
const q = this.#evaluator(this.#capture(predicate).expression);
|
|
500
751
|
for await (const item of this) {
|
|
501
752
|
if (!q.ebv(item, this.#externals().values)) return false;
|
|
502
753
|
}
|
|
@@ -541,14 +792,6 @@ function requireIndex(value, what) {
|
|
|
541
792
|
return value;
|
|
542
793
|
}
|
|
543
794
|
|
|
544
|
-
/** @param {ReadonlyMap<string, any>} params @param {any} fn @param {readonly any[]} [roots] */
|
|
545
|
-
function captureFor(params, fn, roots = ['it']) {
|
|
546
|
-
if (typeof fn !== 'function') {
|
|
547
|
-
throw new LinqBuildError('JL0005', 'this operator takes a callback function');
|
|
548
|
-
}
|
|
549
|
-
return captureExpression(fn, roots, new Set(params.keys()));
|
|
550
|
-
}
|
|
551
|
-
|
|
552
795
|
/** @param {any} bindings */
|
|
553
796
|
function validateParams(bindings) {
|
|
554
797
|
if (bindings === null || typeof bindings !== 'object' || Array.isArray(bindings)) {
|
|
@@ -558,21 +801,31 @@ function validateParams(bindings) {
|
|
|
558
801
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
559
802
|
throw new LinqBuildError('JL0004', `'${name}' is not a valid parameter name`);
|
|
560
803
|
}
|
|
561
|
-
if (name
|
|
804
|
+
if (isReservedBinding(name)) {
|
|
562
805
|
throw new LinqBuildError('JL0004',
|
|
563
|
-
`'${name}' is reserved (the emitted document's own binding names:
|
|
806
|
+
`'${name}' is reserved (the emitted document's own binding names: ${RESERVED_BINDINGS_TEXT})`);
|
|
564
807
|
}
|
|
565
808
|
}
|
|
566
809
|
}
|
|
567
810
|
|
|
568
811
|
/**
|
|
569
|
-
* Build an async sequence over an async source (
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
812
|
+
* Build an async sequence over an async source (QUERY-PEN.md §10), or
|
|
813
|
+
* over a provider (§12): an `execute` duck is asked for BEFORE the
|
|
814
|
+
* iterable shapes, and its items are bound through its own root — as
|
|
815
|
+
* `from()` binds them, so the two surfaces emit one document.
|
|
816
|
+
* @param {any} source - async iterable, sync iterable, cursor, push
|
|
817
|
+
* queue, or a provider
|
|
818
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
819
|
+
* pathFunctions?: any, limits?: any, registry?: object }} [options] -
|
|
820
|
+
* the engine registries, as `from()` takes them
|
|
573
821
|
* @returns {AsyncSequence}
|
|
574
822
|
*/
|
|
575
823
|
export function fromAsync(source, options = {}) {
|
|
824
|
+
if (isProviderSource(source)) {
|
|
825
|
+
return new AsyncSequence(
|
|
826
|
+
{ kind: 'provider', source, root: providerRoot(source) },
|
|
827
|
+
[], new Map(), options, providerRelations(source));
|
|
828
|
+
}
|
|
576
829
|
return new AsyncSequence(
|
|
577
830
|
{ kind: 'source', iterate: adaptAsyncSource(source) },
|
|
578
831
|
[], new Map(), options);
|
|
@@ -583,8 +836,9 @@ export function fromAsync(source, options = {}) {
|
|
|
583
836
|
* PREFIX (compiled in memory or pushed WHOLE to its provider), and the
|
|
584
837
|
* async surface continues locally from its rows. `explain()` reports
|
|
585
838
|
* the split (D8's residual honesty, applied to the async boundary).
|
|
586
|
-
* @param {{ runPrefix: (
|
|
587
|
-
* params: ReadonlyMap<string, any>,
|
|
839
|
+
* @param {{ runPrefix: (params: ReadonlyMap<string, any>) => any[],
|
|
840
|
+
* prefixDocument: () => any, params: ReadonlyMap<string, any>,
|
|
841
|
+
* options: { compileTypeTest?: any } }} carrier
|
|
588
842
|
* @param {any} fn
|
|
589
843
|
* @param {any} options
|
|
590
844
|
* @returns {AsyncSequence}
|