@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.
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
@@ -30,10 +30,44 @@
30
30
  // All runtime error conditions of the operator library (JQ2xxx,
31
31
  // QUERY-FORMAT.md section 10.3) are raised here.
32
32
 
33
- import { equalsJson } from '@jarenjs/core/object';
33
+ import { equalsJson, compareJsonScalarLt } from '@jarenjs/core/object';
34
34
  import { countCodePoints, compareCodePoints } from '@jarenjs/core/string';
35
35
  import { compileIRegexp } from '@jarenjs/core/text/iregexp';
36
- import { JsonQueryRuntimeError } from './errors.js';
36
+ import {
37
+ parseRFC3339Parts,
38
+ epochOfRFC3339Parts,
39
+ formatRFC3339Parts,
40
+ partsFromEpoch,
41
+ isDateOnlyRFC3339,
42
+ isTimeOnlyRFC3339,
43
+ isDateTimeRFC3339,
44
+ isValidDuration,
45
+ isDateUnit,
46
+ addToParts,
47
+ startOfParts,
48
+ endOfParts,
49
+ parseDuration,
50
+ addDuration,
51
+ monthsBetween,
52
+ daysFromCivil,
53
+ isoWeekOfYear,
54
+ isoWeekdayFromDays,
55
+ quarterOfYear,
56
+ fixedUnitMs,
57
+ compileDateFormat,
58
+ } from '@jarenjs/core/dates';
59
+ import {
60
+ isPosition,
61
+ bboxOf,
62
+ bboxIntersects,
63
+ geometryArea,
64
+ geometryLength,
65
+ centroidOf,
66
+ containsPosition,
67
+ geoDistance,
68
+ geohashEncode,
69
+ } from '@jarenjs/core/geo';
70
+ import { JsonQueryCompileError, JsonQueryRuntimeError } from './errors.js';
37
71
  import {
38
72
  EMPTY, Seq, seqOf, appendItem, ebv, itemCount, firstItem,
39
73
  stableKeyString, describeItem,
@@ -67,6 +101,23 @@ const resultOfOperand = (cards) => cards[0];
67
101
  // a singleton operand stays a singleton; anything else may come out empty
68
102
  const resultEmptyPropagates = (cards) => (cards[0] === CARD_ONE ? CARD_ONE : CARD_OPT);
69
103
 
104
+ // Static result-type declarations for `annotateTypes` (QUERY-FORMAT.md
105
+ // Appendix C.8): `resultType(argTypes)` returns a tag name from the
106
+ // closed lattice, or null for unknown. Declared only for the
107
+ // comparison, arithmetic, string and aggregate families, and only where
108
+ // the answer is certain — a wrong tag is a defect, `unknown` never is.
109
+ const RT_BOOLEAN = () => 'boolean';
110
+ const RT_NUMBER = () => 'number';
111
+ const RT_INTEGER = () => 'integer';
112
+ const RT_STRING = () => 'string';
113
+ // $min/$max keep their operand family: numbers yield a number, strings
114
+ // a string, anything else stays unknown (mixed input is JQ2001 anyway).
115
+ const RT_MINMAX = (types) => {
116
+ const t = types[0].type;
117
+ if (t === 'number' || t === 'integer') return 'number';
118
+ return t === 'string' ? 'string' : null;
119
+ };
120
+
70
121
  //#endregion
71
122
 
72
123
  //#region runtime argument helpers
@@ -166,18 +217,10 @@ function itemNe(a, b) {
166
217
  return !equalsJson(a, b);
167
218
  }
168
219
  function itemLt(a, b) {
169
- if (typeof a === 'number')
170
- return typeof b === 'number' && a < b;
171
- if (typeof a === 'string')
172
- return typeof b === 'string' && compareCodePoints(a, b) < 0;
173
- return false;
220
+ return compareJsonScalarLt(a, b);
174
221
  }
175
222
  function itemLe(a, b) {
176
- if (typeof a === 'number')
177
- return typeof b === 'number' && a <= b;
178
- if (typeof a === 'string')
179
- return typeof b === 'string' && compareCodePoints(a, b) <= 0;
180
- return false;
223
+ return compareJsonScalarLt(a, b, true);
181
224
  }
182
225
  function itemGt(a, b) {
183
226
  return itemLt(b, a);
@@ -194,6 +237,7 @@ function comparisonEntry(itemCmp) {
194
237
  return {
195
238
  params: ARGS_2,
196
239
  result: RESULT_ONE,
240
+ resultType: RT_BOOLEAN,
197
241
  compile: (gets, args) => {
198
242
  const left = gets[0];
199
243
  const right = gets[1];
@@ -247,10 +291,11 @@ function arithOperandError(v, docPath) {
247
291
  // non-number singletons and longer sequences are JQ2001. `makeApply`
248
292
  // builds the two-number kernel (it closes over the operator docPath for
249
293
  // the JQ2002 zero-divisor errors of $idiv/$mod).
250
- function arithmeticEntry(makeApply) {
294
+ function arithmeticEntry(makeApply, resultType = RT_NUMBER) {
251
295
  return {
252
296
  params: ARGS_2,
253
297
  result: (cards) => (cards[0] === CARD_ONE && cards[1] === CARD_ONE ? CARD_ONE : CARD_OPT),
298
+ resultType,
254
299
  compile: (gets, args, docPath) => {
255
300
  const apply = makeApply(docPath);
256
301
  const left = gets[0];
@@ -311,10 +356,11 @@ function logicEntry(stopOn) {
311
356
  //#region string operators
312
357
 
313
358
  // unary string operator: coerce per stringArg, apply the kernel
314
- function stringUnaryEntry(apply) {
359
+ function stringUnaryEntry(apply, resultType = RT_STRING) {
315
360
  return {
316
361
  params: UNARY,
317
362
  result: RESULT_ONE,
363
+ resultType,
318
364
  compile: (gets, args) => {
319
365
  const get = gets[0];
320
366
  const docPath = args[0].docPath;
@@ -328,6 +374,7 @@ function stringPairEntry(test) {
328
374
  return {
329
375
  params: ARGS_2,
330
376
  result: RESULT_ONE,
377
+ resultType: RT_BOOLEAN,
331
378
  compile: (gets, args) => {
332
379
  const aGet = gets[0];
333
380
  const aPath = args[0].docPath;
@@ -367,6 +414,7 @@ function regexTestEntry(fullMatch) {
367
414
  return {
368
415
  params: ARGS_2,
369
416
  result: RESULT_ONE,
417
+ resultType: RT_BOOLEAN,
370
418
  compile: (gets, args) => {
371
419
  const inGet = gets[0];
372
420
  const inPath = args[0].docPath;
@@ -473,6 +521,7 @@ function minmaxEntry(isMax) {
473
521
  return {
474
522
  params: UNARY,
475
523
  result: resultEmptyPropagates,
524
+ resultType: RT_MINMAX,
476
525
  compile: (gets, args) => {
477
526
  const get = gets[0];
478
527
  const docPath = args[0].docPath;
@@ -543,11 +592,211 @@ function rangeBoundError(v, docPath) {
543
592
  return runtimeError('JQ2001', `'$range' bounds must be integral numbers, got ${describeItem(v)}`, docPath);
544
593
  }
545
594
 
595
+ /**
596
+ * Check one evaluated `$range` bound. Shared with the compiler's
597
+ * iteration fast path (compile.js), so a range that is iterated rather
598
+ * than materialized still rejects exactly the same bounds.
599
+ * @param {any} v - an evaluated bound
600
+ * @param {string} docPath - the bound's document pointer
601
+ * @returns {number} the bound
602
+ * @throws {JsonQueryRuntimeError} JQ2001 when it is not an integer
603
+ */
604
+ export function checkRangeBound(v, docPath) {
605
+ if (typeof v !== 'number' || !Number.isInteger(v))
606
+ throw rangeBoundError(v, docPath);
607
+ return v;
608
+ }
609
+
546
610
  // the resource guard of $range (section 10.3, JQ2007)
547
611
  const RANGE_LIMIT = 4294967296; // 2^32
548
612
 
549
613
  //#endregion
550
614
 
615
+ //#region date and time operators (section 8.13)
616
+ // Dates are RFC 3339 strings - JSON has no date type - and every operator
617
+ // here is a pure function of its operand: there is deliberately no
618
+ // `current-dateTime`, because a query must give the same answer for the
619
+ // same input document forever (it is cached by document identity, saved
620
+ // as a rule, and used as a validation keyword).
621
+ //
622
+ // Components are read LEXICALLY, in the value's own offset, which is
623
+ // what `fn:year-from-dateTime` returns and what grouping by year or
624
+ // month means. Cross-offset comparison and arithmetic go through
625
+ // `$epoch`, which is the one place a value is shifted to UTC.
626
+
627
+ // a single RFC 3339 operand, decomposed (JQ2001 on anything else)
628
+ function dateParts(v, docPath) {
629
+ const parts = parseRFC3339Parts(v);
630
+ if (parts === null) {
631
+ // a malformed string is the common case here, so show it rather than
632
+ // reporting the useless fact that it was a string
633
+ throw runtimeError('JQ2001', 'expected an RFC 3339 date, time, or date-time string, got '
634
+ + (typeof v === 'string' ? JSON.stringify(v) : describeItem(v)), docPath);
635
+ }
636
+ return parts;
637
+ }
638
+
639
+ // a lexical component operator: empty propagates, a value whose half is
640
+ // missing (asking a full-date for its hours) is JQ2001
641
+ function dateComponentEntry(pick, half) {
642
+ return {
643
+ params: UNARY,
644
+ result: resultEmptyPropagates,
645
+ compile: (gets, args) => {
646
+ const get = gets[0];
647
+ const docPath = args[0].docPath;
648
+ return (f) => {
649
+ const v = get(f);
650
+ if (v === EMPTY)
651
+ return EMPTY;
652
+ const n = pick(dateParts(v, docPath));
653
+ if (n < 0)
654
+ throw runtimeError('JQ2001', `'${v}' carries no ${half} component`, docPath);
655
+ return n;
656
+ };
657
+ },
658
+ };
659
+ }
660
+
661
+ // A calendar unit argument. Units are data, not vocabulary, so a bad one
662
+ // is a runtime JQ2001 rather than a compile error - the same treatment
663
+ // `$orderby`'s registered collation names get.
664
+ function unitArg(v, docPath) {
665
+ if (!isDateUnit(v)) {
666
+ throw runtimeError('JQ2001',
667
+ `expected a calendar unit ('year', 'month', 'day', ...), got ${describeItem(v)}`, docPath);
668
+ }
669
+ return v;
670
+ }
671
+
672
+ // The shared shape of $date-add / $date-sub: [date, duration] applies an
673
+ // ISO 8601 duration, [date, amount, unit] applies one unit. Both return
674
+ // the same lexical form they were given, so a full-date stays a
675
+ // full-date - a query that buckets dates should not silently start
676
+ // producing date-times.
677
+ function dateShiftEntry(sign) {
678
+ return {
679
+ params: ARGS_2_3,
680
+ result: resultEmptyPropagates,
681
+ compile: (gets, args) => {
682
+ const dateGet = gets[0];
683
+ const datePath = args[0].docPath;
684
+ const secondGet = gets[1];
685
+ const secondPath = args[1].docPath;
686
+ const unitGet = gets.length === 3 ? gets[2] : null;
687
+ const unitPath = unitGet === null ? '' : args[2].docPath;
688
+ return (f) => {
689
+ const value = dateGet(f);
690
+ if (value === EMPTY)
691
+ return EMPTY;
692
+ const parts = dateParts(value, datePath);
693
+ const second = secondGet(f);
694
+ if (second === EMPTY)
695
+ return EMPTY;
696
+ if (unitGet === null) {
697
+ const duration = parseDuration(second);
698
+ if (duration === null) {
699
+ throw runtimeError('JQ2001', 'expected an ISO 8601 duration, got '
700
+ + (typeof second === 'string' ? JSON.stringify(second) : describeItem(second)),
701
+ secondPath);
702
+ }
703
+ return formatRFC3339Parts(addDuration(parts, duration, sign));
704
+ }
705
+ if (typeof second !== 'number')
706
+ throw runtimeError('JQ2001', `expected a number of units, got ${describeItem(second)}`, secondPath);
707
+ const unit = unitArg(unitGet(f), unitPath);
708
+ return formatRFC3339Parts(addToParts(parts, sign * second, unit));
709
+ };
710
+ },
711
+ };
712
+ }
713
+
714
+ // $start-of / $end-of: truncate to a calendar unit, keeping the lexical
715
+ // form. A full-date's end of month is that month's last day, not its
716
+ // last millisecond - there is nowhere in a full-date to put one.
717
+ function dateTruncEntry(truncate) {
718
+ return {
719
+ params: ARGS_2,
720
+ result: resultEmptyPropagates,
721
+ compile: (gets, args) => {
722
+ const dateGet = gets[0];
723
+ const datePath = args[0].docPath;
724
+ const unitGet = gets[1];
725
+ const unitPath = args[1].docPath;
726
+ return (f) => {
727
+ const value = dateGet(f);
728
+ if (value === EMPTY)
729
+ return EMPTY;
730
+ const parts = dateParts(value, datePath);
731
+ return formatRFC3339Parts(truncate(parts, unitArg(unitGet(f), unitPath)));
732
+ };
733
+ },
734
+ };
735
+ }
736
+
737
+ //#region spatial operators (section 8.14)
738
+ // PostGIS's ST_* set, in the vocabulary this language already has. The
739
+ // operand is GeoJSON (RFC 7946) — a bare position, a geometry, a Feature
740
+ // or a FeatureCollection — because that is what a JSON document holds;
741
+ // there is no geometry type to construct first.
742
+ //
743
+ // Coordinates are longitude, latitude, in WGS 84 decimal degrees, and
744
+ // measurements are geodesic. That matters: a degree of longitude is not
745
+ // a fixed distance, so a planar answer is wrong by two thirds at Dutch
746
+ // latitudes. These operators never return a planar number.
747
+ //
748
+ // What is deliberately absent is real geometry-to-geometry intersection.
749
+ // `$bbox-intersects` says exactly what it tests, because an operator
750
+ // named `$intersects` that only compared bounding boxes would be a lie
751
+ // the first time two L-shapes shared a box and nothing else.
752
+
753
+ // A spatial operand, rejected uniformly: the empty sequence propagates
754
+ // at the call site, so this only ever sees a real item.
755
+ function geoArg(v, docPath) {
756
+ if (v === EMPTY || v instanceof Seq || v === null || typeof v !== 'object') {
757
+ throw runtimeError('JQ2001',
758
+ `expected a GeoJSON value or a [longitude, latitude] position, got ${describeItem(v)}`,
759
+ docPath);
760
+ }
761
+ return v;
762
+ }
763
+
764
+ // a unary spatial measurement: empty propagates, anything else is JQ2001
765
+ function geoUnaryEntry(measure, resultCard = resultEmptyPropagates) {
766
+ return {
767
+ params: UNARY,
768
+ result: resultCard,
769
+ compile: (gets, args) => {
770
+ const get = gets[0];
771
+ const docPath = args[0].docPath;
772
+ return (f) => {
773
+ const v = get(f);
774
+ if (v === EMPTY)
775
+ return EMPTY;
776
+ const out = measure(geoArg(v, docPath));
777
+ return out === null ? EMPTY : out;
778
+ };
779
+ },
780
+ };
781
+ }
782
+
783
+ //#endregion
784
+
785
+ // the RFC 3339 type tests, shaped like the section 8.10 $is-* family:
786
+ // one item of the right lexical form, never an error
787
+ function dateTestEntry(test) {
788
+ return {
789
+ params: UNARY,
790
+ result: RESULT_ONE,
791
+ compile: (gets) => {
792
+ const get = gets[0];
793
+ return (f) => test(get(f));
794
+ },
795
+ };
796
+ }
797
+
798
+ //#endregion
799
+
551
800
  //#region the registry
552
801
 
553
802
  /**
@@ -632,7 +881,7 @@ export const OPERATORS = Object.freeze({
632
881
  if (b === 0)
633
882
  throw runtimeError('JQ2002', "'$idiv' by zero", docPath);
634
883
  return Math.trunc(a / b);
635
- }),
884
+ }, RT_INTEGER),
636
885
  // XQuery double mod takes the sign of the dividend = JS %
637
886
  '$mod': arithmeticEntry((docPath) => (a, b) => {
638
887
  if (b === 0)
@@ -689,6 +938,7 @@ export const OPERATORS = Object.freeze({
689
938
  '$concat': {
690
939
  params: ARGS_0N,
691
940
  result: RESULT_ONE,
941
+ resultType: RT_STRING,
692
942
  compile: (gets, args) => {
693
943
  if (gets.length === 0)
694
944
  return () => '';
@@ -709,6 +959,7 @@ export const OPERATORS = Object.freeze({
709
959
  '$string-join': {
710
960
  params: ARGS_1_2,
711
961
  result: RESULT_ONE,
962
+ resultType: RT_STRING,
712
963
  compile: (gets, args) => {
713
964
  const seqGet = gets[0];
714
965
  const seqPath = args[0].docPath;
@@ -733,6 +984,7 @@ export const OPERATORS = Object.freeze({
733
984
  '$substring': {
734
985
  params: ARGS_2_3,
735
986
  result: RESULT_ONE,
987
+ resultType: RT_STRING,
736
988
  compile: (gets, args) => {
737
989
  const sGet = gets[0];
738
990
  const sPath = args[0].docPath;
@@ -772,7 +1024,7 @@ export const OPERATORS = Object.freeze({
772
1024
 
773
1025
  '$upper': stringUnaryEntry((s) => s.toUpperCase()),
774
1026
  '$lower': stringUnaryEntry((s) => s.toLowerCase()),
775
- '$string-length': stringUnaryEntry(countCodePoints),
1027
+ '$string-length': stringUnaryEntry(countCodePoints, RT_INTEGER),
776
1028
  '$normalize-space': stringUnaryEntry(normalizeSpace),
777
1029
 
778
1030
  '$match': regexTestEntry(true),
@@ -781,6 +1033,7 @@ export const OPERATORS = Object.freeze({
781
1033
  '$replace': {
782
1034
  params: ARGS_3,
783
1035
  result: RESULT_ONE,
1036
+ resultType: RT_STRING,
784
1037
  compile: compileReplace,
785
1038
  },
786
1039
 
@@ -793,6 +1046,7 @@ export const OPERATORS = Object.freeze({
793
1046
  '$count': {
794
1047
  params: UNARY,
795
1048
  result: RESULT_ONE,
1049
+ resultType: RT_INTEGER,
796
1050
  compile: (gets) => {
797
1051
  const get = gets[0];
798
1052
  return (f) => itemCount(get(f));
@@ -802,6 +1056,7 @@ export const OPERATORS = Object.freeze({
802
1056
  '$sum': {
803
1057
  params: UNARY,
804
1058
  result: RESULT_ONE,
1059
+ resultType: RT_NUMBER,
805
1060
  compile: (gets, args) => {
806
1061
  const get = gets[0];
807
1062
  const docPath = args[0].docPath;
@@ -824,6 +1079,7 @@ export const OPERATORS = Object.freeze({
824
1079
  '$avg': {
825
1080
  params: UNARY,
826
1081
  result: resultEmptyPropagates,
1082
+ resultType: RT_NUMBER,
827
1083
  compile: (gets, args) => {
828
1084
  const get = gets[0];
829
1085
  const docPath = args[0].docPath;
@@ -1008,25 +1264,28 @@ export const OPERATORS = Object.freeze({
1008
1264
  '$range': {
1009
1265
  params: ARGS_2,
1010
1266
  result: RESULT_MANY,
1011
- compile: (gets, args, docPath) => {
1267
+ compile: (gets, args, docPath, node) => {
1012
1268
  const fromGet = gets[0];
1013
1269
  const fromPath = args[0].docPath;
1014
1270
  const toGet = gets[1];
1015
1271
  const toPath = args[1].docPath;
1272
+ // the compilation's limits.sequenceItems tightens the resource
1273
+ // guard below its 2^32 ceiling
1274
+ const cap = node !== undefined && node.limits != null && node.limits.sequenceItems !== null
1275
+ ? Math.min(node.limits.sequenceItems, RANGE_LIMIT)
1276
+ : RANGE_LIMIT;
1016
1277
  return (f) => {
1017
1278
  const a = fromGet(f);
1018
1279
  const b = toGet(f);
1019
1280
  if (a === EMPTY || b === EMPTY) // XQuery `to`: empty operand, empty range
1020
1281
  return EMPTY;
1021
- if (typeof a !== 'number' || !Number.isInteger(a))
1022
- throw rangeBoundError(a, fromPath);
1023
- if (typeof b !== 'number' || !Number.isInteger(b))
1024
- throw rangeBoundError(b, toPath);
1282
+ checkRangeBound(a, fromPath);
1283
+ checkRangeBound(b, toPath);
1025
1284
  if (a > b)
1026
1285
  return EMPTY;
1027
1286
  const n = b - a + 1;
1028
- if (n > RANGE_LIMIT)
1029
- throw runtimeError('JQ2007', `'$range' of ${n} items exceeds the 2^32-item resource guard`, docPath);
1287
+ if (n > cap)
1288
+ throw runtimeError('JQ2007', `'$range' of ${n} items exceeds the ${cap === RANGE_LIMIT ? '2^32-item' : String(cap) + '-item'} resource guard`, docPath);
1030
1289
  const out = new Array(n);
1031
1290
  for (let i = 0; i < n; i++)
1032
1291
  out[i] = a + i;
@@ -1063,6 +1322,59 @@ export const OPERATORS = Object.freeze({
1063
1322
  },
1064
1323
  },
1065
1324
 
1325
+ '$entries': {
1326
+ params: UNARY,
1327
+ result: RESULT_MANY,
1328
+ compile: (gets) => {
1329
+ const get = gets[0];
1330
+ // the member-pair counterpart of `[*]` (which yields values only):
1331
+ // each OBJECT item contributes one `{ "key": name, "value": v }`
1332
+ // per member, in member order; non-object items contribute
1333
+ // nothing, like every other type mismatch in this family
1334
+ return (f) => {
1335
+ const v = get(f);
1336
+ const items = v instanceof Seq ? v.items : (v === EMPTY ? [] : [v]);
1337
+ const out = [];
1338
+ for (let i = 0; i < items.length; i++) {
1339
+ const item = items[i];
1340
+ if (typeof item !== 'object' || item === null || Array.isArray(item))
1341
+ continue;
1342
+ for (const key of Object.keys(item))
1343
+ out.push({ key, value: item[key] });
1344
+ }
1345
+ return seqOf(out);
1346
+ };
1347
+ },
1348
+ },
1349
+
1350
+ '$from-entries': {
1351
+ params: UNARY,
1352
+ result: RESULT_ONE,
1353
+ compile: (gets) => {
1354
+ const get = gets[0];
1355
+ // the inverse of $entries: assemble one object from
1356
+ // `{ "key": name, "value": v }` items, in sequence order. Later
1357
+ // pairs win on duplicate keys, like the $map constructor; items
1358
+ // without a string `key` contribute nothing; a missing `value`
1359
+ // member reads as null (undefined is not a JSON value)
1360
+ return (f) => {
1361
+ const v = get(f);
1362
+ const items = v instanceof Seq ? v.items : (v === EMPTY ? [] : [v]);
1363
+ /** @type {Record<string, any>} */
1364
+ const out = {};
1365
+ for (let i = 0; i < items.length; i++) {
1366
+ const item = items[i];
1367
+ if (typeof item !== 'object' || item === null || Array.isArray(item))
1368
+ continue;
1369
+ if (typeof item.key !== 'string')
1370
+ continue;
1371
+ out[item.key] = hasOwn(item, 'value') ? item.value : null;
1372
+ }
1373
+ return out;
1374
+ };
1375
+ },
1376
+ },
1377
+
1066
1378
  //#endregion
1067
1379
 
1068
1380
  //#region section 8.10 - types and casts
@@ -1191,6 +1503,291 @@ export const OPERATORS = Object.freeze({
1191
1503
  },
1192
1504
 
1193
1505
  //#endregion
1506
+
1507
+ //#region section 8.13 - dates and times
1508
+
1509
+ '$is-date': dateTestEntry(isDateOnlyRFC3339),
1510
+ '$is-time': dateTestEntry(isTimeOnlyRFC3339),
1511
+ '$is-datetime': dateTestEntry(isDateTimeRFC3339),
1512
+ '$is-duration': dateTestEntry(isValidDuration),
1513
+
1514
+ //#endregion
1515
+
1516
+ //#region section 8.14 - spatial
1517
+
1518
+ '$bbox': geoUnaryEntry(bboxOf),
1519
+ '$area': geoUnaryEntry(geometryArea, RESULT_ONE),
1520
+ '$length': geoUnaryEntry(geometryLength, RESULT_ONE),
1521
+ '$centroid': geoUnaryEntry(centroidOf),
1522
+
1523
+ '$distance': { // metres between two values' representative positions
1524
+ params: ARGS_2,
1525
+ result: RESULT_OPT,
1526
+ compile: (gets, args) => {
1527
+ const aGet = gets[0];
1528
+ const aPath = args[0].docPath;
1529
+ const bGet = gets[1];
1530
+ const bPath = args[1].docPath;
1531
+ return (f) => {
1532
+ const a = aGet(f);
1533
+ const b = bGet(f);
1534
+ if (a === EMPTY || b === EMPTY)
1535
+ return EMPTY;
1536
+ const out = geoDistance(geoArg(a, aPath), geoArg(b, bPath));
1537
+ return out === null ? EMPTY : out;
1538
+ };
1539
+ },
1540
+ },
1541
+
1542
+ '$within': { // is the first value's position inside the second's surface
1543
+ params: ARGS_2,
1544
+ result: RESULT_ONE,
1545
+ compile: (gets, args) => {
1546
+ const pointGet = gets[0];
1547
+ const pointPath = args[0].docPath;
1548
+ const areaGet = gets[1];
1549
+ const areaPath = args[1].docPath;
1550
+ return (f) => {
1551
+ const point = pointGet(f);
1552
+ const area = areaGet(f);
1553
+ if (point === EMPTY || area === EMPTY)
1554
+ return false; // nothing is inside nothing
1555
+ const p = geoArg(point, pointPath);
1556
+ // a bare position is itself; anything else is represented by its
1557
+ // centroid, the same rule $distance uses
1558
+ const at = isPosition(p) ? p : centroidOf(p);
1559
+ if (at === null)
1560
+ return false;
1561
+ return containsPosition(geoArg(area, areaPath), at[0], at[1]);
1562
+ };
1563
+ },
1564
+ },
1565
+
1566
+ '$bbox-intersects': { // do the two values' bounding boxes overlap
1567
+ params: ARGS_2,
1568
+ result: RESULT_ONE,
1569
+ compile: (gets, args) => {
1570
+ const aGet = gets[0];
1571
+ const aPath = args[0].docPath;
1572
+ const bGet = gets[1];
1573
+ const bPath = args[1].docPath;
1574
+ return (f) => {
1575
+ const a = aGet(f);
1576
+ const b = bGet(f);
1577
+ if (a === EMPTY || b === EMPTY)
1578
+ return false;
1579
+ const boxA = bboxOf(geoArg(a, aPath));
1580
+ const boxB = bboxOf(geoArg(b, bPath));
1581
+ return boxA !== null && boxB !== null && bboxIntersects(boxA, boxB);
1582
+ };
1583
+ },
1584
+ },
1585
+
1586
+ '$geohash': { // a position as a base-32 cell string
1587
+ params: ARGS_1_2,
1588
+ result: resultEmptyPropagates,
1589
+ compile: (gets, args) => {
1590
+ const get = gets[0];
1591
+ const docPath = args[0].docPath;
1592
+ const precisionGet = gets.length === 2 ? gets[1] : null;
1593
+ const precisionPath = precisionGet === null ? '' : args[1].docPath;
1594
+ return (f) => {
1595
+ const v = get(f);
1596
+ if (v === EMPTY)
1597
+ return EMPTY;
1598
+ const value = geoArg(v, docPath);
1599
+ const at = isPosition(value) ? value : centroidOf(value);
1600
+ if (at === null)
1601
+ return EMPTY;
1602
+ let precision = 9;
1603
+ if (precisionGet !== null) {
1604
+ precision = precisionGet(f);
1605
+ if (!Number.isInteger(precision) || precision < 1 || precision > 12) {
1606
+ throw runtimeError('JQ2001',
1607
+ `a geohash precision must be an integer from 1 to 12, got ${describeItem(precision)}`,
1608
+ precisionPath);
1609
+ }
1610
+ }
1611
+ return geohashEncode(at[0], at[1], precision);
1612
+ };
1613
+ },
1614
+ },
1615
+
1616
+ //#endregion
1617
+
1618
+ //#region section 8.13 - dates and times, continued
1619
+
1620
+ '$date-add': dateShiftEntry(1),
1621
+ '$date-sub': dateShiftEntry(-1),
1622
+ '$start-of': dateTruncEntry(startOfParts),
1623
+ '$end-of': dateTruncEntry(endOfParts),
1624
+
1625
+ '$date-diff': { // whole units from the first date to the second
1626
+ params: ARGS_3,
1627
+ result: resultEmptyPropagates,
1628
+ compile: (gets, args) => {
1629
+ const fromGet = gets[0];
1630
+ const fromPath = args[0].docPath;
1631
+ const toGet = gets[1];
1632
+ const toPath = args[1].docPath;
1633
+ const unitGet = gets[2];
1634
+ const unitPath = args[2].docPath;
1635
+ return (f) => {
1636
+ const fromValue = fromGet(f);
1637
+ const toValue = toGet(f);
1638
+ if (fromValue === EMPTY || toValue === EMPTY)
1639
+ return EMPTY;
1640
+ const from = dateParts(fromValue, fromPath);
1641
+ const to = dateParts(toValue, toPath);
1642
+ const unit = unitArg(unitGet(f), unitPath);
1643
+ // months, quarters and years have no fixed width, so they are
1644
+ // counted on the calendar; everything else divides an exact span
1645
+ if (unit === 'month' || unit === 'quarter' || unit === 'year') {
1646
+ const months = monthsBetween(from, to);
1647
+ return unit === 'month' ? months
1648
+ : Math.trunc(months / (unit === 'quarter' ? 3 : 12));
1649
+ }
1650
+ const fromMs = epochOfRFC3339Parts(from);
1651
+ const toMs = epochOfRFC3339Parts(to);
1652
+ if (fromMs !== fromMs || toMs !== toMs)
1653
+ throw runtimeError('JQ2001', 'cannot measure a span from a value with no date', fromPath);
1654
+ return Math.trunc((toMs - fromMs) / fixedUnitMs(unit));
1655
+ };
1656
+ },
1657
+ },
1658
+
1659
+ '$date-format': { // an LDML pattern, compiled once when it is literal
1660
+ params: ARGS_2,
1661
+ result: resultEmptyPropagates,
1662
+ compile: (gets, args, docPath) => {
1663
+ const dateGet = gets[0];
1664
+ const datePath = args[0].docPath;
1665
+ const patternNode = args[1];
1666
+ // the common case is a literal pattern: compile it at query
1667
+ // compile time, so a bad one is a compile error, not a surprise
1668
+ if (patternNode.kind === 'literal' && typeof patternNode.value === 'string') {
1669
+ let format;
1670
+ try {
1671
+ format = compileDateFormat(patternNode.value);
1672
+ }
1673
+ catch (e) {
1674
+ // a literal pattern is authored, not data: reject the document
1675
+ throw new JsonQueryCompileError('JQ0003',
1676
+ `'$date-format' pattern: ${e instanceof Error ? e.message : 'invalid'}`,
1677
+ docPath, { cause: e });
1678
+ }
1679
+ return (f) => {
1680
+ const v = dateGet(f);
1681
+ return v === EMPTY ? EMPTY : format(dateParts(v, datePath));
1682
+ };
1683
+ }
1684
+ // a dynamic pattern gets the monomorphic per-callsite cache the
1685
+ // regex operators use: a filter almost always sees one pattern
1686
+ const patternGet = gets[1];
1687
+ const patternPath = patternNode.docPath;
1688
+ let lastPattern = null;
1689
+ let lastFormat = null;
1690
+ return (f) => {
1691
+ const v = dateGet(f);
1692
+ if (v === EMPTY)
1693
+ return EMPTY;
1694
+ const pattern = patternGet(f);
1695
+ if (typeof pattern !== 'string')
1696
+ throw runtimeError('JQ2001', `expected a date pattern, got ${describeItem(pattern)}`, patternPath);
1697
+ if (pattern !== lastPattern) {
1698
+ lastPattern = pattern;
1699
+ try {
1700
+ lastFormat = compileDateFormat(pattern);
1701
+ }
1702
+ catch (e) {
1703
+ lastFormat = null;
1704
+ throw runtimeError('JQ2001',
1705
+ `'$date-format' pattern: ${e instanceof Error ? e.message : 'invalid'}`, patternPath);
1706
+ }
1707
+ }
1708
+ return lastFormat(dateParts(v, datePath));
1709
+ };
1710
+ },
1711
+ },
1712
+
1713
+ '$week': dateComponentEntry(
1714
+ (p) => (p.year < 0 ? -1 : isoWeekOfYear(p).week), 'date'),
1715
+ '$week-year': dateComponentEntry(
1716
+ (p) => (p.year < 0 ? -1 : isoWeekOfYear(p).year), 'date'),
1717
+ '$quarter': dateComponentEntry(
1718
+ (p) => (p.year < 0 ? -1 : quarterOfYear(p)), 'date'),
1719
+ '$weekday': dateComponentEntry(
1720
+ (p) => (p.year < 0 ? -1 : isoWeekdayFromDays(daysFromCivil(p.year, p.month, p.day))), 'date'),
1721
+
1722
+ '$year': dateComponentEntry((p) => p.year, 'date'),
1723
+ '$month': dateComponentEntry((p) => p.month, 'date'),
1724
+ '$day': dateComponentEntry((p) => p.day, 'date'),
1725
+ '$hours': dateComponentEntry((p) => p.hours, 'time'),
1726
+ '$minutes': dateComponentEntry((p) => p.minutes, 'time'),
1727
+ '$seconds': dateComponentEntry((p) => p.seconds, 'time'),
1728
+
1729
+ '$offset': { // minutes east of UTC; a bare full-date carries none
1730
+ params: UNARY,
1731
+ result: RESULT_OPT,
1732
+ compile: (gets, args) => {
1733
+ const get = gets[0];
1734
+ const docPath = args[0].docPath;
1735
+ return (f) => {
1736
+ const v = get(f);
1737
+ if (v === EMPTY)
1738
+ return EMPTY;
1739
+ const offset = dateParts(v, docPath).offset;
1740
+ return offset === null ? EMPTY : offset;
1741
+ };
1742
+ },
1743
+ },
1744
+
1745
+ '$epoch': { // the one shift to UTC: milliseconds since 1970-01-01Z
1746
+ params: UNARY,
1747
+ result: resultEmptyPropagates,
1748
+ compile: (gets, args) => {
1749
+ const get = gets[0];
1750
+ const docPath = args[0].docPath;
1751
+ return (f) => {
1752
+ const v = get(f);
1753
+ if (v === EMPTY)
1754
+ return EMPTY;
1755
+ const ms = epochOfRFC3339Parts(dateParts(v, docPath));
1756
+ if (ms !== ms) // a full-time has no instant to place
1757
+ throw runtimeError('JQ2001', `'${v}' carries no date component`, docPath);
1758
+ return ms;
1759
+ };
1760
+ },
1761
+ },
1762
+
1763
+ '$datetime': { // the inverse of $epoch, in canonical UTC form
1764
+ params: UNARY,
1765
+ result: resultEmptyPropagates,
1766
+ compile: (gets, args) => {
1767
+ const get = gets[0];
1768
+ const docPath = args[0].docPath;
1769
+ return (f) => {
1770
+ const v = get(f);
1771
+ if (v === EMPTY)
1772
+ return EMPTY;
1773
+ if (typeof v !== 'number')
1774
+ throw runtimeError('JQ2001',
1775
+ `'$datetime' takes epoch milliseconds, got ${describeItem(v)}`, docPath);
1776
+ // outside ±8.64e15 ms, and outside years 0000-9999, there is no
1777
+ // RFC 3339 spelling of the instant. Rendering goes through the
1778
+ // kernel like every other date operator, so one query cannot
1779
+ // emit two spellings of the same fraction.
1780
+ const iso = Number.isFinite(v) && Math.abs(v) <= 8.64e15
1781
+ ? formatRFC3339Parts(partsFromEpoch(v))
1782
+ : '';
1783
+ if (!isDateTimeRFC3339(iso))
1784
+ throw runtimeError('JQ2001', `${v} is outside the range RFC 3339 can spell`, docPath);
1785
+ return iso;
1786
+ };
1787
+ },
1788
+ },
1789
+
1790
+ //#endregion
1194
1791
  });
1195
1792
 
1196
1793
  // singleton type predicates (section 8.10): true iff the operand is one