@jarenjs/linq 0.49.2 → 0.66.1
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 +227 -0
- package/README.md +650 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1221 -0
- package/docs/DB-CLIENT.md +882 -0
- package/docs/FLOW-PEN.md +1033 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +778 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1092 -0
- package/docs/QUERY-PEN.md +1724 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +251 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +255 -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 +377 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +48 -11
- package/src/contract/define.js +282 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +338 -0
- package/src/db/handle.js +89 -0
- package/src/db/include.js +351 -0
- package/src/db/index.js +24 -0
- package/src/db/ledger.js +195 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +130 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +78 -6
- package/src/expression.js +463 -36
- package/src/federate.js +531 -0
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +316 -0
- package/src/flow/fsm.js +323 -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 +5 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +202 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +318 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +244 -0
- package/src/model/collection.js +273 -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 +468 -0
- package/types/db.d.ts +359 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +296 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +526 -0
- package/types/schema.d.ts +494 -0
package/src/async.js
CHANGED
|
@@ -24,6 +24,15 @@
|
|
|
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 — a terminal's wrapper included, exactly as the
|
|
30
|
+
* synchronous surface pushes it, `execute` answering a promise if it
|
|
31
|
+
* must (D8). Iteration hands that document to the provider's CURSOR
|
|
32
|
+
* when it offers one, so a `for await` pulls one row at a time from
|
|
33
|
+
* an open statement and a `break` releases it; the residual after
|
|
34
|
+
* the split streams locally, and a `join` exists on this surface
|
|
35
|
+
* only inside that pushed document.
|
|
27
36
|
*
|
|
28
37
|
* Early termination CLOSES the source: every consumer is a
|
|
29
38
|
* `for await … break` chain, and async generators propagate `return()`
|
|
@@ -31,20 +40,31 @@
|
|
|
31
40
|
* read transaction open.
|
|
32
41
|
*/
|
|
33
42
|
|
|
34
|
-
import {
|
|
35
|
-
|
|
43
|
+
import {
|
|
44
|
+
compileDocument, isProviderSource, providerRoot, providerRelations, sharesScope,
|
|
45
|
+
} from './provider.js';
|
|
46
|
+
import {
|
|
47
|
+
emitDocument, wrapTerminal, snapshot, fanProjection, isReservedBinding, RESERVED_BINDINGS_TEXT,
|
|
48
|
+
PROJECTING_STAGES,
|
|
49
|
+
} from './document.js';
|
|
36
50
|
import { adaptAsyncSource } from './sources.js';
|
|
37
51
|
import { applyMapAsync, normalizeMapAsyncOptions } from './concurrency.js';
|
|
38
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
captureExpression, toExpression, requireJsonBinding, createHopSink, rowRoot, groupRoot,
|
|
54
|
+
} from './expression.js';
|
|
39
55
|
import { LinqBuildError, LinqRuntimeError } from './errors.js';
|
|
56
|
+
import { schemaOf } from './schema-of.js';
|
|
40
57
|
import { semanticKey } from '@jarenjs/core/object';
|
|
41
58
|
|
|
42
|
-
/** Barrier stage kinds and the reason each materialises.
|
|
59
|
+
/** Barrier stage kinds and the reason each materialises. A `join` is
|
|
60
|
+
* never a barrier: over a provider it rides INSIDE the one pushed
|
|
61
|
+
* document (the only place this surface joins), and over a single-pass
|
|
62
|
+
* source — a cursor, a queue, a stream — it is refused, because the
|
|
63
|
+
* inner side would have to read the source twice (QUERY-PEN.md §10). */
|
|
43
64
|
const BARRIERS = {
|
|
44
65
|
orderBy: '$orderby materialises the tuple stream to sort it',
|
|
45
66
|
thenBy: '$orderby materialises the tuple stream to sort it',
|
|
46
67
|
groupBy: '$groupby materialises the tuple stream to group it',
|
|
47
|
-
join: 'a join needs the whole inner side',
|
|
48
68
|
aggregate: '$fold folds the whole stream into one value',
|
|
49
69
|
reverse: '$reverse needs the last item first',
|
|
50
70
|
};
|
|
@@ -85,24 +105,72 @@ export class AsyncSequence {
|
|
|
85
105
|
#stages;
|
|
86
106
|
#params;
|
|
87
107
|
#options;
|
|
108
|
+
#relations;
|
|
88
109
|
|
|
89
110
|
/**
|
|
90
111
|
* @param {{ kind: 'source', iterate: () => AsyncIterator<any> }
|
|
91
|
-
* | { kind: '
|
|
112
|
+
* | { kind: 'provider', source: any, root: string }
|
|
113
|
+
* | { kind: 'sequence', runPrefix: (params: ReadonlyMap<string, any>) => any[],
|
|
114
|
+
* prefixDocument: () => any }} origin
|
|
92
115
|
* @param {readonly any[]} stages
|
|
93
116
|
* @param {ReadonlyMap<string, any>} params
|
|
94
|
-
* @param {{ compileTypeTest?: any
|
|
117
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
118
|
+
* pathFunctions?: any, limits?: any, registry?: object }} options
|
|
119
|
+
* @param {{ table: any, resolve: (name: string) => any } | null} [relations] -
|
|
120
|
+
* the relation table of the rows the items ARE (a provider's, while
|
|
121
|
+
* no stage has projected them), or null — as on the sync surface
|
|
95
122
|
*/
|
|
96
|
-
constructor(origin, stages, params, options) {
|
|
123
|
+
constructor(origin, stages, params, options, relations = null) {
|
|
97
124
|
this.#origin = origin;
|
|
98
125
|
this.#stages = stages;
|
|
99
126
|
this.#params = params;
|
|
100
127
|
this.#options = options;
|
|
128
|
+
this.#relations = relations;
|
|
101
129
|
}
|
|
102
130
|
|
|
103
|
-
/** @param {any} stage */
|
|
104
|
-
#with(stage) {
|
|
105
|
-
|
|
131
|
+
/** @param {any} stage @param {ReadonlyMap<string, any>} [params] */
|
|
132
|
+
#with(stage, params = this.#params) {
|
|
133
|
+
// the items stop being rows at a projection, and at the host boundary
|
|
134
|
+
const projects = PROJECTING_STAGES.has(stage.kind) || stage.kind === 'mapAsync';
|
|
135
|
+
return new AsyncSequence(this.#origin, [...this.#stages, stage], params, this.#options,
|
|
136
|
+
projects ? null : this.#relations);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Capture one callback over this sequence's items — the expression and
|
|
141
|
+
* the relation hops it navigated — exactly as the synchronous surface
|
|
142
|
+
* does, so the two emit one document.
|
|
143
|
+
* @param {any} fn
|
|
144
|
+
* @param {(sink: ReturnType<typeof createHopSink>) => readonly any[]} [rootsOf]
|
|
145
|
+
* @returns {{ expression: any, hops: readonly any[] }}
|
|
146
|
+
*/
|
|
147
|
+
#capture(fn, rootsOf = (sink) => [this.#rowRoot('it', sink)]) {
|
|
148
|
+
if (typeof fn !== 'function') {
|
|
149
|
+
throw new LinqBuildError('JL0005', 'this operator takes a callback function');
|
|
150
|
+
}
|
|
151
|
+
const sink = createHopSink();
|
|
152
|
+
const expression = captureExpression(fn, rootsOf(sink), new Set(this.#params.keys()));
|
|
153
|
+
return { expression, hops: sink.hops };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Whether this sequence's items are a `groupBy`'s `{ key, items }`,
|
|
157
|
+
* as on the synchronous surface. */
|
|
158
|
+
#grouped() {
|
|
159
|
+
for (let i = this.#stages.length - 1; i >= 0; i--) {
|
|
160
|
+
const kind = this.#stages[i].kind;
|
|
161
|
+
if (PROJECTING_STAGES.has(kind) || kind === 'mapAsync') return kind === 'groupBy';
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @param {string} name @param {ReturnType<typeof createHopSink>} sink */
|
|
167
|
+
#rowRoot(name, sink) {
|
|
168
|
+
return rowRoot(name, this.#relations, sink, this.#grouped());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The relation hops every stage's callbacks navigated, in order. */
|
|
172
|
+
#hops() {
|
|
173
|
+
return this.#stages.flatMap((stage) => stage.hops ?? []);
|
|
106
174
|
}
|
|
107
175
|
|
|
108
176
|
#externals() {
|
|
@@ -112,6 +180,57 @@ export class AsyncSequence {
|
|
|
112
180
|
};
|
|
113
181
|
}
|
|
114
182
|
|
|
183
|
+
/** Whether the chain so far goes to a provider as ONE document: a
|
|
184
|
+
* provider origin with no `mapAsync` yet (D8 — the document arrives
|
|
185
|
+
* whole; a host callback is where it splits). */
|
|
186
|
+
#pushable() {
|
|
187
|
+
return this.#origin.kind === 'provider'
|
|
188
|
+
&& !this.#stages.some((stage) => stage.kind === 'mapAsync');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Whether this sequence is a provider's own root, untouched — the
|
|
192
|
+
* one `$for` source the emitter leaves unpacked (document.js). */
|
|
193
|
+
#isBareRoot() {
|
|
194
|
+
return this.#origin.kind === 'provider' && this.#stages.length === 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The root expression the stages iterate: a provider's own root, the
|
|
198
|
+
* pushed synchronous prefix, or the whole input. */
|
|
199
|
+
#rootExpression() {
|
|
200
|
+
if (this.#origin.kind === 'sequence') return this.#origin.prefixDocument();
|
|
201
|
+
return this.#origin.kind === 'provider' ? this.#origin.root : '$[*]';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The document for a run of stages over this origin's root. */
|
|
205
|
+
#documentOf(stages) {
|
|
206
|
+
return emitDocument(this.#rootExpression(), stages,
|
|
207
|
+
{ bareRoot: this.#origin.kind === 'provider' });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Hand one document to the provider, whole, with the bound
|
|
211
|
+
* externals; `execute` may answer a value or a promise here. */
|
|
212
|
+
async #push(document) {
|
|
213
|
+
return this.#origin.source.execute(document, { externals: this.#externals().values });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A pushed element window over `stages`: the provider must answer
|
|
217
|
+
* exactly one array, as on the synchronous surface (`JL2006`). */
|
|
218
|
+
async #pushWindow(terminal, args = undefined, stages = this.#stages) {
|
|
219
|
+
const result = await this.#push(wrapTerminal(this.#documentOf(stages), terminal, args));
|
|
220
|
+
if (!Array.isArray(result)) {
|
|
221
|
+
throw new LinqRuntimeError('JL2006',
|
|
222
|
+
`the provider answered ${terminal}() with ${result === undefined ? 'undefined'
|
|
223
|
+
: `a ${typeof result}`} — an element terminal emits an array constructor, so a `
|
|
224
|
+
+ 'conforming execute() answers exactly one array (QUERY-PEN.md §8)');
|
|
225
|
+
}
|
|
226
|
+
return /** @type {any[]} */ (result);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A pushed aggregate or quantifier over the whole chain. */
|
|
230
|
+
#pushTerminal(terminal, args = undefined) {
|
|
231
|
+
return this.#push(wrapTerminal(this.#documentOf(this.#stages), terminal, args));
|
|
232
|
+
}
|
|
233
|
+
|
|
115
234
|
/** Compile one per-item evaluator: the stage expression under
|
|
116
235
|
* `{$let: {it: '$'}}` with the terminal-window discipline. */
|
|
117
236
|
#evaluator(returnExpr) {
|
|
@@ -124,13 +243,21 @@ export class AsyncSequence {
|
|
|
124
243
|
|
|
125
244
|
where(predicate) { return this.#chainCaptured('where', 'predicate', predicate); }
|
|
126
245
|
select(projection) { return this.#chainCaptured('select', 'projection', projection); }
|
|
127
|
-
selectMany(selector) {
|
|
246
|
+
selectMany(selector) {
|
|
247
|
+
const { expression, hops } = this.#capture(selector);
|
|
248
|
+
return this.#with({
|
|
249
|
+
kind: 'select', name: 'selectMany',
|
|
250
|
+
projection: fanProjection(expression),
|
|
251
|
+
hops,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
128
254
|
|
|
129
255
|
/** @param {string} kind @param {string} slot @param {any} fn */
|
|
130
256
|
#chainCaptured(kind, slot, fn) {
|
|
131
257
|
// reuse the sync Sequence's capture through a local import-free
|
|
132
258
|
// seam: capture lives in expression.js and is stage-agnostic
|
|
133
|
-
|
|
259
|
+
const { expression, hops } = this.#capture(fn);
|
|
260
|
+
return this.#with({ kind, [slot]: expression, hops });
|
|
134
261
|
}
|
|
135
262
|
|
|
136
263
|
orderBy(key, options) { return this.#orderStage('orderBy', key, false, options); }
|
|
@@ -139,25 +266,109 @@ export class AsyncSequence {
|
|
|
139
266
|
thenByDescending(key, options) { return this.#orderStage('thenBy', key, true, options); }
|
|
140
267
|
|
|
141
268
|
#orderStage(kind, key, desc, options) {
|
|
142
|
-
const
|
|
269
|
+
const { expression, hops } = this.#capture(key);
|
|
270
|
+
const spec = { $key: expression };
|
|
143
271
|
if (desc) spec.$dir = 'desc';
|
|
144
272
|
if (options !== undefined) {
|
|
145
273
|
if (options.empty !== undefined) spec.$empty = options.empty;
|
|
146
274
|
if (options.collation !== undefined) spec.$collation = options.collation;
|
|
147
275
|
}
|
|
148
|
-
return this.#with({ kind, spec });
|
|
276
|
+
return this.#with({ kind, name: desc ? `${kind}Descending` : kind, spec, hops });
|
|
149
277
|
}
|
|
150
278
|
|
|
151
|
-
groupBy(key) {
|
|
279
|
+
groupBy(key) {
|
|
280
|
+
const { expression, hops } = this.#capture(key);
|
|
281
|
+
return this.#with({ kind: 'groupBy', key: expression, hops });
|
|
282
|
+
}
|
|
152
283
|
|
|
153
284
|
aggregate(seed, step) {
|
|
285
|
+
const { expression, hops } = this.#capture(step, (sink) => ['acc', this.#rowRoot('it', sink)]);
|
|
154
286
|
return this.#with({
|
|
155
287
|
kind: 'aggregate',
|
|
156
288
|
seed: toExpression(seed),
|
|
157
|
-
step:
|
|
289
|
+
step: expression,
|
|
290
|
+
hops,
|
|
158
291
|
});
|
|
159
292
|
}
|
|
160
293
|
|
|
294
|
+
/** The inner side of a join, checked (QUERY-PEN.md §10): a join on
|
|
295
|
+
* this surface rides INSIDE the one document a provider receives, so
|
|
296
|
+
* it needs a provider origin with no `mapAsync` before it, an async
|
|
297
|
+
* sequence over the same provider (or one sharing its scope) as the
|
|
298
|
+
* inner side, and — as on the synchronous surface — one binding per
|
|
299
|
+
* parameter name across the two sides. Over a single-pass source there
|
|
300
|
+
* is no join: the inner side would read the source twice.
|
|
301
|
+
* @param {any} inner @param {string} what
|
|
302
|
+
* @returns {ReadonlyMap<string, any>} the merged parameter bindings */
|
|
303
|
+
#requireJoinable(inner, what) {
|
|
304
|
+
if (!this.#pushable()) {
|
|
305
|
+
throw new LinqBuildError('JL0005',
|
|
306
|
+
`${what} on the async surface is pushed whole to a provider — it needs a provider `
|
|
307
|
+
+ 'source and comes before any mapAsync; over an iterable, a cursor or a push queue '
|
|
308
|
+
+ 'there is no join, because a single-pass source cannot be read twice (QUERY-PEN.md §10)');
|
|
309
|
+
}
|
|
310
|
+
if (!(inner instanceof AsyncSequence) || !inner.#pushable()) {
|
|
311
|
+
throw new LinqBuildError('JL0005',
|
|
312
|
+
`${what} takes another async sequence over a provider (with no mapAsync) as its inner side`);
|
|
313
|
+
}
|
|
314
|
+
if (!sharesScope(this.#origin.source, inner.#origin.source)) {
|
|
315
|
+
throw new LinqBuildError('JL0005',
|
|
316
|
+
`${what}'s other side must derive from the same source, or from two providers sharing `
|
|
317
|
+
+ "one scope (one store's entity sets) — a query document reads one input");
|
|
318
|
+
}
|
|
319
|
+
const merged = new Map(this.#params);
|
|
320
|
+
for (const [name, value] of inner.#params) {
|
|
321
|
+
if (merged.has(name) && merged.get(name) !== value) {
|
|
322
|
+
throw new LinqBuildError('JL0004',
|
|
323
|
+
`parameter '${name}' is bound to different values by the two sides of ${what} — `
|
|
324
|
+
+ 'one document carries one binding per name; bind it once, or rename one side');
|
|
325
|
+
}
|
|
326
|
+
merged.set(name, value);
|
|
327
|
+
}
|
|
328
|
+
return merged;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Equi-join, pushed whole: the nested-`$for` shape the synchronous
|
|
332
|
+
* surface emits (a store answers a two-root join in one statement);
|
|
333
|
+
* the inner rows keep their relation table under `it2`. */
|
|
334
|
+
join(inner, outerKey, innerKey, result) {
|
|
335
|
+
const params = this.#requireJoinable(inner, 'join');
|
|
336
|
+
const outer = this.#capture(outerKey);
|
|
337
|
+
const key = this.#capture(innerKey, (sink) => [inner.#rowRoot('it2', sink)]);
|
|
338
|
+
const projection = this.#capture(result,
|
|
339
|
+
(sink) => [this.#rowRoot('it', sink), inner.#rowRoot('it2', sink)]);
|
|
340
|
+
return this.#with({
|
|
341
|
+
kind: 'join',
|
|
342
|
+
inner: inner.toDocument(),
|
|
343
|
+
innerBare: inner.#isBareRoot(),
|
|
344
|
+
on: { $eq: [outer.expression, key.expression] },
|
|
345
|
+
result: projection.expression,
|
|
346
|
+
hops: [...inner.#hops(), ...outer.hops, ...key.hops, ...projection.hops],
|
|
347
|
+
}, params);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Group-join, pushed whole: the `$let`-bound group of the synchronous
|
|
351
|
+
* surface, under the same rule as `join`. */
|
|
352
|
+
groupJoin(inner, outerKey, innerKey, result) {
|
|
353
|
+
const params = this.#requireJoinable(inner, 'groupJoin');
|
|
354
|
+
const innerDoc = inner.toDocument();
|
|
355
|
+
const outer = this.#capture(outerKey);
|
|
356
|
+
const key = this.#capture(innerKey, (sink) => [inner.#rowRoot('it2', sink)]);
|
|
357
|
+
const group = {
|
|
358
|
+
$for: { it2: inner.#isBareRoot() ? innerDoc : [innerDoc] },
|
|
359
|
+
$where: { $eq: [outer.expression, key.expression] },
|
|
360
|
+
$return: '$it2',
|
|
361
|
+
};
|
|
362
|
+
const projection = this.#capture(result,
|
|
363
|
+
(sink) => [this.#rowRoot('it', sink), groupRoot(inner.#relations, sink)]);
|
|
364
|
+
return this.#with({
|
|
365
|
+
kind: 'groupJoin',
|
|
366
|
+
group,
|
|
367
|
+
projection: projection.expression,
|
|
368
|
+
hops: [...inner.#hops(), ...outer.hops, ...key.hops, ...projection.hops],
|
|
369
|
+
}, params);
|
|
370
|
+
}
|
|
371
|
+
|
|
161
372
|
skip(count) { return this.#with({ kind: 'skip', count: requireIndex(count, 'skip') }); }
|
|
162
373
|
take(count) { return this.#with({ kind: 'take', count: requireIndex(count, 'take') }); }
|
|
163
374
|
distinct() { return this.#with({ kind: 'distinct' }); }
|
|
@@ -170,26 +381,30 @@ export class AsyncSequence {
|
|
|
170
381
|
throw new LinqBuildError('JL0005',
|
|
171
382
|
'concat on an async sequence takes a constant array — an async source cannot be re-iterated for a second sequence');
|
|
172
383
|
}
|
|
384
|
+
// the JSON boundary (§5) and one copy at build time: the stage owns
|
|
385
|
+
// its constants, and hands out a fresh copy per enumeration
|
|
386
|
+
const items = toExpression(other).$const;
|
|
173
387
|
return this.#with({
|
|
174
388
|
kind: 'concat',
|
|
175
|
-
other: { $for: { it: { $const:
|
|
176
|
-
items
|
|
389
|
+
other: { $for: { it: { $const: items } }, $return: '$it' },
|
|
390
|
+
items,
|
|
177
391
|
});
|
|
178
392
|
}
|
|
179
393
|
|
|
180
394
|
defaultIfEmpty(fallback = null) {
|
|
181
|
-
|
|
395
|
+
const expr = toExpression(fallback);
|
|
396
|
+
return this.#with({ kind: 'defaultIfEmpty', fallback: expr, value: expr?.$const ?? fallback });
|
|
182
397
|
}
|
|
183
398
|
|
|
184
|
-
ofType(schema) { return this.#with({ kind: 'ofType', schema }); }
|
|
185
|
-
cast(schema) { return this.#with({ kind: 'cast', schema }); }
|
|
399
|
+
ofType(schema) { return this.#with({ kind: 'ofType', schema: schemaOf(schema) }); }
|
|
400
|
+
cast(schema) { return this.#with({ kind: 'cast', schema: schemaOf(schema) }); }
|
|
186
401
|
|
|
187
402
|
zip() {
|
|
188
403
|
throw new LinqBuildError('JL0006',
|
|
189
|
-
'zip is unsupported: the query grammar has no positional co-iteration (see
|
|
404
|
+
'zip is unsupported: the query grammar has no positional co-iteration (see QUERY-PEN.md §4)');
|
|
190
405
|
}
|
|
191
406
|
|
|
192
|
-
/** The bounded-concurrency boundary (
|
|
407
|
+
/** The bounded-concurrency boundary (QUERY-PEN.md §11). */
|
|
193
408
|
mapAsync(fn, options) {
|
|
194
409
|
if (typeof fn !== 'function') {
|
|
195
410
|
throw new LinqBuildError('JL0005', 'mapAsync takes an async callback');
|
|
@@ -200,8 +415,11 @@ export class AsyncSequence {
|
|
|
200
415
|
params(bindings) {
|
|
201
416
|
validateParams(bindings);
|
|
202
417
|
const merged = new Map(this.#params);
|
|
203
|
-
for (const name of Object.keys(bindings))
|
|
204
|
-
|
|
418
|
+
for (const name of Object.keys(bindings)) {
|
|
419
|
+
requireJsonBinding(name, bindings[name]);
|
|
420
|
+
merged.set(name, bindings[name]);
|
|
421
|
+
}
|
|
422
|
+
return new AsyncSequence(this.#origin, this.#stages, merged, this.#options, this.#relations);
|
|
205
423
|
}
|
|
206
424
|
|
|
207
425
|
//#endregion
|
|
@@ -209,38 +427,58 @@ export class AsyncSequence {
|
|
|
209
427
|
//#region documents and reporting
|
|
210
428
|
|
|
211
429
|
/** The chain as one query document — refuses when a `mapAsync` sits
|
|
212
|
-
* in the chain, because a host callback has no document form.
|
|
430
|
+
* in the chain, because a host callback has no document form. A deep
|
|
431
|
+
* snapshot, as on the sync surface: the emitted tree embeds the
|
|
432
|
+
* stages' captured expressions, and handing those out by reference
|
|
433
|
+
* made the document a live window into an immutable sequence. */
|
|
213
434
|
toDocument() {
|
|
214
435
|
if (this.#stages.some((s) => s.kind === 'mapAsync')) {
|
|
215
436
|
throw new LinqBuildError('JL0005',
|
|
216
437
|
'toDocument() cannot represent mapAsync (a host callback); explain() reports the split');
|
|
217
438
|
}
|
|
218
|
-
|
|
219
|
-
? this.#origin.prefixDocument()
|
|
220
|
-
: '$[*]';
|
|
221
|
-
return emitDocument(root, this.#stages);
|
|
439
|
+
return snapshot(this.#documentOf(this.#stages));
|
|
222
440
|
}
|
|
223
441
|
|
|
224
|
-
/** Barriers, the split,
|
|
442
|
+
/** Barriers, the split, the relation hops the callbacks navigated,
|
|
443
|
+
* and — when representable — the document; plus `streaming` and
|
|
444
|
+
* `barrier`, what this surface's own execution does. Stages are named
|
|
445
|
+
* by the OPERATOR the caller wrote (`selectMany`, `orderByDescending`),
|
|
446
|
+
* and a `thenBy` is part of the `$orderby` barrier it extends, not a
|
|
447
|
+
* barrier of its own. */
|
|
225
448
|
explain() {
|
|
449
|
+
const firstMap = this.#stages.findIndex((s) => s.kind === 'mapAsync');
|
|
450
|
+
// over a provider nothing before the split materialises HERE — the
|
|
451
|
+
// provider runs the pushed document — so only the residual can hold
|
|
452
|
+
// a barrier of this surface's own
|
|
453
|
+
const local = this.#origin.kind === 'provider'
|
|
454
|
+
? (firstMap < 0 ? [] : this.#stages.slice(firstMap))
|
|
455
|
+
: this.#stages;
|
|
226
456
|
const barriers = [];
|
|
227
|
-
for (const stage of
|
|
228
|
-
if (BARRIERS[stage.kind] !== undefined) {
|
|
229
|
-
barriers.push({ operator: stage.kind, reason: BARRIERS[stage.kind] });
|
|
457
|
+
for (const stage of local) {
|
|
458
|
+
if (BARRIERS[stage.kind] !== undefined && stage.kind !== 'thenBy') {
|
|
459
|
+
barriers.push({ operator: stage.name ?? stage.kind, reason: BARRIERS[stage.kind] });
|
|
230
460
|
}
|
|
231
461
|
}
|
|
232
|
-
|
|
233
|
-
|
|
462
|
+
// what THIS surface does with the item stream: one item at a time,
|
|
463
|
+
// or a buffer at the first local barrier. Over a provider the pushed
|
|
464
|
+
// document's own class — a set residual, an unbindable external — is
|
|
465
|
+
// the provider's to report: `explain(document, { externals: bindings })`
|
|
466
|
+
// on the source answers it, and its cursor carries the same answer
|
|
467
|
+
const first = barriers[0];
|
|
468
|
+
const out = {
|
|
469
|
+
barriers,
|
|
470
|
+
streaming: first === undefined ? 'row' : 'buffered',
|
|
471
|
+
barrier: first === undefined ? null : { construct: first.operator, reason: first.reason },
|
|
472
|
+
hops: this.#hops(),
|
|
473
|
+
bindings: this.#externals().values,
|
|
474
|
+
};
|
|
234
475
|
if (firstMap < 0) {
|
|
235
476
|
out.document = this.toDocument();
|
|
236
477
|
}
|
|
237
478
|
else {
|
|
238
|
-
const prefix = this.#stages.slice(0, firstMap);
|
|
239
479
|
out.split = {
|
|
240
|
-
pushed: this.#
|
|
241
|
-
|
|
242
|
-
: emitDocument('$[*]', prefix),
|
|
243
|
-
residual: this.#stages.slice(firstMap).map((s) => s.kind),
|
|
480
|
+
pushed: snapshot(this.#documentOf(this.#stages.slice(0, firstMap))),
|
|
481
|
+
residual: this.#stages.slice(firstMap).map((s) => s.name ?? s.kind),
|
|
244
482
|
};
|
|
245
483
|
}
|
|
246
484
|
return out;
|
|
@@ -254,12 +492,22 @@ export class AsyncSequence {
|
|
|
254
492
|
* barrier chunks. @returns {AsyncGenerator<any>} */
|
|
255
493
|
async* [Symbol.asyncIterator]() {
|
|
256
494
|
const { names, values } = this.#externals();
|
|
257
|
-
let stream = this.#origin.kind === 'sequence'
|
|
258
|
-
? arrayStream(this.#origin.runPrefix())
|
|
259
|
-
: this.#origin.iterate();
|
|
260
|
-
|
|
261
495
|
const stages = this.#stages;
|
|
496
|
+
let stream;
|
|
262
497
|
let i = 0;
|
|
498
|
+
if (this.#origin.kind === 'sequence') {
|
|
499
|
+
stream = arrayStream(this.#origin.runPrefix(this.#params));
|
|
500
|
+
}
|
|
501
|
+
else if (this.#origin.kind === 'provider') {
|
|
502
|
+
// everything up to the first mapAsync is ONE document the provider
|
|
503
|
+
// runs whole; the residual continues locally over its rows
|
|
504
|
+
const firstMap = stages.findIndex((s) => s.kind === 'mapAsync');
|
|
505
|
+
i = firstMap < 0 ? stages.length : firstMap;
|
|
506
|
+
stream = this.#providerStream(stages.slice(0, i), values);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
stream = this.#origin.iterate();
|
|
510
|
+
}
|
|
263
511
|
while (i < stages.length) {
|
|
264
512
|
const stage = stages[i];
|
|
265
513
|
if (BARRIERS[stage.kind] !== undefined) {
|
|
@@ -290,6 +538,33 @@ export class AsyncSequence {
|
|
|
290
538
|
yield* iterateAndClose(stream);
|
|
291
539
|
}
|
|
292
540
|
|
|
541
|
+
/**
|
|
542
|
+
* The pushed window as a STREAM. A provider that offers the cursor
|
|
543
|
+
* protocol (`cursor(document, options)` — the store's entity sets and
|
|
544
|
+
* collections do) is handed the unwrapped document and answers one
|
|
545
|
+
* item per pull from an open statement; the generator's `for await`
|
|
546
|
+
* releases that statement on break, throw and exhaustion alike,
|
|
547
|
+
* exactly once. A provider without one keeps the element window it
|
|
548
|
+
* always had: the `toArray`-wrapped document run whole, its array
|
|
549
|
+
* iterated — a materialisation, which is what such a provider can do.
|
|
550
|
+
* @param {readonly any[]} stages - the stages up to the split
|
|
551
|
+
* @param {Record<string, any>} values - the bound externals
|
|
552
|
+
* @returns {AsyncIterator<any>}
|
|
553
|
+
*/
|
|
554
|
+
#providerStream(stages, values) {
|
|
555
|
+
const source = this.#origin.source;
|
|
556
|
+
if (typeof source.cursor === 'function') {
|
|
557
|
+
return source.cursor(this.#documentOf(stages), { externals: values });
|
|
558
|
+
}
|
|
559
|
+
return this.#materializedWindow(stages);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The legacy element window, iterated: one `toArray` push, run whole.
|
|
563
|
+
* @param {readonly any[]} stages */
|
|
564
|
+
async* #materializedWindow(stages) {
|
|
565
|
+
yield* await this.#pushWindow('toArray', undefined, stages);
|
|
566
|
+
}
|
|
567
|
+
|
|
293
568
|
/** @param {AsyncIterator<any>} stream @param {any} stage @param {any} values */
|
|
294
569
|
#applyStreamStage(stream, stage, values) {
|
|
295
570
|
const self = this;
|
|
@@ -360,9 +635,12 @@ export class AsyncSequence {
|
|
|
360
635
|
})();
|
|
361
636
|
}
|
|
362
637
|
case 'concat': {
|
|
638
|
+
// a fresh copy per enumeration: a consumer that writes into a
|
|
639
|
+
// yielded constant must not rewrite what the next enumeration
|
|
640
|
+
// answers (the sync surface hands out the engine's frozen values)
|
|
363
641
|
return (async function* () {
|
|
364
642
|
yield* iterateAndClose(stream);
|
|
365
|
-
|
|
643
|
+
for (const item of stage.items) yield snapshot(item);
|
|
366
644
|
})();
|
|
367
645
|
}
|
|
368
646
|
default: { // 'defaultIfEmpty'
|
|
@@ -373,13 +651,18 @@ export class AsyncSequence {
|
|
|
373
651
|
any = true;
|
|
374
652
|
yield item;
|
|
375
653
|
}
|
|
376
|
-
if (!any) yield stage.value ?? null;
|
|
654
|
+
if (!any) yield snapshot(stage.value ?? null);
|
|
377
655
|
})();
|
|
378
656
|
}
|
|
379
657
|
}
|
|
380
658
|
}
|
|
381
659
|
|
|
660
|
+
/** The whole result as one array. Over a provider with no `mapAsync`
|
|
661
|
+
* this is ONE pushed window — the terminal asked for the array, so the
|
|
662
|
+
* store answers it in one statement rather than one row at a time;
|
|
663
|
+
* everywhere else it drains the item stream. */
|
|
382
664
|
async toArray() {
|
|
665
|
+
if (this.#pushable()) return this.#pushWindow('toArray');
|
|
383
666
|
return collect(this[Symbol.asyncIterator]());
|
|
384
667
|
}
|
|
385
668
|
|
|
@@ -394,19 +677,19 @@ export class AsyncSequence {
|
|
|
394
677
|
}
|
|
395
678
|
|
|
396
679
|
async first() {
|
|
397
|
-
const w = await this.#window(1);
|
|
680
|
+
const w = this.#pushable() ? await this.#pushWindow('first') : await this.#window(1);
|
|
398
681
|
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'first() found no element');
|
|
399
682
|
return w[0];
|
|
400
683
|
}
|
|
401
684
|
|
|
402
685
|
/** @param {any} [defaultValue] */
|
|
403
686
|
async firstOrDefault(defaultValue) {
|
|
404
|
-
const w = await this.#window(1);
|
|
687
|
+
const w = this.#pushable() ? await this.#pushWindow('first') : await this.#window(1);
|
|
405
688
|
return w.length === 0 ? defaultValue : w[0];
|
|
406
689
|
}
|
|
407
690
|
|
|
408
691
|
async single() {
|
|
409
|
-
const w = await this.#window(2);
|
|
692
|
+
const w = this.#pushable() ? await this.#pushWindow('single') : await this.#window(2);
|
|
410
693
|
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'single() found no element');
|
|
411
694
|
if (w.length > 1) throw new LinqRuntimeError('JL2002', 'single() found more than one element');
|
|
412
695
|
return w[0];
|
|
@@ -414,27 +697,34 @@ export class AsyncSequence {
|
|
|
414
697
|
|
|
415
698
|
/** @param {any} [defaultValue] */
|
|
416
699
|
async singleOrDefault(defaultValue) {
|
|
417
|
-
const w = await this.#window(2);
|
|
700
|
+
const w = this.#pushable() ? await this.#pushWindow('single') : await this.#window(2);
|
|
418
701
|
if (w.length > 1) throw new LinqRuntimeError('JL2002', 'singleOrDefault() found more than one element');
|
|
419
702
|
return w.length === 0 ? defaultValue : w[0];
|
|
420
703
|
}
|
|
421
704
|
|
|
422
705
|
async last() {
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
return
|
|
706
|
+
const w = this.#pushable() ? await this.#pushWindow('last') : await this.#lastWindow();
|
|
707
|
+
if (w.length === 0) throw new LinqRuntimeError('JL2001', 'last() found no element');
|
|
708
|
+
return w[0];
|
|
426
709
|
}
|
|
427
710
|
|
|
428
711
|
/** @param {any} [defaultValue] */
|
|
429
712
|
async lastOrDefault(defaultValue) {
|
|
713
|
+
const w = this.#pushable() ? await this.#pushWindow('last') : await this.#lastWindow();
|
|
714
|
+
return w.length === 0 ? defaultValue : w[0];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** The last item as a window, read from the whole stream. */
|
|
718
|
+
async #lastWindow() {
|
|
430
719
|
const all = await this.toArray();
|
|
431
|
-
return all.length === 0 ?
|
|
720
|
+
return all.length === 0 ? [] : [all[all.length - 1]];
|
|
432
721
|
}
|
|
433
722
|
|
|
434
723
|
/** @param {number} index */
|
|
435
724
|
async elementAt(index) {
|
|
436
725
|
requireIndex(index, 'elementAt');
|
|
437
|
-
const w =
|
|
726
|
+
const w = this.#pushable()
|
|
727
|
+
? await this.#pushWindow('elementAt', [index]) : await this.skip(index).#window(1);
|
|
438
728
|
if (w.length === 0) throw new LinqRuntimeError('JL2003', `elementAt(${index}) is out of range`);
|
|
439
729
|
return w[0];
|
|
440
730
|
}
|
|
@@ -442,11 +732,13 @@ export class AsyncSequence {
|
|
|
442
732
|
/** @param {number} index @param {any} [defaultValue] */
|
|
443
733
|
async elementAtOrDefault(index, defaultValue) {
|
|
444
734
|
requireIndex(index, 'elementAtOrDefault');
|
|
445
|
-
const w =
|
|
735
|
+
const w = this.#pushable()
|
|
736
|
+
? await this.#pushWindow('elementAt', [index]) : await this.skip(index).#window(1);
|
|
446
737
|
return w.length === 0 ? defaultValue : w[0];
|
|
447
738
|
}
|
|
448
739
|
|
|
449
740
|
async count() {
|
|
741
|
+
if (this.#pushable()) return this.#pushTerminal('count');
|
|
450
742
|
let n = 0;
|
|
451
743
|
// eslint-disable-next-line no-unused-vars
|
|
452
744
|
for await (const item of this) n++;
|
|
@@ -457,6 +749,7 @@ export class AsyncSequence {
|
|
|
457
749
|
* their semantics (type errors included) match the sync surface
|
|
458
750
|
* exactly. @param {string} terminal */
|
|
459
751
|
async #aggregateTerminal(terminal) {
|
|
752
|
+
if (this.#pushable()) return this.#pushTerminal(terminal);
|
|
460
753
|
const items = await this.toArray();
|
|
461
754
|
const { names, values } = this.#externals();
|
|
462
755
|
const compiled = compileDocument(wrapTerminal('$[*]', terminal),
|
|
@@ -486,6 +779,11 @@ export class AsyncSequence {
|
|
|
486
779
|
|
|
487
780
|
/** @param {any} [predicate] */
|
|
488
781
|
async any(predicate) {
|
|
782
|
+
if (this.#pushable()) {
|
|
783
|
+
return predicate === undefined
|
|
784
|
+
? this.#pushTerminal('exists')
|
|
785
|
+
: this.#pushTerminal('some', [this.#capture(predicate).expression]);
|
|
786
|
+
}
|
|
489
787
|
const seq = predicate === undefined ? this : this.where(predicate);
|
|
490
788
|
for await (const item of seq) {
|
|
491
789
|
void item;
|
|
@@ -496,7 +794,8 @@ export class AsyncSequence {
|
|
|
496
794
|
|
|
497
795
|
/** @param {any} predicate */
|
|
498
796
|
async all(predicate) {
|
|
499
|
-
|
|
797
|
+
if (this.#pushable()) return this.#pushTerminal('every', [this.#capture(predicate).expression]);
|
|
798
|
+
const q = this.#evaluator(this.#capture(predicate).expression);
|
|
500
799
|
for await (const item of this) {
|
|
501
800
|
if (!q.ebv(item, this.#externals().values)) return false;
|
|
502
801
|
}
|
|
@@ -541,14 +840,6 @@ function requireIndex(value, what) {
|
|
|
541
840
|
return value;
|
|
542
841
|
}
|
|
543
842
|
|
|
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
843
|
/** @param {any} bindings */
|
|
553
844
|
function validateParams(bindings) {
|
|
554
845
|
if (bindings === null || typeof bindings !== 'object' || Array.isArray(bindings)) {
|
|
@@ -558,21 +849,31 @@ function validateParams(bindings) {
|
|
|
558
849
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
559
850
|
throw new LinqBuildError('JL0004', `'${name}' is not a valid parameter name`);
|
|
560
851
|
}
|
|
561
|
-
if (name
|
|
852
|
+
if (isReservedBinding(name)) {
|
|
562
853
|
throw new LinqBuildError('JL0004',
|
|
563
|
-
`'${name}' is reserved (the emitted document's own binding names:
|
|
854
|
+
`'${name}' is reserved (the emitted document's own binding names: ${RESERVED_BINDINGS_TEXT})`);
|
|
564
855
|
}
|
|
565
856
|
}
|
|
566
857
|
}
|
|
567
858
|
|
|
568
859
|
/**
|
|
569
|
-
* Build an async sequence over an async source (
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
860
|
+
* Build an async sequence over an async source (QUERY-PEN.md §10), or
|
|
861
|
+
* over a provider (§12): an `execute` duck is asked for BEFORE the
|
|
862
|
+
* iterable shapes, and its items are bound through its own root — as
|
|
863
|
+
* `from()` binds them, so the two surfaces emit one document.
|
|
864
|
+
* @param {any} source - async iterable, sync iterable, cursor, push
|
|
865
|
+
* queue, or a provider
|
|
866
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
867
|
+
* pathFunctions?: any, limits?: any, registry?: object }} [options] -
|
|
868
|
+
* the engine registries, as `from()` takes them
|
|
573
869
|
* @returns {AsyncSequence}
|
|
574
870
|
*/
|
|
575
871
|
export function fromAsync(source, options = {}) {
|
|
872
|
+
if (isProviderSource(source)) {
|
|
873
|
+
return new AsyncSequence(
|
|
874
|
+
{ kind: 'provider', source, root: providerRoot(source) },
|
|
875
|
+
[], new Map(), options, providerRelations(source));
|
|
876
|
+
}
|
|
576
877
|
return new AsyncSequence(
|
|
577
878
|
{ kind: 'source', iterate: adaptAsyncSource(source) },
|
|
578
879
|
[], new Map(), options);
|
|
@@ -583,8 +884,9 @@ export function fromAsync(source, options = {}) {
|
|
|
583
884
|
* PREFIX (compiled in memory or pushed WHOLE to its provider), and the
|
|
584
885
|
* async surface continues locally from its rows. `explain()` reports
|
|
585
886
|
* the split (D8's residual honesty, applied to the async boundary).
|
|
586
|
-
* @param {{ runPrefix: (
|
|
587
|
-
* params: ReadonlyMap<string, any>,
|
|
887
|
+
* @param {{ runPrefix: (params: ReadonlyMap<string, any>) => any[],
|
|
888
|
+
* prefixDocument: () => any, params: ReadonlyMap<string, any>,
|
|
889
|
+
* options: { compileTypeTest?: any } }} carrier
|
|
588
890
|
* @param {any} fn
|
|
589
891
|
* @param {any} options
|
|
590
892
|
* @returns {AsyncSequence}
|