@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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. 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 { compileDocument } from './provider.js';
35
- import { emitDocument, wrapTerminal } from './document.js';
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 { captureExpression, toExpression } from './expression.js';
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: 'sequence', runPrefix: () => any[], prefixDocument: () => any }} origin
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 }} options
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
- return new AsyncSequence(this.#origin, [...this.#stages, stage], this.#params, this.#options);
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) { return this.#chainCaptured('select', 'projection', 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
- return this.#with({ kind, [slot]: captureFor(this.#params, fn) });
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 spec = { $key: captureFor(this.#params, key) };
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) { return this.#with({ kind: 'groupBy', key: captureFor(this.#params, 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: captureFor(this.#params, step, ['acc', 'it']),
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: other } }, $return: '$it' },
176
- items: other,
386
+ other: { $for: { it: { $const: items } }, $return: '$it' },
387
+ items,
177
388
  });
178
389
  }
179
390
 
180
391
  defaultIfEmpty(fallback = null) {
181
- return this.#with({ kind: 'defaultIfEmpty', fallback: toExpression(fallback), value: fallback });
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 LINQ-FORMAT.md §4)');
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 (LINQ-FORMAT.md §11). */
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)) merged.set(name, bindings[name]);
204
- return new AsyncSequence(this.#origin, this.#stages, merged, this.#options);
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
- const root = this.#origin.kind === 'sequence'
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, and when representable — the document. */
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 this.#stages) {
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 firstMap = this.#stages.findIndex((s) => s.kind === 'mapAsync');
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.#origin.kind === 'sequence'
241
- ? this.#origin.prefixDocument()
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
- yield* stage.items;
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 all = await this.toArray();
424
- if (all.length === 0) throw new LinqRuntimeError('JL2001', 'last() found no element');
425
- return all[all.length - 1];
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 ? defaultValue : all[all.length - 1];
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 = await this.skip(index).#window(1);
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 = await this.skip(index).#window(1);
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
- const q = this.#evaluator(captureFor(this.#params, predicate));
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 === 'it' || name === 'it2' || name === 'acc' || name === 'g') {
804
+ if (isReservedBinding(name)) {
562
805
  throw new LinqBuildError('JL0004',
563
- `'${name}' is reserved (the emitted document's own binding names: it, it2, acc, g)`);
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 (LINQ-FORMAT.md §10).
570
- * @param {any} source - async iterable, sync iterable, cursor or push
571
- * queue
572
- * @param {{ compileTypeTest?: any }} [options]
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: () => any[], prefixDocument: () => any,
587
- * params: ReadonlyMap<string, any>, options: { compileTypeTest?: any } }} carrier
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}