@jarenjs/linq 0.49.2 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +227 -0
- package/README.md +650 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1221 -0
- package/docs/DB-CLIENT.md +882 -0
- package/docs/FLOW-PEN.md +1033 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +778 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1092 -0
- package/docs/QUERY-PEN.md +1724 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +251 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +255 -0
- package/src/app/index.js +20 -0
- package/src/app/patch.js +277 -0
- package/src/app/sub.js +106 -0
- package/src/async.js +377 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +48 -11
- package/src/contract/define.js +282 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +338 -0
- package/src/db/handle.js +89 -0
- package/src/db/include.js +351 -0
- package/src/db/index.js +24 -0
- package/src/db/ledger.js +195 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +130 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +78 -6
- package/src/expression.js +463 -36
- package/src/federate.js +531 -0
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +316 -0
- package/src/flow/fsm.js +323 -0
- package/src/flow/index.js +22 -0
- package/src/forms/index.js +43 -0
- package/src/forms/rules.js +170 -0
- package/src/forms/submit.js +177 -0
- package/src/index.js +5 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +202 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +318 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +244 -0
- package/src/model/collection.js +273 -0
- package/src/model/define.js +125 -0
- package/src/model/entity.js +307 -0
- package/src/model/index.js +47 -0
- package/src/model/relation.js +85 -0
- package/src/provider.js +137 -20
- package/src/schema/brand.js +31 -0
- package/src/schema/builders.js +526 -0
- package/src/schema/check.js +29 -0
- package/src/schema/emit.js +394 -0
- package/src/schema/factories.js +239 -0
- package/src/schema/index.js +37 -0
- package/src/schema-of.js +24 -0
- package/src/sequence.js +233 -103
- package/src/sources.js +10 -3
- package/types/app.d.ts +293 -0
- package/types/contract.d.ts +468 -0
- package/types/db.d.ts +359 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +296 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +526 -0
- package/types/schema.d.ts +494 -0
package/src/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
|
-
* (
|
|
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 {
|
|
17
|
-
|
|
18
|
-
import {
|
|
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}
|
|
93
|
-
#
|
|
81
|
+
/** @param {ReadonlyMap<string, any>} params */
|
|
82
|
+
#rebound(params) {
|
|
94
83
|
return new Sequence(this.#source, this.#sourceKind, this.#root,
|
|
95
|
-
|
|
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
|
-
/**
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
const { expression, hops } = this.#capture(projection);
|
|
159
|
+
return this.#with({ kind: 'select', projection: expression, hops });
|
|
120
160
|
}
|
|
121
161
|
|
|
122
|
-
/** Project-and-flatten:
|
|
123
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
*
|
|
174
|
-
|
|
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
|
|
181
|
-
+
|
|
182
|
-
+ '
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
|
206
|
-
$where: { $eq: [
|
|
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: '
|
|
211
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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` (
|
|
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
|
|
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 (
|
|
413
|
+
if (isReservedBinding(name)) {
|
|
316
414
|
throw new LinqBuildError('JL0004',
|
|
317
|
-
`'${name}' is reserved (the emitted document's own binding names:
|
|
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
|
|
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
|
|
343
|
-
* its
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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)
|
|
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;
|