@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/sequence.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * NEW sequence; nothing runs until a terminal operation; a sequence may
5
5
  * be enumerated repeatedly and each enumeration re-reads its source —
6
6
  * the C# contract, including the part that surprises people
7
- * (LINQ-FORMAT.md §5 has the worked example).
7
+ * (QUERY-PEN.md §5 has the worked example).
8
8
  *
9
9
  * The chain is data: `toDocument()` emits one Jaren query document, and
10
10
  * a terminal either compiles it in memory (the reference semantics) or
@@ -13,47 +13,24 @@
13
13
  * authorable by a constrained decoder.
14
14
  */
15
15
 
16
- import { captureExpression, toExpression } from './expression.js';
17
- import { emitDocument, wrapTerminal } from './document.js';
18
- import { classifySource, compileDocument, executeInMemory } from './provider.js';
16
+ import { compileJsonQuery } from '@jarenjs/json/query';
17
+
18
+ import {
19
+ captureExpression, toExpression, requireJsonBinding, createHopSink, rowRoot, groupRoot,
20
+ } from './expression.js';
21
+ import {
22
+ emitDocument, wrapTerminal, snapshot, fanProjection, isReservedBinding, RESERVED_BINDINGS_TEXT,
23
+ PROJECTING_STAGES,
24
+ } from './document.js';
25
+ import {
26
+ classifySource, compileDocument, executeInMemory, providerRoot, providerRelations, sharesScope,
27
+ } from './provider.js';
19
28
  import { asyncFromSequence } from './async.js';
20
29
  import { LinqBuildError, LinqRuntimeError } from './errors.js';
21
-
22
- /** Binding names the emitted documents own; parameters may not shadow
23
- * them (LINQ-FORMAT.md §7). */
24
- const RESERVED_NAMES = new Set(['it', 'it2', 'acc', 'g']);
30
+ import { schemaOf } from './schema-of.js';
25
31
 
26
32
  const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
27
33
 
28
- /**
29
- * A deep, independent copy of an emitted query document. Plain data
30
- * only, which is exactly what a document is — every captured expression
31
- * has already passed the JSON-domain boundary in `expression.js`, so
32
- * there is nothing here a structural copy would lose.
33
- * @param {any} node
34
- * @returns {any}
35
- */
36
- function snapshot(node) {
37
- if (node === null || typeof node !== 'object') return node;
38
- if (Array.isArray(node)) return node.map(snapshot);
39
- /** @type {Record<string, any>} */
40
- const out = {};
41
- for (const key of Object.keys(node)) defineOwn(out, key, snapshot(node[key]));
42
- return out;
43
- }
44
-
45
- /**
46
- * Assign an OWN property, so a `__proto__` member stays a member instead
47
- * of silently replacing the object's prototype and vanishing.
48
- * @param {Record<string, any>} target
49
- * @param {string} key
50
- * @param {any} value
51
- */
52
- function defineOwn(target, key, value) {
53
- Object.defineProperty(target, key,
54
- { value, writable: true, enumerable: true, configurable: true });
55
- }
56
-
57
34
  /** @param {number} value @param {string} what */
