@jarenjs/json 0.9.2 → 0.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/src/query/normalize.js
CHANGED
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
// allocating in the same frame). Free names become external parameters,
|
|
19
19
|
// collected in order of first appearance.
|
|
20
20
|
|
|
21
|
-
import { parseJSONPath, JSONPathSyntaxError } from '../path.js';
|
|
21
|
+
import { parseJSONPath, JSONPathSyntaxError, RE_JSONPATH_VARIABLE_HEAD } from '../path.js';
|
|
22
22
|
import { isSingularSegments } from '../segments.js';
|
|
23
|
+
import { encodeJSONPointerSegment } from '../pointer.js';
|
|
23
24
|
import { JsonQueryCompileError } from './errors.js';
|
|
25
|
+
import { deepFreeze, isJsonObject } from '@jarenjs/core/object';
|
|
24
26
|
// The operator registry: `name -> { params, result, compile }`. Only
|
|
25
27
|
// referenced inside functions (never at module evaluation time), so the
|
|
26
28
|
// import cycle normalize.js <-> operators.js is initialization-safe.
|
|
@@ -28,6 +30,17 @@ import { OPERATORS } from './operators.js';
|
|
|
28
30
|
|
|
29
31
|
//#region cardinality
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The twelve node kinds of the normalized form, sorted — the published
|
|
35
|
+
* AST contract (QUERY-FORMAT.md Appendix C). An exhaustiveness gate
|
|
36
|
+
* asserts a corpus exercising every construct produces exactly this
|
|
37
|
+
* set, so a new kind cannot ship undocumented.
|
|
38
|
+
*/
|
|
39
|
+
export const NODE_KINDS = Object.freeze([
|
|
40
|
+
'array', 'call', 'flwor', 'let', 'literal', 'map',
|
|
41
|
+
'object', 'op', 'path', 'quant', 'raw', 'var',
|
|
42
|
+
]);
|
|
43
|
+
|
|
31
44
|
/** Statically empty (the node always evaluates to the empty sequence). */
|
|
32
45
|
export const CARD_ZERO = 0;
|
|
33
46
|
/** Always exactly one item; compile.js skips all sequence checks. */
|
|
@@ -74,17 +87,38 @@ export function sumCard(a, b) {
|
|
|
74
87
|
const hasOwn = Object.hasOwn;
|
|
75
88
|
|
|
76
89
|
const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
77
|
-
|
|
90
|
+
// The '$name' head of a variable-rooted path string; shared with the
|
|
91
|
+
// json-path-segments format tester so both split at the same point.
|
|
92
|
+
const VAR_HEAD_RE = RE_JSONPATH_VARIABLE_HEAD;
|
|
78
93
|
|
|
79
94
|
// FLWOR clause keys (QUERY-FORMAT.md section 6.1) and quantifier keys
|
|
80
95
|
// (section 7). Clauses apply in the fixed semantic order of section 6.1
|
|
81
96
|
// regardless of JSON key order (D7).
|
|
82
|
-
const FLWOR_KEYS = new Set(['$for', '$let', '$as', '$where', '$groupby', '$orderby', '$count', '$return']);
|
|
97
|
+
const FLWOR_KEYS = new Set(['$fold', '$for', '$let', '$as', '$where', '$groupby', '$orderby', '$count', '$return']);
|
|
83
98
|
const QUANTIFIER_KEYS = new Set(['$some', '$every', '$satisfies']);
|
|
84
99
|
|
|
85
100
|
// Keys of the explicit $orderby key-spec form (section 6.6). Contextual:
|
|
86
101
|
// they are not operators and stay outside the KNOWN_KEYS vocabulary.
|
|
87
|
-
const ORDERBY_SPEC_KEYS = new Set(['$key', '$dir', '$empty']);
|
|
102
|
+
const ORDERBY_SPEC_KEYS = new Set(['$key', '$dir', '$empty', '$collation']);
|
|
103
|
+
|
|
104
|
+
// Keys of the extended $for binding form (sections 6.2, 6.10): the
|
|
105
|
+
// source, a positional variable, the outer-join switch, and the window
|
|
106
|
+
// specification. Also contextual - not operators.
|
|
107
|
+
const FOR_BINDING_KEYS = new Set(['$in', '$at', '$allowing-empty', '$window', '$size', '$step']);
|
|
108
|
+
|
|
109
|
+
// Whether a binding source uses the extended object form rather than
|
|
110
|
+
// being an ordinary expression. Any of its keys marks it, so a malformed
|
|
111
|
+
// combination is diagnosed as a bad binding instead of silently
|
|
112
|
+
// normalizing as a map constructor.
|
|
113
|
+
function isExtendedBinding(source) {
|
|
114
|
+
if (!isJsonObject(source))
|
|
115
|
+
return false;
|
|
116
|
+
for (const key of FOR_BINDING_KEYS) {
|
|
117
|
+
if (hasOwn(source, key))
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
88
122
|
|
|
89
123
|
// Escape hatches (QUERY-FORMAT.md section 3.5): structural forms with
|
|
90
124
|
// dedicated normalizer cases; every other operator lives in the registry.
|
|
@@ -106,19 +140,32 @@ function isVocabularyKey(key, ctx) {
|
|
|
106
140
|
|
|
107
141
|
//#region helpers
|
|
108
142
|
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// RFC 6901 reference token escaping for docPath pointers
|
|
114
|
-
function escapeToken(token) {
|
|
115
|
-
if (token.indexOf('~') < 0 && token.indexOf('/') < 0)
|
|
116
|
-
return token;
|
|
117
|
-
return token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
143
|
+
function fail(code, message, docPath, options) {
|
|
144
|
+
throw new JsonQueryCompileError(code, message, docPath, options);
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Project a safe diagnostic string from whatever a host hook threw:
|
|
149
|
+
* no `.message` read on a raw value, no user coercion, no
|
|
150
|
+
* proxy-observable reflection — the compiler must never fail while
|
|
151
|
+
* describing a host failure.
|
|
152
|
+
* @param {unknown} e
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
export function hostFailureText(e) {
|
|
156
|
+
try {
|
|
157
|
+
if (e instanceof Error) {
|
|
158
|
+
const message = e.message;
|
|
159
|
+
if (typeof message === 'string') return message;
|
|
160
|
+
return 'host error (message unavailable)';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { /* hostile classification or accessor */ }
|
|
164
|
+
if (e === null) return 'null';
|
|
165
|
+
const t = typeof e;
|
|
166
|
+
if (t === 'string') return e.length > 80 ? e.slice(0, 80) + '…' : e;
|
|
167
|
+
if (t === 'number' || t === 'boolean' || t === 'bigint' || t === 'undefined') return String(e);
|
|
168
|
+
return t === 'symbol' ? 'a symbol' : t === 'function' ? 'a function' : 'an object';
|
|
122
169
|
}
|
|
123
170
|
|
|
124
171
|
/**
|
|
@@ -144,22 +191,13 @@ export function deepFreezeCopy(value) {
|
|
|
144
191
|
return Object.freeze(out);
|
|
145
192
|
}
|
|
146
193
|
|
|
147
|
-
function deepFreeze(value) {
|
|
148
|
-
if (typeof value !== 'object' || value === null)
|
|
149
|
-
return value;
|
|
150
|
-
const keys = Object.keys(value);
|
|
151
|
-
for (let i = 0; i < keys.length; i++)
|
|
152
|
-
deepFreeze(value[keys[i]]);
|
|
153
|
-
return Object.freeze(value);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
194
|
//#endregion
|
|
157
195
|
|
|
158
196
|
//#region strings (Rule 2)
|
|
159
197
|
|
|
160
|
-
function parsePathString(source, original, docPath) {
|
|
198
|
+
function parsePathString(source, original, docPath, ctx) {
|
|
161
199
|
try {
|
|
162
|
-
return parseJSONPath(source);
|
|
200
|
+
return parseJSONPath(source, ctx.pathOptions);
|
|
163
201
|
}
|
|
164
202
|
catch (e) {
|
|
165
203
|
/* c8 ignore next 2 -- parseJSONPath only throws syntax errors */
|
|
@@ -186,14 +224,23 @@ function makePathNode(name, rootSlot, external, rootCard, segments, docPath) {
|
|
|
186
224
|
|
|
187
225
|
// resolve a variable reference: walk the lexical scope chain; a free name
|
|
188
226
|
// is an external parameter, allocated a slot on first appearance (spec
|
|
189
|
-
// section 9: use is the declaration)
|
|
190
|
-
|
|
227
|
+
// section 9: use is the declaration). Under a closed-world compilation
|
|
228
|
+
// (options.externals), only declared names may stay free - every other
|
|
229
|
+
// free name is JQ0005 at its own reference site.
|
|
230
|
+
function resolveVariable(name, scope, ctx, docPath) {
|
|
191
231
|
for (let sc = scope; sc !== null; sc = sc.parent) {
|
|
192
232
|
if (sc.name === name)
|
|
193
233
|
return { slot: sc.slot, card: sc.card, external: false };
|
|
194
234
|
}
|
|
195
235
|
let slot = ctx.externals.get(name);
|
|
196
236
|
if (slot === undefined) {
|
|
237
|
+
if (ctx.declaredExternals !== null && !ctx.declaredExternals.has(name)) {
|
|
238
|
+
const declared = [...ctx.declaredExternals];
|
|
239
|
+
fail('JQ0005', `'$${name}' is neither bound by an enclosing phrase nor a declared external`
|
|
240
|
+
+ (declared.length === 0
|
|
241
|
+
? ' (this query was compiled closed-world, declaring no externals)'
|
|
242
|
+
: ` (declared externals: ${declared.map((n) => "'" + n + "'").join(', ')})`), docPath);
|
|
243
|
+
}
|
|
197
244
|
slot = ctx.nextSlot++;
|
|
198
245
|
ctx.externals.set(name, slot);
|
|
199
246
|
}
|
|
@@ -210,7 +257,7 @@ function normalizeString(s, docPath, scope, ctx) {
|
|
|
210
257
|
if (c1 === 0x24) // '$$' escape: drop exactly one leading '$'
|
|
211
258
|
return makeLiteral(s.slice(1), docPath);
|
|
212
259
|
if (c1 === 0x2E || c1 === 0x5B) { // '$.' | '$[' | '$..' - absolute path
|
|
213
|
-
const ast = parsePathString(s, s, docPath);
|
|
260
|
+
const ast = parsePathString(s, s, docPath, ctx);
|
|
214
261
|
return makePathNode('$', 0, false, CARD_ONE, ast.segments, docPath);
|
|
215
262
|
}
|
|
216
263
|
const m = VAR_HEAD_RE.exec(s);
|
|
@@ -218,12 +265,12 @@ function normalizeString(s, docPath, scope, ctx) {
|
|
|
218
265
|
return fail('JQ0004', `'${s}' is not a valid path or escape`, docPath);
|
|
219
266
|
const name = m[1];
|
|
220
267
|
const rest = s.slice(m[0].length);
|
|
221
|
-
const ref = resolveVariable(name, scope, ctx);
|
|
268
|
+
const ref = resolveVariable(name, scope, ctx, docPath);
|
|
222
269
|
if (rest === '') // bare '$name': whole-variable reference
|
|
223
270
|
return Object.freeze({ kind: 'var', card: ref.card, docPath, slot: ref.slot, external: ref.external, name });
|
|
224
271
|
// variable-rooted path: the grammar is RFC 9535 with the root identifier
|
|
225
272
|
// replaced by the variable reference - parse with a substituted '$'
|
|
226
|
-
const ast = parsePathString('$' + rest, s, docPath);
|
|
273
|
+
const ast = parsePathString('$' + rest, s, docPath, ctx);
|
|
227
274
|
return makePathNode(name, ref.slot, ref.external, ref.card, ast.segments, docPath);
|
|
228
275
|
}
|
|
229
276
|
|
|
@@ -244,7 +291,7 @@ function normalizeObject(obj, docPath, scope, ctx) {
|
|
|
244
291
|
const name = keys[i];
|
|
245
292
|
entries[i] = Object.freeze({
|
|
246
293
|
name,
|
|
247
|
-
expr: normalizeExpr(obj[name], docPath + '/' +
|
|
294
|
+
expr: normalizeExpr(obj[name], docPath + '/' + encodeJSONPointerSegment(name), scope, ctx),
|
|
248
295
|
});
|
|
249
296
|
}
|
|
250
297
|
return Object.freeze({ kind: 'object', card: CARD_ONE, docPath, entries: Object.freeze(entries) });
|
|
@@ -265,7 +312,8 @@ function normalizePhrase(obj, keys, docPath, scope, ctx) {
|
|
|
265
312
|
break;
|
|
266
313
|
}
|
|
267
314
|
}
|
|
268
|
-
if (allFlwor && keys.length >= 2 && hasOwn(obj, '$return')
|
|
315
|
+
if (allFlwor && keys.length >= 2 && hasOwn(obj, '$return')
|
|
316
|
+
&& (hasOwn(obj, '$for') || hasOwn(obj, '$let') || hasOwn(obj, '$fold'))) {
|
|
269
317
|
if (keys.length === 2 && hasOwn(obj, '$let'))
|
|
270
318
|
return normalizeLetPhrase(obj, docPath, scope, ctx);
|
|
271
319
|
return normalizeFlworPhrase(obj, docPath, scope, ctx);
|
|
@@ -318,11 +366,57 @@ function levenshtein(a, b) {
|
|
|
318
366
|
// suggestion candidates, built lazily (see the isVocabularyKey note)
|
|
319
367
|
let VOCABULARY_NAMES = null;
|
|
320
368
|
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
369
|
+
// SEMANTIC aliases — the names a writer reaches for from other query
|
|
370
|
+
// languages (SQL, XPath, JSONata, JS array methods) or a plausible
|
|
371
|
+
// synonym, which Levenshtein cannot reach because they are lexically
|
|
372
|
+
// far from the real operator. This is the failure mode an LLM lands in
|
|
373
|
+
// most: it guesses `$first`/`$filter`/`$map` and, without a pointer,
|
|
374
|
+
// abandons the language. Each entry is the CLOSEST real spelling; a
|
|
375
|
+
// value of null means "no operator does this — here is how instead".
|
|
376
|
+
const OPERATOR_ALIASES = {
|
|
377
|
+
// sequence access
|
|
378
|
+
$first: "$head", $last: "$head of $reverse", $nth: "$get", $at: "$get",
|
|
379
|
+
$take: "$subsequence", $skip: "$subsequence", $slice: "$subsequence",
|
|
380
|
+
$drop: "$subsequence", $limit: "$subsequence",
|
|
381
|
+
// filtering / mapping / folding: these are FLWOR or a JSONPath filter,
|
|
382
|
+
// not operators ($where, $map, $fold, $orderby ARE real keys and never
|
|
383
|
+
// reach here — only the guesses that miss them are listed)
|
|
384
|
+
$filter: "a JSONPath filter like $[?(@.x > 1)] or $where in a $for",
|
|
385
|
+
$select: "a $for phrase with $return", $flatmap: "a $for phrase",
|
|
386
|
+
$reduce: "$fold", $foldl: "$fold", $aggregate: "$fold",
|
|
387
|
+
$group: "$groupby", "$group-by": "$groupby",
|
|
388
|
+
$sortby: "$sort (sorts scalars; order objects via a $for over a sorted key)",
|
|
389
|
+
"$sort-by": "$sort (scalars only)", $order: "$sort",
|
|
390
|
+
// aggregates / arithmetic
|
|
391
|
+
$size: "$count", $len: "$length", $abs: null, $round: null, $floor: "$idiv",
|
|
392
|
+
$ceil: null, $sqrt: null, $pow: null, $modulo: "$mod", $remainder: "$mod",
|
|
393
|
+
$subtract: "$sub", $multiply: "$mul", $divide: "$div", $minus: "$sub",
|
|
394
|
+
$times: "$mul", $negate: "$neg", $product: "$mul", $total: "$sum",
|
|
395
|
+
// strings / collections
|
|
396
|
+
$join: "$string-join", $split: null, $includes: "$contains",
|
|
397
|
+
$indexof: "$index-of", $find: "$index-of", $keys: "$entries",
|
|
398
|
+
$values: "$entries then $get", $has: "$exists", $tostring: "$string",
|
|
399
|
+
$tonumber: "$number", $len_str: "$string-length", $trim: "$normalize-space",
|
|
400
|
+
$lowercase: "$lower", $uppercase: "$upper", $startswith: "$starts-with",
|
|
401
|
+
$endswith: "$ends-with", $unique: "$distinct", $flatten: "a $for phrase",
|
|
402
|
+
// conditionals ($coalesce is a real operator; the guesses are here)
|
|
403
|
+
$case: "$if", $cond: "$if", $switch: "$if", $ternary: "$if",
|
|
404
|
+
$ifnull: "$default", $ifempty: "$default", $nvl: "$default",
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// JQ0002 for an unknown $-key, with a "did you mean" suggestion. A
|
|
408
|
+
// curated SEMANTIC alias wins first (the cross-language guesses
|
|
409
|
+
// Levenshtein misses); otherwise the nearest vocabulary key within
|
|
410
|
+
// Levenshtein distance 2. Compile-time only; with host extensions the
|
|
411
|
+
// candidate list is built per call.
|
|
325
412
|
function failUnknownOperator(key, docPath, ctx) {
|
|
413
|
+
if (hasOwn(OPERATOR_ALIASES, key)) {
|
|
414
|
+
const target = OPERATOR_ALIASES[key];
|
|
415
|
+
const hint = target === null
|
|
416
|
+
? ' (no operator does this in jaren-query)'
|
|
417
|
+
: ` (use ${target.startsWith('$') && !target.includes(' ') ? `'${target}'` : target})`;
|
|
418
|
+
return fail('JQ0002', `unknown operator '${key}'${hint}`, docPath);
|
|
419
|
+
}
|
|
326
420
|
if (VOCABULARY_NAMES === null) {
|
|
327
421
|
VOCABULARY_NAMES = [
|
|
328
422
|
...FLWOR_KEYS, ...QUANTIFIER_KEYS, ...ESCAPE_KEYS, ...Object.keys(OPERATORS),
|
|
@@ -378,15 +472,22 @@ function normalizeElements(arg, docPath, scope, ctx) {
|
|
|
378
472
|
// No hook installed is JQ0008; a hook rejection (invalid schema) is
|
|
379
473
|
// JQ0009, both at the owning operator's/clause's docPath.
|
|
380
474
|
function compileSchemaLiteral(value, schemaPath, opPath, ctx) {
|
|
381
|
-
if (ctx.compileTypeTest === null)
|
|
475
|
+
if (ctx.compileTypeTest === null) {
|
|
476
|
+
// Analysis mode (QUERY-FORMAT.md Appendix C.1): the schema literal
|
|
477
|
+
// normalizes without a compiled predicate, so a consumer can analyse
|
|
478
|
+
// a document it could not execute. Strictly opt-in — compilation
|
|
479
|
+
// never sets ctx.analysis, so its behaviour is untouched.
|
|
480
|
+
if (ctx.analysis)
|
|
481
|
+
return { schema: deepFreezeCopy(value), test: null };
|
|
382
482
|
fail('JQ0008', 'schema operators require a type-test compiler (options.compileTypeTest)', opPath);
|
|
483
|
+
}
|
|
383
484
|
const schema = deepFreezeCopy(value);
|
|
384
485
|
let test;
|
|
385
486
|
try {
|
|
386
487
|
test = ctx.compileTypeTest(schema, schemaPath);
|
|
387
488
|
}
|
|
388
489
|
catch (e) {
|
|
389
|
-
fail('JQ0009', `invalid schema literal: ${e
|
|
490
|
+
fail('JQ0009', `invalid schema literal: ${hostFailureText(e)}`, opPath, { cause: e });
|
|
390
491
|
}
|
|
391
492
|
if (typeof test !== 'function')
|
|
392
493
|
fail('JQ0009', 'the type-test compiler did not return a predicate function', opPath);
|
|
@@ -404,7 +505,7 @@ function makeRaw(value, docPath) {
|
|
|
404
505
|
// One argument position of a registry operator, per its declared kind:
|
|
405
506
|
// 'expr' normalizes an ordinary expression; 'raw' captures the value
|
|
406
507
|
// verbatim, unevaluated; 'schema' is 'raw' plus a compiled type-test
|
|
407
|
-
// predicate
|
|
508
|
+
// predicate, for schema-literal arguments (`compileSchemaLiteral`); 'name'
|
|
408
509
|
// captures a validated variable name string. 'raw', 'schema' and 'name'
|
|
409
510
|
// produce inert `raw` nodes - compile-time data, never compiled.
|
|
410
511
|
function normalizeArg(kind, value, argPath, scope, ctx, opPath) {
|
|
@@ -450,9 +551,15 @@ function argCards(args) {
|
|
|
450
551
|
// `result` - individual operators never re-check structure.
|
|
451
552
|
function normalizeOperatorCall(key, entry, arg, docPath, opPath, scope, ctx) {
|
|
452
553
|
const args = normalizeParams(key, entry.params, arg, opPath, scope, ctx);
|
|
453
|
-
|
|
554
|
+
/** @type {any} */
|
|
555
|
+
const node = {
|
|
454
556
|
kind: 'op', card: entry.result(argCards(args)), docPath, name: key, args: Object.freeze(args),
|
|
455
|
-
}
|
|
557
|
+
};
|
|
558
|
+
// $range materializes its whole result: the resource guard becomes
|
|
559
|
+
// configurable through the compilation's limits (compileOp hands the
|
|
560
|
+
// node through to the entry's compile)
|
|
561
|
+
if (key === '$range' && ctx.limits !== null) node.limits = ctx.limits;
|
|
562
|
+
return Object.freeze(node);
|
|
456
563
|
}
|
|
457
564
|
|
|
458
565
|
// Helpers handed to an extension entry's `normalize` override; see
|
|
@@ -485,10 +592,30 @@ function normalizeExtensionCall(key, entry, arg, docPath, opPath, scope, ctx) {
|
|
|
485
592
|
|
|
486
593
|
function normalizeOperator(key, arg, docPath, scope, ctx) {
|
|
487
594
|
const opPath = docPath + '/' + key;
|
|
595
|
+
ctx.usedOps.add(key); // dependency reporting (query.explain)
|
|
488
596
|
switch (key) {
|
|
489
597
|
case '$const': // quote: verbatim single item, nothing inside evaluated
|
|
490
598
|
return Object.freeze({ kind: 'literal', card: CARD_ONE, docPath, value: deepFreezeCopy(arg) });
|
|
491
599
|
|
|
600
|
+
case '$call': { // a registered trusted host function (options.functions)
|
|
601
|
+
if (!Array.isArray(arg) || arg.length < 1 || typeof arg[0] !== 'string')
|
|
602
|
+
return fail('JQ0010', "'$call' requires ['name', ...argument expressions]", opPath);
|
|
603
|
+
const name = arg[0];
|
|
604
|
+
const fn = ctx.functions !== null && hasOwn(ctx.functions, name)
|
|
605
|
+
? ctx.functions[name]
|
|
606
|
+
: undefined;
|
|
607
|
+
if (fn === undefined)
|
|
608
|
+
return fail('JQ0010', `'$call' names no registered function '${name}'`, opPath);
|
|
609
|
+
ctx.usedFunctions.add(name);
|
|
610
|
+
const callArgs = new Array(arg.length - 1);
|
|
611
|
+
for (let i = 1; i < arg.length; i++)
|
|
612
|
+
callArgs[i - 1] = normalizeExpr(arg[i], opPath + '/' + i, scope, ctx);
|
|
613
|
+
return Object.freeze({
|
|
614
|
+
kind: 'call', card: CARD_OPT, docPath, name, fn,
|
|
615
|
+
args: Object.freeze(callArgs),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
492
619
|
case '$map': { // general map constructor (section 3.5.2)
|
|
493
620
|
const list = requireExprArray(key, arg, 0, Infinity, opPath);
|
|
494
621
|
const pairs = new Array(list.length);
|
|
@@ -538,7 +665,7 @@ function bindPhraseName(name, phraseNames, bindPath) {
|
|
|
538
665
|
}
|
|
539
666
|
|
|
540
667
|
function requireBindingObject(clause, bindObj, clausePath) {
|
|
541
|
-
if (!
|
|
668
|
+
if (!isJsonObject(bindObj))
|
|
542
669
|
fail('JQ0003', `'${clause}' takes an object of variable bindings`, clausePath);
|
|
543
670
|
const names = Object.keys(bindObj);
|
|
544
671
|
if (names.length === 0)
|
|
@@ -556,11 +683,11 @@ function normalizeLetBindings(letObj, letPath, scope, ctx, phraseNames, bindings
|
|
|
556
683
|
let sc = scope;
|
|
557
684
|
for (let i = 0; i < names.length; i++) {
|
|
558
685
|
const name = names[i];
|
|
559
|
-
const bindPath = letPath + '/' +
|
|
686
|
+
const bindPath = letPath + '/' + encodeJSONPointerSegment(name);
|
|
560
687
|
bindPhraseName(name, phraseNames, bindPath);
|
|
561
688
|
const source = letObj[name];
|
|
562
|
-
if (
|
|
563
|
-
return fail('JQ0003', "the extended
|
|
689
|
+
if (isExtendedBinding(source))
|
|
690
|
+
return fail('JQ0003', "the extended binding form is not available in '$let'", bindPath);
|
|
564
691
|
const expr = normalizeExpr(source, bindPath, sc, ctx);
|
|
565
692
|
const slot = ctx.nextSlot++;
|
|
566
693
|
sc = { name, slot, card: expr.card, parent: sc };
|
|
@@ -581,6 +708,34 @@ function normalizeLetPhrase(obj, docPath, scope, ctx) {
|
|
|
581
708
|
});
|
|
582
709
|
}
|
|
583
710
|
|
|
711
|
+
// The window specification of an extended $for binding (section 6.10).
|
|
712
|
+
// `$size`/`$step` are integer literals, not expressions: a window width
|
|
713
|
+
// that varied per tuple could not be compiled into a specialized loop,
|
|
714
|
+
// and no use has asked for one.
|
|
715
|
+
//
|
|
716
|
+
// A **tumbling** window partitions the item stream - every item lands in
|
|
717
|
+
// exactly one window - so its final window is kept even when short,
|
|
718
|
+
// because dropping it would silently lose data. A **sliding** window is
|
|
719
|
+
// a moving view of fixed width, so a short window is not one of them and
|
|
720
|
+
// the tail is not emitted. That is the whole rule.
|
|
721
|
+
function normalizeWindowSpec(source, bindPath) {
|
|
722
|
+
const kind = source.$window;
|
|
723
|
+
if (kind !== 'tumbling' && kind !== 'sliding')
|
|
724
|
+
return fail('JQ0003', "'$window' must be 'tumbling' or 'sliding'", bindPath + '/$window');
|
|
725
|
+
if (!hasOwn(source, '$size'))
|
|
726
|
+
return fail('JQ0003', "a '$window' binding requires '$size'", bindPath);
|
|
727
|
+
const size = source.$size;
|
|
728
|
+
if (!Number.isInteger(size) || size < 1)
|
|
729
|
+
return fail('JQ0003', "'$size' must be a positive integer", bindPath + '/$size');
|
|
730
|
+
let step = kind === 'tumbling' ? size : 1;
|
|
731
|
+
if (hasOwn(source, '$step')) {
|
|
732
|
+
step = source.$step;
|
|
733
|
+
if (!Number.isInteger(step) || step < 1)
|
|
734
|
+
return fail('JQ0003', "'$step' must be a positive integer", bindPath + '/$step');
|
|
735
|
+
}
|
|
736
|
+
return Object.freeze({ sliding: kind === 'sliding', size, step });
|
|
737
|
+
}
|
|
738
|
+
|
|
584
739
|
// $for bindings (section 6.2): each name iterates its source, one item
|
|
585
740
|
// per tuple (card ONE), with D4 array unpacking at runtime. The extended
|
|
586
741
|
// {"$in": expr, "$at": name} form additionally binds a 0-based position
|
|
@@ -591,23 +746,45 @@ function normalizeForBindings(forObj, forPath, scope, ctx, phraseNames, bindings
|
|
|
591
746
|
let sc = scope;
|
|
592
747
|
for (let i = 0; i < names.length; i++) {
|
|
593
748
|
const name = names[i];
|
|
594
|
-
const bindPath = forPath + '/' +
|
|
749
|
+
const bindPath = forPath + '/' + encodeJSONPointerSegment(name);
|
|
595
750
|
bindPhraseName(name, phraseNames, bindPath);
|
|
596
751
|
let source = forObj[name];
|
|
597
752
|
let sourcePath = bindPath;
|
|
598
753
|
let atName = null;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
754
|
+
let allowingEmpty = false;
|
|
755
|
+
let window = null;
|
|
756
|
+
if (isExtendedBinding(source)) {
|
|
757
|
+
const bindKeys = Object.keys(source);
|
|
758
|
+
for (let k = 0; k < bindKeys.length; k++) {
|
|
759
|
+
if (!FOR_BINDING_KEYS.has(bindKeys[k]))
|
|
760
|
+
return fail('JQ0003', `'${bindKeys[k]}' is not a valid key of an extended '$for' binding`, bindPath);
|
|
761
|
+
}
|
|
762
|
+
if (!hasOwn(source, '$in'))
|
|
763
|
+
return fail('JQ0003', "an extended '$for' binding requires '$in'", bindPath);
|
|
764
|
+
if (hasOwn(source, '$at')) {
|
|
765
|
+
atName = source.$at;
|
|
766
|
+
if (typeof atName !== 'string' || !VAR_NAME_RE.test(atName))
|
|
767
|
+
return fail('JQ0003', "'$at' takes a variable name string", bindPath + '/$at');
|
|
768
|
+
}
|
|
769
|
+
if (hasOwn(source, '$allowing-empty')) {
|
|
770
|
+
if (typeof source['$allowing-empty'] !== 'boolean')
|
|
771
|
+
return fail('JQ0003', "'$allowing-empty' takes a boolean", bindPath + '/$allowing-empty');
|
|
772
|
+
allowingEmpty = source['$allowing-empty'];
|
|
773
|
+
}
|
|
774
|
+
if (hasOwn(source, '$window'))
|
|
775
|
+
window = normalizeWindowSpec(source, bindPath);
|
|
776
|
+
else if (hasOwn(source, '$size') || hasOwn(source, '$step'))
|
|
777
|
+
return fail('JQ0003', "'$size'/'$step' require '$window'", bindPath);
|
|
605
778
|
source = source.$in;
|
|
606
779
|
sourcePath = bindPath + '/$in';
|
|
607
780
|
}
|
|
608
781
|
const expr = normalizeExpr(source, sourcePath, sc, ctx);
|
|
609
782
|
const slot = ctx.nextSlot++;
|
|
610
|
-
|
|
783
|
+
// a window variable holds the window's items, and an $allowing-empty
|
|
784
|
+
// binding may hold the empty sequence: neither is the plain
|
|
785
|
+
// exactly-one-item binding the compiler specializes for
|
|
786
|
+
const bindCard = window !== null ? CARD_MANY : (allowingEmpty ? CARD_OPT : CARD_ONE);
|
|
787
|
+
sc = { name, slot, card: bindCard, parent: sc };
|
|
611
788
|
tupleSlots.push({ name, slot });
|
|
612
789
|
let atSlot = -1;
|
|
613
790
|
if (atName !== null) {
|
|
@@ -616,19 +793,25 @@ function normalizeForBindings(forObj, forPath, scope, ctx, phraseNames, bindings
|
|
|
616
793
|
sc = { name: atName, slot: atSlot, card: CARD_ONE, parent: sc };
|
|
617
794
|
tupleSlots.push({ name: atName, slot: atSlot });
|
|
618
795
|
}
|
|
619
|
-
bindings.push(Object.freeze({ name, slot, expr, atSlot }));
|
|
796
|
+
bindings.push(Object.freeze({ name, slot, expr, atSlot, allowingEmpty, window }));
|
|
620
797
|
}
|
|
621
798
|
return sc;
|
|
622
799
|
}
|
|
623
800
|
|
|
624
801
|
// One $orderby key spec (section 6.6): an expression (ascending,
|
|
625
|
-
// empty-least) or the explicit {"$key", "$dir"?, "$empty"
|
|
802
|
+
// empty-least) or the explicit {"$key", "$dir"?, "$empty"?,
|
|
803
|
+
// "$collation"?} form. A $collation names a registered pure compare
|
|
804
|
+
// function (options.collations) applied to STRING keys; number keys
|
|
805
|
+
// keep numeric order.
|
|
626
806
|
function normalizeOrderbySpec(spec, specPath, scope, ctx) {
|
|
627
807
|
let key = spec;
|
|
628
808
|
let keyPath = specPath;
|
|
629
809
|
let desc = false;
|
|
630
810
|
let emptyGreatest = false;
|
|
631
|
-
|
|
811
|
+
let collation = null;
|
|
812
|
+
let collationName = null;
|
|
813
|
+
if (isJsonObject(spec)
|
|
814
|
+
&& (hasOwn(spec, '$key') || hasOwn(spec, '$dir') || hasOwn(spec, '$empty') || hasOwn(spec, '$collation'))) {
|
|
632
815
|
const specKeys = Object.keys(spec);
|
|
633
816
|
for (let i = 0; i < specKeys.length; i++) {
|
|
634
817
|
if (!ORDERBY_SPEC_KEYS.has(specKeys[i]))
|
|
@@ -646,21 +829,37 @@ function normalizeOrderbySpec(spec, specPath, scope, ctx) {
|
|
|
646
829
|
return fail('JQ0003', "'$empty' must be 'least' or 'greatest'", specPath + '/$empty');
|
|
647
830
|
emptyGreatest = spec.$empty === 'greatest';
|
|
648
831
|
}
|
|
832
|
+
if (hasOwn(spec, '$collation')) {
|
|
833
|
+
if (typeof spec.$collation !== 'string')
|
|
834
|
+
return fail('JQ0003', "'$collation' must be a registered collation name", specPath + '/$collation');
|
|
835
|
+
collationName = spec.$collation;
|
|
836
|
+
collation = ctx.collations !== null && hasOwn(ctx.collations, collationName)
|
|
837
|
+
? ctx.collations[collationName]
|
|
838
|
+
: undefined;
|
|
839
|
+
if (collation === undefined)
|
|
840
|
+
return fail('JQ0010', `'$collation' names no registered collation '${collationName}'`, specPath + '/$collation');
|
|
841
|
+
ctx.usedCollations.add(collationName);
|
|
842
|
+
}
|
|
649
843
|
key = spec.$key;
|
|
650
844
|
keyPath = specPath + '/$key';
|
|
651
845
|
}
|
|
652
846
|
return Object.freeze({
|
|
653
847
|
key: normalizeExpr(key, keyPath, scope, ctx),
|
|
654
|
-
desc, emptyGreatest, docPath: specPath,
|
|
848
|
+
desc, emptyGreatest, collation, collationName, docPath: specPath,
|
|
655
849
|
});
|
|
656
850
|
}
|
|
657
851
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
|
|
663
|
-
|
|
852
|
+
// Barrier liveness: $groupby/$orderby materialize only the phrase
|
|
853
|
+
// binding slots that later clauses read. Slots written by nested phrases
|
|
854
|
+
// before being read merely widen a snapshot harmlessly. The hash-join
|
|
855
|
+
// planner (compile.js) reuses this to prove a probe side uncorrelated.
|
|
856
|
+
/**
|
|
857
|
+
* Collect the frame slots an expression subtree reads (variable
|
|
858
|
+
* references and path roots) into `out`. Over-approximation is safe.
|
|
859
|
+
* @param {object} node - a frozen AST node
|
|
860
|
+
* @param {Set<number>} out - accumulator of slot indexes
|
|
861
|
+
*/
|
|
862
|
+
export function collectReadSlots(node, out) {
|
|
664
863
|
switch (node.kind) {
|
|
665
864
|
case 'literal':
|
|
666
865
|
return;
|
|
@@ -687,6 +886,7 @@ function collectReadSlots(node, out) {
|
|
|
687
886
|
case 'raw': // compile-time data of a registry operator, never evaluated
|
|
688
887
|
return;
|
|
689
888
|
case 'op':
|
|
889
|
+
case 'call':
|
|
690
890
|
for (let i = 0; i < node.args.length; i++)
|
|
691
891
|
collectReadSlots(node.args[i], out);
|
|
692
892
|
return;
|
|
@@ -701,6 +901,8 @@ function collectReadSlots(node, out) {
|
|
|
701
901
|
collectReadSlots(node.satisfies, out);
|
|
702
902
|
return;
|
|
703
903
|
default: { // 'flwor'
|
|
904
|
+
if (node.fold !== null)
|
|
905
|
+
collectReadSlots(node.fold.expr, out);
|
|
704
906
|
for (let i = 0; i < node.forBindings.length; i++)
|
|
705
907
|
collectReadSlots(node.forBindings[i].expr, out);
|
|
706
908
|
for (let i = 0; i < node.letBindings.length; i++)
|
|
@@ -739,8 +941,34 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
|
739
941
|
const letBindings = [];
|
|
740
942
|
let sc = scope;
|
|
741
943
|
|
|
944
|
+
// $fold (section 6.9): the accumulator clause. Its initial value is
|
|
945
|
+
// evaluated ONCE, in the enclosing scope, before the tuple stream
|
|
946
|
+
// starts; $return then yields the accumulator's next value per
|
|
947
|
+
// surviving tuple, and the phrase's value is the final accumulator
|
|
948
|
+
// instead of the collected $return sequence. This is what gives the
|
|
949
|
+
// language its fold without giving JSON a way to spell a function
|
|
950
|
+
// value: the accumulator is a binding, not a lambda parameter.
|
|
951
|
+
let fold = null;
|
|
952
|
+
if (hasOwn(obj, '$fold')) {
|
|
953
|
+
const foldPath = docPath + '/$fold';
|
|
954
|
+
const names = requireBindingObject('$fold', obj.$fold, foldPath);
|
|
955
|
+
if (names.length !== 1)
|
|
956
|
+
return fail('JQ0003', "'$fold' takes exactly one accumulator binding", foldPath);
|
|
957
|
+
const name = names[0];
|
|
958
|
+
const bindPath = foldPath + '/' + encodeJSONPointerSegment(name);
|
|
959
|
+
bindPhraseName(name, phraseNames, bindPath);
|
|
960
|
+
// the initial value cannot see this phrase's own bindings
|
|
961
|
+
const expr = normalizeExpr(obj.$fold[name], bindPath, scope, ctx);
|
|
962
|
+
fold = { name, slot: ctx.nextSlot++, expr, docPath: bindPath };
|
|
963
|
+
}
|
|
964
|
+
|
|
742
965
|
if (hasOwn(obj, '$for'))
|
|
743
966
|
sc = normalizeForBindings(obj.$for, docPath + '/$for', sc, ctx, phraseNames, forBindings, tupleSlots);
|
|
967
|
+
// the accumulator binds after $for - a $for source is iterated once and
|
|
968
|
+
// must not depend on a value that changes per tuple - and before $let,
|
|
969
|
+
// so every later clause sees the accumulation so far
|
|
970
|
+
if (fold !== null)
|
|
971
|
+
sc = { name: fold.name, slot: fold.slot, card: CARD_MANY, parent: sc };
|
|
744
972
|
if (hasOwn(obj, '$let')) {
|
|
745
973
|
const before = letBindings.length;
|
|
746
974
|
sc = normalizeLetBindings(obj.$let, docPath + '/$let', sc, ctx, phraseNames, letBindings);
|
|
@@ -757,7 +985,7 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
|
757
985
|
if (hasOwn(obj, '$as')) {
|
|
758
986
|
const asPath = docPath + '/$as';
|
|
759
987
|
const asObj = obj.$as;
|
|
760
|
-
if (!
|
|
988
|
+
if (!isJsonObject(asObj))
|
|
761
989
|
return fail('JQ0003', "'$as' takes an object of variable-name to schema members", asPath);
|
|
762
990
|
const names = Object.keys(asObj);
|
|
763
991
|
if (names.length === 0)
|
|
@@ -765,7 +993,7 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
|
765
993
|
const checks = new Array(names.length);
|
|
766
994
|
for (let i = 0; i < names.length; i++) {
|
|
767
995
|
const name = names[i];
|
|
768
|
-
const checkPath = asPath + '/' +
|
|
996
|
+
const checkPath = asPath + '/' + encodeJSONPointerSegment(name);
|
|
769
997
|
let slot = -1;
|
|
770
998
|
for (let j = 0; j < tupleSlots.length; j++) {
|
|
771
999
|
if (tupleSlots[j].name === name) {
|
|
@@ -799,7 +1027,7 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
|
799
1027
|
const keys = new Array(names.length);
|
|
800
1028
|
for (let i = 0; i < names.length; i++) {
|
|
801
1029
|
const name = names[i];
|
|
802
|
-
const bindPath = groupPath + '/' +
|
|
1030
|
+
const bindPath = groupPath + '/' + encodeJSONPointerSegment(name);
|
|
803
1031
|
bindPhraseName(name, phraseNames, bindPath);
|
|
804
1032
|
// key expressions evaluate per tuple, in the pre-group scope
|
|
805
1033
|
const expr = normalizeExpr(obj.$groupby[name], bindPath, sc, ctx);
|
|
@@ -807,6 +1035,10 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
|
807
1035
|
}
|
|
808
1036
|
// post-group scope: same slots, rebound cards
|
|
809
1037
|
sc = scope;
|
|
1038
|
+
// the accumulator is not a tuple variable - $groupby does not rebind
|
|
1039
|
+
// it, it keeps accumulating, now once per group
|
|
1040
|
+
if (fold !== null)
|
|
1041
|
+
sc = { name: fold.name, slot: fold.slot, card: CARD_MANY, parent: sc };
|
|
810
1042
|
for (let i = 0; i < tupleSlots.length; i++)
|
|
811
1043
|
sc = { name: tupleSlots[i].name, slot: tupleSlots[i].slot, card: CARD_MANY, parent: sc };
|
|
812
1044
|
for (let i = 0; i < keys.length; i++)
|
|
@@ -888,19 +1120,25 @@ function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
|
888
1120
|
// phrase cardinality: MANY unless provably otherwise - a $let-only
|
|
889
1121
|
// phrase yields exactly one tuple ($where may still drop it)
|
|
890
1122
|
let card;
|
|
891
|
-
if (
|
|
1123
|
+
if (fold !== null)
|
|
1124
|
+
// the phrase IS the accumulator: either it never updated (the initial
|
|
1125
|
+
// value) or it holds some $return result
|
|
1126
|
+
card = joinCard(fold.expr.card, ret.card);
|
|
1127
|
+
else if (forBindings.length !== 0 || groupby !== null)
|
|
892
1128
|
card = CARD_MANY;
|
|
893
1129
|
else
|
|
894
1130
|
card = where !== null ? joinCard(ret.card, CARD_ZERO) : ret.card;
|
|
895
1131
|
|
|
896
1132
|
return Object.freeze({
|
|
897
1133
|
kind: 'flwor', card, docPath,
|
|
1134
|
+
fold: fold === null ? null : Object.freeze(fold),
|
|
898
1135
|
forBindings: Object.freeze(forBindings),
|
|
899
1136
|
letBindings: Object.freeze(letBindings),
|
|
900
1137
|
asChecks, where,
|
|
901
1138
|
groupby: groupby === null ? null : Object.freeze(groupby),
|
|
902
1139
|
orderby: orderby === null ? null : Object.freeze(orderby),
|
|
903
1140
|
count, ret,
|
|
1141
|
+
limits: ctx.limits,
|
|
904
1142
|
});
|
|
905
1143
|
}
|
|
906
1144
|
|
|
@@ -918,11 +1156,11 @@ function normalizeQuantifierPhrase(obj, docPath, scope, ctx) {
|
|
|
918
1156
|
let sc = scope;
|
|
919
1157
|
for (let i = 0; i < names.length; i++) {
|
|
920
1158
|
const name = names[i];
|
|
921
|
-
const bindPath = clausePath + '/' +
|
|
1159
|
+
const bindPath = clausePath + '/' + encodeJSONPointerSegment(name);
|
|
922
1160
|
bindPhraseName(name, phraseNames, bindPath);
|
|
923
1161
|
const source = bindObj[name];
|
|
924
|
-
if (
|
|
925
|
-
return fail('JQ0003',
|
|
1162
|
+
if (isExtendedBinding(source))
|
|
1163
|
+
return fail('JQ0003', 'the extended binding form is not available in quantifiers', bindPath);
|
|
926
1164
|
const expr = normalizeExpr(source, bindPath, sc, ctx);
|
|
927
1165
|
const slot = ctx.nextSlot++;
|
|
928
1166
|
sc = { name, slot, card: CARD_ONE, parent: sc };
|
|
@@ -945,22 +1183,160 @@ function normalizeQuantifierPhrase(obj, docPath, scope, ctx) {
|
|
|
945
1183
|
// vocabulary - the closed format is unchanged, extensions are host
|
|
946
1184
|
// machinery. Violations are host programming errors (TypeError), not
|
|
947
1185
|
// JQ0xxx document errors.
|
|
1186
|
+
// True when `name` is already part of the closed query vocabulary — a
|
|
1187
|
+
// FLWOR/quantifier/escape key, a reserved member, or a core operator.
|
|
1188
|
+
// A host extension (options.extensions) or a registry pack (the JSLT
|
|
1189
|
+
// operator registry) must not shadow any of these. Exported so the
|
|
1190
|
+
// registry builder can reject a colliding pack at `.use()` time with the
|
|
1191
|
+
// same rule the compiler enforces at normalize time.
|
|
1192
|
+
export function isReservedQueryName(name) {
|
|
1193
|
+
return FLWOR_KEYS.has(name) || QUANTIFIER_KEYS.has(name) || ESCAPE_KEYS.has(name)
|
|
1194
|
+
|| ORDERBY_SPEC_KEYS.has(name) || name === '$in' || name === '$at'
|
|
1195
|
+
|| name === '$query' || name === '$expr' || hasOwn(OPERATORS, name);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
948
1198
|
function validateExtensions(extensions) {
|
|
949
|
-
if (!
|
|
1199
|
+
if (!isJsonObject(extensions))
|
|
950
1200
|
throw new TypeError('options.extensions must be a plain object of operator entries');
|
|
951
1201
|
const names = Object.keys(extensions);
|
|
952
1202
|
for (let i = 0; i < names.length; i++) {
|
|
953
1203
|
const name = names[i];
|
|
954
1204
|
if (name.charCodeAt(0) !== 0x24) // '$'
|
|
955
1205
|
throw new TypeError(`extension operator '${name}' must start with '$'`);
|
|
956
|
-
if (
|
|
957
|
-
|| ORDERBY_SPEC_KEYS.has(name) || name === '$in' || name === '$at'
|
|
958
|
-
|| name === '$query' || name === '$expr' || hasOwn(OPERATORS, name))
|
|
1206
|
+
if (isReservedQueryName(name))
|
|
959
1207
|
throw new TypeError(`extension operator '${name}' collides with the core vocabulary`);
|
|
960
1208
|
}
|
|
961
1209
|
return extensions;
|
|
962
1210
|
}
|
|
963
1211
|
|
|
1212
|
+
// Validate a named registry of trusted pure host functions
|
|
1213
|
+
// (options.functions / options.collations): a plain object of
|
|
1214
|
+
// `name -> function`. Violations are host programming errors
|
|
1215
|
+
// (TypeError), like options.extensions.
|
|
1216
|
+
function validateNamedFunctions(value, what) {
|
|
1217
|
+
if (!isJsonObject(value))
|
|
1218
|
+
throw new TypeError(`options.${what} must be a plain object of named functions`);
|
|
1219
|
+
for (const name in value) {
|
|
1220
|
+
// the schema twins require non-empty names (minLength 1); the
|
|
1221
|
+
// registry enforces the same, so document and runtime agree
|
|
1222
|
+
if (name === '')
|
|
1223
|
+
throw new TypeError(`options.${what} must not register an empty-string name`);
|
|
1224
|
+
if (typeof value[name] !== 'function')
|
|
1225
|
+
throw new TypeError(`options.${what}['${name}'] must be a function`);
|
|
1226
|
+
}
|
|
1227
|
+
return value;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// The known limits (section 8.12). Only limits the engine actually
|
|
1231
|
+
// enforces are accepted - an accepted-but-unenforced limit would be a
|
|
1232
|
+
// silent false guarantee.
|
|
1233
|
+
const LIMIT_NAMES = new Set(['sequenceItems', 'resultItems', 'steps', 'depth']);
|
|
1234
|
+
|
|
1235
|
+
function validateLimits(value) {
|
|
1236
|
+
if (!isJsonObject(value))
|
|
1237
|
+
throw new TypeError('options.limits must be a plain object');
|
|
1238
|
+
for (const name in value) {
|
|
1239
|
+
if (!LIMIT_NAMES.has(name))
|
|
1240
|
+
throw new TypeError(`options.limits.${name} is not a known limit`);
|
|
1241
|
+
const v = value[name];
|
|
1242
|
+
if (!Number.isInteger(v) || v < 1)
|
|
1243
|
+
throw new TypeError(`options.limits.${name} must be a positive integer`);
|
|
1244
|
+
}
|
|
1245
|
+
return Object.freeze({
|
|
1246
|
+
sequenceItems: value.sequenceItems ?? null,
|
|
1247
|
+
resultItems: value.resultItems ?? null,
|
|
1248
|
+
steps: value.steps ?? null,
|
|
1249
|
+
depth: value.depth ?? null,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Validate `options.externals`: the closed-world declaration list. An
|
|
1254
|
+
// array of variable names (without the '$' sigil); `[]` declares none,
|
|
1255
|
+
// so every free variable is JQ0005. Absent means the open world, where
|
|
1256
|
+
// use is the declaration (section 9).
|
|
1257
|
+
function validateDeclaredExternals(value) {
|
|
1258
|
+
if (!Array.isArray(value))
|
|
1259
|
+
throw new TypeError('options.externals must be an array of variable names');
|
|
1260
|
+
const set = new Set();
|
|
1261
|
+
for (let i = 0; i < value.length; i++) {
|
|
1262
|
+
const name = value[i];
|
|
1263
|
+
if (typeof name !== 'string' || !VAR_NAME_RE.test(name))
|
|
1264
|
+
throw new TypeError(`options.externals[${i}] must be a variable name string (no '$' sigil)`);
|
|
1265
|
+
set.add(name);
|
|
1266
|
+
}
|
|
1267
|
+
return set;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Maximum expression nesting of a normalized AST. The language has no
|
|
1271
|
+
// recursion - no user-defined functions, no self-reference - so the
|
|
1272
|
+
// compiled closure tree's evaluation depth is exactly this static depth,
|
|
1273
|
+
// which is why `limits.depth` is a compile-time check (JQ0011) rather
|
|
1274
|
+
// than a runtime counter: exact, and free at evaluation time. Returns
|
|
1275
|
+
// the deepest node found alongside its depth, for the error position.
|
|
1276
|
+
function measureDepth(node, depth, worst) {
|
|
1277
|
+
if (depth > worst.depth) {
|
|
1278
|
+
worst.depth = depth;
|
|
1279
|
+
worst.docPath = node.docPath;
|
|
1280
|
+
}
|
|
1281
|
+
const next = depth + 1;
|
|
1282
|
+
switch (node.kind) {
|
|
1283
|
+
case 'literal':
|
|
1284
|
+
case 'var':
|
|
1285
|
+
case 'path':
|
|
1286
|
+
case 'raw':
|
|
1287
|
+
return;
|
|
1288
|
+
case 'object':
|
|
1289
|
+
for (let i = 0; i < node.entries.length; i++)
|
|
1290
|
+
measureDepth(node.entries[i].expr, next, worst);
|
|
1291
|
+
return;
|
|
1292
|
+
case 'map':
|
|
1293
|
+
for (let i = 0; i < node.pairs.length; i++) {
|
|
1294
|
+
measureDepth(node.pairs[i].key, next, worst);
|
|
1295
|
+
measureDepth(node.pairs[i].value, next, worst);
|
|
1296
|
+
}
|
|
1297
|
+
return;
|
|
1298
|
+
case 'array':
|
|
1299
|
+
for (let i = 0; i < node.elements.length; i++)
|
|
1300
|
+
measureDepth(node.elements[i], next, worst);
|
|
1301
|
+
return;
|
|
1302
|
+
case 'op':
|
|
1303
|
+
case 'call':
|
|
1304
|
+
for (let i = 0; i < node.args.length; i++)
|
|
1305
|
+
measureDepth(node.args[i], next, worst);
|
|
1306
|
+
return;
|
|
1307
|
+
case 'let':
|
|
1308
|
+
for (let i = 0; i < node.bindings.length; i++)
|
|
1309
|
+
measureDepth(node.bindings[i].expr, next, worst);
|
|
1310
|
+
measureDepth(node.ret, next, worst);
|
|
1311
|
+
return;
|
|
1312
|
+
case 'quant':
|
|
1313
|
+
for (let i = 0; i < node.bindings.length; i++)
|
|
1314
|
+
measureDepth(node.bindings[i].expr, next, worst);
|
|
1315
|
+
measureDepth(node.satisfies, next, worst);
|
|
1316
|
+
return;
|
|
1317
|
+
default: { // 'flwor'
|
|
1318
|
+
for (let i = 0; i < node.forBindings.length; i++)
|
|
1319
|
+
measureDepth(node.forBindings[i].expr, next, worst);
|
|
1320
|
+
for (let i = 0; i < node.letBindings.length; i++)
|
|
1321
|
+
measureDepth(node.letBindings[i].expr, next, worst);
|
|
1322
|
+
if (node.fold !== null)
|
|
1323
|
+
measureDepth(node.fold.expr, next, worst);
|
|
1324
|
+
if (node.where !== null)
|
|
1325
|
+
measureDepth(node.where, next, worst);
|
|
1326
|
+
if (node.groupby !== null) {
|
|
1327
|
+
for (let i = 0; i < node.groupby.keys.length; i++)
|
|
1328
|
+
measureDepth(node.groupby.keys[i].expr, next, worst);
|
|
1329
|
+
}
|
|
1330
|
+
if (node.orderby !== null) {
|
|
1331
|
+
for (let i = 0; i < node.orderby.specs.length; i++)
|
|
1332
|
+
measureDepth(node.orderby.specs[i].key, next, worst);
|
|
1333
|
+
}
|
|
1334
|
+
measureDepth(node.ret, next, worst);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
964
1340
|
function normalizeExpr(value, docPath, scope, ctx) {
|
|
965
1341
|
switch (typeof value) {
|
|
966
1342
|
case 'string':
|
|
@@ -1014,11 +1390,31 @@ export function normalizeQuery(doc, options = {}) {
|
|
|
1014
1390
|
? options.compileTypeTest
|
|
1015
1391
|
: null;
|
|
1016
1392
|
const extensions = options.extensions == null ? null : validateExtensions(options.extensions);
|
|
1017
|
-
const
|
|
1393
|
+
const functions = options.functions == null ? null : validateNamedFunctions(options.functions, 'functions');
|
|
1394
|
+
const collations = options.collations == null ? null : validateNamedFunctions(options.collations, 'collations');
|
|
1395
|
+
const limits = options.limits == null ? null : validateLimits(options.limits);
|
|
1396
|
+
const declaredExternals = options.externals == null
|
|
1397
|
+
? null
|
|
1398
|
+
: validateDeclaredExternals(options.externals);
|
|
1399
|
+
// JSONPath function extensions are a separate registry from
|
|
1400
|
+
// options.functions ($call's host functions): they extend the RFC 9535
|
|
1401
|
+
// grammar inside path strings, not the query vocabulary. Built once
|
|
1402
|
+
// per compile so parsePathString hands the parser the same object.
|
|
1403
|
+
const pathOptions = options.pathFunctions == null
|
|
1404
|
+
? undefined
|
|
1405
|
+
: { pathFunctions: options.pathFunctions };
|
|
1406
|
+
const ctx = {
|
|
1407
|
+
nextSlot: 1, externals: new Map(), compileTypeTest, extensions,
|
|
1408
|
+
functions, collations, limits, pathOptions, declaredExternals,
|
|
1409
|
+
// package-internal: set only by analyzeQuery (Appendix C.1); the
|
|
1410
|
+
// compile entry point never passes it
|
|
1411
|
+
analysis: options.analysis === true,
|
|
1412
|
+
usedOps: new Set(), usedFunctions: new Set(), usedCollations: new Set(),
|
|
1413
|
+
};
|
|
1018
1414
|
let expr = doc;
|
|
1019
1415
|
let rootPath = '';
|
|
1020
1416
|
// the version envelope is only recognized at the top level (section 4)
|
|
1021
|
-
if (
|
|
1417
|
+
if (isJsonObject(doc) && (hasOwn(doc, '$query') || hasOwn(doc, '$expr'))) {
|
|
1022
1418
|
const keys = Object.keys(doc);
|
|
1023
1419
|
let allDollar = true;
|
|
1024
1420
|
for (let i = 0; i < keys.length; i++) {
|
|
@@ -1035,11 +1431,26 @@ export function normalizeQuery(doc, options = {}) {
|
|
|
1035
1431
|
}
|
|
1036
1432
|
}
|
|
1037
1433
|
const root = normalizeExpr(expr, rootPath, null, ctx);
|
|
1434
|
+
if (limits !== null && limits.depth !== null) {
|
|
1435
|
+
const worst = { depth: 0, docPath: rootPath };
|
|
1436
|
+
measureDepth(root, 1, worst);
|
|
1437
|
+
if (worst.depth > limits.depth)
|
|
1438
|
+
fail('JQ0011', `the query nests ${worst.depth} expressions deep, more than limits.depth (${limits.depth})`, worst.docPath);
|
|
1439
|
+
}
|
|
1440
|
+
// limits.steps instruments every node evaluation; the counter lives in
|
|
1441
|
+
// its own frame slot, so nothing is threaded through the closures
|
|
1442
|
+
const stepSlot = limits !== null && limits.steps !== null ? ctx.nextSlot++ : -1;
|
|
1038
1443
|
const externals = new Array(ctx.externals.size);
|
|
1039
1444
|
let i = 0;
|
|
1040
1445
|
for (const [name, slot] of ctx.externals)
|
|
1041
1446
|
externals[i++] = Object.freeze({ name, slot });
|
|
1042
|
-
return {
|
|
1447
|
+
return {
|
|
1448
|
+
root, frameSize: ctx.nextSlot, externals: Object.freeze(externals),
|
|
1449
|
+
limits, stepSlot,
|
|
1450
|
+
usedOps: ctx.usedOps,
|
|
1451
|
+
usedFunctions: ctx.usedFunctions,
|
|
1452
|
+
usedCollations: ctx.usedCollations,
|
|
1453
|
+
};
|
|
1043
1454
|
}
|
|
1044
1455
|
|
|
1045
1456
|
//#endregion
|