@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,1047 @@
|
|
|
1
|
+
//#region Jaren JSON Query normalizer
|
|
2
|
+
// Single recursive pass over a query document (QUERY-FORMAT.md sections
|
|
3
|
+
// 3-9) producing the internal AST: plain frozen `{kind, card, docPath, ...}`
|
|
4
|
+
// nodes. All classification decisions - object partitioning, the closed
|
|
5
|
+
// phrase vocabulary, string forms, scope resolution - happen here, so the
|
|
6
|
+
// compiler (compile.js) only ever sees well-formed nodes.
|
|
7
|
+
//
|
|
8
|
+
// Static cardinality analysis: every node carries `card`, an upper
|
|
9
|
+
// approximation of its runtime sequence length (CARD_ONE means "always
|
|
10
|
+
// exactly one item"). compile.js uses it to emit singleton-mode closures
|
|
11
|
+
// that skip all sequence checks - the query-engine analogue of path.js's
|
|
12
|
+
// singular-query fast path.
|
|
13
|
+
//
|
|
14
|
+
// Scoping: lexical environments map variable names to frame slot indices.
|
|
15
|
+
// Each compiled query evaluates against one frame array; slot 0 is the
|
|
16
|
+
// input document, every binding site and every external parameter gets its
|
|
17
|
+
// own slot from a single allocator (nested FLWOR phrases simply keep
|
|
18
|
+
// allocating in the same frame). Free names become external parameters,
|
|
19
|
+
// collected in order of first appearance.
|
|
20
|
+
|
|
21
|
+
import { parseJSONPath, JSONPathSyntaxError } from '../path.js';
|
|
22
|
+
import { isSingularSegments } from '../segments.js';
|
|
23
|
+
import { JsonQueryCompileError } from './errors.js';
|
|
24
|
+
// The operator registry: `name -> { params, result, compile }`. Only
|
|
25
|
+
// referenced inside functions (never at module evaluation time), so the
|
|
26
|
+
// import cycle normalize.js <-> operators.js is initialization-safe.
|
|
27
|
+
import { OPERATORS } from './operators.js';
|
|
28
|
+
|
|
29
|
+
//#region cardinality
|
|
30
|
+
|
|
31
|
+
/** Statically empty (the node always evaluates to the empty sequence). */
|
|
32
|
+
export const CARD_ZERO = 0;
|
|
33
|
+
/** Always exactly one item; compile.js skips all sequence checks. */
|
|
34
|
+
export const CARD_ONE = 1;
|
|
35
|
+
/** Zero or one item. */
|
|
36
|
+
export const CARD_OPT = 2;
|
|
37
|
+
/** Any number of items (the analysis top). */
|
|
38
|
+
export const CARD_MANY = 3;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* join = least upper bound over {ZERO, ONE, OPT, MANY}: the cardinality
|
|
42
|
+
* of "one of the two branches" ($if).
|
|
43
|
+
* @param {number} a - a CARD_* value
|
|
44
|
+
* @param {number} b - a CARD_* value
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
export function joinCard(a, b) {
|
|
48
|
+
if (a === b)
|
|
49
|
+
return a;
|
|
50
|
+
if (a === CARD_MANY || b === CARD_MANY)
|
|
51
|
+
return CARD_MANY;
|
|
52
|
+
return CARD_OPT;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* sum = cardinality of two concatenated sequences ($seq); two non-empty
|
|
57
|
+
* contributions can exceed one item, which only MANY can express.
|
|
58
|
+
* @param {number} a - a CARD_* value
|
|
59
|
+
* @param {number} b - a CARD_* value
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
export function sumCard(a, b) {
|
|
63
|
+
if (a === CARD_ZERO)
|
|
64
|
+
return b;
|
|
65
|
+
if (b === CARD_ZERO)
|
|
66
|
+
return a;
|
|
67
|
+
return CARD_MANY;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
|
|
72
|
+
//#region vocabulary tables
|
|
73
|
+
|
|
74
|
+
const hasOwn = Object.hasOwn;
|
|
75
|
+
|
|
76
|
+
const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
77
|
+
const VAR_HEAD_RE = /^\$([A-Za-z_][A-Za-z0-9_]*)/;
|
|
78
|
+
|
|
79
|
+
// FLWOR clause keys (QUERY-FORMAT.md section 6.1) and quantifier keys
|
|
80
|
+
// (section 7). Clauses apply in the fixed semantic order of section 6.1
|
|
81
|
+
// regardless of JSON key order (D7).
|
|
82
|
+
const FLWOR_KEYS = new Set(['$for', '$let', '$as', '$where', '$groupby', '$orderby', '$count', '$return']);
|
|
83
|
+
const QUANTIFIER_KEYS = new Set(['$some', '$every', '$satisfies']);
|
|
84
|
+
|
|
85
|
+
// Keys of the explicit $orderby key-spec form (section 6.6). Contextual:
|
|
86
|
+
// they are not operators and stay outside the KNOWN_KEYS vocabulary.
|
|
87
|
+
const ORDERBY_SPEC_KEYS = new Set(['$key', '$dir', '$empty']);
|
|
88
|
+
|
|
89
|
+
// Escape hatches (QUERY-FORMAT.md section 3.5): structural forms with
|
|
90
|
+
// dedicated normalizer cases; every other operator lives in the registry.
|
|
91
|
+
const ESCAPE_KEYS = new Set(['$const', '$map']);
|
|
92
|
+
|
|
93
|
+
// The complete closed vocabulary decides JQ0002 (unknown key) versus
|
|
94
|
+
// JQ0003 (known keys in an invalid combination) for phrase objects.
|
|
95
|
+
// A function, not a precomputed set: the registry must never be read at
|
|
96
|
+
// module evaluation time (import cycle with operators.js). Host extension
|
|
97
|
+
// operators (ctx.extensions, see validateExtensions) count as vocabulary
|
|
98
|
+
// for the compile they were passed to.
|
|
99
|
+
function isVocabularyKey(key, ctx) {
|
|
100
|
+
return FLWOR_KEYS.has(key) || QUANTIFIER_KEYS.has(key)
|
|
101
|
+
|| ESCAPE_KEYS.has(key) || hasOwn(OPERATORS, key)
|
|
102
|
+
|| (ctx.extensions !== null && hasOwn(ctx.extensions, key));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
|
|
107
|
+
//#region helpers
|
|
108
|
+
|
|
109
|
+
function isPlainObject(v) {
|
|
110
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
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');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fail(code, message, docPath) {
|
|
121
|
+
throw new JsonQueryCompileError(code, message, docPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Deep-copy a JSON value and freeze every object/array in the copy.
|
|
126
|
+
* Used for `$const` values and the compiled query's `.doc` property, so
|
|
127
|
+
* the engine never freezes (or shares mutable state with) caller objects.
|
|
128
|
+
* @param {any} value - a JSON value
|
|
129
|
+
* @returns {any} an independent, deeply frozen copy
|
|
130
|
+
*/
|
|
131
|
+
export function deepFreezeCopy(value) {
|
|
132
|
+
if (typeof value !== 'object' || value === null)
|
|
133
|
+
return value;
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
const out = new Array(value.length);
|
|
136
|
+
for (let i = 0; i < value.length; i++)
|
|
137
|
+
out[i] = deepFreezeCopy(value[i]);
|
|
138
|
+
return Object.freeze(out);
|
|
139
|
+
}
|
|
140
|
+
const out = {};
|
|
141
|
+
const keys = Object.keys(value);
|
|
142
|
+
for (let i = 0; i < keys.length; i++)
|
|
143
|
+
out[keys[i]] = deepFreezeCopy(value[keys[i]]);
|
|
144
|
+
return Object.freeze(out);
|
|
145
|
+
}
|
|
146
|
+
|
|
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
|
+
//#endregion
|
|
157
|
+
|
|
158
|
+
//#region strings (Rule 2)
|
|
159
|
+
|
|
160
|
+
function parsePathString(source, original, docPath) {
|
|
161
|
+
try {
|
|
162
|
+
return parseJSONPath(source);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
/* c8 ignore next 2 -- parseJSONPath only throws syntax errors */
|
|
166
|
+
if (!(e instanceof JSONPathSyntaxError))
|
|
167
|
+
throw e;
|
|
168
|
+
return fail('JQ0004', `'${original}' is not a valid path: ${e.message}`, docPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeLiteral(value, docPath) {
|
|
173
|
+
return Object.freeze({ kind: 'literal', card: CARD_ONE, docPath, value });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function makePathNode(name, rootSlot, external, rootCard, segments, docPath) {
|
|
177
|
+
const singular = isSingularSegments(segments);
|
|
178
|
+
const card = singular
|
|
179
|
+
? (rootCard === CARD_MANY ? CARD_MANY : CARD_OPT)
|
|
180
|
+
: CARD_MANY;
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
kind: 'path', card, docPath,
|
|
183
|
+
name, rootSlot, external, rootCard, segments: deepFreeze(segments), singular,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// resolve a variable reference: walk the lexical scope chain; a free name
|
|
188
|
+
// is an external parameter, allocated a slot on first appearance (spec
|
|
189
|
+
// section 9: use is the declaration)
|
|
190
|
+
function resolveVariable(name, scope, ctx) {
|
|
191
|
+
for (let sc = scope; sc !== null; sc = sc.parent) {
|
|
192
|
+
if (sc.name === name)
|
|
193
|
+
return { slot: sc.slot, card: sc.card, external: false };
|
|
194
|
+
}
|
|
195
|
+
let slot = ctx.externals.get(name);
|
|
196
|
+
if (slot === undefined) {
|
|
197
|
+
slot = ctx.nextSlot++;
|
|
198
|
+
ctx.externals.set(name, slot);
|
|
199
|
+
}
|
|
200
|
+
// an external is bound by the caller to one JSON value: exactly one item
|
|
201
|
+
return { slot, card: CARD_ONE, external: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeString(s, docPath, scope, ctx) {
|
|
205
|
+
if (s.charCodeAt(0) !== 0x24) // '$'
|
|
206
|
+
return makeLiteral(s, docPath);
|
|
207
|
+
if (s.length === 1) // '$' alone: the input document, frame slot 0
|
|
208
|
+
return Object.freeze({ kind: 'var', card: CARD_ONE, docPath, slot: 0, external: false, name: '$' });
|
|
209
|
+
const c1 = s.charCodeAt(1);
|
|
210
|
+
if (c1 === 0x24) // '$$' escape: drop exactly one leading '$'
|
|
211
|
+
return makeLiteral(s.slice(1), docPath);
|
|
212
|
+
if (c1 === 0x2E || c1 === 0x5B) { // '$.' | '$[' | '$..' - absolute path
|
|
213
|
+
const ast = parsePathString(s, s, docPath);
|
|
214
|
+
return makePathNode('$', 0, false, CARD_ONE, ast.segments, docPath);
|
|
215
|
+
}
|
|
216
|
+
const m = VAR_HEAD_RE.exec(s);
|
|
217
|
+
if (m === null)
|
|
218
|
+
return fail('JQ0004', `'${s}' is not a valid path or escape`, docPath);
|
|
219
|
+
const name = m[1];
|
|
220
|
+
const rest = s.slice(m[0].length);
|
|
221
|
+
const ref = resolveVariable(name, scope, ctx);
|
|
222
|
+
if (rest === '') // bare '$name': whole-variable reference
|
|
223
|
+
return Object.freeze({ kind: 'var', card: ref.card, docPath, slot: ref.slot, external: ref.external, name });
|
|
224
|
+
// variable-rooted path: the grammar is RFC 9535 with the root identifier
|
|
225
|
+
// replaced by the variable reference - parse with a substituted '$'
|
|
226
|
+
const ast = parsePathString('$' + rest, s, docPath);
|
|
227
|
+
return makePathNode(name, ref.slot, ref.external, ref.card, ast.segments, docPath);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
|
|
232
|
+
//#region objects (Rule 1)
|
|
233
|
+
|
|
234
|
+
function normalizeObject(obj, docPath, scope, ctx) {
|
|
235
|
+
const keys = Object.keys(obj);
|
|
236
|
+
let dollarCount = 0;
|
|
237
|
+
for (let i = 0; i < keys.length; i++) {
|
|
238
|
+
if (keys[i].charCodeAt(0) === 0x24)
|
|
239
|
+
dollarCount++;
|
|
240
|
+
}
|
|
241
|
+
if (dollarCount === 0) { // map constructor ({} constructs the empty object)
|
|
242
|
+
const entries = new Array(keys.length);
|
|
243
|
+
for (let i = 0; i < keys.length; i++) {
|
|
244
|
+
const name = keys[i];
|
|
245
|
+
entries[i] = Object.freeze({
|
|
246
|
+
name,
|
|
247
|
+
expr: normalizeExpr(obj[name], docPath + '/' + escapeToken(name), scope, ctx),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return Object.freeze({ kind: 'object', card: CARD_ONE, docPath, entries: Object.freeze(entries) });
|
|
251
|
+
}
|
|
252
|
+
if (dollarCount !== keys.length)
|
|
253
|
+
return fail('JQ0001', 'an object cannot mix $-prefixed and plain keys', docPath);
|
|
254
|
+
return normalizePhrase(obj, keys, docPath, scope, ctx);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizePhrase(obj, keys, docPath, scope, ctx) {
|
|
258
|
+
// FLWOR phrase shape: only FLWOR clause keys, $return plus $for and/or
|
|
259
|
+
// $let present (section 6.1). The degenerate {$let, $return} phrase
|
|
260
|
+
// keeps the direct 'let' node - it needs no tuple stream.
|
|
261
|
+
let allFlwor = true;
|
|
262
|
+
for (let i = 0; i < keys.length; i++) {
|
|
263
|
+
if (!FLWOR_KEYS.has(keys[i])) {
|
|
264
|
+
allFlwor = false;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (allFlwor && keys.length >= 2 && hasOwn(obj, '$return') && (hasOwn(obj, '$for') || hasOwn(obj, '$let'))) {
|
|
269
|
+
if (keys.length === 2 && hasOwn(obj, '$let'))
|
|
270
|
+
return normalizeLetPhrase(obj, docPath, scope, ctx);
|
|
271
|
+
return normalizeFlworPhrase(obj, docPath, scope, ctx);
|
|
272
|
+
}
|
|
273
|
+
// quantifier phrase shape (section 7): {$some|$every, $satisfies}
|
|
274
|
+
if (keys.length === 2 && hasOwn(obj, '$satisfies')
|
|
275
|
+
&& (hasOwn(obj, '$some') || hasOwn(obj, '$every')))
|
|
276
|
+
return normalizeQuantifierPhrase(obj, docPath, scope, ctx);
|
|
277
|
+
|
|
278
|
+
if (keys.length === 1)
|
|
279
|
+
return normalizeOperator(keys[0], obj[keys[0]], docPath, scope, ctx);
|
|
280
|
+
|
|
281
|
+
// multi-key object matching no phrase shape
|
|
282
|
+
for (let i = 0; i < keys.length; i++) {
|
|
283
|
+
if (!isVocabularyKey(keys[i], ctx))
|
|
284
|
+
return failUnknownOperator(keys[i], docPath, ctx);
|
|
285
|
+
}
|
|
286
|
+
return fail('JQ0003', `invalid phrase key combination (${keys.join(', ')})`, docPath);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
|
|
291
|
+
//#region unknown operators ("did you mean")
|
|
292
|
+
|
|
293
|
+
// Bounded Levenshtein distance (two-row DP); vocabulary keys are short,
|
|
294
|
+
// and this only ever runs on the JQ0002 error path.
|
|
295
|
+
function levenshtein(a, b) {
|
|
296
|
+
const alen = a.length;
|
|
297
|
+
const blen = b.length;
|
|
298
|
+
let prev = new Array(blen + 1);
|
|
299
|
+
let curr = new Array(blen + 1);
|
|
300
|
+
for (let j = 0; j <= blen; j++)
|
|
301
|
+
prev[j] = j;
|
|
302
|
+
for (let i = 1; i <= alen; i++) {
|
|
303
|
+
curr[0] = i;
|
|
304
|
+
const ca = a.charCodeAt(i - 1);
|
|
305
|
+
for (let j = 1; j <= blen; j++) {
|
|
306
|
+
const del = prev[j] + 1;
|
|
307
|
+
const ins = curr[j - 1] + 1;
|
|
308
|
+
const sub = prev[j - 1] + (ca === b.charCodeAt(j - 1) ? 0 : 1);
|
|
309
|
+
curr[j] = del < ins ? (del < sub ? del : sub) : (ins < sub ? ins : sub);
|
|
310
|
+
}
|
|
311
|
+
const t = prev;
|
|
312
|
+
prev = curr;
|
|
313
|
+
curr = t;
|
|
314
|
+
}
|
|
315
|
+
return prev[blen];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// suggestion candidates, built lazily (see the isVocabularyKey note)
|
|
319
|
+
let VOCABULARY_NAMES = null;
|
|
320
|
+
|
|
321
|
+
// JQ0002 for an unknown $-key, with a "did you mean" suggestion when a
|
|
322
|
+
// vocabulary key is within Levenshtein distance 2 (compile-time only).
|
|
323
|
+
// With host extensions the candidate list is built per call (this is the
|
|
324
|
+
// error path); the lazy global stays for the core-only case.
|
|
325
|
+
function failUnknownOperator(key, docPath, ctx) {
|
|
326
|
+
if (VOCABULARY_NAMES === null) {
|
|
327
|
+
VOCABULARY_NAMES = [
|
|
328
|
+
...FLWOR_KEYS, ...QUANTIFIER_KEYS, ...ESCAPE_KEYS, ...Object.keys(OPERATORS),
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
const candidates = ctx.extensions === null
|
|
332
|
+
? VOCABULARY_NAMES
|
|
333
|
+
: [...VOCABULARY_NAMES, ...Object.keys(ctx.extensions)];
|
|
334
|
+
let best = null;
|
|
335
|
+
let bestDist = 3;
|
|
336
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
337
|
+
const name = candidates[i];
|
|
338
|
+
const lenDiff = key.length - name.length;
|
|
339
|
+
if (lenDiff > 2 || lenDiff < -2)
|
|
340
|
+
continue;
|
|
341
|
+
const d = levenshtein(key, name);
|
|
342
|
+
if (d < bestDist) {
|
|
343
|
+
bestDist = d;
|
|
344
|
+
best = name;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const hint = best === null ? '' : ` (did you mean '${best}'?)`;
|
|
348
|
+
return fail('JQ0002', `unknown operator '${key}'${hint}`, docPath);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//#endregion
|
|
352
|
+
|
|
353
|
+
//#region operators
|
|
354
|
+
|
|
355
|
+
function requireExprArray(op, arg, min, max, docPath) {
|
|
356
|
+
if (!Array.isArray(arg))
|
|
357
|
+
return fail('JQ0003', `'${op}' takes an array of expressions`, docPath);
|
|
358
|
+
if (arg.length < min || arg.length > max) {
|
|
359
|
+
const arity = min === max ? `exactly ${min}` : (max === Infinity ? `at least ${min}` : `${min} to ${max}`);
|
|
360
|
+
return fail('JQ0003', `'${op}' takes ${arity} operand(s), got ${arg.length}`, docPath);
|
|
361
|
+
}
|
|
362
|
+
return arg;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function normalizeElements(arg, docPath, scope, ctx) {
|
|
366
|
+
const out = new Array(arg.length);
|
|
367
|
+
for (let i = 0; i < arg.length; i++)
|
|
368
|
+
out[i] = normalizeExpr(arg[i], docPath + '/' + i, scope, ctx);
|
|
369
|
+
return Object.freeze(out);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// A JSON Schema literal (the raw schema argument of $valid/$assert and
|
|
373
|
+
// the $as clause, QUERY-FORMAT.md section 8.11): taken verbatim - never
|
|
374
|
+
// normalized as an expression, since JSON Schema keywords are $-prefixed
|
|
375
|
+
// ($ref, $defs) and must not collide with Rule 1 - deep-copied + frozen
|
|
376
|
+
// like $const, and compiled once, at query compile time, into a hot-path
|
|
377
|
+
// boolean predicate by the host-installed `options.compileTypeTest` hook.
|
|
378
|
+
// No hook installed is JQ0008; a hook rejection (invalid schema) is
|
|
379
|
+
// JQ0009, both at the owning operator's/clause's docPath.
|
|
380
|
+
function compileSchemaLiteral(value, schemaPath, opPath, ctx) {
|
|
381
|
+
if (ctx.compileTypeTest === null)
|
|
382
|
+
fail('JQ0008', 'schema operators require a type-test compiler (options.compileTypeTest)', opPath);
|
|
383
|
+
const schema = deepFreezeCopy(value);
|
|
384
|
+
let test;
|
|
385
|
+
try {
|
|
386
|
+
test = ctx.compileTypeTest(schema, schemaPath);
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
fail('JQ0009', `invalid schema literal: ${e.message}`, opPath);
|
|
390
|
+
}
|
|
391
|
+
if (typeof test !== 'function')
|
|
392
|
+
fail('JQ0009', 'the type-test compiler did not return a predicate function', opPath);
|
|
393
|
+
return { schema, test };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The inert raw node: a verbatim JSON value captured as compile-time
|
|
397
|
+
// data - deep-copied and frozen, walked by nothing, never compiled
|
|
398
|
+
// (compileOp hands 'raw' positions a null getter). Also exposed to host
|
|
399
|
+
// extension operators through the `normalize` override helpers.
|
|
400
|
+
function makeRaw(value, docPath) {
|
|
401
|
+
return Object.freeze({ kind: 'raw', card: CARD_ONE, docPath, value: deepFreezeCopy(value) });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// One argument position of a registry operator, per its declared kind:
|
|
405
|
+
// 'expr' normalizes an ordinary expression; 'raw' captures the value
|
|
406
|
+
// verbatim, unevaluated; 'schema' is 'raw' plus a compiled type-test
|
|
407
|
+
// predicate (the type-system work order's schema arguments); 'name'
|
|
408
|
+
// captures a validated variable name string. 'raw', 'schema' and 'name'
|
|
409
|
+
// produce inert `raw` nodes - compile-time data, never compiled.
|
|
410
|
+
function normalizeArg(kind, value, argPath, scope, ctx, opPath) {
|
|
411
|
+
if (kind === 'expr')
|
|
412
|
+
return normalizeExpr(value, argPath, scope, ctx);
|
|
413
|
+
if (kind === 'raw')
|
|
414
|
+
return makeRaw(value, argPath);
|
|
415
|
+
if (kind === 'schema') {
|
|
416
|
+
const { schema, test } = compileSchemaLiteral(value, argPath, opPath, ctx);
|
|
417
|
+
return Object.freeze({ kind: 'raw', card: CARD_ONE, docPath: argPath, value: schema, test });
|
|
418
|
+
}
|
|
419
|
+
// 'name'
|
|
420
|
+
if (typeof value !== 'string' || !VAR_NAME_RE.test(value))
|
|
421
|
+
fail('JQ0003', 'expected a variable name string', argPath);
|
|
422
|
+
return Object.freeze({ kind: 'raw', card: CARD_ONE, docPath: argPath, value });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Argument list of an operator call, uniformly from its `params`
|
|
426
|
+
// descriptor (arity and shape violations are JQ0003).
|
|
427
|
+
function normalizeParams(key, params, arg, opPath, scope, ctx) {
|
|
428
|
+
if (typeof params === 'string') // the single form: the value IS the argument
|
|
429
|
+
return [normalizeArg(params, arg, opPath, scope, ctx, opPath)];
|
|
430
|
+
const kinds = params.kinds;
|
|
431
|
+
const max = params.variadic === true ? Infinity : kinds.length;
|
|
432
|
+
const list = requireExprArray(key, arg, params.min, max, opPath);
|
|
433
|
+
const args = new Array(list.length);
|
|
434
|
+
for (let i = 0; i < list.length; i++) {
|
|
435
|
+
const kind = kinds[i < kinds.length ? i : kinds.length - 1];
|
|
436
|
+
args[i] = normalizeArg(kind, list[i], opPath + '/' + i, scope, ctx, opPath);
|
|
437
|
+
}
|
|
438
|
+
return args;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function argCards(args) {
|
|
442
|
+
const cards = new Array(args.length);
|
|
443
|
+
for (let i = 0; i < args.length; i++)
|
|
444
|
+
cards[i] = args[i].card;
|
|
445
|
+
return cards;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// A registry operator call: arity and shape come uniformly from the
|
|
449
|
+
// table's `params` descriptor (JQ0003), the static cardinality from its
|
|
450
|
+
// `result` - individual operators never re-check structure.
|
|
451
|
+
function normalizeOperatorCall(key, entry, arg, docPath, opPath, scope, ctx) {
|
|
452
|
+
const args = normalizeParams(key, entry.params, arg, opPath, scope, ctx);
|
|
453
|
+
return Object.freeze({
|
|
454
|
+
kind: 'op', card: entry.result(argCards(args)), docPath, name: key, args: Object.freeze(args),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Helpers handed to an extension entry's `normalize` override; see
|
|
459
|
+
// normalizeExtensionCall. Function declarations hoist, so freezing at
|
|
460
|
+
// module evaluation time is safe.
|
|
461
|
+
const EXTENSION_HELPERS = Object.freeze({ normalizeExpr, fail, makeRaw });
|
|
462
|
+
|
|
463
|
+
// A host extension operator call (options.extensions, package-internal):
|
|
464
|
+
// the registry contract plus an optional `normalize(arg, docPath, opPath,
|
|
465
|
+
// scope, ctx, helpers) -> { args, card? }` override for operators whose
|
|
466
|
+
// value shape the uniform `params` descriptor cannot express. `helpers`
|
|
467
|
+
// is `{ normalizeExpr, fail, makeRaw }`. The op node is built uniformly
|
|
468
|
+
// from the returned args - `card = card ?? entry.result(argCards)` - and
|
|
469
|
+
// carries the resolved entry so compileOp can dispatch without the table.
|
|
470
|
+
function normalizeExtensionCall(key, entry, arg, docPath, opPath, scope, ctx) {
|
|
471
|
+
let args;
|
|
472
|
+
let card = null;
|
|
473
|
+
if (typeof entry.normalize === 'function') {
|
|
474
|
+
const out = entry.normalize(arg, docPath, opPath, scope, ctx, EXTENSION_HELPERS);
|
|
475
|
+
args = Object.freeze(out.args.slice());
|
|
476
|
+
card = out.card ?? null;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
args = Object.freeze(normalizeParams(key, entry.params, arg, opPath, scope, ctx));
|
|
480
|
+
}
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
kind: 'op', card: card ?? entry.result(argCards(args)), docPath, name: key, args, entry,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function normalizeOperator(key, arg, docPath, scope, ctx) {
|
|
487
|
+
const opPath = docPath + '/' + key;
|
|
488
|
+
switch (key) {
|
|
489
|
+
case '$const': // quote: verbatim single item, nothing inside evaluated
|
|
490
|
+
return Object.freeze({ kind: 'literal', card: CARD_ONE, docPath, value: deepFreezeCopy(arg) });
|
|
491
|
+
|
|
492
|
+
case '$map': { // general map constructor (section 3.5.2)
|
|
493
|
+
const list = requireExprArray(key, arg, 0, Infinity, opPath);
|
|
494
|
+
const pairs = new Array(list.length);
|
|
495
|
+
for (let i = 0; i < list.length; i++) {
|
|
496
|
+
const entry = list[i];
|
|
497
|
+
const entryPath = opPath + '/' + i;
|
|
498
|
+
if (!Array.isArray(entry) || entry.length !== 2)
|
|
499
|
+
return fail('JQ0003', 'a $map entry must be an array of exactly two expressions', entryPath);
|
|
500
|
+
pairs[i] = Object.freeze({
|
|
501
|
+
key: normalizeExpr(entry[0], entryPath + '/0', scope, ctx),
|
|
502
|
+
value: normalizeExpr(entry[1], entryPath + '/1', scope, ctx),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return Object.freeze({ kind: 'map', card: CARD_ONE, docPath, pairs: Object.freeze(pairs) });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
default: {
|
|
509
|
+
// the registry vocabulary (section 8); note the section 6.7
|
|
510
|
+
// collision rule holds by construction: a single-key {"$count": e}
|
|
511
|
+
// object always reaches this lookup and is the operator
|
|
512
|
+
if (hasOwn(OPERATORS, key))
|
|
513
|
+
return normalizeOperatorCall(key, OPERATORS[key], arg, docPath, opPath, scope, ctx);
|
|
514
|
+
// host extension operators: after the core registry, before JQ0002
|
|
515
|
+
if (ctx.extensions !== null && hasOwn(ctx.extensions, key))
|
|
516
|
+
return normalizeExtensionCall(key, ctx.extensions[key], arg, docPath, opPath, scope, ctx);
|
|
517
|
+
if (FLWOR_KEYS.has(key) || QUANTIFIER_KEYS.has(key))
|
|
518
|
+
return fail('JQ0003', `'${key}' cannot form a phrase on its own`, docPath);
|
|
519
|
+
return failUnknownOperator(key, docPath, ctx);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
//#endregion
|
|
525
|
+
|
|
526
|
+
//#region FLWOR and quantifier phrases
|
|
527
|
+
|
|
528
|
+
// One JQ0007 duplicate set spans all of a phrase's binding sites: $for
|
|
529
|
+
// names, $at names, $let names, $groupby key names, and the $count name
|
|
530
|
+
// (section 6.3). Rebinding a name from an enclosing phrase is ordinary
|
|
531
|
+
// shadowing and never hits this check.
|
|
532
|
+
function bindPhraseName(name, phraseNames, bindPath) {
|
|
533
|
+
if (!VAR_NAME_RE.test(name))
|
|
534
|
+
fail('JQ0003', `'${name}' is not a valid variable name`, bindPath);
|
|
535
|
+
if (phraseNames.has(name))
|
|
536
|
+
fail('JQ0007', `duplicate binding of variable '${name}' within one phrase`, bindPath);
|
|
537
|
+
phraseNames.add(name);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function requireBindingObject(clause, bindObj, clausePath) {
|
|
541
|
+
if (!isPlainObject(bindObj))
|
|
542
|
+
fail('JQ0003', `'${clause}' takes an object of variable bindings`, clausePath);
|
|
543
|
+
const names = Object.keys(bindObj);
|
|
544
|
+
if (names.length === 0)
|
|
545
|
+
fail('JQ0003', `'${clause}' requires at least one binding`, clausePath);
|
|
546
|
+
return names;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// $let bindings (section 6.3): each name binds the full sequence of its
|
|
550
|
+
// expression - no iteration, no array unpacking. Bindings evaluate
|
|
551
|
+
// sequentially in document key order; later sources see earlier names of
|
|
552
|
+
// the same object (correlation). Shared by the degenerate {$let, $return}
|
|
553
|
+
// phrase and the full FLWOR normalizer; returns the extended scope.
|
|
554
|
+
function normalizeLetBindings(letObj, letPath, scope, ctx, phraseNames, bindings) {
|
|
555
|
+
const names = requireBindingObject('$let', letObj, letPath);
|
|
556
|
+
let sc = scope;
|
|
557
|
+
for (let i = 0; i < names.length; i++) {
|
|
558
|
+
const name = names[i];
|
|
559
|
+
const bindPath = letPath + '/' + escapeToken(name);
|
|
560
|
+
bindPhraseName(name, phraseNames, bindPath);
|
|
561
|
+
const source = letObj[name];
|
|
562
|
+
if (isPlainObject(source) && (hasOwn(source, '$in') || hasOwn(source, '$at')))
|
|
563
|
+
return fail('JQ0003', "the extended '$in'/'$at' binding form is not available in '$let'", bindPath);
|
|
564
|
+
const expr = normalizeExpr(source, bindPath, sc, ctx);
|
|
565
|
+
const slot = ctx.nextSlot++;
|
|
566
|
+
sc = { name, slot, card: expr.card, parent: sc };
|
|
567
|
+
bindings.push(Object.freeze({ name, slot, expr }));
|
|
568
|
+
}
|
|
569
|
+
return sc;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// The degenerate FLWOR phrase {$let, $return}: a straight-line binding
|
|
573
|
+
// chain, no tuple stream (the compiler keeps its direct fast path).
|
|
574
|
+
function normalizeLetPhrase(obj, docPath, scope, ctx) {
|
|
575
|
+
const bindings = [];
|
|
576
|
+
const sc = normalizeLetBindings(obj.$let, docPath + '/$let', scope, ctx, new Set(), bindings);
|
|
577
|
+
const ret = normalizeExpr(obj.$return, docPath + '/$return', sc, ctx);
|
|
578
|
+
return Object.freeze({
|
|
579
|
+
kind: 'let', card: ret.card, docPath,
|
|
580
|
+
bindings: Object.freeze(bindings), ret,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// $for bindings (section 6.2): each name iterates its source, one item
|
|
585
|
+
// per tuple (card ONE), with D4 array unpacking at runtime. The extended
|
|
586
|
+
// {"$in": expr, "$at": name} form additionally binds a 0-based position
|
|
587
|
+
// (D6). Multiple bindings nest left-to-right in document key order and
|
|
588
|
+
// may be correlated. Returns the extended scope.
|
|
589
|
+
function normalizeForBindings(forObj, forPath, scope, ctx, phraseNames, bindings, tupleSlots) {
|
|
590
|
+
const names = requireBindingObject('$for', forObj, forPath);
|
|
591
|
+
let sc = scope;
|
|
592
|
+
for (let i = 0; i < names.length; i++) {
|
|
593
|
+
const name = names[i];
|
|
594
|
+
const bindPath = forPath + '/' + escapeToken(name);
|
|
595
|
+
bindPhraseName(name, phraseNames, bindPath);
|
|
596
|
+
let source = forObj[name];
|
|
597
|
+
let sourcePath = bindPath;
|
|
598
|
+
let atName = null;
|
|
599
|
+
if (isPlainObject(source) && (hasOwn(source, '$in') || hasOwn(source, '$at'))) {
|
|
600
|
+
if (Object.keys(source).length !== 2 || !hasOwn(source, '$in') || !hasOwn(source, '$at'))
|
|
601
|
+
return fail('JQ0003', "the extended binding form takes exactly the keys '$in' and '$at'", bindPath);
|
|
602
|
+
atName = source.$at;
|
|
603
|
+
if (typeof atName !== 'string' || !VAR_NAME_RE.test(atName))
|
|
604
|
+
return fail('JQ0003', "'$at' takes a variable name string", bindPath + '/$at');
|
|
605
|
+
source = source.$in;
|
|
606
|
+
sourcePath = bindPath + '/$in';
|
|
607
|
+
}
|
|
608
|
+
const expr = normalizeExpr(source, sourcePath, sc, ctx);
|
|
609
|
+
const slot = ctx.nextSlot++;
|
|
610
|
+
sc = { name, slot, card: CARD_ONE, parent: sc };
|
|
611
|
+
tupleSlots.push({ name, slot });
|
|
612
|
+
let atSlot = -1;
|
|
613
|
+
if (atName !== null) {
|
|
614
|
+
bindPhraseName(atName, phraseNames, bindPath + '/$at');
|
|
615
|
+
atSlot = ctx.nextSlot++;
|
|
616
|
+
sc = { name: atName, slot: atSlot, card: CARD_ONE, parent: sc };
|
|
617
|
+
tupleSlots.push({ name: atName, slot: atSlot });
|
|
618
|
+
}
|
|
619
|
+
bindings.push(Object.freeze({ name, slot, expr, atSlot }));
|
|
620
|
+
}
|
|
621
|
+
return sc;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// One $orderby key spec (section 6.6): an expression (ascending,
|
|
625
|
+
// empty-least) or the explicit {"$key", "$dir"?, "$empty"?} form.
|
|
626
|
+
function normalizeOrderbySpec(spec, specPath, scope, ctx) {
|
|
627
|
+
let key = spec;
|
|
628
|
+
let keyPath = specPath;
|
|
629
|
+
let desc = false;
|
|
630
|
+
let emptyGreatest = false;
|
|
631
|
+
if (isPlainObject(spec) && (hasOwn(spec, '$key') || hasOwn(spec, '$dir') || hasOwn(spec, '$empty'))) {
|
|
632
|
+
const specKeys = Object.keys(spec);
|
|
633
|
+
for (let i = 0; i < specKeys.length; i++) {
|
|
634
|
+
if (!ORDERBY_SPEC_KEYS.has(specKeys[i]))
|
|
635
|
+
return fail('JQ0003', `'${specKeys[i]}' is not a valid key of an $orderby key spec`, specPath);
|
|
636
|
+
}
|
|
637
|
+
if (!hasOwn(spec, '$key'))
|
|
638
|
+
return fail('JQ0003', "an explicit $orderby key spec requires '$key'", specPath);
|
|
639
|
+
if (hasOwn(spec, '$dir')) {
|
|
640
|
+
if (spec.$dir !== 'asc' && spec.$dir !== 'desc')
|
|
641
|
+
return fail('JQ0003', "'$dir' must be 'asc' or 'desc'", specPath + '/$dir');
|
|
642
|
+
desc = spec.$dir === 'desc';
|
|
643
|
+
}
|
|
644
|
+
if (hasOwn(spec, '$empty')) {
|
|
645
|
+
if (spec.$empty !== 'least' && spec.$empty !== 'greatest')
|
|
646
|
+
return fail('JQ0003', "'$empty' must be 'least' or 'greatest'", specPath + '/$empty');
|
|
647
|
+
emptyGreatest = spec.$empty === 'greatest';
|
|
648
|
+
}
|
|
649
|
+
key = spec.$key;
|
|
650
|
+
keyPath = specPath + '/$key';
|
|
651
|
+
}
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
key: normalizeExpr(key, keyPath, scope, ctx),
|
|
654
|
+
desc, emptyGreatest, docPath: specPath,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Collect the frame slots an expression subtree reads (variable
|
|
659
|
+
// references and path roots). Barrier liveness: $groupby/$orderby
|
|
660
|
+
// materialize only the phrase binding slots that later clauses read.
|
|
661
|
+
// Over-approximation is safe; slots written by nested phrases before
|
|
662
|
+
// being read merely widen a snapshot harmlessly.
|
|
663
|
+
function collectReadSlots(node, out) {
|
|
664
|
+
switch (node.kind) {
|
|
665
|
+
case 'literal':
|
|
666
|
+
return;
|
|
667
|
+
case 'var':
|
|
668
|
+
out.add(node.slot);
|
|
669
|
+
return;
|
|
670
|
+
case 'path':
|
|
671
|
+
out.add(node.rootSlot);
|
|
672
|
+
return;
|
|
673
|
+
case 'object':
|
|
674
|
+
for (let i = 0; i < node.entries.length; i++)
|
|
675
|
+
collectReadSlots(node.entries[i].expr, out);
|
|
676
|
+
return;
|
|
677
|
+
case 'map':
|
|
678
|
+
for (let i = 0; i < node.pairs.length; i++) {
|
|
679
|
+
collectReadSlots(node.pairs[i].key, out);
|
|
680
|
+
collectReadSlots(node.pairs[i].value, out);
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
case 'array':
|
|
684
|
+
for (let i = 0; i < node.elements.length; i++)
|
|
685
|
+
collectReadSlots(node.elements[i], out);
|
|
686
|
+
return;
|
|
687
|
+
case 'raw': // compile-time data of a registry operator, never evaluated
|
|
688
|
+
return;
|
|
689
|
+
case 'op':
|
|
690
|
+
for (let i = 0; i < node.args.length; i++)
|
|
691
|
+
collectReadSlots(node.args[i], out);
|
|
692
|
+
return;
|
|
693
|
+
case 'let':
|
|
694
|
+
for (let i = 0; i < node.bindings.length; i++)
|
|
695
|
+
collectReadSlots(node.bindings[i].expr, out);
|
|
696
|
+
collectReadSlots(node.ret, out);
|
|
697
|
+
return;
|
|
698
|
+
case 'quant':
|
|
699
|
+
for (let i = 0; i < node.bindings.length; i++)
|
|
700
|
+
collectReadSlots(node.bindings[i].expr, out);
|
|
701
|
+
collectReadSlots(node.satisfies, out);
|
|
702
|
+
return;
|
|
703
|
+
default: { // 'flwor'
|
|
704
|
+
for (let i = 0; i < node.forBindings.length; i++)
|
|
705
|
+
collectReadSlots(node.forBindings[i].expr, out);
|
|
706
|
+
for (let i = 0; i < node.letBindings.length; i++)
|
|
707
|
+
collectReadSlots(node.letBindings[i].expr, out);
|
|
708
|
+
if (node.asChecks !== null) { // reads its own binding slots per tuple
|
|
709
|
+
for (let i = 0; i < node.asChecks.length; i++)
|
|
710
|
+
out.add(node.asChecks[i].slot);
|
|
711
|
+
}
|
|
712
|
+
if (node.where !== null)
|
|
713
|
+
collectReadSlots(node.where, out);
|
|
714
|
+
if (node.groupby !== null) {
|
|
715
|
+
for (let i = 0; i < node.groupby.keys.length; i++)
|
|
716
|
+
collectReadSlots(node.groupby.keys[i].expr, out);
|
|
717
|
+
}
|
|
718
|
+
if (node.orderby !== null) {
|
|
719
|
+
for (let i = 0; i < node.orderby.specs.length; i++)
|
|
720
|
+
collectReadSlots(node.orderby.specs[i].key, out);
|
|
721
|
+
}
|
|
722
|
+
collectReadSlots(node.ret, out);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// The full FLWOR phrase (section 6). Clauses normalize - and their
|
|
729
|
+
// bindings scope - in the fixed semantic order of section 6.1 (D7):
|
|
730
|
+
// $for -> $let -> $as -> $where -> $groupby -> $orderby -> $count -> $return.
|
|
731
|
+
// A $groupby rebinds the phrase's tuple variables for every later
|
|
732
|
+
// clause: key names become singletons (card OPT: a key may be the empty
|
|
733
|
+
// sequence), every other binding becomes the sequence of its values
|
|
734
|
+
// across the group's tuples (card MANY) - same slots, new static cards.
|
|
735
|
+
function normalizeFlworPhrase(obj, docPath, scope, ctx) {
|
|
736
|
+
const phraseNames = new Set();
|
|
737
|
+
const tupleSlots = []; // {name, slot} per pre-group binding site, in order
|
|
738
|
+
const forBindings = [];
|
|
739
|
+
const letBindings = [];
|
|
740
|
+
let sc = scope;
|
|
741
|
+
|
|
742
|
+
if (hasOwn(obj, '$for'))
|
|
743
|
+
sc = normalizeForBindings(obj.$for, docPath + '/$for', sc, ctx, phraseNames, forBindings, tupleSlots);
|
|
744
|
+
if (hasOwn(obj, '$let')) {
|
|
745
|
+
const before = letBindings.length;
|
|
746
|
+
sc = normalizeLetBindings(obj.$let, docPath + '/$let', sc, ctx, phraseNames, letBindings);
|
|
747
|
+
for (let i = before; i < letBindings.length; i++)
|
|
748
|
+
tupleSlots.push({ name: letBindings[i].name, slot: letBindings[i].slot });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// $as (section 6.4): schema assertions on the phrase's own bindings,
|
|
752
|
+
// applied per tuple after $for/$let and before $where. A $for/$at
|
|
753
|
+
// variable is validated as its one bound item; a $let variable per item
|
|
754
|
+
// of its bound sequence. The names must be binding sites of THIS phrase
|
|
755
|
+
// (JQ0005 otherwise); each schema compiles once via compileSchemaLiteral.
|
|
756
|
+
let asChecks = null;
|
|
757
|
+
if (hasOwn(obj, '$as')) {
|
|
758
|
+
const asPath = docPath + '/$as';
|
|
759
|
+
const asObj = obj.$as;
|
|
760
|
+
if (!isPlainObject(asObj))
|
|
761
|
+
return fail('JQ0003', "'$as' takes an object of variable-name to schema members", asPath);
|
|
762
|
+
const names = Object.keys(asObj);
|
|
763
|
+
if (names.length === 0)
|
|
764
|
+
return fail('JQ0003', "'$as' requires at least one member", asPath);
|
|
765
|
+
const checks = new Array(names.length);
|
|
766
|
+
for (let i = 0; i < names.length; i++) {
|
|
767
|
+
const name = names[i];
|
|
768
|
+
const checkPath = asPath + '/' + escapeToken(name);
|
|
769
|
+
let slot = -1;
|
|
770
|
+
for (let j = 0; j < tupleSlots.length; j++) {
|
|
771
|
+
if (tupleSlots[j].name === name) {
|
|
772
|
+
slot = tupleSlots[j].slot;
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (slot < 0)
|
|
777
|
+
return fail('JQ0005', `'$as' names '${name}', which is not bound by this phrase's '$for'/'$let'`, checkPath);
|
|
778
|
+
let isLet = false;
|
|
779
|
+
for (let j = 0; j < letBindings.length; j++) {
|
|
780
|
+
if (letBindings[j].name === name) {
|
|
781
|
+
isLet = true;
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const { schema, test } = compileSchemaLiteral(asObj[name], checkPath, checkPath, ctx);
|
|
786
|
+
checks[i] = Object.freeze({ name, slot, isLet, schema, test, docPath: checkPath });
|
|
787
|
+
}
|
|
788
|
+
asChecks = Object.freeze(checks);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const where = hasOwn(obj, '$where')
|
|
792
|
+
? normalizeExpr(obj.$where, docPath + '/$where', sc, ctx)
|
|
793
|
+
: null;
|
|
794
|
+
|
|
795
|
+
let groupby = null;
|
|
796
|
+
if (hasOwn(obj, '$groupby')) {
|
|
797
|
+
const groupPath = docPath + '/$groupby';
|
|
798
|
+
const names = requireBindingObject('$groupby', obj.$groupby, groupPath);
|
|
799
|
+
const keys = new Array(names.length);
|
|
800
|
+
for (let i = 0; i < names.length; i++) {
|
|
801
|
+
const name = names[i];
|
|
802
|
+
const bindPath = groupPath + '/' + escapeToken(name);
|
|
803
|
+
bindPhraseName(name, phraseNames, bindPath);
|
|
804
|
+
// key expressions evaluate per tuple, in the pre-group scope
|
|
805
|
+
const expr = normalizeExpr(obj.$groupby[name], bindPath, sc, ctx);
|
|
806
|
+
keys[i] = Object.freeze({ name, slot: ctx.nextSlot++, expr, docPath: bindPath });
|
|
807
|
+
}
|
|
808
|
+
// post-group scope: same slots, rebound cards
|
|
809
|
+
sc = scope;
|
|
810
|
+
for (let i = 0; i < tupleSlots.length; i++)
|
|
811
|
+
sc = { name: tupleSlots[i].name, slot: tupleSlots[i].slot, card: CARD_MANY, parent: sc };
|
|
812
|
+
for (let i = 0; i < keys.length; i++)
|
|
813
|
+
sc = { name: keys[i].name, slot: keys[i].slot, card: CARD_OPT, parent: sc };
|
|
814
|
+
groupby = { keys: Object.freeze(keys), docPath: groupPath, accSlots: null };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let orderby = null;
|
|
818
|
+
if (hasOwn(obj, '$orderby')) {
|
|
819
|
+
const orderPath = docPath + '/$orderby';
|
|
820
|
+
const raw = obj.$orderby;
|
|
821
|
+
let specs;
|
|
822
|
+
if (Array.isArray(raw)) { // always a list of key specs, major to minor
|
|
823
|
+
if (raw.length === 0)
|
|
824
|
+
return fail('JQ0003', "'$orderby' takes a key spec or a non-empty array of key specs", orderPath);
|
|
825
|
+
specs = new Array(raw.length);
|
|
826
|
+
for (let i = 0; i < raw.length; i++)
|
|
827
|
+
specs[i] = normalizeOrderbySpec(raw[i], orderPath + '/' + i, sc, ctx);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
specs = [normalizeOrderbySpec(raw, orderPath, sc, ctx)];
|
|
831
|
+
}
|
|
832
|
+
orderby = { specs: Object.freeze(specs), docPath: orderPath, liveSlots: null };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let count = null;
|
|
836
|
+
if (hasOwn(obj, '$count')) {
|
|
837
|
+
const countPath = docPath + '/$count';
|
|
838
|
+
const name = obj.$count;
|
|
839
|
+
if (typeof name !== 'string')
|
|
840
|
+
return fail('JQ0003', "'$count' takes a variable name string", countPath);
|
|
841
|
+
bindPhraseName(name, phraseNames, countPath);
|
|
842
|
+
const slot = ctx.nextSlot++;
|
|
843
|
+
sc = { name, slot, card: CARD_ONE, parent: sc };
|
|
844
|
+
count = Object.freeze({ name, slot });
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const ret = normalizeExpr(obj.$return, docPath + '/$return', sc, ctx);
|
|
848
|
+
|
|
849
|
+
// barrier liveness (compile-time): $groupby accumulates - and $orderby
|
|
850
|
+
// snapshots - only the binding slots that later clauses actually read.
|
|
851
|
+
// The $count slot is written after both barriers and is never live.
|
|
852
|
+
const retReads = new Set();
|
|
853
|
+
collectReadSlots(ret, retReads);
|
|
854
|
+
if (groupby !== null) {
|
|
855
|
+
const laterReads = new Set(retReads);
|
|
856
|
+
if (orderby !== null) {
|
|
857
|
+
for (let i = 0; i < orderby.specs.length; i++)
|
|
858
|
+
collectReadSlots(orderby.specs[i].key, laterReads);
|
|
859
|
+
}
|
|
860
|
+
const accSlots = [];
|
|
861
|
+
for (let i = 0; i < tupleSlots.length; i++) {
|
|
862
|
+
if (laterReads.has(tupleSlots[i].slot))
|
|
863
|
+
accSlots.push(tupleSlots[i].slot);
|
|
864
|
+
}
|
|
865
|
+
groupby.accSlots = Object.freeze(accSlots);
|
|
866
|
+
}
|
|
867
|
+
if (orderby !== null) {
|
|
868
|
+
// slots that still vary per tuple at the $orderby barrier
|
|
869
|
+
const barrierSlots = [];
|
|
870
|
+
if (groupby !== null) {
|
|
871
|
+
for (let i = 0; i < groupby.keys.length; i++)
|
|
872
|
+
barrierSlots.push(groupby.keys[i].slot);
|
|
873
|
+
for (let i = 0; i < groupby.accSlots.length; i++)
|
|
874
|
+
barrierSlots.push(groupby.accSlots[i]);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
for (let i = 0; i < tupleSlots.length; i++)
|
|
878
|
+
barrierSlots.push(tupleSlots[i].slot);
|
|
879
|
+
}
|
|
880
|
+
const liveSlots = [];
|
|
881
|
+
for (let i = 0; i < barrierSlots.length; i++) {
|
|
882
|
+
if (retReads.has(barrierSlots[i]))
|
|
883
|
+
liveSlots.push(barrierSlots[i]);
|
|
884
|
+
}
|
|
885
|
+
orderby.liveSlots = Object.freeze(liveSlots);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// phrase cardinality: MANY unless provably otherwise - a $let-only
|
|
889
|
+
// phrase yields exactly one tuple ($where may still drop it)
|
|
890
|
+
let card;
|
|
891
|
+
if (forBindings.length !== 0 || groupby !== null)
|
|
892
|
+
card = CARD_MANY;
|
|
893
|
+
else
|
|
894
|
+
card = where !== null ? joinCard(ret.card, CARD_ZERO) : ret.card;
|
|
895
|
+
|
|
896
|
+
return Object.freeze({
|
|
897
|
+
kind: 'flwor', card, docPath,
|
|
898
|
+
forBindings: Object.freeze(forBindings),
|
|
899
|
+
letBindings: Object.freeze(letBindings),
|
|
900
|
+
asChecks, where,
|
|
901
|
+
groupby: groupby === null ? null : Object.freeze(groupby),
|
|
902
|
+
orderby: orderby === null ? null : Object.freeze(orderby),
|
|
903
|
+
count, ret,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Quantifier phrases (section 7): {$some|$every, $satisfies}. The
|
|
908
|
+
// binding object follows $for rules (names, key-order nesting,
|
|
909
|
+
// correlation, D4 unpacking) except the extended $in/$at form (JQ0003).
|
|
910
|
+
function normalizeQuantifierPhrase(obj, docPath, scope, ctx) {
|
|
911
|
+
const some = hasOwn(obj, '$some');
|
|
912
|
+
const clause = some ? '$some' : '$every';
|
|
913
|
+
const clausePath = docPath + '/' + clause;
|
|
914
|
+
const bindObj = obj[clause];
|
|
915
|
+
const names = requireBindingObject(clause, bindObj, clausePath);
|
|
916
|
+
const phraseNames = new Set();
|
|
917
|
+
const bindings = new Array(names.length);
|
|
918
|
+
let sc = scope;
|
|
919
|
+
for (let i = 0; i < names.length; i++) {
|
|
920
|
+
const name = names[i];
|
|
921
|
+
const bindPath = clausePath + '/' + escapeToken(name);
|
|
922
|
+
bindPhraseName(name, phraseNames, bindPath);
|
|
923
|
+
const source = bindObj[name];
|
|
924
|
+
if (isPlainObject(source) && (hasOwn(source, '$in') || hasOwn(source, '$at')))
|
|
925
|
+
return fail('JQ0003', "the extended '$in'/'$at' binding form is not available in quantifiers", bindPath);
|
|
926
|
+
const expr = normalizeExpr(source, bindPath, sc, ctx);
|
|
927
|
+
const slot = ctx.nextSlot++;
|
|
928
|
+
sc = { name, slot, card: CARD_ONE, parent: sc };
|
|
929
|
+
bindings[i] = Object.freeze({ name, slot, expr });
|
|
930
|
+
}
|
|
931
|
+
const satisfies = normalizeExpr(obj.$satisfies, docPath + '/$satisfies', sc, ctx);
|
|
932
|
+
return Object.freeze({
|
|
933
|
+
kind: 'quant', card: CARD_ONE, docPath, some,
|
|
934
|
+
bindings: Object.freeze(bindings), satisfies,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
//#endregion
|
|
939
|
+
|
|
940
|
+
//#region entry points
|
|
941
|
+
|
|
942
|
+
// Validate `options.extensions` (package-internal, used by the JSLT
|
|
943
|
+
// layer; not a public contract): a plain object of `name -> entry`.
|
|
944
|
+
// Every name must start with '$' and must not collide with the core
|
|
945
|
+
// vocabulary - the closed format is unchanged, extensions are host
|
|
946
|
+
// machinery. Violations are host programming errors (TypeError), not
|
|
947
|
+
// JQ0xxx document errors.
|
|
948
|
+
function validateExtensions(extensions) {
|
|
949
|
+
if (!isPlainObject(extensions))
|
|
950
|
+
throw new TypeError('options.extensions must be a plain object of operator entries');
|
|
951
|
+
const names = Object.keys(extensions);
|
|
952
|
+
for (let i = 0; i < names.length; i++) {
|
|
953
|
+
const name = names[i];
|
|
954
|
+
if (name.charCodeAt(0) !== 0x24) // '$'
|
|
955
|
+
throw new TypeError(`extension operator '${name}' must start with '$'`);
|
|
956
|
+
if (FLWOR_KEYS.has(name) || QUANTIFIER_KEYS.has(name) || ESCAPE_KEYS.has(name)
|
|
957
|
+
|| ORDERBY_SPEC_KEYS.has(name) || name === '$in' || name === '$at'
|
|
958
|
+
|| name === '$query' || name === '$expr' || hasOwn(OPERATORS, name))
|
|
959
|
+
throw new TypeError(`extension operator '${name}' collides with the core vocabulary`);
|
|
960
|
+
}
|
|
961
|
+
return extensions;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function normalizeExpr(value, docPath, scope, ctx) {
|
|
965
|
+
switch (typeof value) {
|
|
966
|
+
case 'string':
|
|
967
|
+
return normalizeString(value, docPath, scope, ctx);
|
|
968
|
+
case 'number':
|
|
969
|
+
case 'boolean':
|
|
970
|
+
return makeLiteral(value, docPath);
|
|
971
|
+
case 'object': {
|
|
972
|
+
if (value === null)
|
|
973
|
+
return makeLiteral(null, docPath);
|
|
974
|
+
if (Array.isArray(value)) { // Rule 3: array constructor
|
|
975
|
+
return Object.freeze({
|
|
976
|
+
kind: 'array', card: CARD_ONE, docPath,
|
|
977
|
+
elements: normalizeElements(value, docPath, scope, ctx),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
return normalizeObject(value, docPath, scope, ctx);
|
|
981
|
+
}
|
|
982
|
+
default:
|
|
983
|
+
return fail('JQ0003', `a query document cannot contain a ${typeof value}`, docPath);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Normalize a query document into the internal AST.
|
|
989
|
+
* @param {any} doc - the query document (any JSON value)
|
|
990
|
+
* @param {object} [options] - compile options
|
|
991
|
+
* @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
|
|
992
|
+
* [options.compileTypeTest] - host hook compiling a JSON Schema literal
|
|
993
|
+
* into a boolean item predicate (QUERY-FORMAT.md section 8.11). Called
|
|
994
|
+
* once per schema literal, at query compile time. Without it, schema
|
|
995
|
+
* operators ($valid/$assert/$as) are compile error JQ0008.
|
|
996
|
+
* @param {object} [options.extensions] - package-internal operator
|
|
997
|
+
* extension point, the operator analogue of `compileTypeTest` (used by
|
|
998
|
+
* the JSLT layer; not a public contract). A plain object of
|
|
999
|
+
* `name -> entry`, where entry follows the operator registry contract
|
|
1000
|
+
* (`params`/`result`/`compile`, operators.js header) plus an optional
|
|
1001
|
+
* `normalize(arg, docPath, opPath, scope, ctx, helpers) -> {args, card?}`
|
|
1002
|
+
* override for polymorphic value shapes. Names must start with '$' and
|
|
1003
|
+
* must not collide with the core vocabulary (TypeError - a host
|
|
1004
|
+
* programming error, not a JQ0xxx document error). The published format
|
|
1005
|
+
* and its schema are unchanged: without extensions, the same documents
|
|
1006
|
+
* fail JQ0002.
|
|
1007
|
+
* @returns {{ root: object, frameSize: number, externals: {name: string, slot: number}[] }}
|
|
1008
|
+
* the AST root, the frame size, and the external parameters in order of
|
|
1009
|
+
* first appearance (slot order)
|
|
1010
|
+
* @throws {JsonQueryCompileError} on any JQ0xxx condition
|
|
1011
|
+
*/
|
|
1012
|
+
export function normalizeQuery(doc, options = {}) {
|
|
1013
|
+
const compileTypeTest = typeof options.compileTypeTest === 'function'
|
|
1014
|
+
? options.compileTypeTest
|
|
1015
|
+
: null;
|
|
1016
|
+
const extensions = options.extensions == null ? null : validateExtensions(options.extensions);
|
|
1017
|
+
const ctx = { nextSlot: 1, externals: new Map(), compileTypeTest, extensions };
|
|
1018
|
+
let expr = doc;
|
|
1019
|
+
let rootPath = '';
|
|
1020
|
+
// the version envelope is only recognized at the top level (section 4)
|
|
1021
|
+
if (isPlainObject(doc) && (hasOwn(doc, '$query') || hasOwn(doc, '$expr'))) {
|
|
1022
|
+
const keys = Object.keys(doc);
|
|
1023
|
+
let allDollar = true;
|
|
1024
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1025
|
+
if (keys[i].charCodeAt(0) !== 0x24)
|
|
1026
|
+
allDollar = false;
|
|
1027
|
+
}
|
|
1028
|
+
if (allDollar) {
|
|
1029
|
+
if (hasOwn(doc, '$query') && doc.$query !== '0.1')
|
|
1030
|
+
fail('JQ0006', `unknown query format version ${JSON.stringify(doc.$query)}`, '/$query');
|
|
1031
|
+
if (!hasOwn(doc, '$query') || !hasOwn(doc, '$expr') || keys.length !== 2)
|
|
1032
|
+
fail('JQ0003', "the version envelope requires exactly the keys '$query' and '$expr'", '');
|
|
1033
|
+
expr = doc.$expr;
|
|
1034
|
+
rootPath = '/$expr';
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const root = normalizeExpr(expr, rootPath, null, ctx);
|
|
1038
|
+
const externals = new Array(ctx.externals.size);
|
|
1039
|
+
let i = 0;
|
|
1040
|
+
for (const [name, slot] of ctx.externals)
|
|
1041
|
+
externals[i++] = Object.freeze({ name, slot });
|
|
1042
|
+
return { root, frameSize: ctx.nextSlot, externals: Object.freeze(externals) };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
//#endregion
|
|
1046
|
+
|
|
1047
|
+
//#endregion
|