@jarenjs/json 0.9.2 → 0.34.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 +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/src/query/compile.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// check - the main reason compiled queries are fast.
|
|
11
11
|
|
|
12
12
|
import { compareCodePoints } from '@jarenjs/core/string';
|
|
13
|
+
import { setObjectMember } from '@jarenjs/core/object';
|
|
13
14
|
import {
|
|
14
15
|
NOTHING,
|
|
15
16
|
compileSingularGetter,
|
|
@@ -18,11 +19,12 @@ import {
|
|
|
18
19
|
} from '../segments.js';
|
|
19
20
|
import { JsonQueryRuntimeError } from './errors.js';
|
|
20
21
|
import { EMPTY, Seq, seqOf, appendItem, ebv, stableKeyString, describeItem } from './runtime.js';
|
|
21
|
-
import { CARD_ONE } from './normalize.js';
|
|
22
|
+
import { CARD_ONE, CARD_MANY, hostFailureText, collectReadSlots } from './normalize.js';
|
|
22
23
|
// The operator registry: every section-8 operator compiles through its
|
|
23
24
|
// table entry (compileOp). Only referenced inside functions, so the
|
|
24
25
|
// import cycle compile.js <-> operators.js is initialization-safe.
|
|
25
|
-
import { OPERATORS } from './operators.js';
|
|
26
|
+
import { OPERATORS, checkRangeBound } from './operators.js';
|
|
27
|
+
import { bboxOf, createBboxIndex } from '@jarenjs/core/geo';
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Sentinel stored in the frame slot of an external parameter the caller
|
|
@@ -183,18 +185,30 @@ function compileObject(node) {
|
|
|
183
185
|
for (let i = 0; i < entries.length; i++) {
|
|
184
186
|
const { name, expr } = entries[i];
|
|
185
187
|
const get = compileNode(expr);
|
|
188
|
+
// a constructed '__proto__' member is data, not the prototype; the
|
|
189
|
+
// name is static here, so the slow defineProperty path is chosen once
|
|
190
|
+
// at compile time and the ordinary member keeps a bare assignment
|
|
191
|
+
const proto = name === '__proto__';
|
|
186
192
|
if (expr.card === CARD_ONE) {
|
|
187
|
-
appliers[i] =
|
|
188
|
-
out
|
|
189
|
-
|
|
193
|
+
appliers[i] = proto
|
|
194
|
+
? (f, out) => setObjectMember(out, name, get(f))
|
|
195
|
+
: (f, out) => {
|
|
196
|
+
out[name] = get(f);
|
|
197
|
+
};
|
|
190
198
|
}
|
|
191
199
|
else {
|
|
192
200
|
const docPath = expr.docPath;
|
|
193
|
-
appliers[i] =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
appliers[i] = proto
|
|
202
|
+
? (f, out) => {
|
|
203
|
+
const v = get(f);
|
|
204
|
+
if (v !== EMPTY)
|
|
205
|
+
setObjectMember(out, name, memberValue(v, name, docPath));
|
|
206
|
+
}
|
|
207
|
+
: (f, out) => {
|
|
208
|
+
const v = get(f);
|
|
209
|
+
if (v !== EMPTY)
|
|
210
|
+
out[name] = memberValue(v, name, docPath);
|
|
211
|
+
};
|
|
198
212
|
}
|
|
199
213
|
}
|
|
200
214
|
const alen = appliers.length;
|
|
@@ -223,13 +237,14 @@ function compileMap(node) {
|
|
|
223
237
|
if (typeof k !== 'string')
|
|
224
238
|
throw new JsonQueryRuntimeError('JQ2004',
|
|
225
239
|
`a $map key must evaluate to a single string, got ${describeItem(k)}`, keyPath);
|
|
240
|
+
// the key is dynamic, so the '__proto__' test is a runtime one
|
|
226
241
|
if (valOne) {
|
|
227
|
-
out
|
|
242
|
+
setObjectMember(out, k, valGet(f));
|
|
228
243
|
return;
|
|
229
244
|
}
|
|
230
245
|
const v = valGet(f);
|
|
231
246
|
if (v !== EMPTY)
|
|
232
|
-
out
|
|
247
|
+
setObjectMember(out, k, memberValue(v, k, valPath));
|
|
233
248
|
};
|
|
234
249
|
}
|
|
235
250
|
const alen = appliers.length;
|
|
@@ -281,7 +296,42 @@ function compileOp(node) {
|
|
|
281
296
|
const gets = new Array(args.length);
|
|
282
297
|
for (let i = 0; i < args.length; i++)
|
|
283
298
|
gets[i] = args[i].kind === 'raw' ? null : compileNode(args[i]);
|
|
284
|
-
|
|
299
|
+
// the node rides along for entries that read compilation context
|
|
300
|
+
// (e.g. $range's configurable resource guard via node.limits)
|
|
301
|
+
return entry.compile(gets, args, node.docPath + '/' + node.name, node);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// A '$call' node: a registered trusted pure host function
|
|
305
|
+
// (options.functions). Sequences cross the boundary as arrays, the
|
|
306
|
+
// empty sequence as undefined; a returned undefined is the empty
|
|
307
|
+
// sequence, anything else is one item. A throwing function is JQ2010.
|
|
308
|
+
function compileCall(node) {
|
|
309
|
+
const fn = node.fn;
|
|
310
|
+
const name = node.name;
|
|
311
|
+
const docPath = node.docPath;
|
|
312
|
+
const argGets = new Array(node.args.length);
|
|
313
|
+
for (let i = 0; i < node.args.length; i++)
|
|
314
|
+
argGets[i] = compileNode(node.args[i]);
|
|
315
|
+
return (f) => {
|
|
316
|
+
const argv = new Array(argGets.length);
|
|
317
|
+
for (let i = 0; i < argGets.length; i++) {
|
|
318
|
+
const v = argGets[i](f);
|
|
319
|
+
argv[i] = v === EMPTY ? undefined : v instanceof Seq ? v.items.slice() : v;
|
|
320
|
+
}
|
|
321
|
+
let out;
|
|
322
|
+
try {
|
|
323
|
+
out = fn(...argv);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
// TOTAL: the registered function is host code — no .message read
|
|
327
|
+
// on the raw value, no coercion; the original thrown value is
|
|
328
|
+
// retained as an own cause (present even for undefined)
|
|
329
|
+
throw new JsonQueryRuntimeError('JQ2010',
|
|
330
|
+
`registered function '${name}' threw: ${hostFailureText(err)}`, docPath,
|
|
331
|
+
{ cause: err });
|
|
332
|
+
}
|
|
333
|
+
return out === undefined ? EMPTY : out;
|
|
334
|
+
};
|
|
285
335
|
}
|
|
286
336
|
|
|
287
337
|
//#endregion
|
|
@@ -328,12 +378,350 @@ function compileLet(node) {
|
|
|
328
378
|
// live binding slots per group, $orderby snapshots only the live slots
|
|
329
379
|
// per tuple next to its pre-evaluated key row (Schwartzian transform).
|
|
330
380
|
//
|
|
331
|
-
//#region
|
|
332
|
-
// The
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
381
|
+
//#region hash joins
|
|
382
|
+
// The default pipeline is nested loops, so `$for a, $for b` with an
|
|
383
|
+
// equality `$where` costs O(|a| x |b|). When the inner binding is
|
|
384
|
+
// *uncorrelated* - its source does not read any outer binding - that
|
|
385
|
+
// equality can be answered by a hash table built once over the inner
|
|
386
|
+
// side, which makes the join O(|a| + |b|).
|
|
387
|
+
//
|
|
388
|
+
// The rewrite is only applied where it is provably invisible:
|
|
389
|
+
//
|
|
390
|
+
// - the phrase has no `$as` and no `$let`. Both run per tuple BETWEEN
|
|
391
|
+
// `$for` and `$where`, so they can observe - or fail on - a tuple the
|
|
392
|
+
// equality would later have dropped. A hash join never forms that
|
|
393
|
+
// tuple, which would silently retract a `$as` assertion;
|
|
394
|
+
// - the probe side is the innermost $for, with no `$at` (a position
|
|
395
|
+
// would have to survive bucketing), no `$allowing-empty`, no window;
|
|
396
|
+
// - its source reads no slot bound by an outer binding;
|
|
397
|
+
// - the equality is the whole `$where`, or its FIRST `$and` conjunct,
|
|
398
|
+
// so nothing that used to be evaluated before it is skipped;
|
|
399
|
+
// - both key expressions are paths or variable references, whose only
|
|
400
|
+
// failure is JQ2006 - so moving when they are evaluated cannot move
|
|
401
|
+
// an error;
|
|
402
|
+
// - neither key is statically MANY, because `$eq` is an existential
|
|
403
|
+
// comparison over sequences and a bucket holds one key per item.
|
|
404
|
+
//
|
|
405
|
+
// Equality itself stays exact: buckets key on `stableKeyString`, which
|
|
406
|
+
// agrees with the `$eq` relation (`equalsJson`) on every JSON value
|
|
407
|
+
// except NaN - and a NaN key is dropped on both sides, which is what
|
|
408
|
+
// `$eq` already does, since NaN equals nothing.
|
|
409
|
+
|
|
410
|
+
function isEqOp(node) {
|
|
411
|
+
return node.kind === 'op' && node.name === '$eq' && node.args.length === 2;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// The spatial predicates a bounding box can screen. Box overlap is a
|
|
415
|
+
// NECESSARY condition for both - a position inside a surface lies inside
|
|
416
|
+
// that surface's box, and box intersection is what the second one tests
|
|
417
|
+
// outright - so an index over boxes can only ever remove candidates that
|
|
418
|
+
// would have failed anyway.
|
|
419
|
+
const SPATIAL_JOIN_OPS = new Set(['$within', '$bbox-intersects']);
|
|
420
|
+
|
|
421
|
+
function isSpatialOp(node) {
|
|
422
|
+
return node.kind === 'op' && SPATIAL_JOIN_OPS.has(node.name) && node.args.length === 2;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isKeyExpr(node) {
|
|
426
|
+
return (node.kind === 'path' || node.kind === 'var') && node.card !== CARD_MANY;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function readsOf(node) {
|
|
430
|
+
const set = new Set();
|
|
431
|
+
collectReadSlots(node, set);
|
|
432
|
+
return set;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function intersects(set, slots) {
|
|
436
|
+
for (let i = 0; i < slots.length; i++) {
|
|
437
|
+
if (set.has(slots[i]))
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Decide whether `node`'s innermost $for can become a hash-join probe.
|
|
444
|
+
// Returns { inner, outerKey, innerKey, residual } or null.
|
|
445
|
+
function planHashJoin(node) {
|
|
446
|
+
const fors = node.forBindings;
|
|
447
|
+
if (fors.length < 2 || node.where === null)
|
|
448
|
+
return null;
|
|
449
|
+
if (node.asChecks !== null || node.letBindings.length !== 0)
|
|
450
|
+
return null; // they run per tuple before $where and would see fewer
|
|
451
|
+
const inner = fors[fors.length - 1];
|
|
452
|
+
if (inner.atSlot >= 0 || inner.allowingEmpty === true
|
|
453
|
+
|| (inner.window !== null && inner.window !== undefined))
|
|
454
|
+
return null;
|
|
455
|
+
|
|
456
|
+
const outerSlots = [];
|
|
457
|
+
for (let i = 0; i < fors.length - 1; i++) {
|
|
458
|
+
outerSlots.push(fors[i].slot);
|
|
459
|
+
if (fors[i].atSlot >= 0)
|
|
460
|
+
outerSlots.push(fors[i].atSlot);
|
|
461
|
+
}
|
|
462
|
+
if (intersects(readsOf(inner.expr), outerSlots))
|
|
463
|
+
return null; // correlated: the table would differ per outer tuple
|
|
464
|
+
|
|
465
|
+
const where = node.where;
|
|
466
|
+
let eq = null;
|
|
467
|
+
let rest = null;
|
|
468
|
+
if (isEqOp(where)) {
|
|
469
|
+
eq = where;
|
|
470
|
+
}
|
|
471
|
+
else if (where.kind === 'op' && where.name === '$and' && isEqOp(where.args[0])) {
|
|
472
|
+
eq = where.args[0];
|
|
473
|
+
// a one-conjunct $and is just the equality; leave no empty filter behind
|
|
474
|
+
rest = where.args.length > 1 ? where.args.slice(1) : null;
|
|
475
|
+
}
|
|
476
|
+
if (eq === null || !isKeyExpr(eq.args[0]) || !isKeyExpr(eq.args[1]))
|
|
477
|
+
return null;
|
|
478
|
+
|
|
479
|
+
// one side must be the probe's key, the other must not mention it
|
|
480
|
+
const reads0 = readsOf(eq.args[0]);
|
|
481
|
+
const reads1 = readsOf(eq.args[1]);
|
|
482
|
+
const uses0 = reads0.has(inner.slot);
|
|
483
|
+
const uses1 = reads1.has(inner.slot);
|
|
484
|
+
if (uses0 === uses1)
|
|
485
|
+
return null;
|
|
486
|
+
const innerKey = uses0 ? eq.args[0] : eq.args[1];
|
|
487
|
+
const outerKey = uses0 ? eq.args[1] : eq.args[0];
|
|
488
|
+
if (readsOf(outerKey).has(inner.slot) || intersects(readsOf(innerKey), outerSlots))
|
|
489
|
+
return null;
|
|
490
|
+
|
|
491
|
+
return { inner, outerKey, innerKey, residual: rest };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Decide whether the innermost `$for` can be probed through a spatial
|
|
496
|
+
* index. Returns `{ inner, innerGeo, outerGeo }` or null.
|
|
497
|
+
*
|
|
498
|
+
* Unlike the hash join this does NOT consume the predicate: the index
|
|
499
|
+
* only narrows the candidate set, and `$where` still runs unchanged on
|
|
500
|
+
* every candidate. That makes the rewrite correct by construction — the
|
|
501
|
+
* surviving tuples are decided by the same closure either way — and
|
|
502
|
+
* leaves only one thing to be careful about, which is that a tuple the
|
|
503
|
+
* index rejects never reaches the predicate at all. `compileSpatialProbe`
|
|
504
|
+
* handles that by keeping any item whose box cannot be computed in an
|
|
505
|
+
* always-check list, so a malformed operand still raises the error a
|
|
506
|
+
* scan would have raised.
|
|
507
|
+
*/
|
|
508
|
+
function planSpatialJoin(node) {
|
|
509
|
+
const fors = node.forBindings;
|
|
510
|
+
if (fors.length < 2 || node.where === null)
|
|
511
|
+
return null;
|
|
512
|
+
if (node.asChecks !== null || node.letBindings.length !== 0)
|
|
513
|
+
return null; // they run per tuple before $where and would see fewer
|
|
514
|
+
const where = node.where;
|
|
515
|
+
if (!isSpatialOp(where))
|
|
516
|
+
return null; // only a bare spatial predicate; an $and could throw first
|
|
517
|
+
const inner = fors[fors.length - 1];
|
|
518
|
+
if (inner.atSlot >= 0 || inner.allowingEmpty === true
|
|
519
|
+
|| (inner.window !== null && inner.window !== undefined))
|
|
520
|
+
return null;
|
|
521
|
+
|
|
522
|
+
const outerSlots = [];
|
|
523
|
+
for (let i = 0; i < fors.length - 1; i++) {
|
|
524
|
+
outerSlots.push(fors[i].slot);
|
|
525
|
+
if (fors[i].atSlot >= 0)
|
|
526
|
+
outerSlots.push(fors[i].atSlot);
|
|
527
|
+
}
|
|
528
|
+
if (intersects(readsOf(inner.expr), outerSlots))
|
|
529
|
+
return null; // correlated: the index would differ per outer tuple
|
|
530
|
+
|
|
531
|
+
// one operand must be the probe's geometry, the other must not mention it
|
|
532
|
+
const [a, b] = where.args;
|
|
533
|
+
if (!isKeyExpr(a) || !isKeyExpr(b))
|
|
534
|
+
return null; // paths and variables only, so evaluation cannot throw
|
|
535
|
+
const usesA = readsOf(a).has(inner.slot);
|
|
536
|
+
const usesB = readsOf(b).has(inner.slot);
|
|
537
|
+
if (usesA === usesB)
|
|
538
|
+
return null;
|
|
539
|
+
const innerGeo = usesA ? a : b;
|
|
540
|
+
const outerGeo = usesA ? b : a;
|
|
541
|
+
if (readsOf(outerGeo).has(inner.slot) || intersects(readsOf(innerGeo), outerSlots))
|
|
542
|
+
return null;
|
|
543
|
+
return { inner, innerGeo, outerGeo };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The spatial probe clause plus the prologue that indexes the inner
|
|
548
|
+
* side. Same closure-state discipline as the hash join: the index is
|
|
549
|
+
* rebuilt per phrase evaluation and saved/restored around the tuple
|
|
550
|
+
* stream, because a registered `$call` function can re-enter the query.
|
|
551
|
+
*/
|
|
552
|
+
function compileSpatialProbe(plan, next, where, wherePath) {
|
|
553
|
+
const slot = plan.inner.slot;
|
|
554
|
+
const srcGet = compileNode(plan.inner.expr);
|
|
555
|
+
const innerGeoGet = compileNode(plan.innerGeo);
|
|
556
|
+
const outerGeoGet = compileNode(plan.outerGeo);
|
|
557
|
+
const cond = compileNode(where);
|
|
558
|
+
let items = [];
|
|
559
|
+
let always = [];
|
|
560
|
+
let index = null;
|
|
561
|
+
|
|
562
|
+
const collect = (f, item) => {
|
|
563
|
+
if (Array.isArray(item)) { // D4, exactly as a $for would unpack it
|
|
564
|
+
for (let j = 0; j < item.length; j++)
|
|
565
|
+
items.push(item[j]);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
items.push(item);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const build = (f) => {
|
|
572
|
+
items = [];
|
|
573
|
+
always = [];
|
|
574
|
+
const v = srcGet(f);
|
|
575
|
+
if (v === EMPTY) {
|
|
576
|
+
index = null;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (v instanceof Seq) {
|
|
580
|
+
const list = v.items;
|
|
581
|
+
for (let i = 0; i < list.length; i++)
|
|
582
|
+
collect(f, list[i]);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
collect(f, v);
|
|
586
|
+
}
|
|
587
|
+
const boxes = new Array(items.length);
|
|
588
|
+
for (let i = 0; i < items.length; i++) {
|
|
589
|
+
f[slot] = items[i];
|
|
590
|
+
const box = bboxOf(innerGeoGet(f));
|
|
591
|
+
boxes[i] = box;
|
|
592
|
+
// no box means the index cannot speak for it - a malformed operand
|
|
593
|
+
// must still reach the predicate and raise what a scan would raise
|
|
594
|
+
if (box === null)
|
|
595
|
+
always.push(i);
|
|
596
|
+
}
|
|
597
|
+
index = createBboxIndex(boxes);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const emit = (f, out, i) => {
|
|
601
|
+
f[slot] = items[i];
|
|
602
|
+
if (ebv(cond(f), wherePath))
|
|
603
|
+
next(f, out);
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const probe = (f, out) => {
|
|
607
|
+
if (index === null)
|
|
608
|
+
return;
|
|
609
|
+
const box = bboxOf(outerGeoGet(f));
|
|
610
|
+
if (box === null) {
|
|
611
|
+
// nothing to screen with: fall back to the full scan, which is
|
|
612
|
+
// what this phrase would have done without an index at all
|
|
613
|
+
for (let i = 0; i < items.length; i++)
|
|
614
|
+
emit(f, out, i);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const hits = index.search(box[0], box[1], box[2], box[3]);
|
|
618
|
+
for (let i = 0; i < hits.length; i++)
|
|
619
|
+
emit(f, out, hits[i]);
|
|
620
|
+
for (let i = 0; i < always.length; i++)
|
|
621
|
+
emit(f, out, always[i]);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const drive = (f, out, chain) => {
|
|
625
|
+
const savedItems = items;
|
|
626
|
+
const savedAlways = always;
|
|
627
|
+
const savedIndex = index;
|
|
628
|
+
build(f);
|
|
629
|
+
try {
|
|
630
|
+
chain(f, out);
|
|
631
|
+
}
|
|
632
|
+
finally {
|
|
633
|
+
items = savedItems;
|
|
634
|
+
always = savedAlways;
|
|
635
|
+
index = savedIndex;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
return { drive, probe };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// The key a bucket is filed under, or null when the value cannot take
|
|
642
|
+
// part in an equality at all (empty, a multi-item sequence, or NaN).
|
|
643
|
+
function joinKey(v) {
|
|
644
|
+
if (v === EMPTY || v instanceof Seq)
|
|
645
|
+
return null;
|
|
646
|
+
if (typeof v === 'number' && v !== v)
|
|
647
|
+
return null;
|
|
648
|
+
return stableKeyString(v);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// The probe clause plus the `drive` wrapper that fills its table. The
|
|
652
|
+
// table is closure state, refreshed once per phrase evaluation. The
|
|
653
|
+
// language has no recursion, so a phrase cannot appear inside its own
|
|
654
|
+
// subtree - but a registered `$call` function is host code, and host code
|
|
655
|
+
// CAN re-enter the same compiled query from inside `$return`. `drive`
|
|
656
|
+
// therefore saves and restores the table around the tuple stream, so a
|
|
657
|
+
// nested evaluation cannot leave its own table behind for the outer
|
|
658
|
+
// probe to read.
|
|
659
|
+
function compileJoinProbe(plan, next) {
|
|
660
|
+
const slot = plan.inner.slot;
|
|
661
|
+
const srcGet = compileNode(plan.inner.expr);
|
|
662
|
+
const innerKeyGet = compileNode(plan.innerKey);
|
|
663
|
+
const outerKeyGet = compileNode(plan.outerKey);
|
|
664
|
+
let table = new Map();
|
|
665
|
+
|
|
666
|
+
const file = (f, item) => {
|
|
667
|
+
f[slot] = item;
|
|
668
|
+
const key = joinKey(innerKeyGet(f));
|
|
669
|
+
if (key === null)
|
|
670
|
+
return;
|
|
671
|
+
const bucket = table.get(key);
|
|
672
|
+
if (bucket === undefined)
|
|
673
|
+
table.set(key, [item]);
|
|
674
|
+
else
|
|
675
|
+
bucket.push(item);
|
|
676
|
+
};
|
|
677
|
+
const fileItem = (f, item) => {
|
|
678
|
+
if (Array.isArray(item)) { // D4, exactly as a $for would unpack it
|
|
679
|
+
for (let j = 0; j < item.length; j++)
|
|
680
|
+
file(f, item[j]);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
file(f, item);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const build = (f) => {
|
|
687
|
+
table = new Map();
|
|
688
|
+
const v = srcGet(f);
|
|
689
|
+
if (v === EMPTY)
|
|
690
|
+
return;
|
|
691
|
+
if (v instanceof Seq) {
|
|
692
|
+
const items = v.items;
|
|
693
|
+
for (let i = 0; i < items.length; i++)
|
|
694
|
+
fileItem(f, items[i]);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
fileItem(f, v);
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const probe = (f, out) => {
|
|
701
|
+
const key = joinKey(outerKeyGet(f));
|
|
702
|
+
if (key === null)
|
|
703
|
+
return;
|
|
704
|
+
const bucket = table.get(key);
|
|
705
|
+
if (bucket === undefined)
|
|
706
|
+
return;
|
|
707
|
+
for (let i = 0; i < bucket.length; i++) {
|
|
708
|
+
f[slot] = bucket[i];
|
|
709
|
+
next(f, out);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const drive = (f, out, chain) => {
|
|
713
|
+
const saved = table;
|
|
714
|
+
build(f);
|
|
715
|
+
try {
|
|
716
|
+
chain(f, out);
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
table = saved;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
return { drive, probe };
|
|
723
|
+
}
|
|
724
|
+
|
|
337
725
|
//#endregion
|
|
338
726
|
|
|
339
727
|
// D4 iteration step: an item that is an array contributes its members
|
|
@@ -368,10 +756,188 @@ function emitForItemAt(item, f, slot, atSlot, pos, next, out) {
|
|
|
368
756
|
return pos + 1;
|
|
369
757
|
}
|
|
370
758
|
|
|
371
|
-
|
|
759
|
+
// Iterating a `$range` never needs the range to exist. A `$for` (or a
|
|
760
|
+
// quantifier) whose source is *statically* a `$range` compiles to a
|
|
761
|
+
// counting loop instead of materializing 2^32 numbers to walk them once:
|
|
762
|
+
// the memory goes from O(n) to O(1) and the JQ2007 resource guard stops
|
|
763
|
+
// being the thing standing between a query and the heap. What bounds
|
|
764
|
+
// such a loop is time, which is what `limits.steps` is for.
|
|
765
|
+
//
|
|
766
|
+
// This is a compile-time specialization of the one shape that matters,
|
|
767
|
+
// not general lazy-sequence evaluation: a `$range` bound by `$let`, or
|
|
768
|
+
// handed to an aggregate, still materializes.
|
|
769
|
+
// The bound getters of a statically-recognized `$range` source, or null
|
|
770
|
+
// when the source is anything else. The loop itself is written out at
|
|
771
|
+
// each use site rather than shared through a callback: an indirect call
|
|
772
|
+
// per iterated number would cost more than the duplication saves.
|
|
773
|
+
function rangeSource(expr) {
|
|
774
|
+
if (expr.kind !== 'op' || expr.name !== '$range' || expr.args.length !== 2)
|
|
775
|
+
return null;
|
|
776
|
+
return {
|
|
777
|
+
fromGet: compileNode(expr.args[0]),
|
|
778
|
+
fromPath: expr.args[0].docPath,
|
|
779
|
+
toGet: compileNode(expr.args[1]),
|
|
780
|
+
toPath: expr.args[1].docPath,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Whether a source sequence yields at least one tuple, accounting for
|
|
785
|
+
// the D4 unpacking step (an empty array item contributes nothing). This
|
|
786
|
+
// is what `$allowing-empty` asks about: "did this binding produce a
|
|
787
|
+
// tuple", not "was the sequence empty".
|
|
788
|
+
function yieldsTuple(v) {
|
|
789
|
+
if (v === EMPTY)
|
|
790
|
+
return false;
|
|
791
|
+
if (v instanceof Seq) {
|
|
792
|
+
const items = v.items;
|
|
793
|
+
for (let i = 0; i < items.length; i++) {
|
|
794
|
+
const item = items[i];
|
|
795
|
+
if (!Array.isArray(item) || item.length !== 0)
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
return !Array.isArray(v) || v.length !== 0;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// The item stream a $for would iterate, flattened once (D4), which is
|
|
804
|
+
// what a window partitions.
|
|
805
|
+
function unpackedItems(v) {
|
|
806
|
+
const out = [];
|
|
807
|
+
if (v === EMPTY)
|
|
808
|
+
return out;
|
|
809
|
+
if (v instanceof Seq) {
|
|
810
|
+
const items = v.items;
|
|
811
|
+
for (let i = 0; i < items.length; i++)
|
|
812
|
+
appendItem(out, Array.isArray(items[i]) ? seqOf(items[i].slice()) : items[i]);
|
|
813
|
+
return out;
|
|
814
|
+
}
|
|
815
|
+
if (Array.isArray(v)) {
|
|
816
|
+
for (let i = 0; i < v.length; i++)
|
|
817
|
+
out.push(v[i]);
|
|
818
|
+
return out;
|
|
819
|
+
}
|
|
820
|
+
out.push(v);
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// A window binding (section 6.10): the source materializes once, then a
|
|
825
|
+
// window starts every `step` items. A tumbling window keeps its short
|
|
826
|
+
// final window (it partitions the stream); a sliding one emits only
|
|
827
|
+
// full-width windows.
|
|
828
|
+
function compileWindowClause(binding, next) {
|
|
372
829
|
const get = compileNode(binding.expr);
|
|
373
830
|
const slot = binding.slot;
|
|
374
831
|
const atSlot = binding.atSlot;
|
|
832
|
+
const { sliding, size, step } = binding.window;
|
|
833
|
+
const allowingEmpty = binding.allowingEmpty;
|
|
834
|
+
return (f, out) => {
|
|
835
|
+
const items = unpackedItems(get(f));
|
|
836
|
+
const n = items.length;
|
|
837
|
+
let w = 0;
|
|
838
|
+
for (let start = 0; start < n; start += step) {
|
|
839
|
+
let end = start + size;
|
|
840
|
+
if (end > n) {
|
|
841
|
+
if (sliding)
|
|
842
|
+
break;
|
|
843
|
+
end = n;
|
|
844
|
+
}
|
|
845
|
+
f[slot] = seqOf(items.slice(start, end));
|
|
846
|
+
if (atSlot >= 0)
|
|
847
|
+
f[atSlot] = w;
|
|
848
|
+
w++;
|
|
849
|
+
next(f, out);
|
|
850
|
+
}
|
|
851
|
+
if (w === 0 && allowingEmpty) {
|
|
852
|
+
f[slot] = EMPTY;
|
|
853
|
+
if (atSlot >= 0)
|
|
854
|
+
f[atSlot] = -1;
|
|
855
|
+
next(f, out);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function compileForClause(binding, next) {
|
|
861
|
+
if (binding.window !== null && binding.window !== undefined)
|
|
862
|
+
return compileWindowClause(binding, next);
|
|
863
|
+
const slot = binding.slot;
|
|
864
|
+
const atSlot = binding.atSlot;
|
|
865
|
+
// a range source counts instead of materializing; a range is never
|
|
866
|
+
// empty-yielding in a way $allowing-empty could not also see, so the
|
|
867
|
+
// two compose
|
|
868
|
+
const range = binding.allowingEmpty ? null : rangeSource(binding.expr);
|
|
869
|
+
if (range !== null) {
|
|
870
|
+
const { fromGet, fromPath, toGet, toPath } = range;
|
|
871
|
+
if (atSlot < 0) {
|
|
872
|
+
return (f, out) => {
|
|
873
|
+
const a = fromGet(f);
|
|
874
|
+
const b = toGet(f);
|
|
875
|
+
if (a === EMPTY || b === EMPTY)
|
|
876
|
+
return;
|
|
877
|
+
checkRangeBound(a, fromPath);
|
|
878
|
+
checkRangeBound(b, toPath);
|
|
879
|
+
for (let i = a; i <= b; i++) {
|
|
880
|
+
f[slot] = i;
|
|
881
|
+
next(f, out);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return (f, out) => {
|
|
886
|
+
const a = fromGet(f);
|
|
887
|
+
const b = toGet(f);
|
|
888
|
+
if (a === EMPTY || b === EMPTY)
|
|
889
|
+
return;
|
|
890
|
+
checkRangeBound(a, fromPath);
|
|
891
|
+
checkRangeBound(b, toPath);
|
|
892
|
+
let pos = 0;
|
|
893
|
+
for (let i = a; i <= b; i++) {
|
|
894
|
+
f[slot] = i;
|
|
895
|
+
f[atSlot] = pos++;
|
|
896
|
+
next(f, out);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const get = compileNode(binding.expr);
|
|
901
|
+
if (binding.allowingEmpty) {
|
|
902
|
+
// outer-join iteration: when the binding would produce no tuple at
|
|
903
|
+
// all, produce exactly one with the variable bound to the empty
|
|
904
|
+
// sequence. The position of that tuple is -1: every real position is
|
|
905
|
+
// a 0-based one (D6), so there is no non-negative "no position".
|
|
906
|
+
if (atSlot < 0) {
|
|
907
|
+
return (f, out) => {
|
|
908
|
+
const v = get(f);
|
|
909
|
+
if (!yieldsTuple(v)) {
|
|
910
|
+
f[slot] = EMPTY;
|
|
911
|
+
next(f, out);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (v instanceof Seq) {
|
|
915
|
+
const items = v.items;
|
|
916
|
+
for (let i = 0; i < items.length; i++)
|
|
917
|
+
emitForItem(items[i], f, slot, next, out);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
emitForItem(v, f, slot, next, out);
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return (f, out) => {
|
|
924
|
+
const v = get(f);
|
|
925
|
+
if (!yieldsTuple(v)) {
|
|
926
|
+
f[slot] = EMPTY;
|
|
927
|
+
f[atSlot] = -1;
|
|
928
|
+
next(f, out);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (v instanceof Seq) {
|
|
932
|
+
const items = v.items;
|
|
933
|
+
let pos = 0;
|
|
934
|
+
for (let i = 0; i < items.length; i++)
|
|
935
|
+
pos = emitForItemAt(items[i], f, slot, atSlot, pos, next, out);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
emitForItemAt(v, f, slot, atSlot, 0, next, out);
|
|
939
|
+
};
|
|
940
|
+
}
|
|
375
941
|
if (atSlot < 0) {
|
|
376
942
|
return (f, out) => {
|
|
377
943
|
const v = get(f);
|
|
@@ -472,9 +1038,11 @@ function compileRowComparator(specs, keyPaths) {
|
|
|
472
1038
|
const keyCount = specs.length;
|
|
473
1039
|
const descs = new Array(keyCount);
|
|
474
1040
|
const emptyGreatests = new Array(keyCount);
|
|
1041
|
+
const collations = new Array(keyCount);
|
|
475
1042
|
for (let i = 0; i < keyCount; i++) {
|
|
476
1043
|
descs[i] = specs[i].desc;
|
|
477
1044
|
emptyGreatests[i] = specs[i].emptyGreatest;
|
|
1045
|
+
collations[i] = specs[i].collation ?? null;
|
|
478
1046
|
}
|
|
479
1047
|
return (a, b) => {
|
|
480
1048
|
for (let i = 0; i < keyCount; i++) {
|
|
@@ -504,7 +1072,9 @@ function compileRowComparator(specs, keyPaths) {
|
|
|
504
1072
|
if (typeof y !== 'string')
|
|
505
1073
|
throw new JsonQueryRuntimeError('JQ2005',
|
|
506
1074
|
'cannot order a string against a number in $orderby', keyPaths[i]);
|
|
507
|
-
|
|
1075
|
+
// a registered $collation orders the STRING keys; the default
|
|
1076
|
+
// stays the format's code-point order
|
|
1077
|
+
c = collations[i] !== null ? collations[i](x, y) : compareCodePoints(x, y);
|
|
508
1078
|
}
|
|
509
1079
|
if (c !== 0)
|
|
510
1080
|
return descs[i] ? -c : c;
|
|
@@ -518,8 +1088,17 @@ function compileFlwor(node) {
|
|
|
518
1088
|
// numbers surviving tuples through its own frame slot (0-based, D6),
|
|
519
1089
|
// reset once per phrase evaluation by the drivers below
|
|
520
1090
|
const retGet = compileNode(node.ret);
|
|
1091
|
+
// $fold (section 6.9) replaces the collecting sink with an assigning
|
|
1092
|
+
// one: $return names the accumulator's next value instead of an item
|
|
1093
|
+
// of the result, and nothing is materialized.
|
|
1094
|
+
const foldSlot = node.fold === null ? -1 : node.fold.slot;
|
|
521
1095
|
let sink;
|
|
522
|
-
if (
|
|
1096
|
+
if (foldSlot >= 0) {
|
|
1097
|
+
sink = (f) => {
|
|
1098
|
+
f[foldSlot] = retGet(f);
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
else if (node.ret.card === CARD_ONE)
|
|
523
1102
|
sink = (f, out) => out.push(retGet(f));
|
|
524
1103
|
else
|
|
525
1104
|
sink = (f, out) => appendItem(out, retGet(f));
|
|
@@ -532,6 +1111,26 @@ function compileFlwor(node) {
|
|
|
532
1111
|
};
|
|
533
1112
|
}
|
|
534
1113
|
|
|
1114
|
+
// limits.sequenceItems bounds every phrase materialization: the guard
|
|
1115
|
+
// fires while the accumulator grows, deterministically, inside the
|
|
1116
|
+
// synchronous engine (never a wall-clock claim)
|
|
1117
|
+
const seqLimit = node.limits !== null && node.limits !== undefined
|
|
1118
|
+
&& node.limits.sequenceItems !== null
|
|
1119
|
+
? node.limits.sequenceItems
|
|
1120
|
+
: 0;
|
|
1121
|
+
// a $fold materializes nothing, so the phrase-output cap has nothing
|
|
1122
|
+
// to bound and is not installed
|
|
1123
|
+
if (seqLimit > 0 && foldSlot < 0) {
|
|
1124
|
+
const inner = sink;
|
|
1125
|
+
const limitPath = node.docPath;
|
|
1126
|
+
sink = (f, out) => {
|
|
1127
|
+
inner(f, out);
|
|
1128
|
+
if (out.length > seqLimit)
|
|
1129
|
+
throw new JsonQueryRuntimeError('JQ2009',
|
|
1130
|
+
`a phrase materialized more than ${seqLimit} items (limits.sequenceItems)`, limitPath);
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
535
1134
|
const groupby = node.groupby;
|
|
536
1135
|
const orderby = node.orderby;
|
|
537
1136
|
|
|
@@ -602,7 +1201,29 @@ function compileFlwor(node) {
|
|
|
602
1201
|
// the streaming prefix $for -> $let -> $as -> $where, feeding the first
|
|
603
1202
|
// barrier's collector (or the final sink when there is none)
|
|
604
1203
|
let emit = groupby !== null ? groupSink : (orderby !== null ? rowSink : sink);
|
|
605
|
-
|
|
1204
|
+
// an equijoin the planner can serve from a hash table is answered by
|
|
1205
|
+
// the probe clause below, so its conjunct never reaches $where
|
|
1206
|
+
const join = planHashJoin(node);
|
|
1207
|
+
// a spatial predicate is screened by an index instead: the probe keeps
|
|
1208
|
+
// $where intact and runs it on every candidate, so it installs no
|
|
1209
|
+
// where-stage of its own
|
|
1210
|
+
// ... and when there is one, no where-stage is installed at all: the
|
|
1211
|
+
// probe evaluates the predicate itself, on the candidates
|
|
1212
|
+
const spatial = join === null ? planSpatialJoin(node) : null;
|
|
1213
|
+
if (spatial === null && join !== null && join.residual !== null) {
|
|
1214
|
+
const conds = join.residual.map(compileNode);
|
|
1215
|
+
const paths = join.residual.map((a) => a.docPath);
|
|
1216
|
+
const clen = conds.length;
|
|
1217
|
+
const next = emit;
|
|
1218
|
+
emit = (f, out) => {
|
|
1219
|
+
for (let i = 0; i < clen; i++) {
|
|
1220
|
+
if (!ebv(conds[i](f), paths[i]))
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
next(f, out);
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
else if (spatial === null && join === null && node.where !== null) {
|
|
606
1227
|
const cond = compileNode(node.where);
|
|
607
1228
|
const condPath = node.where.docPath;
|
|
608
1229
|
const next = emit;
|
|
@@ -626,31 +1247,96 @@ function compileFlwor(node) {
|
|
|
626
1247
|
};
|
|
627
1248
|
}
|
|
628
1249
|
const fors = node.forBindings;
|
|
629
|
-
|
|
1250
|
+
let driveJoin = null;
|
|
1251
|
+
for (let i = fors.length - 1; i >= 0; i--) {
|
|
1252
|
+
if (i === fors.length - 1 && (join !== null || spatial !== null)) {
|
|
1253
|
+
const probe = join !== null
|
|
1254
|
+
? compileJoinProbe(join, emit)
|
|
1255
|
+
: compileSpatialProbe(spatial, emit, node.where, node.where.docPath);
|
|
1256
|
+
driveJoin = probe.drive;
|
|
1257
|
+
emit = probe.probe;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
630
1260
|
emit = compileForClause(fors[i], emit);
|
|
631
|
-
|
|
1261
|
+
}
|
|
1262
|
+
// the table is filled once per phrase evaluation, before the outer
|
|
1263
|
+
// loops start
|
|
1264
|
+
const head = driveJoin === null
|
|
1265
|
+
? emit
|
|
1266
|
+
: ((chain) => (f, out) => driveJoin(f, out, chain))(emit);
|
|
1267
|
+
|
|
1268
|
+
// drivers, one per barrier combination. A $fold wraps whichever driver
|
|
1269
|
+
// this builds rather than adding a fifth pair: the tuple stream, both
|
|
1270
|
+
// barriers and $count all behave identically, only the phrase's value
|
|
1271
|
+
// is read from the accumulator instead of the collector.
|
|
1272
|
+
const drive = buildDriver();
|
|
1273
|
+
if (foldSlot < 0)
|
|
1274
|
+
return drive;
|
|
1275
|
+
const initGet = compileNode(node.fold.expr);
|
|
1276
|
+
return (f) => {
|
|
1277
|
+
f[foldSlot] = initGet(f);
|
|
1278
|
+
drive(f);
|
|
1279
|
+
return f[foldSlot];
|
|
1280
|
+
};
|
|
632
1281
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1282
|
+
function buildDriver() {
|
|
1283
|
+
if (groupby === null && orderby === null) {
|
|
1284
|
+
if (countSlot < 0) {
|
|
1285
|
+
return (f) => {
|
|
1286
|
+
const out = [];
|
|
1287
|
+
head(f, out);
|
|
1288
|
+
return seqOf(out);
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
636
1291
|
return (f) => {
|
|
637
1292
|
const out = [];
|
|
1293
|
+
f[countSlot] = 0;
|
|
638
1294
|
head(f, out);
|
|
639
1295
|
return seqOf(out);
|
|
640
1296
|
};
|
|
641
1297
|
}
|
|
1298
|
+
if (groupby === null) { // $orderby only
|
|
1299
|
+
return (f) => {
|
|
1300
|
+
const rows = [];
|
|
1301
|
+
head(f, rows);
|
|
1302
|
+
rows.sort(comparator); // stable
|
|
1303
|
+
const out = [];
|
|
1304
|
+
if (countSlot >= 0)
|
|
1305
|
+
f[countSlot] = 0;
|
|
1306
|
+
const liveCount = liveSlots.length;
|
|
1307
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1308
|
+
const snap = rows[i][keyCount];
|
|
1309
|
+
for (let j = 0; j < liveCount; j++)
|
|
1310
|
+
f[liveSlots[j]] = snap[j];
|
|
1311
|
+
sink(f, out);
|
|
1312
|
+
}
|
|
1313
|
+
return seqOf(out);
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
if (orderby === null) { // $groupby only
|
|
1317
|
+
return (f) => {
|
|
1318
|
+
const map = new Map();
|
|
1319
|
+
head(f, map);
|
|
1320
|
+
const out = [];
|
|
1321
|
+
if (countSlot >= 0)
|
|
1322
|
+
f[countSlot] = 0;
|
|
1323
|
+
for (const group of map.values()) { // first-appearance order
|
|
1324
|
+
writeGroup(f, group);
|
|
1325
|
+
sink(f, out);
|
|
1326
|
+
}
|
|
1327
|
+
return seqOf(out);
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
// $groupby then $orderby: sort the per-group tuples
|
|
642
1331
|
return (f) => {
|
|
643
|
-
const
|
|
644
|
-
f
|
|
645
|
-
head(f, out);
|
|
646
|
-
return seqOf(out);
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
if (groupby === null) { // $orderby only
|
|
650
|
-
return (f) => {
|
|
1332
|
+
const map = new Map();
|
|
1333
|
+
head(f, map);
|
|
651
1334
|
const rows = [];
|
|
652
|
-
|
|
653
|
-
|
|
1335
|
+
for (const group of map.values()) {
|
|
1336
|
+
writeGroup(f, group);
|
|
1337
|
+
rowSink(f, rows);
|
|
1338
|
+
}
|
|
1339
|
+
rows.sort(comparator);
|
|
654
1340
|
const out = [];
|
|
655
1341
|
if (countSlot >= 0)
|
|
656
1342
|
f[countSlot] = 0;
|
|
@@ -664,42 +1350,6 @@ function compileFlwor(node) {
|
|
|
664
1350
|
return seqOf(out);
|
|
665
1351
|
};
|
|
666
1352
|
}
|
|
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
1353
|
}
|
|
704
1354
|
|
|
705
1355
|
//#endregion
|
|
@@ -736,8 +1386,28 @@ function quantVisitEvery(item, f, slot, next) {
|
|
|
736
1386
|
}
|
|
737
1387
|
|
|
738
1388
|
function compileQuantLevel(binding, next, some) {
|
|
739
|
-
const get = compileNode(binding.expr);
|
|
740
1389
|
const slot = binding.slot;
|
|
1390
|
+
// a quantified range counts too, and stops at its witness: `$some` over
|
|
1391
|
+
// a billion numbers should cost the numbers it actually examines
|
|
1392
|
+
const range = rangeSource(binding.expr);
|
|
1393
|
+
if (range !== null) {
|
|
1394
|
+
const { fromGet, fromPath, toGet, toPath } = range;
|
|
1395
|
+
return (f) => {
|
|
1396
|
+
const a = fromGet(f);
|
|
1397
|
+
const b = toGet(f);
|
|
1398
|
+
if (a === EMPTY || b === EMPTY)
|
|
1399
|
+
return !some; // empty source: no witness / vacuously true
|
|
1400
|
+
checkRangeBound(a, fromPath);
|
|
1401
|
+
checkRangeBound(b, toPath);
|
|
1402
|
+
for (let i = a; i <= b; i++) {
|
|
1403
|
+
f[slot] = i;
|
|
1404
|
+
if (next(f) === some)
|
|
1405
|
+
return some;
|
|
1406
|
+
}
|
|
1407
|
+
return !some;
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
const get = compileNode(binding.expr);
|
|
741
1411
|
if (some) {
|
|
742
1412
|
return (f) => {
|
|
743
1413
|
const v = get(f);
|
|
@@ -782,12 +1452,55 @@ function compileQuant(node) {
|
|
|
782
1452
|
|
|
783
1453
|
//#endregion
|
|
784
1454
|
|
|
1455
|
+
// Step instrumentation (`limits.steps`). Null unless the compilation in
|
|
1456
|
+
// flight set a step limit, so an ordinary compile emits exactly the
|
|
1457
|
+
// closures it always did and pays nothing. Compilation is synchronous,
|
|
1458
|
+
// and compileQueryRoot saves/restores around the whole tree, so a nested
|
|
1459
|
+
// compile - a `compileTypeTest` hook that compiles another query - keeps
|
|
1460
|
+
// its own setting.
|
|
1461
|
+
let STEPS = null;
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Compile a query's AST root, optionally instrumenting every node
|
|
1465
|
+
* evaluation against a step limit.
|
|
1466
|
+
* @param {object} root - the AST root from normalizeQuery
|
|
1467
|
+
* @param {{ slot: number, limit: number } | null} steps - the step
|
|
1468
|
+
* counter's frame slot and its limit, or null for no instrumentation
|
|
1469
|
+
* @returns {(frame: any[]) => any} the root getter
|
|
1470
|
+
*/
|
|
1471
|
+
export function compileQueryRoot(root, steps) {
|
|
1472
|
+
const prev = STEPS;
|
|
1473
|
+
STEPS = steps;
|
|
1474
|
+
try {
|
|
1475
|
+
return compileNode(root);
|
|
1476
|
+
}
|
|
1477
|
+
finally {
|
|
1478
|
+
STEPS = prev;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
785
1482
|
/**
|
|
786
1483
|
* Compile a normalized AST node into its getter closure.
|
|
787
1484
|
* @param {object} node - a frozen AST node from normalize.js
|
|
788
1485
|
* @returns {(frame: any[]) => any} getter returning an item, EMPTY, or a Seq
|
|
789
1486
|
*/
|
|
790
1487
|
export function compileNode(node) {
|
|
1488
|
+
const get = compileNodeKind(node);
|
|
1489
|
+
if (STEPS === null)
|
|
1490
|
+
return get;
|
|
1491
|
+
// one step = one expression-node evaluation (section 8.12)
|
|
1492
|
+
const slot = STEPS.slot;
|
|
1493
|
+
const limit = STEPS.limit;
|
|
1494
|
+
const docPath = node.docPath;
|
|
1495
|
+
return (f) => {
|
|
1496
|
+
if (++f[slot] > limit)
|
|
1497
|
+
throw new JsonQueryRuntimeError('JQ2009',
|
|
1498
|
+
`the query exceeded limits.steps (${limit} expression evaluations)`, docPath);
|
|
1499
|
+
return get(f);
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function compileNodeKind(node) {
|
|
791
1504
|
switch (node.kind) {
|
|
792
1505
|
case 'literal': {
|
|
793
1506
|
const value = node.value;
|
|
@@ -805,6 +1518,8 @@ export function compileNode(node) {
|
|
|
805
1518
|
return compileArray(node);
|
|
806
1519
|
case 'op':
|
|
807
1520
|
return compileOp(node);
|
|
1521
|
+
case 'call':
|
|
1522
|
+
return compileCall(node);
|
|
808
1523
|
case 'let':
|
|
809
1524
|
return compileLet(node);
|
|
810
1525
|
case 'quant':
|