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