58
35
  function requireIndex(value, what) {
59
36
  if (!Number.isInteger(value) || value < 0) {
@@ -70,6 +47,7 @@ export class Sequence {
70
47
  #stages;
71
48
  #params;
72
49
  #options;
50
+ #relations;
73
51
 
74
52
  /**
75
53
  * @param {any} source
@@ -79,61 +57,127 @@ export class Sequence {
79
57
  * @param {ReadonlyMap<string, any>} params
80
58
  * @param {{ compileTypeTest?: any, functions?: any, collations?: any,
81
59
  * pathFunctions?: any, limits?: any, registry?: object }} options
60
+ * @param {{ table: any, resolve: (name: string) => any } | null} [relations] -
61
+ * the relation table of the rows the items ARE (a provider's, while
62
+ * no stage has projected them), or null
82
63
  */
83
- constructor(source, sourceKind, root, stages, params, options) {
64
+ constructor(source, sourceKind, root, stages, params, options, relations = null) {
84
65
  this.#source = source;
85
66
  this.#sourceKind = sourceKind;
86
67
  this.#root = root;
87
68
  this.#stages = stages;
88
69
  this.#params = params;
89
70
  this.#options = options;
71
+ this.#relations = relations;
72
+ }
73
+
74
+ /** @param {any} stage @param {ReadonlyMap<string, any>} [params] */
75
+ #with(stage, params = this.#params) {
76
+ return new Sequence(this.#source, this.#sourceKind, this.#root,
77
+ [...this.#stages, stage], params, this.#options,
78
+ PROJECTING_STAGES.has(stage.kind) ? null : this.#relations);
90
79
  }
91
80
 
92
- /** @param {any} stage */
93
- #with(stage) {
81
+ /** @param {ReadonlyMap<string, any>} params */
82
+ #rebound(params) {
94
83
  return new Sequence(this.#source, this.#sourceKind, this.#root,
95
- [...this.#stages, stage], this.#params, this.#options);
84
+ this.#stages, params, this.#options, this.#relations);
85
+ }
86
+
87
+ /** Whether this sequence's items are a `groupBy`'s `{ key, items }`
88
+ * — the last stage that reseated them was the grouping. Read from the
89
+ * stages rather than carried, so it cannot fall out of step with what
90
+ * the emitter writes. */
91
+ #grouped() {
92
+ for (let i = this.#stages.length - 1; i >= 0; i--) {
93
+ if (PROJECTING_STAGES.has(this.#stages[i].kind)) {
94
+ return this.#stages[i].kind === 'groupBy';
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /** The `it` (or `it2`) root of a capture over this sequence's items:
101
+ * an entity's rows carry their relation table, so a relation name
102
+ * hops; a group's rows carry their member, so `g.items` aggregates as
103
+ * rows.
104
+ * @param {string} name @param {ReturnType<typeof createHopSink>} sink */
105
+ #rowRoot(name, sink) {
106
+ return rowRoot(name, this.#relations, sink, this.#grouped());
107
+ }
108
+
109
+ /** The group root of a group-join over this sequence's items as the
110
+ * inner side. @param {ReturnType<typeof createHopSink>} sink */
111
+ #groupRoot(sink) {
112
+ return groupRoot(this.#relations, sink);
113
+ }
114
+
115
+ /** The relation hops every stage's callbacks navigated, in order. */
116
+ #hops() {
117
+ return this.#stages.flatMap((stage) => stage.hops ?? []);
118
+ }
119
+
120
+ /** Whether this sequence is a provider's own root, untouched — the
121
+ * one `$for` source the emitter leaves unpacked (document.js). */
122
+ #isBareRoot() {
123
+ return this.#sourceKind === 'provider' && this.#stages.length === 0;
96
124
  }
97
125
 
98
126
  #declared() {
99
127
  return new Set(this.#params.keys());
100
128
  }
101
129
 
102
- /** @param {(...roots: any[]) => any} fn @param {readonly any[]} roots */
103
- #capture(fn, roots = ['it']) {
130
+ /**
131
+ * Capture one callback over this sequence's items: the expression and
132
+ * the relation hops it navigated (one sink per capture, so two roots
133
+ * of one callback number their hop bindings together).
134
+ * @param {(...roots: any[]) => any} fn
135
+ * @param {(sink: ReturnType<typeof createHopSink>) => readonly any[]} [rootsOf] -
136
+ * the roots, given the capture's sink; the item root by default
137
+ * @returns {{ expression: any, hops: readonly any[] }}
138
+ */
139
+ #capture(fn, rootsOf = (sink) => [this.#rowRoot('it', sink)]) {
104
140
  if (typeof fn !== 'function') {
105
141
  throw new LinqBuildError('JL0005', 'this operator takes a callback function');
106
142
  }
107
- return captureExpression(fn, roots, this.#declared());
143
+ const sink = createHopSink();
144
+ const expression = captureExpression(fn, rootsOf(sink), this.#declared());
145
+ return { expression, hops: sink.hops };
108
146
  }
109
147
 
110
148
  //#region operators (each returns a new immutable Sequence)
111
149
 
112
150
  /** Filter: `.where(it => it.age.gt(21))` → FLWOR `$where`. */
113
151
  where(predicate) {
114
- return this.#with({ kind: 'where', predicate: this.#capture(predicate) });
152
+ const { expression, hops } = this.#capture(predicate);
153
+ return this.#with({ kind: 'where', predicate: expression, hops });
115
154
  }
116
155
 
117
156
  /** Project: `.select(it => ({ id: it.id }))` → `$return`. */
118
157
  select(projection) {
119
- return this.#with({ kind: 'select', projection: this.#capture(projection) });
158
+ const { expression, hops } = this.#capture(projection);
159
+ return this.#with({ kind: 'select', projection: expression, hops });
120
160
  }
121
161
 
122
- /** Project-and-flatten: a multi-item projection concatenates (the
123
- * FLWOR `$return` already flattens per tuple). */
162
+ /** Project-and-flatten: the projected value is iterated one level
163
+ * (an array member's elements, a constructed array's members), so
164
+ * `Seq<R[]>` really answers `Seq<R>`; the FLWOR `$return` then
165
+ * concatenates per tuple. */
124
166
  selectMany(selector) {
125
- return this.#with({ kind: 'select', projection: this.#capture(selector) });
167
+ const { expression, hops } = this.#capture(selector);
168
+ return this.#with({ kind: 'select', projection: fanProjection(expression), hops });
126
169
  }
127
170
 
128
171
  /** @param {any} key @param {boolean} desc @param {any} [options] */
129
172
  #orderStage(kind, key, desc, options) {
130
- const spec = { $key: this.#capture(key) };
173
+ const { expression, hops } = this.#capture(key);
174
+ const spec = { $key: expression };
131
175
  if (desc) spec.$dir = 'desc';
132
176
  if (options !== undefined) {
133
177
  if (options.empty !== undefined) spec.$empty = options.empty;
134
178
  if (options.collation !== undefined) spec.$collation = options.collation;
135
179
  }
136
- return this.#with({ kind, spec });
180
+ return this.#with({ kind, spec, hops });
137
181
  }
138
182
 
139
183
  /** Sort ascending → an `$orderby` key spec (`$dir`/`$empty`/
@@ -165,61 +209,108 @@ export class Sequence {
165
209
 
166
210
  /** Group → `$groupby`; downstream items are `{ key, items }`. */
167
211
  groupBy(key) {
168
- return this.#with({ kind: 'groupBy', key: this.#capture(key) });
169
- }
170
-
171
- /** Both sides read ONE input document in 0.1 — a query document has
172
- * one root. Cross-source composition arrives with the relational order.
173
- * @param {Sequence} inner @param {string} what */
174
- #requireSameSource(inner, what) {
212
+ const { expression, hops } = this.#capture(key);
213
+ return this.#with({ kind: 'groupBy', key: expression, hops });
214
+ }
215
+
216
+ /** Both sides read ONE input document a query document has one
217
+ * root so the other side derives from the same source, or (for a
218
+ * join) from a provider sharing this one's scope: two entity sets of
219
+ * one store are two roots of ONE multi-entity input, and the store
220
+ * answers their equijoin in one statement (QUERY-PEN.md §8). A
221
+ * `concat` stays same-source: its other side contributes an
222
+ * expression over THIS input, never a second one. Returns the merged
223
+ * parameter bindings: the inner side's declared externals ride along,
224
+ * because its document is embedded whole and would otherwise run
225
+ * against the outer's bindings only — a name both sides bind
226
+ * differently is `JL0004`, never silently the outer's.
227
+ * @param {Sequence} inner @param {string} what
228
+ * @param {boolean} [scoped] - whether a shared provider scope suffices
229
+ * @returns {ReadonlyMap<string, any>} */
230
+ #requireSameSource(inner, what, scoped = false) {
175
231
  if (!(inner instanceof Sequence)) {
176
232
  throw new LinqBuildError('JL0005', `${what} takes another sequence as its inner side`);
177
233
  }
178
- if (inner.#source !== this.#source) {
234
+ if (inner.#source !== this.#source && !(scoped && sharesScope(this.#source, inner.#source))) {
179
235
  throw new LinqBuildError('JL0005',
180
- `${what}'s other side must derive from the same source in 0.1 — `
181
- + 'a query document reads one input; load both collections under one root '
182
- + '(the relational order lifts this)');
236
+ `${what}'s other side must derive from the same source`
237
+ + (scoped ? ", or from two providers sharing one scope (one store's entity sets)" : '')
238
+ + ' a query document reads one input; load both collections under one root'
239
+ + (scoped ? ', or join two entity sets of one store' : ''));
183
240
  }
241
+ const merged = new Map(this.#params);
242
+ for (const [name, value] of inner.#params) {
243
+ if (merged.has(name) && merged.get(name) !== value) {
244
+ throw new LinqBuildError('JL0004',
245
+ `parameter '${name}' is bound to different values by the two sides of ${what} — `
246
+ + 'one document carries one binding per name; bind it once, or rename one side');
247
+ }
248
+ merged.set(name, value);
249
+ }
250
+ return merged;
184
251
  }
185
252
 
186
253
  /** Equi-join → nested `$for` + `$where` equality (the engine rewrites
187
- * this shape to a hash join; that is why it is fast). */
254
+ * this shape to a hash join; that is why it is fast). The inner side's
255
+ * rows keep their relation table under `it2`, so a hop from the joined
256
+ * row lowers as one from the root does. */
188
257
  join(inner, outerKey, innerKey, result) {
189
- this.#requireSameSource(inner, 'join');
258
+ const params = this.#requireSameSource(inner, 'join', true);
259
+ const outer = this.#capture(outerKey);
260
+ const key = this.#capture(innerKey, (sink) => [inner.#rowRoot('it2', sink)]);
261
+ const projection = this.#capture(result,
262
+ (sink) => [this.#rowRoot('it', sink), inner.#rowRoot('it2', sink)]);
190
263
  return this.#with({
191
264
  kind: 'join',
192
265
  inner: inner.toDocument(),
193
- on: {
194
- $eq: [this.#capture(outerKey), this.#capture(innerKey, ['it2'])],
195
- },
196
- result: this.#capture(result, ['it', 'it2']),
197
- });
266
+ innerBare: inner.#isBareRoot(),
267
+ on: { $eq: [outer.expression, key.expression] },
268
+ result: projection.expression,
269
+ hops: [...inner.#hops(), ...outer.hops, ...key.hops, ...projection.hops],
270
+ }, params);
198
271
  }
199
272
 
200
273
  /** Group-join: the result selector receives the outer item and the
201
- * MATCHING inner group as an expression (`(u, g) => ({ n: g.count() })`). */
274
+ * MATCHING inner group, bound as an array value (`$let`) so it can be
275
+ * indexed (`g.at(0)`), fanned (`g.all()`), placed in a member
276
+ * (`{ matches: g }`) and aggregated over its members
277
+ * (`(u, g) => ({ n: g.count() })`); the fanned rows keep the inner
278
+ * side's relation table (`g.all().author`). */
202
279
  groupJoin(inner, outerKey, innerKey, result) {
203
- this.#requireSameSource(inner, 'groupJoin');
280
+ const params = this.#requireSameSource(inner, 'groupJoin', true);
281
+ const innerDoc = inner.toDocument();
282
+ const outer = this.#capture(outerKey);
283
+ const key = this.#capture(innerKey, (sink) => [inner.#rowRoot('it2', sink)]);
204
284
  const group = {
205
- $for: { it2: inner.toDocument() },
206
- $where: { $eq: [this.#capture(outerKey), this.#capture(innerKey, ['it2'])] },
285
+ $for: { it2: inner.#isBareRoot() ? innerDoc : [innerDoc] },
286
+ $where: { $eq: [outer.expression, key.expression] },
207
287
  $return: '$it2',
208
288
  };
289
+ const projection = this.#capture(result,
290
+ (sink) => [this.#rowRoot('it', sink), inner.#groupRoot(sink)]);
209
291
  return this.#with({
210
- kind: 'select',
211
- projection: this.#capture(result, ['it', { doc: group, pathable: false }]),
212
- });
292
+ kind: 'groupJoin',
293
+ group,
294
+ projection: projection.expression,
295
+ hops: [...inner.#hops(), ...outer.hops, ...key.hops, ...projection.hops],
296
+ }, params);
213
297
  }
214
298
 
215
299
  /** Seeded fold → `$fold` (the accumulator clause). Only the seeded
216
300
  * form exists: JSON has no way to spell an unseeded lambda's implicit
217
301
  * first element without one. */
218
302
  aggregate(seed, step) {
303
+ if (typeof seed === 'function' && step === undefined) {
304
+ throw new LinqBuildError('JL0006',
305
+ 'aggregate(fn) is unsupported: JSON cannot spell the implicit first element as a '
306
+ + 'lambda seed — pass a seed, aggregate(seed, fn) (see QUERY-PEN.md §4)');
307
+ }
308
+ const { expression, hops } = this.#capture(step, (sink) => ['acc', this.#rowRoot('it', sink)]);
219
309
  return this.#with({
220
310
  kind: 'aggregate',
221
311
  seed: toExpression(seed),
222
- step: this.#capture(step, ['acc', 'it']),
312
+ step: expression,
313
+ hops,
223
314
  });
224
315
  }
225
316
 
@@ -249,17 +340,20 @@ export class Sequence {
249
340
  * rows twice instead of concatenating two inputs. */
250
341
  concat(other) {
251
342
  let expr;
343
+ let params = this.#params;
252
344
  if (other instanceof Sequence) {
253
- this.#requireSameSource(other, 'concat');
345
+ params = this.#requireSameSource(other, 'concat');
254
346
  expr = other.toDocument();
255
347
  }
256
348
  else if (Array.isArray(other)) {
257
- expr = { $for: { it: { $const: other } }, $return: '$it' };
349
+ // the same JSON boundary a captured constant crosses (§5): a Date
350
+ // or a Map in the array would embed as {} and NaN would fold to null
351
+ expr = { $for: { it: { $const: toExpression(other).$const } }, $return: '$it' };
258
352
  }
259
353
  else {
260
354
  throw new LinqBuildError('JL0005', 'concat takes a sequence or a constant array');
261
355
  }
262
- return this.#with({ kind: 'concat', other: expr });
356
+ return this.#with({ kind: 'concat', other: expr }, params);
263
357
  }
264
358
 
265
359
  /** `$default`: the sequence, or the fallback when it is empty. */
@@ -267,36 +361,40 @@ export class Sequence {
267
361
  return this.#with({ kind: 'defaultIfEmpty', fallback: toExpression(fallback) });
268
362
  }
269
363
 
270
- /** Keep only items matching the JSON Schema (`$valid` filter). */
364
+ /** Keep only items matching the JSON Schema (`$valid` filter) — a
365
+ * document, or a schema-pen builder, whose document is taken. */
271
366
  ofType(schema) {
272
- return this.#with({ kind: 'ofType', schema });
367
+ return this.#with({ kind: 'ofType', schema: schemaOf(schema) });
273
368
  }
274
369
 
275
370
  /** Assert every item against the JSON Schema (`$assert`). */
276
371
  cast(schema) {
277
- return this.#with({ kind: 'cast', schema });
372
+ return this.#with({ kind: 'cast', schema: schemaOf(schema) });
278
373
  }
279
374
 
280
375
  /** Cross into the async surface: everything BEFORE this call is the
281
376
  * prefix — compiled in memory, or pushed WHOLE to the provider — and
282
377
  * `mapAsync` plus everything after runs locally over its rows.
283
- * `explain()` on the result reports the split (LINQ-FORMAT.md §11).
378
+ * `explain()` on the result reports the split (QUERY-PEN.md §11).
284
379
  * @param {(item: any, signal: AbortSignal) => any} fn
285
380
  * @param {{ concurrency: number, mode?: string, ordered?: boolean }} options */
286
381
  mapAsync(fn, options) {
287
382
  return asyncFromSequence({
288
- runPrefix: () => this.toArray(),
383
+ // the prefix runs under the bindings the ASYNC sequence holds at
384
+ // enumeration time — a `params()` after the split rebinds the
385
+ // whole document, exactly as it would on the sync surface
386
+ runPrefix: (params) => this.#rebound(params).toArray(),
289
387
  prefixDocument: () => this.toDocument(),
290
388
  params: this.#params,
291
389
  options: this.#options,
292
390
  }, fn, options);
293
391
  }
294
392
 
295
- /** Recorded `unsupported` (LINQ-FORMAT.md §4): the grammar has no
393
+ /** Recorded `unsupported` (QUERY-PEN.md §4): the grammar has no
296
394
  * positional co-iteration. */
297
395
  zip() {
298
396
  throw new LinqBuildError('JL0006',
299
- 'zip is unsupported: the query grammar has no positional co-iteration (see LINQ-FORMAT.md §4)');
397
+ 'zip is unsupported: the query grammar has no positional co-iteration (see QUERY-PEN.md §4)');
300
398
  }
301
399
 
302
400
  /** Declare (and bind) external parameters: `.params({ tenantId })`.
@@ -312,14 +410,14 @@ export class Sequence {
312
410
  if (!VAR_NAME_RE.test(name)) {
313
411
  throw new LinqBuildError('JL0004', `'${name}' is not a valid parameter name`);
314
412
  }
315
- if (RESERVED_NAMES.has(name)) {
413
+ if (isReservedBinding(name)) {
316
414
  throw new LinqBuildError('JL0004',
317
- `'${name}' is reserved (the emitted document's own binding names: it, it2, acc, g)`);
415
+ `'${name}' is reserved (the emitted document's own binding names: ${RESERVED_BINDINGS_TEXT})`);
318
416
  }
417
+ requireJsonBinding(name, bindings[name]);
319
418
  merged.set(name, bindings[name]);
320
419
  }
321
- return new Sequence(this.#source, this.#sourceKind, this.#root,
322
- this.#stages, merged, this.#options);
420
+ return this.#rebound(merged);
323
421
  }
324
422
 
325
423
  //#endregion
@@ -336,18 +434,21 @@ export class Sequence {
336
434
  * the returned document rewrote the predicate, and the next
337
435
  * enumeration answered differently. A snapshot cannot do that. */
338
436
  toDocument() {
339
- return snapshot(emitDocument(this.#root, this.#stages));
437
+ return snapshot(emitDocument(this.#root, this.#stages,
438
+ { bareRoot: this.#sourceKind === 'provider' }));
340
439
  }
341
440
 
342
- /** The compiled view of the chain: the document, its externals and
343
- * its dependency sets.
441
+ /** The compiled view of the chain: the document, its externals, its
442
+ * dependency sets and the relation hops its callbacks navigated (the
443
+ * member, the relation's kind and the binding the lowered phrase
444
+ * ranges over — QUERY-PEN §4, relation navigation).
344
445
  *
345
446
  * This always explains the IN-MEMORY compilation — it is the reference
346
447
  * semantics, and it is not the provider's plan. It cannot report SQL
347
448
  * pushdown, index use, residual execution or a strict refusal, and it
348
449
  * will fail on an operator or collation only the provider can compile.
349
450
  * For a provider's real plan, emit `toDocument()` and call that
350
- * provider's own explanation. */
451
+ * provider's own explanation — a lowered hop is a residual there. */
351
452
  explain() {
352
453
  const document = this.toDocument();
353
454
  const compiled = compileDocument(document, {
@@ -358,6 +459,8 @@ export class Sequence {
358
459
  document,
359
460
  externals: [...compiled.externals],
360
461
  dependencies: compiled.dependencies,
462
+ hops: this.#hops(),
463
+ bindings: Object.fromEntries(this.#params),
361
464
  };
362
465
  }
363
466
 
@@ -394,8 +497,17 @@ export class Sequence {
394
497
  /** @param {string} terminal @param {readonly any[]} [args] */
395
498
  #window(terminal, args) {
396
499
  // element terminals emit `[window]`, so the result is always one
397
- // array item and element extraction is unambiguous
398
- return /** @type {any[]} */ (this.#execute(terminal, args));
500
+ // array item and element extraction is unambiguous — for a provider
501
+ // that keeps the contract; one that answers anything else is named,
502
+ // rather than indexed into a TypeError or an `undefined` typed `T[]`
503
+ const result = this.#execute(terminal, args);
504
+ if (!Array.isArray(result)) {
505
+ throw new LinqRuntimeError('JL2006',
506
+ `the provider answered ${terminal}() with ${result === undefined ? 'undefined'
507
+ : `a ${typeof result}`} — an element terminal emits an array constructor, so a `
508
+ + 'conforming execute() answers exactly one array (QUERY-PEN.md §8)');
509
+ }
510
+ return /** @type {any[]} */ (result);
399
511
  }
400
512
 
401
513
  toArray() {
@@ -489,12 +601,12 @@ export class Sequence {
489
601
  * @param {(...roots: any[]) => any} [predicate] */
490
602
  any(predicate) {
491
603
  if (predicate === undefined) return this.#execute('exists');
492
- return this.#execute('some', [this.#capture(predicate)]);
604
+ return this.#execute('some', [this.#capture(predicate).expression]);
493
605
  }
494
606
 
495
607
  /** The `$every` quantifier (vacuously true over the empty sequence). */
496
608
  all(predicate) {
497
- return this.#execute('every', [this.#capture(predicate)]);
609
+ return this.#execute('every', [this.#capture(predicate).expression]);
498
610
  }
499
611
 
500
612
  //#endregion
@@ -505,7 +617,10 @@ export class Sequence {
505
617
  * happens ONCE, here: an `execute` duck is a provider and is never
506
618
  * enumerated locally; any iterable gets the in-memory reference
507
619
  * semantics; anything else is `JL0001` now, not at enumeration time.
508
- * @param {any} source
620
+ * @param {any} source - an iterable, or a provider; a provider carrying
621
+ * `root` binds its items through that root (`'$.Post[*]'` for an entity
622
+ * set), one carrying `relations` lets a relation member navigate (§3),
623
+ * one carrying `roots` and no `root` of its own is `JL0007`
509
624
  * @param {{ compileTypeTest?: any, functions?: any, collations?: any,
510
625
  * pathFunctions?: any, limits?: any, registry?: object }} [options] -
511
626
  * the engine registries this sequence compiles against, under the
@@ -518,13 +633,21 @@ export class Sequence {
518
633
  * @returns {Sequence}
519
634
  */
520
635
  export function from(source, options = {}) {
521
- return new Sequence(source, classifySource(source), '$[*]', [], new Map(), options);
636
+ const kind = classifySource(source);
637
+ const root = kind === 'provider' ? providerRoot(source) : '$[*]';
638
+ const relations = kind === 'provider' ? providerRelations(source) : null;
639
+ return new Sequence(source, kind, root, [], new Map(), options, relations);
522
640
  }
523
641
 
524
642
  /**
525
643
  * Attach a hand-written (or stored) query document to a source. The
526
644
  * document's result is the item sequence; further operators chain over
527
- * it. A version envelope is unwrapped so the expression embeds.
645
+ * it. A version envelope is unwrapped so the expression embeds — only
646
+ * the envelope this version knows (`{ $query: '0.1', $expr }`, nothing
647
+ * else); any other spelling is handed to the engine first so its own
648
+ * verdict (`JQ0006` for a version this consumer does not implement,
649
+ * `JQ0003` for a stray member) is what surfaces, never a silent run of
650
+ * a future document as a 0.1 one.
528
651
  * @param {any} source - iterable or provider, as `from`
529
652
  * @param {any} document - a Jaren query document
530
653
  * @param {{ compileTypeTest?: any, functions?: any, collations?: any,
@@ -536,7 +659,14 @@ export function from(source, options = {}) {
536
659
  export function fromDocument(source, document, options = {}) {
537
660
  let root = document;
538
661
  if (root !== null && typeof root === 'object' && !Array.isArray(root)
539
- && Object.hasOwn(root, '$expr')) {
662
+ && (Object.hasOwn(root, '$expr') || Object.hasOwn(root, '$query'))) {
663
+ const keys = Object.keys(root);
664
+ const known = root.$query === '0.1' && keys.length === 2 && Object.hasOwn(root, '$expr');
665
+ if (!known) {
666
+ compileJsonQuery(root); // the engine's verdict, or —
667
+ throw new LinqBuildError('JL0005',
668
+ "a version envelope is exactly { $query: '0.1', $expr: … } (QUERY-FORMAT §4.1)");
669
+ }
540
670
  root = root.$expr;
541
671
  }
542
672
  return new Sequence(source, classifySource(source), root, [], new Map(), options);
package/src/sources.js CHANGED
@@ -1,6 +1,6 @@
1
1
  //@ts-check
2
2
  /**
3
- * @file Async source adapters (LINQ-FORMAT.md §12): everything
3
+ * @file Async source adapters (QUERY-PEN.md §12): everything
4
4
  * `fromAsync` accepts normalizes to "a factory of async iterators" —
5
5
  * a fresh iterator per enumeration, so the deferred re-enumeration
6
6
  * contract carries over exactly (a one-shot generator object simply
@@ -12,7 +12,7 @@
12
12
  * for feed/end-style readers that have no pull protocol of their own.
13
13
  */
14
14
 
15
- import { LinqBuildError } from './errors.js';
15
+ import { LinqBuildError, LinqRuntimeError } from './errors.js';
16
16
 
17
17
  /**
18
18
  * Normalize an async source into an iterator factory, or throw
@@ -25,6 +25,10 @@ export function adaptAsyncSource(source) {
25
25
  if (typeof source[Symbol.asyncIterator] === 'function') {
26
26
  return () => source[Symbol.asyncIterator]();
27
27
  }
28
+ // a string is refused on purpose: on this surface a string is a
29
+ // CHUNK source (feed it through a push queue), never a character
30
+ // stream — `from('abc')` iterates characters, and the twins differ
31
+ // here by design (QUERY-PEN.md §12)
28
32
  if (typeof source[Symbol.iterator] === 'function' && typeof source !== 'string') {
29
33
  return () => (async function* () { yield* source; })();
30
34
  }
@@ -71,7 +75,10 @@ export function createPushQueue(options = {}) {
71
75
 
72
76
  return {
73
77
  feed(value) {
74
- if (ended) throw new LinqBuildError('JL0005', 'feed() after end()');
78
+ if (ended) {
79
+ throw new LinqRuntimeError('JL2005',
80
+ 'feed() after end(): the push queue is closed and takes no more values');
81
+ }
75
82
  buffer.push(value);
76
83
  signal();
77
84
  return buffer.length <= highWaterMark;