@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.
- 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/operators.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
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 >
|
|
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
|