@jarenjs/json 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
|
@@ -0,0 +1,1253 @@
|
|
|
1
|
+
//#region Jaren JSON Query operator registry
|
|
2
|
+
// The single table of the section-8 operator vocabulary (QUERY-FORMAT.md):
|
|
3
|
+
// every operator declares its signature once - `name -> { params, result,
|
|
4
|
+
// compile }` - and gets uniform compile-time checking from it. The
|
|
5
|
+
// query-language analogue of the `FUNCTIONS` table in path.js.
|
|
6
|
+
//
|
|
7
|
+
// - `params` describes the argument shape. The string 'expr' is the
|
|
8
|
+
// single form (`{"$op": operand}`: the value IS the one argument);
|
|
9
|
+
// `{ kinds, min, variadic? }` is the array form (`{"$op": [a, b]}`),
|
|
10
|
+
// where `kinds[i]` is the expectation of position i - 'expr' (an
|
|
11
|
+
// ordinary expression), 'raw' (a verbatim JSON value, not evaluated),
|
|
12
|
+
// 'schema' (a verbatim JSON Schema literal, compiled at query compile
|
|
13
|
+
// time into an item predicate at `args[i].test` - the schema
|
|
14
|
+
// arguments of $valid/$assert) or
|
|
15
|
+
// 'name' (a variable name string). `min` is the minimum argument
|
|
16
|
+
// count; the maximum is `kinds.length`, or unbounded when `variadic`
|
|
17
|
+
// (the last kind repeats). normalize.js validates arity and shape
|
|
18
|
+
// from this descriptor (JQ0003), so compile functions never re-check
|
|
19
|
+
// structure.
|
|
20
|
+
// - `result(cards)` is the operator's static cardinality contribution:
|
|
21
|
+
// given the argument cardinalities it returns the result cardinality
|
|
22
|
+
// (an upper approximation; CARD_ONE means "always exactly one item").
|
|
23
|
+
// - `compile(gets, args, docPath)` returns the getter closure.
|
|
24
|
+
// `gets[i]` is the compiled getter of argument i (`null` for
|
|
25
|
+
// 'raw'/'name' positions - their value is compile-time data at
|
|
26
|
+
// `args[i].value`); `args[i]` is the frozen argument AST node
|
|
27
|
+
// carrying `card` and `docPath`; `docPath` locates the operator key
|
|
28
|
+
// itself (e.g. `/$idiv`).
|
|
29
|
+
//
|
|
30
|
+
// All runtime error conditions of the operator library (JQ2xxx,
|
|
31
|
+
// QUERY-FORMAT.md section 10.3) are raised here.
|
|
32
|
+
|
|
33
|
+
import { equalsJson } from '@jarenjs/core/object';
|
|
34
|
+
import { countCodePoints, compareCodePoints } from '@jarenjs/core/string';
|
|
35
|
+
import { compileIRegexp } from '@jarenjs/core/text/iregexp';
|
|
36
|
+
import { JsonQueryRuntimeError } from './errors.js';
|
|
37
|
+
import {
|
|
38
|
+
EMPTY, Seq, seqOf, appendItem, ebv, itemCount, firstItem,
|
|
39
|
+
stableKeyString, describeItem,
|
|
40
|
+
} from './runtime.js';
|
|
41
|
+
import {
|
|
42
|
+
CARD_ZERO, CARD_ONE, CARD_OPT, CARD_MANY, joinCard, sumCard,
|
|
43
|
+
} from './normalize.js';
|
|
44
|
+
import { compileExistsTest } from './compile.js';
|
|
45
|
+
|
|
46
|
+
const hasOwn = Object.hasOwn;
|
|
47
|
+
|
|
48
|
+
//#region parameter and result descriptors
|
|
49
|
+
|
|
50
|
+
// the single form: the operator's value is its one operand expression
|
|
51
|
+
const UNARY = 'expr';
|
|
52
|
+
// array forms; frozen and shared across entries
|
|
53
|
+
const ARGS_2 = Object.freeze({ kinds: Object.freeze(['expr', 'expr']), min: 2 });
|
|
54
|
+
const ARGS_3 = Object.freeze({ kinds: Object.freeze(['expr', 'expr', 'expr']), min: 3 });
|
|
55
|
+
const ARGS_1_2 = Object.freeze({ kinds: Object.freeze(['expr', 'expr']), min: 1 });
|
|
56
|
+
const ARGS_2_3 = Object.freeze({ kinds: Object.freeze(['expr', 'expr', 'expr']), min: 2 });
|
|
57
|
+
const ARGS_0N = Object.freeze({ kinds: Object.freeze(['expr']), min: 0, variadic: true });
|
|
58
|
+
const ARGS_1N = Object.freeze({ kinds: Object.freeze(['expr']), min: 1, variadic: true });
|
|
59
|
+
// the schema operators $valid/$assert: [expr, schema] (section 8.11)
|
|
60
|
+
const ARGS_EXPR_SCHEMA = Object.freeze({ kinds: Object.freeze(['expr', 'schema']), min: 2 });
|
|
61
|
+
|
|
62
|
+
const RESULT_ONE = () => CARD_ONE;
|
|
63
|
+
const RESULT_OPT = () => CARD_OPT;
|
|
64
|
+
const RESULT_MANY = () => CARD_MANY;
|
|
65
|
+
// the operand's cardinality is preserved ($distinct, $reverse, $sort)
|
|
66
|
+
const resultOfOperand = (cards) => cards[0];
|
|
67
|
+
// a singleton operand stays a singleton; anything else may come out empty
|
|
68
|
+
const resultEmptyPropagates = (cards) => (cards[0] === CARD_ONE ? CARD_ONE : CARD_OPT);
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
|
|
72
|
+
//#region runtime argument helpers
|
|
73
|
+
|
|
74
|
+
function runtimeError(code, message, docPath) {
|
|
75
|
+
return new JsonQueryRuntimeError(code, message, docPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// string parameter rule (section 8.7): the empty sequence reads as '',
|
|
79
|
+
// a single string is itself, anything else is JQ2001
|
|
80
|
+
function stringArg(v, docPath) {
|
|
81
|
+
if (typeof v === 'string')
|
|
82
|
+
return v;
|
|
83
|
+
if (v === EMPTY)
|
|
84
|
+
return '';
|
|
85
|
+
throw runtimeError('JQ2001', `expected a string, got ${describeItem(v)}`, docPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// numeric position/length parameter: a single number, everything else -
|
|
89
|
+
// including the empty sequence - is JQ2001
|
|
90
|
+
function numberArg(v, docPath) {
|
|
91
|
+
if (typeof v !== 'number')
|
|
92
|
+
throw runtimeError('JQ2001', `expected a number, got ${describeItem(v)}`, docPath);
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// the $string cast table (section 8.10); callers handle the empty sequence
|
|
97
|
+
function castString(v, docPath) {
|
|
98
|
+
switch (typeof v) {
|
|
99
|
+
case 'string':
|
|
100
|
+
return v;
|
|
101
|
+
case 'number':
|
|
102
|
+
return String(v);
|
|
103
|
+
case 'boolean':
|
|
104
|
+
return v ? 'true' : 'false';
|
|
105
|
+
default:
|
|
106
|
+
if (v === null)
|
|
107
|
+
return 'null';
|
|
108
|
+
throw runtimeError('JQ2001', `cannot cast ${describeItem(v)} to a string`, docPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// the $number cast table (section 8.10): strings must be JSON numbers
|
|
113
|
+
// (strict RFC 8259 grammar - no leading '+', no bare '.', no whitespace)
|
|
114
|
+
const JSON_NUMBER_RE = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
115
|
+
|
|
116
|
+
function castNumber(v, docPath) {
|
|
117
|
+
switch (typeof v) {
|
|
118
|
+
case 'number':
|
|
119
|
+
return v;
|
|
120
|
+
case 'boolean':
|
|
121
|
+
return v ? 1 : 0;
|
|
122
|
+
case 'string':
|
|
123
|
+
if (JSON_NUMBER_RE.test(v))
|
|
124
|
+
return Number(v);
|
|
125
|
+
throw runtimeError('JQ2001', `'${v}' is not a JSON number`, docPath);
|
|
126
|
+
default:
|
|
127
|
+
throw runtimeError('JQ2001', `cannot cast ${describeItem(v)} to a number`, docPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
|
|
133
|
+
//#region shared compile helpers
|
|
134
|
+
|
|
135
|
+
// per-element appliers of $seq: CARD_ONE elements push directly,
|
|
136
|
+
// everything else flattens through appendItem (same discipline as the
|
|
137
|
+
// array constructor in compile.js)
|
|
138
|
+
function compileSeqOp(gets, args) {
|
|
139
|
+
if (gets.length === 0)
|
|
140
|
+
return () => EMPTY;
|
|
141
|
+
if (gets.length === 1) // {"$seq": [e]} is e
|
|
142
|
+
return gets[0];
|
|
143
|
+
const alen = gets.length;
|
|
144
|
+
const appliers = new Array(alen);
|
|
145
|
+
for (let i = 0; i < alen; i++) {
|
|
146
|
+
const get = gets[i];
|
|
147
|
+
appliers[i] = args[i].card === CARD_ONE
|
|
148
|
+
? (f, acc) => acc.push(get(f))
|
|
149
|
+
: (f, acc) => appendItem(acc, get(f));
|
|
150
|
+
}
|
|
151
|
+
return (f) => {
|
|
152
|
+
const acc = [];
|
|
153
|
+
for (let i = 0; i < alen; i++)
|
|
154
|
+
appliers[i](f, acc);
|
|
155
|
+
return seqOf(acc);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// item comparison rules (section 8.4): $eq/$ne deep structural JSON
|
|
160
|
+
// equality (D2); ordering only between two numbers or two strings, any
|
|
161
|
+
// other pair is simply false (no witness, no error)
|
|
162
|
+
function itemEq(a, b) {
|
|
163
|
+
return equalsJson(a, b);
|
|
164
|
+
}
|
|
165
|
+
function itemNe(a, b) {
|
|
166
|
+
return !equalsJson(a, b);
|
|
167
|
+
}
|
|
168
|
+
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;
|
|
174
|
+
}
|
|
175
|
+
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;
|
|
181
|
+
}
|
|
182
|
+
function itemGt(a, b) {
|
|
183
|
+
return itemLt(b, a);
|
|
184
|
+
}
|
|
185
|
+
function itemGe(a, b) {
|
|
186
|
+
return itemLe(b, a);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// existential general comparison (section 8.4): true iff some pair of
|
|
190
|
+
// items compares true; either side empty means no witnessing pair
|
|
191
|
+
// (contrast the path filter dialect where Nothing == Nothing holds -
|
|
192
|
+
// section 5.2)
|
|
193
|
+
function comparisonEntry(itemCmp) {
|
|
194
|
+
return {
|
|
195
|
+
params: ARGS_2,
|
|
196
|
+
result: RESULT_ONE,
|
|
197
|
+
compile: (gets, args) => {
|
|
198
|
+
const left = gets[0];
|
|
199
|
+
const right = gets[1];
|
|
200
|
+
if (args[0].card === CARD_ONE && args[1].card === CARD_ONE)
|
|
201
|
+
// the common case: two singletons, one direct item comparison
|
|
202
|
+
return (f) => itemCmp(left(f), right(f));
|
|
203
|
+
return (f) => {
|
|
204
|
+
const lv = left(f);
|
|
205
|
+
if (lv === EMPTY)
|
|
206
|
+
return false;
|
|
207
|
+
const rv = right(f);
|
|
208
|
+
if (rv === EMPTY)
|
|
209
|
+
return false;
|
|
210
|
+
if (lv instanceof Seq) {
|
|
211
|
+
const li = lv.items;
|
|
212
|
+
if (rv instanceof Seq) {
|
|
213
|
+
const ri = rv.items;
|
|
214
|
+
for (let i = 0; i < li.length; i++) {
|
|
215
|
+
for (let j = 0; j < ri.length; j++) {
|
|
216
|
+
if (itemCmp(li[i], ri[j]))
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i < li.length; i++) {
|
|
223
|
+
if (itemCmp(li[i], rv))
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (rv instanceof Seq) {
|
|
229
|
+
const ri = rv.items;
|
|
230
|
+
for (let j = 0; j < ri.length; j++) {
|
|
231
|
+
if (itemCmp(lv, ri[j]))
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return itemCmp(lv, rv);
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function arithOperandError(v, docPath) {
|
|
243
|
+
return runtimeError('JQ2001', `arithmetic requires a number operand, got ${describeItem(v)}`, docPath);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// IEEE double arithmetic (section 8.5, D1): empty operands propagate,
|
|
247
|
+
// non-number singletons and longer sequences are JQ2001. `makeApply`
|
|
248
|
+
// builds the two-number kernel (it closes over the operator docPath for
|
|
249
|
+
// the JQ2002 zero-divisor errors of $idiv/$mod).
|
|
250
|
+
function arithmeticEntry(makeApply) {
|
|
251
|
+
return {
|
|
252
|
+
params: ARGS_2,
|
|
253
|
+
result: (cards) => (cards[0] === CARD_ONE && cards[1] === CARD_ONE ? CARD_ONE : CARD_OPT),
|
|
254
|
+
compile: (gets, args, docPath) => {
|
|
255
|
+
const apply = makeApply(docPath);
|
|
256
|
+
const left = gets[0];
|
|
257
|
+
const right = gets[1];
|
|
258
|
+
const leftPath = args[0].docPath;
|
|
259
|
+
const rightPath = args[1].docPath;
|
|
260
|
+
if (args[0].card === CARD_ONE && args[1].card === CARD_ONE) {
|
|
261
|
+
// singleton operands: type guard only, no sequence checks
|
|
262
|
+
return (f) => {
|
|
263
|
+
const a = left(f);
|
|
264
|
+
if (typeof a !== 'number')
|
|
265
|
+
throw arithOperandError(a, leftPath);
|
|
266
|
+
const b = right(f);
|
|
267
|
+
if (typeof b !== 'number')
|
|
268
|
+
throw arithOperandError(b, rightPath);
|
|
269
|
+
return apply(a, b);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return (f) => {
|
|
273
|
+
const a = left(f);
|
|
274
|
+
if (a === EMPTY) // empty propagation (XQuery)
|
|
275
|
+
return EMPTY;
|
|
276
|
+
if (typeof a !== 'number')
|
|
277
|
+
throw arithOperandError(a, leftPath);
|
|
278
|
+
const b = right(f);
|
|
279
|
+
if (b === EMPTY)
|
|
280
|
+
return EMPTY;
|
|
281
|
+
if (typeof b !== 'number')
|
|
282
|
+
throw arithOperandError(b, rightPath);
|
|
283
|
+
return apply(a, b);
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// variadic EBV logic (section 8.6): left-to-right with short-circuit -
|
|
290
|
+
// operands after the deciding one are not evaluated
|
|
291
|
+
function logicEntry(stopOn) {
|
|
292
|
+
return {
|
|
293
|
+
params: ARGS_1N,
|
|
294
|
+
result: RESULT_ONE,
|
|
295
|
+
compile: (gets, args) => {
|
|
296
|
+
const paths = args.map((a) => a.docPath);
|
|
297
|
+
const flen = gets.length;
|
|
298
|
+
return (f) => {
|
|
299
|
+
for (let i = 0; i < flen; i++) {
|
|
300
|
+
if (ebv(gets[i](f), paths[i]) === stopOn)
|
|
301
|
+
return stopOn;
|
|
302
|
+
}
|
|
303
|
+
return !stopOn;
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
|
|
311
|
+
//#region string operators
|
|
312
|
+
|
|
313
|
+
// unary string operator: coerce per stringArg, apply the kernel
|
|
314
|
+
function stringUnaryEntry(apply) {
|
|
315
|
+
return {
|
|
316
|
+
params: UNARY,
|
|
317
|
+
result: RESULT_ONE,
|
|
318
|
+
compile: (gets, args) => {
|
|
319
|
+
const get = gets[0];
|
|
320
|
+
const docPath = args[0].docPath;
|
|
321
|
+
return (f) => apply(stringArg(get(f), docPath));
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// binary string operator over two coerced string arguments
|
|
327
|
+
function stringPairEntry(test) {
|
|
328
|
+
return {
|
|
329
|
+
params: ARGS_2,
|
|
330
|
+
result: RESULT_ONE,
|
|
331
|
+
compile: (gets, args) => {
|
|
332
|
+
const aGet = gets[0];
|
|
333
|
+
const aPath = args[0].docPath;
|
|
334
|
+
const bGet = gets[1];
|
|
335
|
+
const bPath = args[1].docPath;
|
|
336
|
+
return (f) => test(stringArg(aGet(f), aPath), stringArg(bGet(f), bPath));
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// fn:normalize-space: strip leading/trailing XML whitespace (space, tab,
|
|
342
|
+
// LF, CR) and collapse internal runs to a single space
|
|
343
|
+
function normalizeSpace(s) {
|
|
344
|
+
let out = '';
|
|
345
|
+
let pending = false;
|
|
346
|
+
for (let i = 0; i < s.length; i++) {
|
|
347
|
+
const c = s.charCodeAt(i);
|
|
348
|
+
if (c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D) {
|
|
349
|
+
pending = out !== '';
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (pending) {
|
|
353
|
+
out += ' ';
|
|
354
|
+
pending = false;
|
|
355
|
+
}
|
|
356
|
+
out += s[i];
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// $match (anchored) / $search (free): I-Regexp (RFC 9485, D5). Literal
|
|
362
|
+
// patterns compile at query-compile time; dynamic patterns get the
|
|
363
|
+
// per-callsite monomorphic cache (the compileRegexTest approach in
|
|
364
|
+
// segments.js). A syntactically invalid I-Regexp yields false, the
|
|
365
|
+
// RFC 9535 match()/search() behavior.
|
|
366
|
+
function regexTestEntry(fullMatch) {
|
|
367
|
+
return {
|
|
368
|
+
params: ARGS_2,
|
|
369
|
+
result: RESULT_ONE,
|
|
370
|
+
compile: (gets, args) => {
|
|
371
|
+
const inGet = gets[0];
|
|
372
|
+
const inPath = args[0].docPath;
|
|
373
|
+
const patNode = args[1];
|
|
374
|
+
if (patNode.kind === 'literal' && typeof patNode.value === 'string') {
|
|
375
|
+
const re = compileIRegexp(patNode.value, fullMatch);
|
|
376
|
+
if (re === null)
|
|
377
|
+
return (f) => {
|
|
378
|
+
stringArg(inGet(f), inPath); // the input type rule still applies
|
|
379
|
+
return false;
|
|
380
|
+
};
|
|
381
|
+
return (f) => re.test(stringArg(inGet(f), inPath));
|
|
382
|
+
}
|
|
383
|
+
const patGet = gets[1];
|
|
384
|
+
const patPath = patNode.docPath;
|
|
385
|
+
let lastPattern = null;
|
|
386
|
+
let lastRegExp = null;
|
|
387
|
+
return (f) => {
|
|
388
|
+
const s = stringArg(inGet(f), inPath);
|
|
389
|
+
const p = stringArg(patGet(f), patPath);
|
|
390
|
+
if (p !== lastPattern) {
|
|
391
|
+
lastPattern = p;
|
|
392
|
+
lastRegExp = compileIRegexp(p, fullMatch);
|
|
393
|
+
}
|
|
394
|
+
return lastRegExp !== null && lastRegExp.test(s);
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// $replace pattern compilation: null marks an invalid I-Regexp, false a
|
|
401
|
+
// pattern that matches the zero-length string (an error per F&O
|
|
402
|
+
// fn:replace, err:FORX0002/FORX0003 -> JQ2001), otherwise the global
|
|
403
|
+
// RegExp used for replace-all
|
|
404
|
+
function replaceRegexp(pattern) {
|
|
405
|
+
const re = compileIRegexp(pattern, false);
|
|
406
|
+
if (re === null)
|
|
407
|
+
return null;
|
|
408
|
+
if (re.test(''))
|
|
409
|
+
return false;
|
|
410
|
+
return new RegExp(re.source, 'gu');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function replaceRegexpError(gre, pattern, docPath) {
|
|
414
|
+
return runtimeError('JQ2001', gre === null
|
|
415
|
+
? `'${pattern}' is not a valid I-Regexp pattern`
|
|
416
|
+
: `'$replace' pattern '${pattern}' matches the zero-length string`, docPath);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function compileReplace(gets, args) {
|
|
420
|
+
const sGet = gets[0];
|
|
421
|
+
const sPath = args[0].docPath;
|
|
422
|
+
const patNode = args[1];
|
|
423
|
+
const patPath = patNode.docPath;
|
|
424
|
+
const repGet = gets[2];
|
|
425
|
+
const repPath = args[2].docPath;
|
|
426
|
+
if (patNode.kind === 'literal' && typeof patNode.value === 'string') {
|
|
427
|
+
const gre = replaceRegexp(patNode.value);
|
|
428
|
+
return (f) => {
|
|
429
|
+
const s = stringArg(sGet(f), sPath);
|
|
430
|
+
const rep = stringArg(repGet(f), repPath);
|
|
431
|
+
if (gre === null || gre === false)
|
|
432
|
+
throw replaceRegexpError(gre, patNode.value, patPath);
|
|
433
|
+
// function replacement: `rep` is inserted literally, no `$`-group
|
|
434
|
+
// references (I-Regexp guarantees no capture semantics - D5)
|
|
435
|
+
return s.replace(gre, () => rep);
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const patGet = gets[1];
|
|
439
|
+
let lastPattern = null;
|
|
440
|
+
let lastGre = null;
|
|
441
|
+
return (f) => {
|
|
442
|
+
const s = stringArg(sGet(f), sPath);
|
|
443
|
+
const p = stringArg(patGet(f), patPath);
|
|
444
|
+
const rep = stringArg(repGet(f), repPath);
|
|
445
|
+
if (p !== lastPattern) {
|
|
446
|
+
lastPattern = p;
|
|
447
|
+
lastGre = replaceRegexp(p);
|
|
448
|
+
}
|
|
449
|
+
if (lastGre === null || lastGre === false)
|
|
450
|
+
throw replaceRegexpError(lastGre, p, patPath);
|
|
451
|
+
return s.replace(lastGre, () => rep);
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
|
|
457
|
+
//#region aggregate operators
|
|
458
|
+
|
|
459
|
+
function aggregateNumber(v, docPath) {
|
|
460
|
+
if (typeof v !== 'number')
|
|
461
|
+
throw runtimeError('JQ2001', `aggregate items must be numbers, got ${describeItem(v)}`, docPath);
|
|
462
|
+
return v;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function minmaxError(v, docPath) {
|
|
466
|
+
return runtimeError('JQ2001', `'$min'/'$max' items must be all numbers or all strings, got ${describeItem(v)}`, docPath);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// $min/$max (section 8.8): empty -> empty; items all numbers or all
|
|
470
|
+
// strings (Unicode scalar value order), mixed or anything else JQ2001;
|
|
471
|
+
// any NaN item makes a numeric result NaN (F&O fn:min/fn:max)
|
|
472
|
+
function minmaxEntry(isMax) {
|
|
473
|
+
return {
|
|
474
|
+
params: UNARY,
|
|
475
|
+
result: resultEmptyPropagates,
|
|
476
|
+
compile: (gets, args) => {
|
|
477
|
+
const get = gets[0];
|
|
478
|
+
const docPath = args[0].docPath;
|
|
479
|
+
return (f) => {
|
|
480
|
+
const v = get(f);
|
|
481
|
+
if (v === EMPTY)
|
|
482
|
+
return EMPTY;
|
|
483
|
+
if (!(v instanceof Seq)) {
|
|
484
|
+
const t = typeof v;
|
|
485
|
+
if (t !== 'number' && t !== 'string')
|
|
486
|
+
throw minmaxError(v, docPath);
|
|
487
|
+
return v;
|
|
488
|
+
}
|
|
489
|
+
const items = v.items;
|
|
490
|
+
let best = items[0];
|
|
491
|
+
const numeric = typeof best === 'number';
|
|
492
|
+
if (!numeric && typeof best !== 'string')
|
|
493
|
+
throw minmaxError(best, docPath);
|
|
494
|
+
let sawNaN = numeric && best !== best;
|
|
495
|
+
for (let i = 1; i < items.length; i++) {
|
|
496
|
+
const item = items[i];
|
|
497
|
+
if (typeof item === 'number') {
|
|
498
|
+
if (!numeric)
|
|
499
|
+
throw minmaxError(item, docPath);
|
|
500
|
+
if (item !== item)
|
|
501
|
+
sawNaN = true;
|
|
502
|
+
else if (isMax ? item > best : item < best)
|
|
503
|
+
best = item;
|
|
504
|
+
}
|
|
505
|
+
else if (typeof item === 'string') {
|
|
506
|
+
if (numeric)
|
|
507
|
+
throw minmaxError(item, docPath);
|
|
508
|
+
const c = compareCodePoints(item, best);
|
|
509
|
+
if (isMax ? c > 0 : c < 0)
|
|
510
|
+
best = item;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
throw minmaxError(item, docPath);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return sawNaN ? NaN : best;
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
|
|
524
|
+
//#region sequence operators
|
|
525
|
+
|
|
526
|
+
function sortError(v, docPath) {
|
|
527
|
+
return runtimeError('JQ2005', `'$sort' items must be all numbers or all strings, got ${describeItem(v)}`, docPath);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// number order for $sort: NaN equal to itself and less than every other
|
|
531
|
+
// number (the section 6.6 order-by rule)
|
|
532
|
+
function compareNumberKeys(a, b) {
|
|
533
|
+
if (a < b)
|
|
534
|
+
return -1;
|
|
535
|
+
if (a > b)
|
|
536
|
+
return 1;
|
|
537
|
+
if (a !== a)
|
|
538
|
+
return b !== b ? 0 : -1;
|
|
539
|
+
return b !== b ? 1 : 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function rangeBoundError(v, docPath) {
|
|
543
|
+
return runtimeError('JQ2001', `'$range' bounds must be integral numbers, got ${describeItem(v)}`, docPath);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// the resource guard of $range (section 10.3, JQ2007)
|
|
547
|
+
const RANGE_LIMIT = 4294967296; // 2^32
|
|
548
|
+
|
|
549
|
+
//#endregion
|
|
550
|
+
|
|
551
|
+
//#region the registry
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The complete v0.1 operator vocabulary (QUERY-FORMAT.md section 8), one
|
|
555
|
+
* entry per operator: `{ params, result, compile }` (see the module
|
|
556
|
+
* comment for the descriptor contract). Frozen; the closed vocabulary is
|
|
557
|
+
* exactly the keys of this table plus the phrase keys and escape hatches
|
|
558
|
+
* handled by normalize.js.
|
|
559
|
+
*/
|
|
560
|
+
export const OPERATORS = Object.freeze({
|
|
561
|
+
|
|
562
|
+
//#region section 8.2 - sequences
|
|
563
|
+
|
|
564
|
+
'$seq': {
|
|
565
|
+
params: ARGS_0N,
|
|
566
|
+
result: (cards) => {
|
|
567
|
+
let card = CARD_ZERO;
|
|
568
|
+
for (let i = 0; i < cards.length; i++)
|
|
569
|
+
card = sumCard(card, cards[i]);
|
|
570
|
+
return card;
|
|
571
|
+
},
|
|
572
|
+
compile: compileSeqOp,
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
'$exists': {
|
|
576
|
+
params: UNARY,
|
|
577
|
+
result: RESULT_ONE,
|
|
578
|
+
// existence never materializes a result sequence (the path.js
|
|
579
|
+
// compileExists analogue lives in compile.js, keyed on the node)
|
|
580
|
+
compile: (gets, args) => compileExistsTest(args[0]),
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
'$empty': {
|
|
584
|
+
params: UNARY,
|
|
585
|
+
result: RESULT_ONE,
|
|
586
|
+
compile: (gets, args) => {
|
|
587
|
+
const test = compileExistsTest(args[0]);
|
|
588
|
+
return (f) => !test(f);
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
//#endregion
|
|
593
|
+
|
|
594
|
+
//#region section 8.3 - conditional
|
|
595
|
+
|
|
596
|
+
'$if': {
|
|
597
|
+
params: ARGS_2_3,
|
|
598
|
+
result: (cards) => joinCard(cards[1], cards.length === 3 ? cards[2] : CARD_ZERO),
|
|
599
|
+
compile: (gets, args) => {
|
|
600
|
+
const cond = gets[0];
|
|
601
|
+
const condPath = args[0].docPath;
|
|
602
|
+
const then = gets[1];
|
|
603
|
+
if (gets.length === 2) // missing else means the empty sequence
|
|
604
|
+
return (f) => (ebv(cond(f), condPath) ? then(f) : EMPTY);
|
|
605
|
+
const alt = gets[2];
|
|
606
|
+
return (f) => (ebv(cond(f), condPath) ? then(f) : alt(f));
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
//#endregion
|
|
611
|
+
|
|
612
|
+
//#region section 8.4 - comparisons
|
|
613
|
+
|
|
614
|
+
'$eq': comparisonEntry(itemEq),
|
|
615
|
+
'$ne': comparisonEntry(itemNe),
|
|
616
|
+
'$lt': comparisonEntry(itemLt),
|
|
617
|
+
'$le': comparisonEntry(itemLe),
|
|
618
|
+
'$gt': comparisonEntry(itemGt),
|
|
619
|
+
'$ge': comparisonEntry(itemGe),
|
|
620
|
+
|
|
621
|
+
//#endregion
|
|
622
|
+
|
|
623
|
+
//#region section 8.5 - arithmetic
|
|
624
|
+
|
|
625
|
+
'$add': arithmeticEntry(() => (a, b) => a + b),
|
|
626
|
+
'$sub': arithmeticEntry(() => (a, b) => a - b),
|
|
627
|
+
'$mul': arithmeticEntry(() => (a, b) => a * b),
|
|
628
|
+
// IEEE 754 double division: /0 is +-Infinity or NaN (D1)
|
|
629
|
+
'$div': arithmeticEntry(() => (a, b) => a / b),
|
|
630
|
+
// truncating division; zero divisor errors (section 8.5)
|
|
631
|
+
'$idiv': arithmeticEntry((docPath) => (a, b) => {
|
|
632
|
+
if (b === 0)
|
|
633
|
+
throw runtimeError('JQ2002', "'$idiv' by zero", docPath);
|
|
634
|
+
return Math.trunc(a / b);
|
|
635
|
+
}),
|
|
636
|
+
// XQuery double mod takes the sign of the dividend = JS %
|
|
637
|
+
'$mod': arithmeticEntry((docPath) => (a, b) => {
|
|
638
|
+
if (b === 0)
|
|
639
|
+
throw runtimeError('JQ2002', "'$mod' by zero", docPath);
|
|
640
|
+
return a % b;
|
|
641
|
+
}),
|
|
642
|
+
|
|
643
|
+
'$neg': {
|
|
644
|
+
params: UNARY,
|
|
645
|
+
result: resultEmptyPropagates,
|
|
646
|
+
compile: (gets, args) => {
|
|
647
|
+
const get = gets[0];
|
|
648
|
+
const docPath = args[0].docPath;
|
|
649
|
+
if (args[0].card === CARD_ONE) {
|
|
650
|
+
return (f) => {
|
|
651
|
+
const a = get(f);
|
|
652
|
+
if (typeof a !== 'number')
|
|
653
|
+
throw arithOperandError(a, docPath);
|
|
654
|
+
return -a;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return (f) => {
|
|
658
|
+
const a = get(f);
|
|
659
|
+
if (a === EMPTY)
|
|
660
|
+
return EMPTY;
|
|
661
|
+
if (typeof a !== 'number')
|
|
662
|
+
throw arithOperandError(a, docPath);
|
|
663
|
+
return -a;
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
//#endregion
|
|
669
|
+
|
|
670
|
+
//#region section 8.6 - logic
|
|
671
|
+
|
|
672
|
+
'$and': logicEntry(false),
|
|
673
|
+
'$or': logicEntry(true),
|
|
674
|
+
|
|
675
|
+
'$not': {
|
|
676
|
+
params: UNARY,
|
|
677
|
+
result: RESULT_ONE,
|
|
678
|
+
compile: (gets, args) => {
|
|
679
|
+
const get = gets[0];
|
|
680
|
+
const docPath = args[0].docPath;
|
|
681
|
+
return (f) => !ebv(get(f), docPath);
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
//#endregion
|
|
686
|
+
|
|
687
|
+
//#region section 8.7 - strings
|
|
688
|
+
|
|
689
|
+
'$concat': {
|
|
690
|
+
params: ARGS_0N,
|
|
691
|
+
result: RESULT_ONE,
|
|
692
|
+
compile: (gets, args) => {
|
|
693
|
+
if (gets.length === 0)
|
|
694
|
+
return () => '';
|
|
695
|
+
const paths = args.map((a) => a.docPath);
|
|
696
|
+
const flen = gets.length;
|
|
697
|
+
return (f) => {
|
|
698
|
+
let s = '';
|
|
699
|
+
for (let i = 0; i < flen; i++) {
|
|
700
|
+
const v = gets[i](f);
|
|
701
|
+
// an empty operand contributes '' (section 8.7)
|
|
702
|
+
s += v === EMPTY ? '' : castString(v, paths[i]);
|
|
703
|
+
}
|
|
704
|
+
return s;
|
|
705
|
+
};
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
'$string-join': {
|
|
710
|
+
params: ARGS_1_2,
|
|
711
|
+
result: RESULT_ONE,
|
|
712
|
+
compile: (gets, args) => {
|
|
713
|
+
const seqGet = gets[0];
|
|
714
|
+
const seqPath = args[0].docPath;
|
|
715
|
+
const sepGet = gets.length === 2 ? gets[1] : null;
|
|
716
|
+
const sepPath = sepGet === null ? '' : args[1].docPath;
|
|
717
|
+
return (f) => {
|
|
718
|
+
const v = seqGet(f);
|
|
719
|
+
const sep = sepGet === null ? '' : stringArg(sepGet(f), sepPath);
|
|
720
|
+
if (v === EMPTY)
|
|
721
|
+
return '';
|
|
722
|
+
if (!(v instanceof Seq)) // items cast per $string, like $concat
|
|
723
|
+
return castString(v, seqPath);
|
|
724
|
+
const items = v.items;
|
|
725
|
+
let s = castString(items[0], seqPath);
|
|
726
|
+
for (let i = 1; i < items.length; i++)
|
|
727
|
+
s += sep + castString(items[i], seqPath);
|
|
728
|
+
return s;
|
|
729
|
+
};
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
'$substring': {
|
|
734
|
+
params: ARGS_2_3,
|
|
735
|
+
result: RESULT_ONE,
|
|
736
|
+
compile: (gets, args) => {
|
|
737
|
+
const sGet = gets[0];
|
|
738
|
+
const sPath = args[0].docPath;
|
|
739
|
+
const startGet = gets[1];
|
|
740
|
+
const startPath = args[1].docPath;
|
|
741
|
+
const lenGet = gets.length === 3 ? gets[2] : null;
|
|
742
|
+
const lenPath = lenGet === null ? '' : args[2].docPath;
|
|
743
|
+
return (f) => {
|
|
744
|
+
const s = stringArg(sGet(f), sPath);
|
|
745
|
+
// F&O fn:substring bounds (fn:round = Math.round), 0-based (D6):
|
|
746
|
+
// code points at positions p with round(start) <= p, and
|
|
747
|
+
// p < round(start) + round(len) when len is given. NaN bounds
|
|
748
|
+
// satisfy no comparison and select nothing.
|
|
749
|
+
const from = Math.round(numberArg(startGet(f), startPath));
|
|
750
|
+
const to = lenGet === null ? Infinity : from + Math.round(numberArg(lenGet(f), lenPath));
|
|
751
|
+
if (from !== from || to !== to) // a NaN bound selects nothing
|
|
752
|
+
return '';
|
|
753
|
+
if (from <= 0 && to === Infinity)
|
|
754
|
+
return s;
|
|
755
|
+
let out = '';
|
|
756
|
+
let p = 0;
|
|
757
|
+
for (const ch of s) {
|
|
758
|
+
if (p >= to)
|
|
759
|
+
break;
|
|
760
|
+
if (p >= from)
|
|
761
|
+
out += ch;
|
|
762
|
+
p++;
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
'$contains': stringPairEntry((s, sub) => s.includes(sub)),
|
|
770
|
+
'$starts-with': stringPairEntry((s, prefix) => s.startsWith(prefix)),
|
|
771
|
+
'$ends-with': stringPairEntry((s, suffix) => s.endsWith(suffix)),
|
|
772
|
+
|
|
773
|
+
'$upper': stringUnaryEntry((s) => s.toUpperCase()),
|
|
774
|
+
'$lower': stringUnaryEntry((s) => s.toLowerCase()),
|
|
775
|
+
'$string-length': stringUnaryEntry(countCodePoints),
|
|
776
|
+
'$normalize-space': stringUnaryEntry(normalizeSpace),
|
|
777
|
+
|
|
778
|
+
'$match': regexTestEntry(true),
|
|
779
|
+
'$search': regexTestEntry(false),
|
|
780
|
+
|
|
781
|
+
'$replace': {
|
|
782
|
+
params: ARGS_3,
|
|
783
|
+
result: RESULT_ONE,
|
|
784
|
+
compile: compileReplace,
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
//#endregion
|
|
788
|
+
|
|
789
|
+
//#region section 8.8 - aggregates
|
|
790
|
+
// Aggregates see their operand sequence as-is: an array item is one
|
|
791
|
+
// item - D4 unpacking is a $for/quantifier rule, not a sequence rule.
|
|
792
|
+
|
|
793
|
+
'$count': {
|
|
794
|
+
params: UNARY,
|
|
795
|
+
result: RESULT_ONE,
|
|
796
|
+
compile: (gets) => {
|
|
797
|
+
const get = gets[0];
|
|
798
|
+
return (f) => itemCount(get(f));
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
'$sum': {
|
|
803
|
+
params: UNARY,
|
|
804
|
+
result: RESULT_ONE,
|
|
805
|
+
compile: (gets, args) => {
|
|
806
|
+
const get = gets[0];
|
|
807
|
+
const docPath = args[0].docPath;
|
|
808
|
+
return (f) => {
|
|
809
|
+
const v = get(f);
|
|
810
|
+
if (v === EMPTY) // $sum of the empty sequence is 0 (F&O)
|
|
811
|
+
return 0;
|
|
812
|
+
if (v instanceof Seq) {
|
|
813
|
+
const items = v.items;
|
|
814
|
+
let sum = 0;
|
|
815
|
+
for (let i = 0; i < items.length; i++)
|
|
816
|
+
sum += aggregateNumber(items[i], docPath);
|
|
817
|
+
return sum;
|
|
818
|
+
}
|
|
819
|
+
return aggregateNumber(v, docPath);
|
|
820
|
+
};
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
'$avg': {
|
|
825
|
+
params: UNARY,
|
|
826
|
+
result: resultEmptyPropagates,
|
|
827
|
+
compile: (gets, args) => {
|
|
828
|
+
const get = gets[0];
|
|
829
|
+
const docPath = args[0].docPath;
|
|
830
|
+
return (f) => {
|
|
831
|
+
const v = get(f);
|
|
832
|
+
if (v === EMPTY) // $avg of the empty sequence is empty (F&O)
|
|
833
|
+
return EMPTY;
|
|
834
|
+
if (v instanceof Seq) {
|
|
835
|
+
const items = v.items;
|
|
836
|
+
let sum = 0;
|
|
837
|
+
for (let i = 0; i < items.length; i++)
|
|
838
|
+
sum += aggregateNumber(items[i], docPath);
|
|
839
|
+
return sum / items.length;
|
|
840
|
+
}
|
|
841
|
+
return aggregateNumber(v, docPath);
|
|
842
|
+
};
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
|
|
846
|
+
'$min': minmaxEntry(false),
|
|
847
|
+
'$max': minmaxEntry(true),
|
|
848
|
+
|
|
849
|
+
//#endregion
|
|
850
|
+
|
|
851
|
+
//#region section 8.9 - sequence operators
|
|
852
|
+
|
|
853
|
+
'$distinct': {
|
|
854
|
+
params: UNARY,
|
|
855
|
+
result: resultOfOperand,
|
|
856
|
+
compile: (gets) => {
|
|
857
|
+
const get = gets[0];
|
|
858
|
+
return (f) => {
|
|
859
|
+
const v = get(f);
|
|
860
|
+
if (!(v instanceof Seq)) // zero or one item: already distinct
|
|
861
|
+
return v;
|
|
862
|
+
// deep equality via the grouping key relation (section 6.5):
|
|
863
|
+
// NaN is one distinct value, -0 deduplicates with 0
|
|
864
|
+
const items = v.items;
|
|
865
|
+
const seen = new Set();
|
|
866
|
+
const out = [];
|
|
867
|
+
for (let i = 0; i < items.length; i++) {
|
|
868
|
+
const key = stableKeyString(items[i]);
|
|
869
|
+
if (!seen.has(key)) {
|
|
870
|
+
seen.add(key);
|
|
871
|
+
out.push(items[i]);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return seqOf(out);
|
|
875
|
+
};
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
'$reverse': {
|
|
880
|
+
params: UNARY,
|
|
881
|
+
result: resultOfOperand,
|
|
882
|
+
compile: (gets) => {
|
|
883
|
+
const get = gets[0];
|
|
884
|
+
return (f) => {
|
|
885
|
+
const v = get(f);
|
|
886
|
+
if (!(v instanceof Seq))
|
|
887
|
+
return v;
|
|
888
|
+
const items = v.items;
|
|
889
|
+
const out = new Array(items.length);
|
|
890
|
+
for (let i = 0; i < items.length; i++)
|
|
891
|
+
out[i] = items[items.length - 1 - i];
|
|
892
|
+
return seqOf(out);
|
|
893
|
+
};
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
|
|
897
|
+
'$sort': {
|
|
898
|
+
params: UNARY,
|
|
899
|
+
result: resultOfOperand,
|
|
900
|
+
compile: (gets, args) => {
|
|
901
|
+
const get = gets[0];
|
|
902
|
+
const docPath = args[0].docPath;
|
|
903
|
+
return (f) => {
|
|
904
|
+
const v = get(f);
|
|
905
|
+
if (v === EMPTY)
|
|
906
|
+
return EMPTY;
|
|
907
|
+
if (!(v instanceof Seq)) {
|
|
908
|
+
const t = typeof v;
|
|
909
|
+
if (t !== 'number' && t !== 'string')
|
|
910
|
+
throw sortError(v, docPath);
|
|
911
|
+
return v;
|
|
912
|
+
}
|
|
913
|
+
const items = v.items;
|
|
914
|
+
const numeric = typeof items[0] === 'number';
|
|
915
|
+
const expected = numeric ? 'number' : 'string';
|
|
916
|
+
for (let i = 0; i < items.length; i++) {
|
|
917
|
+
if (typeof items[i] !== expected)
|
|
918
|
+
throw sortError(items[i], docPath);
|
|
919
|
+
}
|
|
920
|
+
const out = items.slice();
|
|
921
|
+
out.sort(numeric ? compareNumberKeys : compareCodePoints); // stable
|
|
922
|
+
return seqOf(out);
|
|
923
|
+
};
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
|
|
927
|
+
'$head': {
|
|
928
|
+
params: UNARY,
|
|
929
|
+
result: (cards) => (cards[0] === CARD_MANY ? CARD_OPT : cards[0]),
|
|
930
|
+
compile: (gets) => {
|
|
931
|
+
const get = gets[0];
|
|
932
|
+
return (f) => firstItem(get(f));
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
'$tail': {
|
|
937
|
+
params: UNARY,
|
|
938
|
+
result: (cards) => (cards[0] === CARD_MANY ? CARD_MANY : CARD_ZERO),
|
|
939
|
+
compile: (gets) => {
|
|
940
|
+
const get = gets[0];
|
|
941
|
+
return (f) => {
|
|
942
|
+
const v = get(f);
|
|
943
|
+
if (!(v instanceof Seq)) // zero or one item: nothing after the head
|
|
944
|
+
return EMPTY;
|
|
945
|
+
return seqOf(v.items.slice(1));
|
|
946
|
+
};
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
|
|
950
|
+
'$subsequence': {
|
|
951
|
+
params: ARGS_2_3,
|
|
952
|
+
result: (cards) => (cards[0] === CARD_MANY ? CARD_MANY : (cards[0] === CARD_ZERO ? CARD_ZERO : CARD_OPT)),
|
|
953
|
+
compile: (gets, args) => {
|
|
954
|
+
const seqGet = gets[0];
|
|
955
|
+
const startGet = gets[1];
|
|
956
|
+
const startPath = args[1].docPath;
|
|
957
|
+
const lenGet = gets.length === 3 ? gets[2] : null;
|
|
958
|
+
const lenPath = lenGet === null ? '' : args[2].docPath;
|
|
959
|
+
return (f) => {
|
|
960
|
+
const v = seqGet(f);
|
|
961
|
+
// same F&O bounds as $substring, over item positions (D6)
|
|
962
|
+
const from = Math.round(numberArg(startGet(f), startPath));
|
|
963
|
+
const to = lenGet === null ? Infinity : from + Math.round(numberArg(lenGet(f), lenPath));
|
|
964
|
+
if (v === EMPTY || from !== from || to !== to)
|
|
965
|
+
return EMPTY;
|
|
966
|
+
if (!(v instanceof Seq))
|
|
967
|
+
return (from <= 0 && to > 0) ? v : EMPTY;
|
|
968
|
+
const items = v.items;
|
|
969
|
+
const lo = from > 0 ? from : 0;
|
|
970
|
+
const hi = to < items.length ? to : items.length;
|
|
971
|
+
if (lo >= hi)
|
|
972
|
+
return EMPTY;
|
|
973
|
+
return seqOf(items.slice(lo, hi));
|
|
974
|
+
};
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
'$index-of': {
|
|
979
|
+
params: ARGS_2,
|
|
980
|
+
result: (cards) => (cards[0] === CARD_MANY ? CARD_MANY : (cards[0] === CARD_ZERO ? CARD_ZERO : CARD_OPT)),
|
|
981
|
+
compile: (gets, args) => {
|
|
982
|
+
const seqGet = gets[0];
|
|
983
|
+
const itemGet = gets[1];
|
|
984
|
+
const itemPath = args[1].docPath;
|
|
985
|
+
return (f) => {
|
|
986
|
+
const v = seqGet(f);
|
|
987
|
+
const target = itemGet(f);
|
|
988
|
+
if (target === EMPTY || target instanceof Seq)
|
|
989
|
+
throw runtimeError('JQ2001',
|
|
990
|
+
`'$index-of' takes a single search item, got ${describeItem(target)}`, itemPath);
|
|
991
|
+
// deep equality per D2 (the $eq relation: NaN matches nothing),
|
|
992
|
+
// 0-based positions (D6)
|
|
993
|
+
if (v === EMPTY)
|
|
994
|
+
return EMPTY;
|
|
995
|
+
if (!(v instanceof Seq))
|
|
996
|
+
return equalsJson(v, target) ? 0 : EMPTY;
|
|
997
|
+
const items = v.items;
|
|
998
|
+
const out = [];
|
|
999
|
+
for (let i = 0; i < items.length; i++) {
|
|
1000
|
+
if (equalsJson(items[i], target))
|
|
1001
|
+
out.push(i);
|
|
1002
|
+
}
|
|
1003
|
+
return seqOf(out);
|
|
1004
|
+
};
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
|
|
1008
|
+
'$range': {
|
|
1009
|
+
params: ARGS_2,
|
|
1010
|
+
result: RESULT_MANY,
|
|
1011
|
+
compile: (gets, args, docPath) => {
|
|
1012
|
+
const fromGet = gets[0];
|
|
1013
|
+
const fromPath = args[0].docPath;
|
|
1014
|
+
const toGet = gets[1];
|
|
1015
|
+
const toPath = args[1].docPath;
|
|
1016
|
+
return (f) => {
|
|
1017
|
+
const a = fromGet(f);
|
|
1018
|
+
const b = toGet(f);
|
|
1019
|
+
if (a === EMPTY || b === EMPTY) // XQuery `to`: empty operand, empty range
|
|
1020
|
+
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);
|
|
1025
|
+
if (a > b)
|
|
1026
|
+
return EMPTY;
|
|
1027
|
+
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);
|
|
1030
|
+
const out = new Array(n);
|
|
1031
|
+
for (let i = 0; i < n; i++)
|
|
1032
|
+
out[i] = a + i;
|
|
1033
|
+
return seqOf(out);
|
|
1034
|
+
};
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
'$get': {
|
|
1039
|
+
params: ARGS_2,
|
|
1040
|
+
result: RESULT_OPT,
|
|
1041
|
+
compile: (gets) => {
|
|
1042
|
+
const targetGet = gets[0];
|
|
1043
|
+
const keyGet = gets[1];
|
|
1044
|
+
// the dynamic counterpart of path leaves: object + string key,
|
|
1045
|
+
// array + integer index (negative counts from the end, like the
|
|
1046
|
+
// RFC 9535 index selector); anything else is simply empty
|
|
1047
|
+
return (f) => {
|
|
1048
|
+
const target = targetGet(f);
|
|
1049
|
+
const key = keyGet(f);
|
|
1050
|
+
if (target === EMPTY || target instanceof Seq || key === EMPTY || key instanceof Seq)
|
|
1051
|
+
return EMPTY;
|
|
1052
|
+
if (typeof key === 'string') {
|
|
1053
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target))
|
|
1054
|
+
return EMPTY;
|
|
1055
|
+
return hasOwn(target, key) ? target[key] : EMPTY;
|
|
1056
|
+
}
|
|
1057
|
+
if (typeof key === 'number' && Array.isArray(target) && Number.isInteger(key)) {
|
|
1058
|
+
const idx = key < 0 ? target.length + key : key;
|
|
1059
|
+
return (idx >= 0 && idx < target.length) ? target[idx] : EMPTY;
|
|
1060
|
+
}
|
|
1061
|
+
return EMPTY;
|
|
1062
|
+
};
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
|
|
1066
|
+
//#endregion
|
|
1067
|
+
|
|
1068
|
+
//#region section 8.10 - types and casts
|
|
1069
|
+
|
|
1070
|
+
'$is-string': isTypeEntry((v) => typeof v === 'string'),
|
|
1071
|
+
'$is-number': isTypeEntry((v) => typeof v === 'number'),
|
|
1072
|
+
'$is-boolean': isTypeEntry((v) => typeof v === 'boolean'),
|
|
1073
|
+
'$is-null': isTypeEntry((v) => v === null),
|
|
1074
|
+
'$is-array': isTypeEntry(Array.isArray),
|
|
1075
|
+
'$is-object': isTypeEntry((v) =>
|
|
1076
|
+
typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Seq)),
|
|
1077
|
+
|
|
1078
|
+
'$string': {
|
|
1079
|
+
params: UNARY,
|
|
1080
|
+
result: resultEmptyPropagates,
|
|
1081
|
+
compile: (gets, args) => {
|
|
1082
|
+
const get = gets[0];
|
|
1083
|
+
const docPath = args[0].docPath;
|
|
1084
|
+
return (f) => {
|
|
1085
|
+
const v = get(f);
|
|
1086
|
+
return v === EMPTY ? EMPTY : castString(v, docPath);
|
|
1087
|
+
};
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
|
|
1091
|
+
'$number': {
|
|
1092
|
+
params: UNARY,
|
|
1093
|
+
result: resultEmptyPropagates,
|
|
1094
|
+
compile: (gets, args) => {
|
|
1095
|
+
const get = gets[0];
|
|
1096
|
+
const docPath = args[0].docPath;
|
|
1097
|
+
return (f) => {
|
|
1098
|
+
const v = get(f);
|
|
1099
|
+
return v === EMPTY ? EMPTY : castNumber(v, docPath);
|
|
1100
|
+
};
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
|
|
1104
|
+
'$boolean': { // the EBV as an operator: a 2+ item operand is JQ2003
|
|
1105
|
+
params: UNARY,
|
|
1106
|
+
result: RESULT_ONE,
|
|
1107
|
+
compile: (gets, args) => {
|
|
1108
|
+
const get = gets[0];
|
|
1109
|
+
const docPath = args[0].docPath;
|
|
1110
|
+
return (f) => ebv(get(f), docPath);
|
|
1111
|
+
},
|
|
1112
|
+
},
|
|
1113
|
+
|
|
1114
|
+
'$coalesce': {
|
|
1115
|
+
params: ARGS_1N,
|
|
1116
|
+
result: coalesceCard,
|
|
1117
|
+
compile: compileCoalesce,
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
'$default': { // sugar for $coalesce of exactly two
|
|
1121
|
+
params: ARGS_2,
|
|
1122
|
+
result: coalesceCard,
|
|
1123
|
+
compile: compileCoalesce,
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
//#endregion
|
|
1127
|
+
|
|
1128
|
+
//#region section 8.11 - schema operators
|
|
1129
|
+
// JSON Schema as the type system: the schema argument is a verbatim
|
|
1130
|
+
// literal ('schema' kind), compiled once at query compile time into the
|
|
1131
|
+
// predicate at `args[1].test` by the host's compileTypeTest hook
|
|
1132
|
+
// (JQ0008 without one). Validation is per ITEM of the operand's result
|
|
1133
|
+
// sequence - the schema sees items, never the sequence itself.
|
|
1134
|
+
|
|
1135
|
+
'$valid': {
|
|
1136
|
+
params: ARGS_EXPR_SCHEMA,
|
|
1137
|
+
result: RESULT_ONE,
|
|
1138
|
+
compile: (gets, args) => {
|
|
1139
|
+
const get = gets[0];
|
|
1140
|
+
const test = args[1].test;
|
|
1141
|
+
if (args[0].card === CARD_ONE)
|
|
1142
|
+
return (f) => test(get(f));
|
|
1143
|
+
return (f) => {
|
|
1144
|
+
const v = get(f);
|
|
1145
|
+
if (v === EMPTY) // vacuously true, like $every
|
|
1146
|
+
return true;
|
|
1147
|
+
if (v instanceof Seq) {
|
|
1148
|
+
const items = v.items;
|
|
1149
|
+
for (let i = 0; i < items.length; i++) {
|
|
1150
|
+
if (!test(items[i]))
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
return test(v);
|
|
1156
|
+
};
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
|
|
1160
|
+
'$assert': {
|
|
1161
|
+
params: ARGS_EXPR_SCHEMA,
|
|
1162
|
+
result: resultOfOperand,
|
|
1163
|
+
compile: (gets, args, docPath) => {
|
|
1164
|
+
const get = gets[0];
|
|
1165
|
+
const test = args[1].test;
|
|
1166
|
+
if (args[0].card === CARD_ONE) {
|
|
1167
|
+
return (f) => {
|
|
1168
|
+
const v = get(f);
|
|
1169
|
+
if (!test(v))
|
|
1170
|
+
throw runtimeError('JQ2008', `'$assert' failed: ${describeItem(v)} does not satisfy the schema`, docPath);
|
|
1171
|
+
return v;
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
return (f) => {
|
|
1175
|
+
const v = get(f);
|
|
1176
|
+
if (v === EMPTY) // no items, nothing to fail
|
|
1177
|
+
return v;
|
|
1178
|
+
if (v instanceof Seq) {
|
|
1179
|
+
const items = v.items;
|
|
1180
|
+
for (let i = 0; i < items.length; i++) {
|
|
1181
|
+
if (!test(items[i]))
|
|
1182
|
+
throw runtimeError('JQ2008', `'$assert' failed: item ${i} (${describeItem(items[i])}) does not satisfy the schema`, docPath);
|
|
1183
|
+
}
|
|
1184
|
+
return v;
|
|
1185
|
+
}
|
|
1186
|
+
if (!test(v))
|
|
1187
|
+
throw runtimeError('JQ2008', `'$assert' failed: ${describeItem(v)} does not satisfy the schema`, docPath);
|
|
1188
|
+
return v;
|
|
1189
|
+
};
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
|
|
1193
|
+
//#endregion
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// singleton type predicates (section 8.10): true iff the operand is one
|
|
1197
|
+
// item of the type - the empty sequence and 2+ item sequences are false,
|
|
1198
|
+
// not errors (EMPTY is a symbol and Seq is excluded per test)
|
|
1199
|
+
function isTypeEntry(test) {
|
|
1200
|
+
return {
|
|
1201
|
+
params: UNARY,
|
|
1202
|
+
result: RESULT_ONE,
|
|
1203
|
+
compile: (gets) => {
|
|
1204
|
+
const get = gets[0];
|
|
1205
|
+
return (f) => test(get(f));
|
|
1206
|
+
},
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// $coalesce cardinality: operands past the first CARD_ONE one are never
|
|
1211
|
+
// reached, statically empty operands contribute nothing
|
|
1212
|
+
function coalesceCard(cards) {
|
|
1213
|
+
let seen = false;
|
|
1214
|
+
let many = false;
|
|
1215
|
+
let terminal = false;
|
|
1216
|
+
for (let i = 0; i < cards.length; i++) {
|
|
1217
|
+
const c = cards[i];
|
|
1218
|
+
if (c === CARD_ZERO)
|
|
1219
|
+
continue;
|
|
1220
|
+
seen = true;
|
|
1221
|
+
if (c === CARD_MANY)
|
|
1222
|
+
many = true;
|
|
1223
|
+
if (c === CARD_ONE) {
|
|
1224
|
+
terminal = true;
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!seen)
|
|
1229
|
+
return CARD_ZERO;
|
|
1230
|
+
if (many)
|
|
1231
|
+
return CARD_MANY;
|
|
1232
|
+
return terminal ? CARD_ONE : CARD_OPT;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// first operand whose result is non-empty, lazily: operands after it are
|
|
1236
|
+
// not evaluated and cannot raise errors
|
|
1237
|
+
function compileCoalesce(gets) {
|
|
1238
|
+
if (gets.length === 1)
|
|
1239
|
+
return gets[0];
|
|
1240
|
+
const flen = gets.length;
|
|
1241
|
+
return (f) => {
|
|
1242
|
+
for (let i = 0; i < flen; i++) {
|
|
1243
|
+
const v = gets[i](f);
|
|
1244
|
+
if (v !== EMPTY)
|
|
1245
|
+
return v;
|
|
1246
|
+
}
|
|
1247
|
+
return EMPTY;
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
//#endregion
|
|
1252
|
+
|
|
1253
|
+
//#endregion
|