@jarenjs/json 0.9.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
@@ -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] = (f, out) => {
188
- out[name] = get(f);
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] = (f, out) => {
194
- const v = get(f);
195
- if (v !== EMPTY)
196
- out[name] = memberValue(v, name, docPath);
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[k] = valGet(f);
242
+ setObjectMember(out, k, valGet(f));
228
243
  return;
229
244
  }
230
245
  const v = valGet(f);
231
246
  if (v !== EMPTY)
232
- out[k] = memberValue(v, k, valPath);
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
- return entry.compile(gets, args, node.docPath + '/' + node.name);
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 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.
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
- function compileForClause(binding, next) {
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
- c = compareCodePoints(x, y);
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 (node.ret.card === CARD_ONE)
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
- if (node.where !== null) {
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
- for (let i = fors.length - 1; i >= 0; i--)
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
- const head = emit;
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
- // drivers, one per barrier combination
634
- if (groupby === null && orderby === null) {
635
- if (countSlot < 0) {
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 out = [];
644
- f[countSlot] = 0;
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
- head(f, rows);
653
- rows.sort(comparator); // stable
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':