@jarenjs/json 0.9.2
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 +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
//#region Jaren JSON Query compiler
|
|
2
|
+
// Compiles the normalized AST (normalize.js) into a tree of specialized
|
|
3
|
+
// closures. Every node compiles to `get(frame) -> item | EMPTY | Seq`
|
|
4
|
+
// where `frame` is a plain array: slot 0 holds the input document,
|
|
5
|
+
// externals and $let bindings live in their allocated slots.
|
|
6
|
+
//
|
|
7
|
+
// All decisions are taken at compile time: operator dispatch, operand
|
|
8
|
+
// cardinality, singular-path detection. Nodes whose static cardinality is
|
|
9
|
+
// CARD_ONE compile to singleton-mode closures that skip every sequence
|
|
10
|
+
// check - the main reason compiled queries are fast.
|
|
11
|
+
|
|
12
|
+
import { compareCodePoints } from '@jarenjs/core/string';
|
|
13
|
+
import {
|
|
14
|
+
NOTHING,
|
|
15
|
+
compileSingularGetter,
|
|
16
|
+
compileSegmentV,
|
|
17
|
+
runSegmentsV,
|
|
18
|
+
} from '../segments.js';
|
|
19
|
+
import { JsonQueryRuntimeError } from './errors.js';
|
|
20
|
+
import { EMPTY, Seq, seqOf, appendItem, ebv, stableKeyString, describeItem } from './runtime.js';
|
|
21
|
+
import { CARD_ONE } from './normalize.js';
|
|
22
|
+
// The operator registry: every section-8 operator compiles through its
|
|
23
|
+
// table entry (compileOp). Only referenced inside functions, so the
|
|
24
|
+
// import cycle compile.js <-> operators.js is initialization-safe.
|
|
25
|
+
import { OPERATORS } from './operators.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sentinel stored in the frame slot of an external parameter the caller
|
|
29
|
+
* did not bind; evaluating a reference to it raises JQ2006.
|
|
30
|
+
*/
|
|
31
|
+
export const UNBOUND = Symbol('JsonQuery.Unbound');
|
|
32
|
+
|
|
33
|
+
//#region variables & paths
|
|
34
|
+
|
|
35
|
+
function compileVarGetter(slot, external, name, docPath) {
|
|
36
|
+
if (!external)
|
|
37
|
+
return (f) => f[slot];
|
|
38
|
+
return (f) => {
|
|
39
|
+
const v = f[slot];
|
|
40
|
+
if (v === UNBOUND)
|
|
41
|
+
throw new JsonQueryRuntimeError('JQ2006', `external parameter '${name}' was not bound`, docPath);
|
|
42
|
+
return v;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compileVar(node) {
|
|
47
|
+
return compileVarGetter(node.slot, node.external, node.name, node.docPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A path leaf: run the (pre-parsed) RFC 9535 segments against the root
|
|
51
|
+
// value. The root of an embedded `[?...]` filter's `$` is always frame
|
|
52
|
+
// slot 0, the input document (section 3.2). A variable-rooted path runs
|
|
53
|
+
// its segments against each item of the variable's bound sequence in
|
|
54
|
+
// order, concatenating results; an item that is an array flows as-is
|
|
55
|
+
// (one RFC 9535 node).
|
|
56
|
+
function compilePath(node) {
|
|
57
|
+
const base = compileVarGetter(node.rootSlot, node.external, node.name, node.docPath);
|
|
58
|
+
if (node.singular) {
|
|
59
|
+
const getter = compileSingularGetter(node.segments, true);
|
|
60
|
+
if (node.rootCard === CARD_ONE) {
|
|
61
|
+
// singleton root (input document, external, or ONE-card binding):
|
|
62
|
+
// a direct property walk, no sequence checks
|
|
63
|
+
return (f) => {
|
|
64
|
+
const v = getter(base(f), f[0]);
|
|
65
|
+
return v === NOTHING ? EMPTY : v;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return (f) => {
|
|
69
|
+
const b = base(f);
|
|
70
|
+
if (b === EMPTY)
|
|
71
|
+
return EMPTY;
|
|
72
|
+
if (b instanceof Seq) {
|
|
73
|
+
const items = b.items;
|
|
74
|
+
const acc = [];
|
|
75
|
+
for (let i = 0; i < items.length; i++) {
|
|
76
|
+
const v = getter(items[i], f[0]);
|
|
77
|
+
if (v !== NOTHING)
|
|
78
|
+
acc.push(v);
|
|
79
|
+
}
|
|
80
|
+
return seqOf(acc);
|
|
81
|
+
}
|
|
82
|
+
const v = getter(b, f[0]);
|
|
83
|
+
return v === NOTHING ? EMPTY : v;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const segs = node.segments.map(compileSegmentV);
|
|
87
|
+
if (node.rootCard === CARD_ONE)
|
|
88
|
+
return (f) => seqOf(runSegmentsV(segs, base(f), f[0]));
|
|
89
|
+
return (f) => {
|
|
90
|
+
const b = base(f);
|
|
91
|
+
if (b === EMPTY)
|
|
92
|
+
return EMPTY;
|
|
93
|
+
if (b instanceof Seq) {
|
|
94
|
+
const items = b.items;
|
|
95
|
+
const acc = [];
|
|
96
|
+
for (let i = 0; i < items.length; i++) {
|
|
97
|
+
const out = runSegmentsV(segs, items[i], f[0]);
|
|
98
|
+
for (let j = 0; j < out.length; j++)
|
|
99
|
+
acc.push(out[j]);
|
|
100
|
+
}
|
|
101
|
+
return seqOf(acc);
|
|
102
|
+
}
|
|
103
|
+
return seqOf(runSegmentsV(segs, b, f[0]));
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Existence-only compilation for `$exists`/`$empty` (and any future
|
|
109
|
+
* boolean context): paths never materialize a result sequence (the
|
|
110
|
+
* analogue of path.js's compileExists). Takes the operand AST node -
|
|
111
|
+
* this is why registry `compile` functions receive arg nodes, not just
|
|
112
|
+
* getters.
|
|
113
|
+
* @param {object} node - a frozen AST node from normalize.js
|
|
114
|
+
* @returns {(frame: any[]) => boolean}
|
|
115
|
+
*/
|
|
116
|
+
export function compileExistsTest(node) {
|
|
117
|
+
if (node.kind === 'path') {
|
|
118
|
+
const base = compileVarGetter(node.rootSlot, node.external, node.name, node.docPath);
|
|
119
|
+
if (node.singular) {
|
|
120
|
+
const getter = compileSingularGetter(node.segments, true);
|
|
121
|
+
if (node.rootCard === CARD_ONE)
|
|
122
|
+
return (f) => getter(base(f), f[0]) !== NOTHING;
|
|
123
|
+
return (f) => {
|
|
124
|
+
const b = base(f);
|
|
125
|
+
if (b === EMPTY)
|
|
126
|
+
return false;
|
|
127
|
+
if (b instanceof Seq) {
|
|
128
|
+
const items = b.items;
|
|
129
|
+
for (let i = 0; i < items.length; i++) {
|
|
130
|
+
if (getter(items[i], f[0]) !== NOTHING)
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return getter(b, f[0]) !== NOTHING;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const segs = node.segments.map(compileSegmentV);
|
|
139
|
+
if (node.rootCard === CARD_ONE)
|
|
140
|
+
return (f) => runSegmentsV(segs, base(f), f[0]).length !== 0;
|
|
141
|
+
return (f) => {
|
|
142
|
+
const b = base(f);
|
|
143
|
+
if (b === EMPTY)
|
|
144
|
+
return false;
|
|
145
|
+
if (b instanceof Seq) {
|
|
146
|
+
const items = b.items;
|
|
147
|
+
for (let i = 0; i < items.length; i++) {
|
|
148
|
+
if (runSegmentsV(segs, items[i], f[0]).length !== 0)
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return runSegmentsV(segs, b, f[0]).length !== 0;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (node.kind === 'var') {
|
|
157
|
+
const get = compileVar(node);
|
|
158
|
+
return (f) => get(f) !== EMPTY;
|
|
159
|
+
}
|
|
160
|
+
const get = compileNode(node);
|
|
161
|
+
return (f) => get(f) !== EMPTY;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
|
|
166
|
+
//#region constructors
|
|
167
|
+
|
|
168
|
+
// member applier for map constructors and $map values: ONE-card values
|
|
169
|
+
// assign directly; otherwise an empty result omits the member and a
|
|
170
|
+
// multi-item result is JQ2001 (a JSON member holds exactly one value)
|
|
171
|
+
function memberValue(v, name, docPath) {
|
|
172
|
+
if (v instanceof Seq)
|
|
173
|
+
throw new JsonQueryRuntimeError('JQ2001',
|
|
174
|
+
`member '${name}' evaluated to ${v.items.length} items; an object member takes exactly one`, docPath);
|
|
175
|
+
return v;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function compileObject(node) {
|
|
179
|
+
const entries = node.entries;
|
|
180
|
+
if (entries.length === 0)
|
|
181
|
+
return () => ({});
|
|
182
|
+
const appliers = new Array(entries.length);
|
|
183
|
+
for (let i = 0; i < entries.length; i++) {
|
|
184
|
+
const { name, expr } = entries[i];
|
|
185
|
+
const get = compileNode(expr);
|
|
186
|
+
if (expr.card === CARD_ONE) {
|
|
187
|
+
appliers[i] = (f, out) => {
|
|
188
|
+
out[name] = get(f);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const docPath = expr.docPath;
|
|
193
|
+
appliers[i] = (f, out) => {
|
|
194
|
+
const v = get(f);
|
|
195
|
+
if (v !== EMPTY)
|
|
196
|
+
out[name] = memberValue(v, name, docPath);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const alen = appliers.length;
|
|
201
|
+
return (f) => {
|
|
202
|
+
const out = {};
|
|
203
|
+
for (let i = 0; i < alen; i++)
|
|
204
|
+
appliers[i](f, out);
|
|
205
|
+
return out;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function compileMap(node) {
|
|
210
|
+
const pairs = node.pairs;
|
|
211
|
+
if (pairs.length === 0)
|
|
212
|
+
return () => ({});
|
|
213
|
+
const appliers = new Array(pairs.length);
|
|
214
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
215
|
+
const { key, value } = pairs[i];
|
|
216
|
+
const keyGet = compileNode(key);
|
|
217
|
+
const valGet = compileNode(value);
|
|
218
|
+
const keyPath = key.docPath;
|
|
219
|
+
const valPath = value.docPath;
|
|
220
|
+
const valOne = value.card === CARD_ONE;
|
|
221
|
+
appliers[i] = (f, out) => {
|
|
222
|
+
const k = keyGet(f);
|
|
223
|
+
if (typeof k !== 'string')
|
|
224
|
+
throw new JsonQueryRuntimeError('JQ2004',
|
|
225
|
+
`a $map key must evaluate to a single string, got ${describeItem(k)}`, keyPath);
|
|
226
|
+
if (valOne) {
|
|
227
|
+
out[k] = valGet(f);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const v = valGet(f);
|
|
231
|
+
if (v !== EMPTY)
|
|
232
|
+
out[k] = memberValue(v, k, valPath);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const alen = appliers.length;
|
|
236
|
+
return (f) => {
|
|
237
|
+
const out = {};
|
|
238
|
+
for (let i = 0; i < alen; i++)
|
|
239
|
+
appliers[i](f, out); // later pairs win on duplicate keys
|
|
240
|
+
return out;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// shared flattening accumulator of array constructors and $seq
|
|
245
|
+
function compileElementAppliers(elements) {
|
|
246
|
+
const appliers = new Array(elements.length);
|
|
247
|
+
for (let i = 0; i < elements.length; i++) {
|
|
248
|
+
const get = compileNode(elements[i]);
|
|
249
|
+
appliers[i] = elements[i].card === CARD_ONE
|
|
250
|
+
? (f, acc) => acc.push(get(f))
|
|
251
|
+
: (f, acc) => appendItem(acc, get(f));
|
|
252
|
+
}
|
|
253
|
+
return appliers;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function compileArray(node) {
|
|
257
|
+
if (node.elements.length === 0)
|
|
258
|
+
return () => [];
|
|
259
|
+
const appliers = compileElementAppliers(node.elements);
|
|
260
|
+
const alen = appliers.length;
|
|
261
|
+
return (f) => {
|
|
262
|
+
const acc = [];
|
|
263
|
+
for (let i = 0; i < alen; i++)
|
|
264
|
+
appliers[i](f, acc);
|
|
265
|
+
return acc;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
|
|
271
|
+
//#region operators
|
|
272
|
+
|
|
273
|
+
// A registry operator call (normalize.js `op` node): compile the argument
|
|
274
|
+
// getters, hand them - with the argument nodes, which carry `card` and
|
|
275
|
+
// `docPath` - to the table entry's `compile`. Arguments declared 'raw' or
|
|
276
|
+
// 'name' are compile-time data (`args[i].value`), not getters. Extension
|
|
277
|
+
// op nodes (options.extensions) carry their resolved entry themselves.
|
|
278
|
+
function compileOp(node) {
|
|
279
|
+
const entry = OPERATORS[node.name] ?? node.entry;
|
|
280
|
+
const args = node.args;
|
|
281
|
+
const gets = new Array(args.length);
|
|
282
|
+
for (let i = 0; i < args.length; i++)
|
|
283
|
+
gets[i] = args[i].kind === 'raw' ? null : compileNode(args[i]);
|
|
284
|
+
return entry.compile(gets, args, node.docPath + '/' + node.name);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
|
|
289
|
+
//#region $let
|
|
290
|
+
|
|
291
|
+
function compileLet(node) {
|
|
292
|
+
const ret = compileNode(node.ret);
|
|
293
|
+
if (node.bindings.length === 1) {
|
|
294
|
+
const { slot, expr } = node.bindings[0];
|
|
295
|
+
const get = compileNode(expr);
|
|
296
|
+
return (f) => {
|
|
297
|
+
f[slot] = get(f);
|
|
298
|
+
return ret(f);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const blen = node.bindings.length;
|
|
302
|
+
const slots = new Array(blen);
|
|
303
|
+
const gets = new Array(blen);
|
|
304
|
+
for (let i = 0; i < blen; i++) {
|
|
305
|
+
slots[i] = node.bindings[i].slot;
|
|
306
|
+
gets[i] = compileNode(node.bindings[i].expr);
|
|
307
|
+
}
|
|
308
|
+
return (f) => {
|
|
309
|
+
for (let i = 0; i < blen; i++)
|
|
310
|
+
f[slots[i]] = gets[i](f);
|
|
311
|
+
return ret(f);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
//#endregion
|
|
316
|
+
|
|
317
|
+
//#region FLWOR
|
|
318
|
+
// The tuple stream is a chain of nested closures, each of signature
|
|
319
|
+
// `(frame, out) -> void`: a $for clause iterates its source and calls the
|
|
320
|
+
// next stage per item, $let assigns and calls once, $as validates bound
|
|
321
|
+
// slots against compiled type tests, $where gates on EBV.
|
|
322
|
+
// A tuple IS the current state of the frame slots - no tuple objects, no
|
|
323
|
+
// intermediate arrays. `out` threads the current stage's collector
|
|
324
|
+
// through untouched: the result accumulator, or a barrier's state.
|
|
325
|
+
//
|
|
326
|
+
// $groupby and $orderby are barriers; they materialize the minimum via
|
|
327
|
+
// compile-time liveness (normalize.js): $groupby accumulates only the
|
|
328
|
+
// live binding slots per group, $orderby snapshots only the live slots
|
|
329
|
+
// per tuple next to its pre-evaluated key row (Schwartzian transform).
|
|
330
|
+
//
|
|
331
|
+
//#region roadmap: FLWOR optimizer
|
|
332
|
+
// The compiled form is the straightforward nested-loop pipeline: a join
|
|
333
|
+
// ($for x $for + $where equality) runs O(n*m). Hash joins (build a table
|
|
334
|
+
// on one side of an equijoin), filter hoisting into the deepest binding
|
|
335
|
+
// that covers the predicate's variables, and orderby/groupby fusion are
|
|
336
|
+
// future optimizer work orders.
|
|
337
|
+
//#endregion
|
|
338
|
+
|
|
339
|
+
// D4 iteration step: an item that is an array contributes its members
|
|
340
|
+
// (one level - nested arrays inside stay items); everything else is one
|
|
341
|
+
// tuple. Shared by $for and the quantifier loops.
|
|
342
|
+
function emitForItem(item, f, slot, next, out) {
|
|
343
|
+
if (Array.isArray(item)) {
|
|
344
|
+
for (let j = 0; j < item.length; j++) {
|
|
345
|
+
f[slot] = item[j];
|
|
346
|
+
next(f, out);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
f[slot] = item;
|
|
351
|
+
next(f, out);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ... and the positional variant: `pos` is the 0-based (D6) position
|
|
355
|
+
// within the iterated (post-unpacking) sequence; returns the next one.
|
|
356
|
+
function emitForItemAt(item, f, slot, atSlot, pos, next, out) {
|
|
357
|
+
if (Array.isArray(item)) {
|
|
358
|
+
for (let j = 0; j < item.length; j++) {
|
|
359
|
+
f[slot] = item[j];
|
|
360
|
+
f[atSlot] = pos++;
|
|
361
|
+
next(f, out);
|
|
362
|
+
}
|
|
363
|
+
return pos;
|
|
364
|
+
}
|
|
365
|
+
f[slot] = item;
|
|
366
|
+
f[atSlot] = pos;
|
|
367
|
+
next(f, out);
|
|
368
|
+
return pos + 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function compileForClause(binding, next) {
|
|
372
|
+
const get = compileNode(binding.expr);
|
|
373
|
+
const slot = binding.slot;
|
|
374
|
+
const atSlot = binding.atSlot;
|
|
375
|
+
if (atSlot < 0) {
|
|
376
|
+
return (f, out) => {
|
|
377
|
+
const v = get(f);
|
|
378
|
+
if (v === EMPTY)
|
|
379
|
+
return;
|
|
380
|
+
if (v instanceof Seq) {
|
|
381
|
+
const items = v.items;
|
|
382
|
+
for (let i = 0; i < items.length; i++)
|
|
383
|
+
emitForItem(items[i], f, slot, next, out);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
emitForItem(v, f, slot, next, out);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// positional counter per binding activation
|
|
390
|
+
return (f, out) => {
|
|
391
|
+
const v = get(f);
|
|
392
|
+
if (v === EMPTY)
|
|
393
|
+
return;
|
|
394
|
+
if (v instanceof Seq) {
|
|
395
|
+
const items = v.items;
|
|
396
|
+
let pos = 0;
|
|
397
|
+
for (let i = 0; i < items.length; i++)
|
|
398
|
+
pos = emitForItemAt(items[i], f, slot, atSlot, pos, next, out);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
emitForItemAt(v, f, slot, atSlot, 0, next, out);
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// One $as check (section 6.4): validate a phrase binding's frame slot per
|
|
406
|
+
// tuple against its compiled type-test predicate. A $for/$at variable is
|
|
407
|
+
// always exactly one item; a $let variable is validated per item of its
|
|
408
|
+
// bound sequence (the empty sequence passes vacuously). Failure is JQ2008,
|
|
409
|
+
// naming the variable.
|
|
410
|
+
function compileAsCheck(check, next) {
|
|
411
|
+
const { name, slot, test, docPath } = check;
|
|
412
|
+
if (!check.isLet) { // a $for/$at binding: one item per tuple
|
|
413
|
+
return (f, out) => {
|
|
414
|
+
const v = f[slot];
|
|
415
|
+
if (!test(v))
|
|
416
|
+
throw new JsonQueryRuntimeError('JQ2008',
|
|
417
|
+
`variable '${name}' failed its '$as' schema: ${describeItem(v)} does not satisfy it`, docPath);
|
|
418
|
+
next(f, out);
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return (f, out) => {
|
|
422
|
+
const v = f[slot];
|
|
423
|
+
if (v !== EMPTY) {
|
|
424
|
+
if (v instanceof Seq) {
|
|
425
|
+
const items = v.items;
|
|
426
|
+
for (let i = 0; i < items.length; i++) {
|
|
427
|
+
if (!test(items[i]))
|
|
428
|
+
throw new JsonQueryRuntimeError('JQ2008',
|
|
429
|
+
`variable '${name}' failed its '$as' schema: item ${i} (${describeItem(items[i])}) does not satisfy it`, docPath);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else if (!test(v)) {
|
|
433
|
+
throw new JsonQueryRuntimeError('JQ2008',
|
|
434
|
+
`variable '${name}' failed its '$as' schema: ${describeItem(v)} does not satisfy it`, docPath);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
next(f, out);
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// $orderby tuple collector: evaluate the N key expressions once into a
|
|
442
|
+
// keys row, snapshot the live slots, push [keys..., snapshot]. A key
|
|
443
|
+
// value must be the empty sequence, one number, or one string (JQ2005).
|
|
444
|
+
function compileRowSink(specs, keyGets, keyPaths, liveSlots) {
|
|
445
|
+
const keyCount = specs.length;
|
|
446
|
+
const liveCount = liveSlots.length;
|
|
447
|
+
return (f, rows) => {
|
|
448
|
+
const row = new Array(keyCount + 1);
|
|
449
|
+
for (let i = 0; i < keyCount; i++) {
|
|
450
|
+
const v = keyGets[i](f);
|
|
451
|
+
if (v !== EMPTY && typeof v !== 'number' && typeof v !== 'string')
|
|
452
|
+
throw new JsonQueryRuntimeError('JQ2005',
|
|
453
|
+
`an $orderby key must be the empty sequence, a number, or a string, got ${describeItem(v)}`, keyPaths[i]);
|
|
454
|
+
row[i] = v;
|
|
455
|
+
}
|
|
456
|
+
const snap = new Array(liveCount);
|
|
457
|
+
for (let j = 0; j < liveCount; j++)
|
|
458
|
+
snap[j] = f[liveSlots[j]];
|
|
459
|
+
row[keyCount] = snap;
|
|
460
|
+
rows.push(row);
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Row comparator over the key specs (direction, $empty least/greatest,
|
|
465
|
+
// number/string type check per pair - JQ2005 on mismatch). Empty keys
|
|
466
|
+
// order as -Infinity under 'least' and +Infinity under 'greatest',
|
|
467
|
+
// before the direction applies (section 6.6). NaN keys order equal to
|
|
468
|
+
// themselves and less than every other number (the XQuery order-by
|
|
469
|
+
// rule). Ties fall through to the next key; JS sort is stable, so equal
|
|
470
|
+
// rows keep tuple order.
|
|
471
|
+
function compileRowComparator(specs, keyPaths) {
|
|
472
|
+
const keyCount = specs.length;
|
|
473
|
+
const descs = new Array(keyCount);
|
|
474
|
+
const emptyGreatests = new Array(keyCount);
|
|
475
|
+
for (let i = 0; i < keyCount; i++) {
|
|
476
|
+
descs[i] = specs[i].desc;
|
|
477
|
+
emptyGreatests[i] = specs[i].emptyGreatest;
|
|
478
|
+
}
|
|
479
|
+
return (a, b) => {
|
|
480
|
+
for (let i = 0; i < keyCount; i++) {
|
|
481
|
+
const x = a[i];
|
|
482
|
+
const y = b[i];
|
|
483
|
+
if (x === y) // also EMPTY vs EMPTY, and -0 vs 0 (mathematically equal)
|
|
484
|
+
continue;
|
|
485
|
+
let c;
|
|
486
|
+
if (x === EMPTY)
|
|
487
|
+
c = emptyGreatests[i] ? 1 : -1;
|
|
488
|
+
else if (y === EMPTY)
|
|
489
|
+
c = emptyGreatests[i] ? -1 : 1;
|
|
490
|
+
else if (typeof x === 'number') {
|
|
491
|
+
if (typeof y !== 'number')
|
|
492
|
+
throw new JsonQueryRuntimeError('JQ2005',
|
|
493
|
+
'cannot order a number against a string in $orderby', keyPaths[i]);
|
|
494
|
+
if (x < y)
|
|
495
|
+
c = -1;
|
|
496
|
+
else if (x > y)
|
|
497
|
+
c = 1;
|
|
498
|
+
else if (x !== x) // x is NaN: equal to NaN, less than all others
|
|
499
|
+
c = y !== y ? 0 : -1;
|
|
500
|
+
else // y is NaN (x === y was false, so they are not both non-NaN equal)
|
|
501
|
+
c = 1;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
if (typeof y !== 'string')
|
|
505
|
+
throw new JsonQueryRuntimeError('JQ2005',
|
|
506
|
+
'cannot order a string against a number in $orderby', keyPaths[i]);
|
|
507
|
+
c = compareCodePoints(x, y);
|
|
508
|
+
}
|
|
509
|
+
if (c !== 0)
|
|
510
|
+
return descs[i] ? -c : c;
|
|
511
|
+
}
|
|
512
|
+
return 0;
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function compileFlwor(node) {
|
|
517
|
+
// final sink: $return collects into the accumulator; the $count clause
|
|
518
|
+
// numbers surviving tuples through its own frame slot (0-based, D6),
|
|
519
|
+
// reset once per phrase evaluation by the drivers below
|
|
520
|
+
const retGet = compileNode(node.ret);
|
|
521
|
+
let sink;
|
|
522
|
+
if (node.ret.card === CARD_ONE)
|
|
523
|
+
sink = (f, out) => out.push(retGet(f));
|
|
524
|
+
else
|
|
525
|
+
sink = (f, out) => appendItem(out, retGet(f));
|
|
526
|
+
const countSlot = node.count === null ? -1 : node.count.slot;
|
|
527
|
+
if (countSlot >= 0) {
|
|
528
|
+
const inner = sink;
|
|
529
|
+
sink = (f, out) => {
|
|
530
|
+
inner(f, out);
|
|
531
|
+
f[countSlot] += 1;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const groupby = node.groupby;
|
|
536
|
+
const orderby = node.orderby;
|
|
537
|
+
|
|
538
|
+
// $orderby machinery (Schwartzian rows + compiled comparator)
|
|
539
|
+
let rowSink = null;
|
|
540
|
+
let comparator = null;
|
|
541
|
+
let liveSlots = null;
|
|
542
|
+
let keyCount = 0;
|
|
543
|
+
if (orderby !== null) {
|
|
544
|
+
const specs = orderby.specs;
|
|
545
|
+
const keyGets = specs.map((s) => compileNode(s.key));
|
|
546
|
+
const keyPaths = specs.map((s) => s.docPath);
|
|
547
|
+
keyCount = specs.length;
|
|
548
|
+
liveSlots = orderby.liveSlots;
|
|
549
|
+
rowSink = compileRowSink(specs, keyGets, keyPaths, liveSlots);
|
|
550
|
+
comparator = compileRowComparator(specs, keyPaths);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// $groupby machinery: Map<stableKeyString composite, group>; the Map
|
|
554
|
+
// preserves first-appearance order. Key variables rebind as singletons,
|
|
555
|
+
// every other live binding as the concatenation of its values across
|
|
556
|
+
// the group's tuples (spec section 6.5).
|
|
557
|
+
let groupSink = null;
|
|
558
|
+
let writeGroup = null;
|
|
559
|
+
if (groupby !== null) {
|
|
560
|
+
const keys = groupby.keys;
|
|
561
|
+
const groupCount = keys.length;
|
|
562
|
+
const keyGets = keys.map((k) => compileNode(k.expr));
|
|
563
|
+
const keyPaths = keys.map((k) => k.docPath);
|
|
564
|
+
const keySlots = keys.map((k) => k.slot);
|
|
565
|
+
const accSlots = groupby.accSlots;
|
|
566
|
+
const accCount = accSlots.length;
|
|
567
|
+
groupSink = (f, map) => {
|
|
568
|
+
const keyValues = new Array(groupCount);
|
|
569
|
+
let composite = '';
|
|
570
|
+
for (let i = 0; i < groupCount; i++) {
|
|
571
|
+
const v = keyGets[i](f);
|
|
572
|
+
if (v instanceof Seq)
|
|
573
|
+
throw new JsonQueryRuntimeError('JQ2001',
|
|
574
|
+
`a $groupby key must be the empty sequence or a single item, got ${describeItem(v)}`, keyPaths[i]);
|
|
575
|
+
keyValues[i] = v;
|
|
576
|
+
// '\u0000' never occurs in stableKeyString output, '~' never
|
|
577
|
+
// starts one: the composite cannot collide across keys
|
|
578
|
+
composite += v === EMPTY ? '\u0000~' : '\u0000' + stableKeyString(v);
|
|
579
|
+
}
|
|
580
|
+
let group = map.get(composite);
|
|
581
|
+
if (group === undefined) {
|
|
582
|
+
const accs = new Array(accCount);
|
|
583
|
+
for (let j = 0; j < accCount; j++)
|
|
584
|
+
accs[j] = [];
|
|
585
|
+
group = { keyValues, accs };
|
|
586
|
+
map.set(composite, group);
|
|
587
|
+
}
|
|
588
|
+
const accs = group.accs;
|
|
589
|
+
for (let j = 0; j < accCount; j++)
|
|
590
|
+
appendItem(accs[j], f[accSlots[j]]);
|
|
591
|
+
};
|
|
592
|
+
writeGroup = (f, group) => {
|
|
593
|
+
const keyValues = group.keyValues;
|
|
594
|
+
for (let i = 0; i < groupCount; i++)
|
|
595
|
+
f[keySlots[i]] = keyValues[i];
|
|
596
|
+
const accs = group.accs;
|
|
597
|
+
for (let j = 0; j < accCount; j++)
|
|
598
|
+
f[accSlots[j]] = seqOf(accs[j]);
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// the streaming prefix $for -> $let -> $as -> $where, feeding the first
|
|
603
|
+
// barrier's collector (or the final sink when there is none)
|
|
604
|
+
let emit = groupby !== null ? groupSink : (orderby !== null ? rowSink : sink);
|
|
605
|
+
if (node.where !== null) {
|
|
606
|
+
const cond = compileNode(node.where);
|
|
607
|
+
const condPath = node.where.docPath;
|
|
608
|
+
const next = emit;
|
|
609
|
+
emit = (f, out) => {
|
|
610
|
+
if (ebv(cond(f), condPath))
|
|
611
|
+
next(f, out);
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (node.asChecks !== null) {
|
|
615
|
+
for (let i = node.asChecks.length - 1; i >= 0; i--)
|
|
616
|
+
emit = compileAsCheck(node.asChecks[i], emit);
|
|
617
|
+
}
|
|
618
|
+
const lets = node.letBindings;
|
|
619
|
+
for (let i = lets.length - 1; i >= 0; i--) {
|
|
620
|
+
const slot = lets[i].slot;
|
|
621
|
+
const get = compileNode(lets[i].expr);
|
|
622
|
+
const next = emit;
|
|
623
|
+
emit = (f, out) => {
|
|
624
|
+
f[slot] = get(f);
|
|
625
|
+
next(f, out);
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const fors = node.forBindings;
|
|
629
|
+
for (let i = fors.length - 1; i >= 0; i--)
|
|
630
|
+
emit = compileForClause(fors[i], emit);
|
|
631
|
+
const head = emit;
|
|
632
|
+
|
|
633
|
+
// drivers, one per barrier combination
|
|
634
|
+
if (groupby === null && orderby === null) {
|
|
635
|
+
if (countSlot < 0) {
|
|
636
|
+
return (f) => {
|
|
637
|
+
const out = [];
|
|
638
|
+
head(f, out);
|
|
639
|
+
return seqOf(out);
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return (f) => {
|
|
643
|
+
const out = [];
|
|
644
|
+
f[countSlot] = 0;
|
|
645
|
+
head(f, out);
|
|
646
|
+
return seqOf(out);
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (groupby === null) { // $orderby only
|
|
650
|
+
return (f) => {
|
|
651
|
+
const rows = [];
|
|
652
|
+
head(f, rows);
|
|
653
|
+
rows.sort(comparator); // stable
|
|
654
|
+
const out = [];
|
|
655
|
+
if (countSlot >= 0)
|
|
656
|
+
f[countSlot] = 0;
|
|
657
|
+
const liveCount = liveSlots.length;
|
|
658
|
+
for (let i = 0; i < rows.length; i++) {
|
|
659
|
+
const snap = rows[i][keyCount];
|
|
660
|
+
for (let j = 0; j < liveCount; j++)
|
|
661
|
+
f[liveSlots[j]] = snap[j];
|
|
662
|
+
sink(f, out);
|
|
663
|
+
}
|
|
664
|
+
return seqOf(out);
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (orderby === null) { // $groupby only
|
|
668
|
+
return (f) => {
|
|
669
|
+
const map = new Map();
|
|
670
|
+
head(f, map);
|
|
671
|
+
const out = [];
|
|
672
|
+
if (countSlot >= 0)
|
|
673
|
+
f[countSlot] = 0;
|
|
674
|
+
for (const group of map.values()) { // first-appearance order
|
|
675
|
+
writeGroup(f, group);
|
|
676
|
+
sink(f, out);
|
|
677
|
+
}
|
|
678
|
+
return seqOf(out);
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
// $groupby then $orderby: sort the per-group tuples
|
|
682
|
+
return (f) => {
|
|
683
|
+
const map = new Map();
|
|
684
|
+
head(f, map);
|
|
685
|
+
const rows = [];
|
|
686
|
+
for (const group of map.values()) {
|
|
687
|
+
writeGroup(f, group);
|
|
688
|
+
rowSink(f, rows);
|
|
689
|
+
}
|
|
690
|
+
rows.sort(comparator);
|
|
691
|
+
const out = [];
|
|
692
|
+
if (countSlot >= 0)
|
|
693
|
+
f[countSlot] = 0;
|
|
694
|
+
const liveCount = liveSlots.length;
|
|
695
|
+
for (let i = 0; i < rows.length; i++) {
|
|
696
|
+
const snap = rows[i][keyCount];
|
|
697
|
+
for (let j = 0; j < liveCount; j++)
|
|
698
|
+
f[liveSlots[j]] = snap[j];
|
|
699
|
+
sink(f, out);
|
|
700
|
+
}
|
|
701
|
+
return seqOf(out);
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
//#endregion
|
|
706
|
+
|
|
707
|
+
//#region quantifiers
|
|
708
|
+
|
|
709
|
+
// $some/$every + $satisfies (section 7): a $for-style loop nest with
|
|
710
|
+
// early exit - $some stops at the first EBV-true tuple, $every at the
|
|
711
|
+
// first EBV-false one. D4 unpacking applies; nothing materializes.
|
|
712
|
+
function quantVisitSome(item, f, slot, next) {
|
|
713
|
+
if (Array.isArray(item)) { // D4
|
|
714
|
+
for (let j = 0; j < item.length; j++) {
|
|
715
|
+
f[slot] = item[j];
|
|
716
|
+
if (next(f))
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
f[slot] = item;
|
|
722
|
+
return next(f);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function quantVisitEvery(item, f, slot, next) {
|
|
726
|
+
if (Array.isArray(item)) { // D4
|
|
727
|
+
for (let j = 0; j < item.length; j++) {
|
|
728
|
+
f[slot] = item[j];
|
|
729
|
+
if (!next(f))
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
f[slot] = item;
|
|
735
|
+
return next(f);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function compileQuantLevel(binding, next, some) {
|
|
739
|
+
const get = compileNode(binding.expr);
|
|
740
|
+
const slot = binding.slot;
|
|
741
|
+
if (some) {
|
|
742
|
+
return (f) => {
|
|
743
|
+
const v = get(f);
|
|
744
|
+
if (v === EMPTY) // empty source: no witnessing tuple
|
|
745
|
+
return false;
|
|
746
|
+
if (v instanceof Seq) {
|
|
747
|
+
const items = v.items;
|
|
748
|
+
for (let i = 0; i < items.length; i++) {
|
|
749
|
+
if (quantVisitSome(items[i], f, slot, next))
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return quantVisitSome(v, f, slot, next);
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return (f) => {
|
|
758
|
+
const v = get(f);
|
|
759
|
+
if (v === EMPTY) // empty source: vacuously true
|
|
760
|
+
return true;
|
|
761
|
+
if (v instanceof Seq) {
|
|
762
|
+
const items = v.items;
|
|
763
|
+
for (let i = 0; i < items.length; i++) {
|
|
764
|
+
if (!quantVisitEvery(items[i], f, slot, next))
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
return quantVisitEvery(v, f, slot, next);
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function compileQuant(node) {
|
|
774
|
+
const sat = compileNode(node.satisfies);
|
|
775
|
+
const satPath = node.satisfies.docPath;
|
|
776
|
+
const some = node.some;
|
|
777
|
+
let test = (f) => ebv(sat(f), satPath);
|
|
778
|
+
for (let i = node.bindings.length - 1; i >= 0; i--)
|
|
779
|
+
test = compileQuantLevel(node.bindings[i], test, some);
|
|
780
|
+
return test;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
//#endregion
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Compile a normalized AST node into its getter closure.
|
|
787
|
+
* @param {object} node - a frozen AST node from normalize.js
|
|
788
|
+
* @returns {(frame: any[]) => any} getter returning an item, EMPTY, or a Seq
|
|
789
|
+
*/
|
|
790
|
+
export function compileNode(node) {
|
|
791
|
+
switch (node.kind) {
|
|
792
|
+
case 'literal': {
|
|
793
|
+
const value = node.value;
|
|
794
|
+
return () => value;
|
|
795
|
+
}
|
|
796
|
+
case 'var':
|
|
797
|
+
return compileVar(node);
|
|
798
|
+
case 'path':
|
|
799
|
+
return compilePath(node);
|
|
800
|
+
case 'object':
|
|
801
|
+
return compileObject(node);
|
|
802
|
+
case 'map':
|
|
803
|
+
return compileMap(node);
|
|
804
|
+
case 'array':
|
|
805
|
+
return compileArray(node);
|
|
806
|
+
case 'op':
|
|
807
|
+
return compileOp(node);
|
|
808
|
+
case 'let':
|
|
809
|
+
return compileLet(node);
|
|
810
|
+
case 'quant':
|
|
811
|
+
return compileQuant(node);
|
|
812
|
+
default: // 'flwor'
|
|
813
|
+
return compileFlwor(node);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
//#endregion
|