@tellaro/tql 1.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +73 -65
- package/dist/src/builder.d.ts +6 -0
- package/dist/src/completionProvider.js +276 -195
- package/dist/src/complexityAnalyzer.js +41 -32
- package/dist/src/index.d.ts +3 -3
- package/dist/src/parser.d.ts +193 -1
- package/dist/src/parser.js +614 -127
- package/dist/src/semanticPreview.d.ts +23 -0
- package/dist/src/semanticPreview.js +111 -41
- package/dist/src/tokenizer.d.ts +134 -5
- package/dist/src/tokenizer.js +47 -30
- package/dist/src/types.d.ts +138 -10
- package/dist/src/validator.d.ts +235 -1
- package/dist/src/validator.js +468 -150
- package/dist/tql.css +1 -1
- package/dist/vue/TqlAutocomplete.vue.d.ts +24 -1
- package/dist/vue/TqlChart.vue.d.ts +19 -1
- package/dist/vue/TqlChart.vue.js +1 -1
- package/dist/vue/TqlInput.vue.d.ts +92 -1
- package/dist/vue/TqlInput.vue.js +1 -1
- package/dist/vue/TqlInput.vue2.js +282 -256
- package/dist/vue/components/TqlPreview.vue.d.ts +89 -2
- package/dist/vue/components/TqlPreview.vue.js +4 -4
- package/dist/vue/index.d.ts +1 -1
- package/dist/vue/useTqlInput.d.ts +38 -29
- package/package.json +3 -5
package/dist/src/parser.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import { tokenizeTql as
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
import { tokenizeTql as T, TYPE_HINTS as f, BOOLEAN_PREDICATE_MUTATOR_SET as N, COMPARISON_OPERATORS as k, NEGATION_SYMBOL_OPERATORS as g, MUTATORS as w, SYMBOL_LOGICAL_OPERATORS as E } from "./tokenizer.js";
|
|
2
|
+
const L = 50, O = /* @__PURE__ */ new Set(["logicalOp", "paren"]), v = /* @__PURE__ */ new Map([
|
|
3
|
+
["any", { op: "ANY", negated: !1 }],
|
|
4
|
+
["all", { op: "ALL", negated: !1 }],
|
|
5
|
+
["none", { op: "NONE", negated: !1 }],
|
|
6
|
+
["not_any", { op: "ANY", negated: !0 }],
|
|
7
|
+
["not_all", { op: "ALL", negated: !0 }],
|
|
8
|
+
["not_none", { op: "NONE", negated: !0 }],
|
|
9
|
+
["!any", { op: "ANY", negated: !0 }],
|
|
10
|
+
["!all", { op: "ALL", negated: !0 }],
|
|
11
|
+
["!none", { op: "NONE", negated: !0 }]
|
|
12
|
+
]);
|
|
13
|
+
function S(o, e) {
|
|
14
|
+
for (const [t, s] of v)
|
|
15
|
+
if (s.op === o && s.negated === e && /^[a-z_]+$/.test(t))
|
|
16
|
+
return t;
|
|
17
|
+
throw new Error(`no word spelling for collection operator ${o} (negated=${e})`);
|
|
18
|
+
}
|
|
19
|
+
const y = "Collection syntax accepted by the Rust engine only; the Python query API rejects it", _ = "Aggregation modifier on '*' accepted by the Rust engine only; the Python query API rejects it", A = /* @__PURE__ */ new Set([
|
|
20
|
+
"field",
|
|
21
|
+
"mutator",
|
|
22
|
+
"typeHint",
|
|
23
|
+
"aggregation",
|
|
24
|
+
"keyword",
|
|
25
|
+
"visualization",
|
|
26
|
+
"collectionOp"
|
|
27
|
+
]);
|
|
28
|
+
class I {
|
|
4
29
|
tokens;
|
|
5
30
|
allTokens;
|
|
6
31
|
// Including whitespace for highlighting
|
|
@@ -9,7 +34,7 @@ class v {
|
|
|
9
34
|
warnings;
|
|
10
35
|
depth;
|
|
11
36
|
constructor(e) {
|
|
12
|
-
this.allTokens =
|
|
37
|
+
this.allTokens = T(e), this.tokens = this.allTokens.filter((t) => t.type !== "whitespace"), this.current = 0, this.errors = [], this.warnings = [], this.depth = 0;
|
|
13
38
|
}
|
|
14
39
|
/**
|
|
15
40
|
* Parse the input and return the result
|
|
@@ -24,13 +49,13 @@ class v {
|
|
|
24
49
|
success: !0
|
|
25
50
|
};
|
|
26
51
|
if (this.isStatsQuery())
|
|
27
|
-
return {
|
|
52
|
+
return this.checkStatsModifiers(), {
|
|
28
53
|
ast: null,
|
|
29
54
|
// Stats AST is produced by the Python/Rust parser
|
|
30
55
|
tokens: this.allTokens,
|
|
31
|
-
errors:
|
|
32
|
-
warnings:
|
|
33
|
-
success:
|
|
56
|
+
errors: this.errors,
|
|
57
|
+
warnings: this.warnings,
|
|
58
|
+
success: this.errors.length === 0
|
|
34
59
|
};
|
|
35
60
|
try {
|
|
36
61
|
const e = this.parseExpression();
|
|
@@ -67,7 +92,7 @@ class v {
|
|
|
67
92
|
* Parse a full expression (lowest precedence)
|
|
68
93
|
*/
|
|
69
94
|
parseExpression() {
|
|
70
|
-
if (this.depth++, this.depth >
|
|
95
|
+
if (this.depth++, this.depth > L)
|
|
71
96
|
throw new Error("Maximum query depth exceeded");
|
|
72
97
|
try {
|
|
73
98
|
return this.parseOr();
|
|
@@ -89,14 +114,14 @@ class v {
|
|
|
89
114
|
"MISSING_OPERAND",
|
|
90
115
|
"Add a condition after 'OR'"
|
|
91
116
|
), e;
|
|
92
|
-
const
|
|
117
|
+
const s = this.parseAnd();
|
|
93
118
|
e = {
|
|
94
119
|
type: "binary",
|
|
95
120
|
operator: "OR",
|
|
96
121
|
left: e,
|
|
97
|
-
right:
|
|
122
|
+
right: s,
|
|
98
123
|
start: e.start,
|
|
99
|
-
end:
|
|
124
|
+
end: s.end
|
|
100
125
|
};
|
|
101
126
|
}
|
|
102
127
|
return e;
|
|
@@ -115,14 +140,14 @@ class v {
|
|
|
115
140
|
"MISSING_OPERAND",
|
|
116
141
|
"Add a condition after 'AND'"
|
|
117
142
|
), e;
|
|
118
|
-
const
|
|
143
|
+
const s = this.parseUnary();
|
|
119
144
|
e = {
|
|
120
145
|
type: "binary",
|
|
121
146
|
operator: "AND",
|
|
122
147
|
left: e,
|
|
123
|
-
right:
|
|
148
|
+
right: s,
|
|
124
149
|
start: e.start,
|
|
125
|
-
end:
|
|
150
|
+
end: s.end
|
|
126
151
|
};
|
|
127
152
|
}
|
|
128
153
|
return e;
|
|
@@ -149,6 +174,8 @@ class v {
|
|
|
149
174
|
parsePrimary() {
|
|
150
175
|
if (this.check("paren") && this.peek().value === "(")
|
|
151
176
|
return this.parseGroup();
|
|
177
|
+
if (this.check("collectionOp") && v.has(this.peek().value.toLowerCase()))
|
|
178
|
+
return this.parseCollectionFirst();
|
|
152
179
|
if (this.check("field") || this.check("aggregation") || this.check("mutator") || this.check("typeHint"))
|
|
153
180
|
return this.parseComparison();
|
|
154
181
|
const e = this.peek();
|
|
@@ -185,53 +212,222 @@ class v {
|
|
|
185
212
|
* - Optional mutator chain: field | lowercase | trim eq "value"
|
|
186
213
|
*/
|
|
187
214
|
parseComparison() {
|
|
188
|
-
const e = this.advance();
|
|
215
|
+
const e = this.advance(), t = this.parseFieldTail(e), s = this.tryParseCollectionTail(t);
|
|
216
|
+
return s !== null ? s : this.parseComparisonTail(e, t);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The part of a field reference after its name token: `::type` and `| mutator` chains.
|
|
220
|
+
*
|
|
221
|
+
* Split out of `parseComparison` so the operator-first collection form
|
|
222
|
+
* (`ANY field::string | lowercase eq "x"`) reads the same field grammar rather than a
|
|
223
|
+
* reduced copy of it. A second copy is how `field::geo` came to be accepted in one place
|
|
224
|
+
* and rejected in another.
|
|
225
|
+
*/
|
|
226
|
+
parseFieldTail(e) {
|
|
189
227
|
let t = this.createFieldReference(e);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
t = {
|
|
194
|
-
...t,
|
|
195
|
-
typeHint: s.value.toLowerCase(),
|
|
196
|
-
end: s.end
|
|
197
|
-
};
|
|
198
|
-
} else {
|
|
199
|
-
const s = this.isAtEnd() ? this.previous() : this.peek();
|
|
200
|
-
this.addWarning(
|
|
201
|
-
"Expected type hint after '::'",
|
|
202
|
-
s,
|
|
203
|
-
"Valid types: string, int, float, date, bool, array"
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
const r = [];
|
|
228
|
+
const s = this.readTypeHintSlot(t);
|
|
229
|
+
t = s.field;
|
|
230
|
+
const n = [], i = [];
|
|
207
231
|
for (; this.check("pipe"); )
|
|
208
|
-
if (this.advance(), this.check("mutator")) {
|
|
209
|
-
const
|
|
210
|
-
|
|
232
|
+
if (this.advance(), this.check("mutator") || this.isCatalogMutatorToken()) {
|
|
233
|
+
const r = this.advance();
|
|
234
|
+
n.push(r.value.toLowerCase()), i.push(this.parseMutatorCall(r));
|
|
211
235
|
} else if (this.check("field")) {
|
|
212
|
-
const
|
|
213
|
-
|
|
236
|
+
const r = this.advance();
|
|
237
|
+
n.push(r.value.toLowerCase()), this.addWarning(`Unknown mutator '${r.value}'`, r, "Check the mutator name spelling"), i.push(this.parseMutatorCall(r));
|
|
214
238
|
} else {
|
|
215
|
-
const
|
|
239
|
+
const r = this.isAtEnd() ? this.previous() : this.peek();
|
|
216
240
|
this.addError(
|
|
217
241
|
"Expected mutator name after '|'",
|
|
218
|
-
|
|
242
|
+
r,
|
|
219
243
|
"MISSING_MUTATOR",
|
|
220
244
|
"Add a mutator like 'lowercase', 'uppercase', 'trim', etc."
|
|
221
245
|
);
|
|
222
246
|
break;
|
|
223
247
|
}
|
|
224
|
-
if (
|
|
248
|
+
if (n.length > 0 && (t = {
|
|
225
249
|
...t,
|
|
226
|
-
mutators:
|
|
250
|
+
mutators: n,
|
|
251
|
+
mutatorCalls: i,
|
|
227
252
|
end: this.previous().end
|
|
228
|
-
}),
|
|
229
|
-
|
|
253
|
+
}), this.check("doubleColon"))
|
|
254
|
+
if (s.filled) {
|
|
255
|
+
const r = this.advance();
|
|
256
|
+
this.addError(
|
|
257
|
+
"A field may carry only one type hint",
|
|
258
|
+
r,
|
|
259
|
+
"DUPLICATE_TYPE_HINT",
|
|
260
|
+
`Write the hint once — either '${e.value}::${t.typeHint ?? "type"} | mutator' or '${e.value} | mutator::type'`
|
|
261
|
+
), (this.check("field") || this.check("typeHint") || this.check("mutator")) && this.advance();
|
|
262
|
+
} else
|
|
263
|
+
t = this.readTypeHintSlot(t).field;
|
|
264
|
+
return t;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Read one `::type` slot, if the current token opens one. Returns the field with the hint
|
|
268
|
+
* applied and whether a hint name was actually consumed.
|
|
269
|
+
*
|
|
270
|
+
* Called from BOTH sides of the mutator chain so the two spellings share one definition of
|
|
271
|
+
* what may follow `::`.
|
|
272
|
+
*
|
|
273
|
+
* Match on the token's VALUE, not its type. `classifyIdentifier` tests the mutator catalog
|
|
274
|
+
* before the type-hint catalog, so a name in both — `geo` is the live case, exported by
|
|
275
|
+
* TYPE_HINTS and by MUTATORS — arrives here typed "mutator" and was rejected. The package
|
|
276
|
+
* exported `geo` as a valid type hint while its own parser refused `field::geo`. After
|
|
277
|
+
* `::` the only thing a token can be is a type name, so the catalog is the right authority
|
|
278
|
+
* and the lexical class is noise.
|
|
279
|
+
*/
|
|
280
|
+
readTypeHintSlot(e) {
|
|
281
|
+
if (!this.check("doubleColon")) return { field: e, filled: !1 };
|
|
282
|
+
this.advance();
|
|
283
|
+
const t = this.isAtEnd() ? void 0 : this.peek();
|
|
284
|
+
if (t !== void 0 && f.includes(t.value.toLowerCase())) {
|
|
285
|
+
const s = this.advance();
|
|
286
|
+
return {
|
|
287
|
+
field: { ...e, typeHint: s.value.toLowerCase(), end: s.end },
|
|
288
|
+
filled: !0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (this.check("field")) {
|
|
292
|
+
const s = this.advance();
|
|
293
|
+
return {
|
|
294
|
+
field: { ...e, typeHint: s.value.toLowerCase(), end: s.end },
|
|
295
|
+
filled: !0
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return this.isAtEnd() ? (this.addWarning(
|
|
299
|
+
"Expected type hint after '::'",
|
|
300
|
+
this.previous(),
|
|
301
|
+
// Derived from the catalog rather than a hand-written excerpt, which had drifted
|
|
302
|
+
// to six of the fifteen names and omitted the very one being rejected.
|
|
303
|
+
`Valid types: ${f.join(", ")}`
|
|
304
|
+
), { field: e, filled: !1 }) : (this.addError(
|
|
305
|
+
"Expected type hint after '::'",
|
|
306
|
+
this.peek(),
|
|
307
|
+
"MISSING_TYPE_HINT",
|
|
308
|
+
// Catalog-derived for the same reason as the warning arm, so the two cannot drift.
|
|
309
|
+
`Name a type (${f.join(", ")}), or remove the '::'`
|
|
310
|
+
), { field: e, filled: !1 });
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* `ANY field [op] value` — the operator-first collection forms.
|
|
314
|
+
*
|
|
315
|
+
* `grammar.pest:226-231` gives `collection_comparison` four alternatives; this is the two
|
|
316
|
+
* that lead with the operator. The Python parser accepts NEITHER — probed, not inferred:
|
|
317
|
+
* `ANY f eq 'x'` and `ANY f 'x'` both raise `TQLSyntaxError at position 4`. They are
|
|
318
|
+
* therefore parsed AND flagged: see `RUST_ONLY_COLLECTION_MESSAGE`.
|
|
319
|
+
*
|
|
320
|
+
* The current token is the collection operator.
|
|
321
|
+
*/
|
|
322
|
+
parseCollectionFirst() {
|
|
323
|
+
const e = this.advance(), t = v.get(e.value.toLowerCase());
|
|
324
|
+
if (t === void 0)
|
|
325
|
+
return this.createPlaceholderExpression(e);
|
|
326
|
+
if (this.addWarning(
|
|
327
|
+
y,
|
|
328
|
+
e,
|
|
329
|
+
`Write it field-first instead: field ${e.value.toLowerCase()} value`,
|
|
330
|
+
"ENGINE_DIVERGENCE"
|
|
331
|
+
), !(this.check("field") || this.check("aggregation") || this.check("mutator") || this.check("typeHint"))) {
|
|
332
|
+
const n = this.isAtEnd() ? this.previous() : this.peek();
|
|
230
333
|
return this.addError(
|
|
231
|
-
`Expected
|
|
232
|
-
|
|
334
|
+
`Expected a field name after '${e.value}'`,
|
|
335
|
+
n,
|
|
336
|
+
"MISSING_FIELD",
|
|
337
|
+
`Use: ${e.value} field eq "value"`
|
|
338
|
+
), this.createPlaceholderExpression(e);
|
|
339
|
+
}
|
|
340
|
+
const s = this.parseFieldTail(this.advance());
|
|
341
|
+
return this.finishCollection(e, t.op, t.negated, s, e.start);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* `field [not] ANY [op] value`, or null when the token after the field is not a
|
|
345
|
+
* collection operator at all.
|
|
346
|
+
*
|
|
347
|
+
* Only `field ANY value` — no inner comparison operator — is accepted by both engines.
|
|
348
|
+
* `field ANY eq value` is `grammar.pest`'s third alternative and Python rejects it
|
|
349
|
+
* (`f any eq 'x'` -> `TQLSyntaxError at position 9`), so it parses with the divergence
|
|
350
|
+
* warning rather than being silently accepted or silently refused.
|
|
351
|
+
*/
|
|
352
|
+
tryParseCollectionTail(e) {
|
|
353
|
+
const t = this.check("logicalOp") && this.logicalWord(this.peek()) === "NOT", s = t ? 1 : 0, n = this.peekAt(s);
|
|
354
|
+
if (n === void 0 || n.type !== "collectionOp" && n.type !== "operator") return null;
|
|
355
|
+
const i = v.get(n.value.toLowerCase());
|
|
356
|
+
if (i === void 0 || t && i.negated) return null;
|
|
357
|
+
t && this.advance();
|
|
358
|
+
const r = this.advance();
|
|
359
|
+
return this.finishCollection(
|
|
360
|
+
r,
|
|
361
|
+
i.op,
|
|
362
|
+
i.negated || t,
|
|
363
|
+
e,
|
|
364
|
+
e.start
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The shared tail of both collection forms: an OPTIONAL inner comparison operator
|
|
369
|
+
* (defaulting to `eq`, as both grammars do) followed by a value or a value list.
|
|
370
|
+
*/
|
|
371
|
+
finishCollection(e, t, s, n, i) {
|
|
372
|
+
let r = "eq";
|
|
373
|
+
if (this.check("operator")) {
|
|
374
|
+
const h = this.advance();
|
|
375
|
+
r = this.normalizeOperator(h.value), this.addWarning(
|
|
376
|
+
y,
|
|
377
|
+
h,
|
|
378
|
+
`Drop the '${h.value}': the Python parser accepts only field ${e.value.toLowerCase()} value`,
|
|
379
|
+
"ENGINE_DIVERGENCE"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const u = this.check("bracket") && this.peek().value === "[" ? this.parseValueList() : this.parseValue(), a = this.previous().end;
|
|
383
|
+
return {
|
|
384
|
+
type: "collection",
|
|
385
|
+
operator: t,
|
|
386
|
+
negated: s,
|
|
387
|
+
field: n,
|
|
388
|
+
condition: {
|
|
389
|
+
type: "comparison",
|
|
390
|
+
field: n,
|
|
391
|
+
operator: r,
|
|
392
|
+
value: u,
|
|
393
|
+
start: n.start,
|
|
394
|
+
end: a
|
|
395
|
+
},
|
|
396
|
+
start: i,
|
|
397
|
+
end: a
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* `<field> <comparison-op> <value>` — everything after the field reference.
|
|
402
|
+
*
|
|
403
|
+
* `fieldToken` is threaded through only for the error messages, which name the field the
|
|
404
|
+
* user typed rather than the normalised reference.
|
|
405
|
+
*/
|
|
406
|
+
parseComparisonTail(e, t) {
|
|
407
|
+
const s = t.mutators ?? [], n = this.consumeNegationPrefix();
|
|
408
|
+
if (!this.check("operator")) {
|
|
409
|
+
const a = s.length > 0 ? s[s.length - 1] : null;
|
|
410
|
+
if (n === null && a !== null && N.has(a))
|
|
411
|
+
return {
|
|
412
|
+
type: "comparison",
|
|
413
|
+
field: t,
|
|
414
|
+
operator: "eq",
|
|
415
|
+
value: {
|
|
416
|
+
type: "literal",
|
|
417
|
+
valueType: "boolean",
|
|
418
|
+
value: !0,
|
|
419
|
+
start: t.end,
|
|
420
|
+
end: t.end
|
|
421
|
+
},
|
|
422
|
+
start: t.start,
|
|
423
|
+
end: t.end
|
|
424
|
+
};
|
|
425
|
+
const h = this.isAtEnd() ? this.previous() : this.peek(), p = s.length > 0 ? `'${e.value}' with mutator` : `'${e.value}'`;
|
|
426
|
+
return this.addError(
|
|
427
|
+
`Expected operator after field ${p}`,
|
|
428
|
+
h,
|
|
233
429
|
"MISSING_OPERATOR",
|
|
234
|
-
"Add an operator like 'eq', 'contains', '
|
|
430
|
+
"Add an operator like 'eq', 'contains', '=', etc."
|
|
235
431
|
), {
|
|
236
432
|
type: "comparison",
|
|
237
433
|
field: t,
|
|
@@ -247,96 +443,128 @@ class v {
|
|
|
247
443
|
end: t.end
|
|
248
444
|
};
|
|
249
445
|
}
|
|
250
|
-
const
|
|
251
|
-
|
|
446
|
+
const i = this.advance();
|
|
447
|
+
i.value === "==" && this.addError(
|
|
448
|
+
"Invalid operator '=='",
|
|
449
|
+
i,
|
|
450
|
+
"INVALID_OPERATOR",
|
|
451
|
+
"Use '=' or 'eq' for equality"
|
|
452
|
+
);
|
|
453
|
+
let r = this.normalizeOperator(i.value);
|
|
454
|
+
if (n !== null) {
|
|
455
|
+
const a = this.negateOperator(r);
|
|
456
|
+
a === null ? this.addError(
|
|
457
|
+
`'${n.value} ${i.value}' is not a TQL operator`,
|
|
458
|
+
n,
|
|
459
|
+
"INVALID_OPERATOR",
|
|
460
|
+
`'${r}' has no negated form; use 'ne' or negate the whole expression: not (field ${r} value)`
|
|
461
|
+
) : r = a;
|
|
462
|
+
}
|
|
463
|
+
if (r === "is" && this.check("logicalOp") && this.logicalWord(this.peek()) === "NOT" && (this.advance(), r = "is_not"), r === "exists" || r === "not_exists")
|
|
252
464
|
return {
|
|
253
465
|
type: "comparison",
|
|
254
466
|
field: t,
|
|
255
|
-
operator:
|
|
467
|
+
operator: r,
|
|
256
468
|
value: {
|
|
257
469
|
type: "literal",
|
|
258
470
|
valueType: "boolean",
|
|
259
471
|
value: !0,
|
|
260
|
-
start:
|
|
261
|
-
end:
|
|
472
|
+
start: i.end,
|
|
473
|
+
end: i.end
|
|
262
474
|
},
|
|
263
475
|
start: t.start,
|
|
264
|
-
end:
|
|
476
|
+
end: i.end
|
|
265
477
|
};
|
|
266
|
-
if ((
|
|
267
|
-
const
|
|
478
|
+
if ((r === "is" || r === "is_not") && this.check("null")) {
|
|
479
|
+
const a = this.advance();
|
|
268
480
|
return {
|
|
269
481
|
type: "comparison",
|
|
270
482
|
field: t,
|
|
271
|
-
operator:
|
|
483
|
+
operator: r,
|
|
272
484
|
value: {
|
|
273
485
|
type: "literal",
|
|
274
486
|
valueType: "null",
|
|
275
487
|
value: null,
|
|
276
|
-
start:
|
|
277
|
-
end:
|
|
488
|
+
start: a.start,
|
|
489
|
+
end: a.end
|
|
278
490
|
},
|
|
279
491
|
start: t.start,
|
|
280
|
-
end:
|
|
492
|
+
end: a.end
|
|
281
493
|
};
|
|
282
494
|
}
|
|
283
|
-
if (
|
|
284
|
-
const
|
|
495
|
+
if (/^(not_)?in(_cs)?$/.test(r)) {
|
|
496
|
+
const a = this.parseValueList();
|
|
285
497
|
return {
|
|
286
498
|
type: "comparison",
|
|
287
499
|
field: t,
|
|
288
|
-
operator:
|
|
289
|
-
value:
|
|
500
|
+
operator: r,
|
|
501
|
+
value: a,
|
|
290
502
|
start: t.start,
|
|
291
503
|
end: this.previous().end
|
|
292
504
|
};
|
|
293
505
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
506
|
+
if (r === "between" || r === "not_between") {
|
|
507
|
+
if (this.check("bracket") && this.peek().value === "[") {
|
|
508
|
+
const p = this.peek(), c = this.parseValueList();
|
|
509
|
+
return c.length !== 2 && this.addError(
|
|
510
|
+
`'${r}' needs exactly 2 values, got ${c.length}`,
|
|
511
|
+
p,
|
|
512
|
+
"INVALID_VALUE",
|
|
513
|
+
`Use: field ${r} [low, high] or field ${r} low AND high`
|
|
514
|
+
), {
|
|
515
|
+
type: "comparison",
|
|
516
|
+
field: t,
|
|
517
|
+
operator: r,
|
|
518
|
+
value: c,
|
|
519
|
+
start: t.start,
|
|
520
|
+
end: this.previous().end
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const a = this.parseValue();
|
|
524
|
+
this.matchLogicalWord("AND") || this.addError(
|
|
297
525
|
"Expected 'AND' between values in 'between' operator",
|
|
298
526
|
this.isAtEnd() ? this.previous() : this.peek(),
|
|
299
527
|
"INVALID_SYNTAX",
|
|
300
528
|
"Use format: field between value1 AND value2"
|
|
301
529
|
);
|
|
302
|
-
const
|
|
530
|
+
const h = this.parseValue();
|
|
303
531
|
return {
|
|
304
532
|
type: "comparison",
|
|
305
533
|
field: t,
|
|
306
|
-
operator:
|
|
307
|
-
value: [
|
|
534
|
+
operator: r,
|
|
535
|
+
value: [a, h],
|
|
308
536
|
start: t.start,
|
|
309
|
-
end:
|
|
537
|
+
end: h.end
|
|
310
538
|
};
|
|
311
539
|
}
|
|
312
540
|
if (this.isAtEnd())
|
|
313
541
|
return this.addError(
|
|
314
|
-
`Expected a value after operator '${
|
|
315
|
-
|
|
542
|
+
`Expected a value after operator '${r}'`,
|
|
543
|
+
i,
|
|
316
544
|
"MISSING_VALUE",
|
|
317
545
|
'Add a value like "text", 123, or true'
|
|
318
546
|
), {
|
|
319
547
|
type: "comparison",
|
|
320
548
|
field: t,
|
|
321
|
-
operator:
|
|
549
|
+
operator: r,
|
|
322
550
|
value: {
|
|
323
551
|
type: "literal",
|
|
324
552
|
valueType: "string",
|
|
325
553
|
value: "",
|
|
326
|
-
start:
|
|
327
|
-
end:
|
|
554
|
+
start: i.end,
|
|
555
|
+
end: i.end
|
|
328
556
|
},
|
|
329
557
|
start: t.start,
|
|
330
|
-
end:
|
|
558
|
+
end: i.end
|
|
331
559
|
};
|
|
332
|
-
const
|
|
560
|
+
const u = this.parseValue();
|
|
333
561
|
return {
|
|
334
562
|
type: "comparison",
|
|
335
563
|
field: t,
|
|
336
|
-
operator:
|
|
337
|
-
value:
|
|
564
|
+
operator: r,
|
|
565
|
+
value: u,
|
|
338
566
|
start: t.start,
|
|
339
|
-
end:
|
|
567
|
+
end: u.end
|
|
340
568
|
};
|
|
341
569
|
}
|
|
342
570
|
/**
|
|
@@ -407,12 +635,122 @@ class v {
|
|
|
407
635
|
*/
|
|
408
636
|
parseValueList() {
|
|
409
637
|
const e = [];
|
|
410
|
-
|
|
638
|
+
let t = null;
|
|
639
|
+
this.check("bracket") && this.peek().value === "[" ? t = this.advance() : this.check("paren") && this.peek().value === "(" && (t = this.advance());
|
|
411
640
|
do {
|
|
412
641
|
if (this.check("bracket") && this.peek().value === "]" || this.check("paren") && this.peek().value === ")") break;
|
|
413
642
|
e.push(this.parseValue());
|
|
414
643
|
} while (this.match("comma"));
|
|
415
|
-
|
|
644
|
+
if (this.check("bracket") && this.peek().value === "]")
|
|
645
|
+
this.advance();
|
|
646
|
+
else if (this.check("paren") && this.peek().value === ")")
|
|
647
|
+
this.advance();
|
|
648
|
+
else if (t !== null) {
|
|
649
|
+
const s = t.value === "[" ? "]" : ")";
|
|
650
|
+
this.addError(
|
|
651
|
+
`Missing closing '${s}' in value list`,
|
|
652
|
+
this.isAtEnd() ? this.previous() : this.peek(),
|
|
653
|
+
"UNBALANCED_BRACKETS",
|
|
654
|
+
`Close the list: ${t.value}...${s}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return t !== null && e.length === 0 && this.addError(
|
|
658
|
+
"Empty value list",
|
|
659
|
+
t,
|
|
660
|
+
"INVALID_VALUE",
|
|
661
|
+
"A list needs at least one value. Every TQL engine rejects an empty list, so this query will not run in the editor, in the backend, or in the agent."
|
|
662
|
+
), e;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Parse the optional argument list of a mutator call.
|
|
666
|
+
*
|
|
667
|
+
* `field | split(delimiter=",") eq "bob"` is valid TQL that both engines accept
|
|
668
|
+
* (`mutator = { "|" ~ !^"stats" ~ mutator_name ~ ("(" ~ mutator_args? ~ ")")? }` in
|
|
669
|
+
* tql/src/parser/grammar.pest; `mutator_with_params` in
|
|
670
|
+
* src/tql/parser_components/grammar.py). The JS parser used to stop at the mutator
|
|
671
|
+
* NAME, so the '(' fell through to the top-level "Unexpected token" arm and the editor
|
|
672
|
+
* red-squiggled a query the backend runs happily -- while this package's own completion
|
|
673
|
+
* documentation advertised that exact call form.
|
|
674
|
+
*
|
|
675
|
+
* The name token has already been consumed by the caller.
|
|
676
|
+
*/
|
|
677
|
+
parseMutatorCall(e) {
|
|
678
|
+
const t = e.value.toLowerCase(), s = [];
|
|
679
|
+
if (!(this.check("paren") && this.peek().value === "("))
|
|
680
|
+
return { name: t, args: s, parenthesized: !1, start: e.start, end: e.end };
|
|
681
|
+
if (this.advance(), !(this.check("paren") && this.peek().value === ")"))
|
|
682
|
+
for (; ; ) {
|
|
683
|
+
const n = this.parseMutatorArgument(t);
|
|
684
|
+
if (n === null) break;
|
|
685
|
+
if (s.push(n), this.check("comma")) {
|
|
686
|
+
this.advance();
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
return this.check("paren") && this.peek().value === ")" ? this.advance() : this.addError(
|
|
692
|
+
`Missing closing parenthesis ')' after arguments to '${t}'`,
|
|
693
|
+
this.isAtEnd() ? this.previous() : this.peek(),
|
|
694
|
+
"UNBALANCED_PARENS",
|
|
695
|
+
`Close the argument list: ${t}(...)`
|
|
696
|
+
), { name: t, args: s, parenthesized: !0, start: e.start, end: this.previous().end };
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Parse one mutator argument: `value`, or `name=value`.
|
|
700
|
+
*
|
|
701
|
+
* Returns null (after recording an error) when the current token cannot start an
|
|
702
|
+
* argument, so the caller stops rather than spinning.
|
|
703
|
+
*/
|
|
704
|
+
parseMutatorArgument(e) {
|
|
705
|
+
const t = this.isAtEnd() ? this.previous() : this.peek();
|
|
706
|
+
if (A.has(t.type) && this.peekAt(1)?.type === "operator" && this.peekAt(1)?.value === "=") {
|
|
707
|
+
const n = this.advance();
|
|
708
|
+
this.advance();
|
|
709
|
+
const i = this.parseMutatorArgumentValue(e);
|
|
710
|
+
return i === null ? null : { name: n.value, ...i, start: n.start, end: i.end };
|
|
711
|
+
}
|
|
712
|
+
const s = this.parseMutatorArgumentValue(e);
|
|
713
|
+
return s === null ? null : s;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Parse the value half of a mutator argument.
|
|
717
|
+
*
|
|
718
|
+
* Neither engine's grammar admits a list literal here, so `[...]` is an error rather
|
|
719
|
+
* than a silently-accepted superset.
|
|
720
|
+
*/
|
|
721
|
+
parseMutatorArgumentValue(e) {
|
|
722
|
+
const t = this.isAtEnd() ? this.previous() : this.peek();
|
|
723
|
+
if (this.check("string")) {
|
|
724
|
+
const s = this.advance();
|
|
725
|
+
return { valueType: "string", value: s.value, start: s.start, end: s.end };
|
|
726
|
+
}
|
|
727
|
+
if (this.check("number")) {
|
|
728
|
+
const s = this.advance();
|
|
729
|
+
return { valueType: "number", value: parseFloat(s.value), start: s.start, end: s.end };
|
|
730
|
+
}
|
|
731
|
+
if (this.check("boolean")) {
|
|
732
|
+
const s = this.advance();
|
|
733
|
+
return { valueType: "boolean", value: s.value.toLowerCase() === "true", start: s.start, end: s.end };
|
|
734
|
+
}
|
|
735
|
+
if (this.check("null")) {
|
|
736
|
+
const s = this.advance();
|
|
737
|
+
return { valueType: "null", value: null, start: s.start, end: s.end };
|
|
738
|
+
}
|
|
739
|
+
if (A.has(t.type) && !this.isAtEnd()) {
|
|
740
|
+
const s = this.advance();
|
|
741
|
+
return { valueType: "identifier", value: s.value, start: s.start, end: s.end };
|
|
742
|
+
}
|
|
743
|
+
return this.check("bracket") ? (this.addError(
|
|
744
|
+
`List arguments are not supported by '${e}'`,
|
|
745
|
+
t,
|
|
746
|
+
"INVALID_VALUE",
|
|
747
|
+
"Pass a string, number, boolean or bare identifier"
|
|
748
|
+
), null) : (this.addError(
|
|
749
|
+
`Expected an argument value in '${e}(...)'`,
|
|
750
|
+
t,
|
|
751
|
+
"MISSING_VALUE",
|
|
752
|
+
`Write it as ${e}(name="value") or ${e}("value")`
|
|
753
|
+
), null);
|
|
416
754
|
}
|
|
417
755
|
// ============================================================================
|
|
418
756
|
// Helper Methods
|
|
@@ -426,9 +764,52 @@ class v {
|
|
|
426
764
|
end: e.end
|
|
427
765
|
};
|
|
428
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Consume a `not` / `!` sitting between a field and its operator, if one is there.
|
|
769
|
+
*
|
|
770
|
+
* The two-token spelling is the PRIMARY form in both grammars, not an alias: fourteen
|
|
771
|
+
* rules in `tql/src/parser/grammar.pest` are declared as
|
|
772
|
+
* `not_X_op = { neg_prefix ~ X_op | not_X_word }`, and Python builds the same shape with
|
|
773
|
+
* `(CaselessKeyword("not") | "!") + CaselessKeyword("X")`. `field not contains "x"` is
|
|
774
|
+
* how the documentation and the UI examples spell it.
|
|
775
|
+
*
|
|
776
|
+
* This package accepted only the underscore form (`not_contains`) and the glued symbol
|
|
777
|
+
* (`!contains`). Every two-token negation -- `not exists`, `not in`, `not between`,
|
|
778
|
+
* `not startswith`, `not cidr`, ten more -- was reported as
|
|
779
|
+
* `Expected operator after field 'f'`, a false red on the most readable spelling of a
|
|
780
|
+
* third of the operator surface.
|
|
781
|
+
*
|
|
782
|
+
* Only consumed when a comparison operator actually follows. Without that lookahead this
|
|
783
|
+
* would swallow the `not` in `a eq 1 and b not (c eq 2)`, where it belongs to the unary
|
|
784
|
+
* arm; leaving it in place lets the existing error recovery report on it as before.
|
|
785
|
+
*/
|
|
786
|
+
consumeNegationPrefix() {
|
|
787
|
+
if (!(this.check("logicalOp") && this.logicalWord(this.peek()) === "NOT"))
|
|
788
|
+
return null;
|
|
789
|
+
const e = this.peekAt(1);
|
|
790
|
+
return e === void 0 || e.type !== "operator" ? null : this.advance();
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* The negated form of an operator, or null when it has none.
|
|
794
|
+
*
|
|
795
|
+
* DERIVED from `COMPARISON_OPERATORS`, not transcribed: `not X` is a TQL operator exactly
|
|
796
|
+
* when `not_X` is. That catalog is itself derived from both engine grammars and pinned by
|
|
797
|
+
* `tests/catalogParity.spec.ts`, so this rule cannot drift from the grammars without that
|
|
798
|
+
* test going red first -- and `tests/negatedOperatorForms.spec.ts` re-derives the
|
|
799
|
+
* `neg_prefix ~ X_op` rules straight out of `grammar.pest` and asserts this parser accepts
|
|
800
|
+
* exactly them.
|
|
801
|
+
*
|
|
802
|
+
* Returning null rather than fabricating `not_eq` matters. Neither engine has a
|
|
803
|
+
* `neg_prefix ~ eq_op` rule, so `f not eq 1` is a syntax error in both, and inventing an
|
|
804
|
+
* operator no evaluator dispatches on would turn a parse error into an unknown-operator
|
|
805
|
+
* failure at execution -- later, and further from the user.
|
|
806
|
+
*/
|
|
807
|
+
negateOperator(e) {
|
|
808
|
+
const t = `not_${e}`;
|
|
809
|
+
return k.includes(t) ? t : null;
|
|
810
|
+
}
|
|
429
811
|
normalizeOperator(e) {
|
|
430
|
-
const t = e.toLowerCase(),
|
|
431
|
-
"==": "eq",
|
|
812
|
+
const t = e.toLowerCase(), s = {
|
|
432
813
|
"=": "eq",
|
|
433
814
|
"!=": "ne",
|
|
434
815
|
">": "gt",
|
|
@@ -436,32 +817,16 @@ class v {
|
|
|
436
817
|
"<": "lt",
|
|
437
818
|
"<=": "lte"
|
|
438
819
|
};
|
|
439
|
-
if (e in
|
|
440
|
-
return
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"!contains": "not_contains",
|
|
444
|
-
"!startswith": "not_startswith",
|
|
445
|
-
"!endswith": "not_endswith",
|
|
446
|
-
"!regexp": "not_regexp",
|
|
447
|
-
"!regex": "not_regex",
|
|
448
|
-
"!matches": "not_matches",
|
|
449
|
-
"!between": "not_between",
|
|
450
|
-
"!exists": "not_exists",
|
|
451
|
-
"!is": "is_not",
|
|
452
|
-
"!cidr": "not_cidr",
|
|
453
|
-
"!any": "not_any",
|
|
454
|
-
"!all": "not_all",
|
|
455
|
-
"!none": "not_none"
|
|
456
|
-
};
|
|
457
|
-
if (t in a)
|
|
458
|
-
return a[t];
|
|
820
|
+
if (e in s)
|
|
821
|
+
return s[e];
|
|
822
|
+
if (t in g)
|
|
823
|
+
return g[t];
|
|
459
824
|
const n = {
|
|
460
825
|
regex: "regexp",
|
|
461
826
|
starts_with: "startswith",
|
|
462
827
|
ends_with: "endswith"
|
|
463
828
|
};
|
|
464
|
-
return t in n ? n[t] : (
|
|
829
|
+
return t in n ? n[t] : (k.includes(t), t);
|
|
465
830
|
}
|
|
466
831
|
isAtEnd() {
|
|
467
832
|
return this.current >= this.tokens.length;
|
|
@@ -469,6 +834,10 @@ class v {
|
|
|
469
834
|
peek() {
|
|
470
835
|
return this.tokens[this.current];
|
|
471
836
|
}
|
|
837
|
+
/** Lookahead by `offset` tokens without consuming; undefined past the end. */
|
|
838
|
+
peekAt(e) {
|
|
839
|
+
return this.tokens[this.current + e];
|
|
840
|
+
}
|
|
472
841
|
previous() {
|
|
473
842
|
return this.tokens[this.current - 1];
|
|
474
843
|
}
|
|
@@ -478,6 +847,19 @@ class v {
|
|
|
478
847
|
check(e) {
|
|
479
848
|
return this.isAtEnd() ? !1 : this.peek().type === e;
|
|
480
849
|
}
|
|
850
|
+
/**
|
|
851
|
+
* True when the current token names a mutator in `MUTATORS` but was lexed as something
|
|
852
|
+
* else because an earlier catalog claimed the name first.
|
|
853
|
+
*
|
|
854
|
+
* Deliberately consults the catalog rather than listing the seven affected names: a list
|
|
855
|
+
* would be an eighth transcription of a set this package already maintains, and it would
|
|
856
|
+
* go stale the moment a mutator is added that collides with an aggregation.
|
|
857
|
+
*/
|
|
858
|
+
isCatalogMutatorToken() {
|
|
859
|
+
if (this.isAtEnd()) return !1;
|
|
860
|
+
const e = this.peek();
|
|
861
|
+
return e.type !== "collectionOp" && e.type !== "aggregation" ? !1 : w.includes(e.value.toLowerCase());
|
|
862
|
+
}
|
|
481
863
|
match(...e) {
|
|
482
864
|
for (const t of e)
|
|
483
865
|
if (this.check(t))
|
|
@@ -485,9 +867,31 @@ class v {
|
|
|
485
867
|
return !1;
|
|
486
868
|
}
|
|
487
869
|
matchLogical(e) {
|
|
488
|
-
return this.check("logicalOp") && this.
|
|
870
|
+
return this.check("logicalOp") && this.logicalWord(this.peek()) === e ? (this.advance(), !0) : !1;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* The canonical word a `logicalOp` token spells: `AND`, `OR` or `NOT`.
|
|
874
|
+
*
|
|
875
|
+
* `&&`, `||` and `!` are the symbolic halves of the same three grammar rules as `and`,
|
|
876
|
+
* `or` and `not` (see SYMBOL_LOGICAL_OPERATORS), so every consumer must see one name.
|
|
877
|
+
* Normalising HERE rather than in the tokenizer keeps the token's `value` equal to the
|
|
878
|
+
* source text, which the highlighting and completion surfaces read.
|
|
879
|
+
*/
|
|
880
|
+
logicalWord(e) {
|
|
881
|
+
return E[e.value] ?? e.value.toUpperCase();
|
|
489
882
|
}
|
|
490
|
-
|
|
883
|
+
/**
|
|
884
|
+
* Match the WORD spelling of a logical operator only, never the symbol.
|
|
885
|
+
*
|
|
886
|
+
* Used for the `and` inside `field between low AND high`. Both engines spell that one
|
|
887
|
+
* literally -- `value ~ ^"and" ~ value` in `grammar.pest`, `CaselessKeyword("and")` in
|
|
888
|
+
* `between_comparison_natural` -- and neither accepts `between 1 && 5`. Accepting it here
|
|
889
|
+
* would make this package the only one of the three that does.
|
|
890
|
+
*/
|
|
891
|
+
matchLogicalWord(e) {
|
|
892
|
+
return this.check("logicalOp") && this.peek().value.toUpperCase() === e && !(this.peek().value in E) ? (this.advance(), !0) : !1;
|
|
893
|
+
}
|
|
894
|
+
addError(e, t, s, n) {
|
|
491
895
|
this.errors.push({
|
|
492
896
|
message: e,
|
|
493
897
|
start: t.start,
|
|
@@ -495,11 +899,11 @@ class v {
|
|
|
495
899
|
line: t.line,
|
|
496
900
|
column: t.column,
|
|
497
901
|
severity: "error",
|
|
498
|
-
code:
|
|
499
|
-
suggestion:
|
|
902
|
+
code: s,
|
|
903
|
+
suggestion: n
|
|
500
904
|
});
|
|
501
905
|
}
|
|
502
|
-
addWarning(e, t,
|
|
906
|
+
addWarning(e, t, s, n = "TYPE_MISMATCH") {
|
|
503
907
|
this.warnings.push({
|
|
504
908
|
message: e,
|
|
505
909
|
start: t.start,
|
|
@@ -507,8 +911,8 @@ class v {
|
|
|
507
911
|
line: t.line,
|
|
508
912
|
column: t.column,
|
|
509
913
|
severity: "warning",
|
|
510
|
-
code:
|
|
511
|
-
suggestion:
|
|
914
|
+
code: n,
|
|
915
|
+
suggestion: s
|
|
512
916
|
});
|
|
513
917
|
}
|
|
514
918
|
/**
|
|
@@ -517,7 +921,7 @@ class v {
|
|
|
517
921
|
*/
|
|
518
922
|
synchronize() {
|
|
519
923
|
for (this.advance(); !this.isAtEnd(); ) {
|
|
520
|
-
if (
|
|
924
|
+
if (O.has(this.peek().type) || this.peek().type === "paren" && this.peek().value === ")")
|
|
521
925
|
return;
|
|
522
926
|
this.advance();
|
|
523
927
|
}
|
|
@@ -548,6 +952,88 @@ class v {
|
|
|
548
952
|
end: e.end
|
|
549
953
|
};
|
|
550
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Flag the malformed `top N` / `bottom N` spellings on a stats aggregation.
|
|
957
|
+
*
|
|
958
|
+
* ## Why this is a scan and not a stats parser
|
|
959
|
+
*
|
|
960
|
+
* This package deliberately does not build a stats AST — the engines do. But "we do not
|
|
961
|
+
* parse it" had silently become "we accept anything after `stats`": measured at HEAD, every
|
|
962
|
+
* one of `sum(x, top 10) top 5`, `sum(x) top by y` and `count(*, top 2)` is a parse error in
|
|
963
|
+
* BOTH engines and was reported valid here.
|
|
964
|
+
*
|
|
965
|
+
* A full stats parser would have to take a side on five spellings the engines currently
|
|
966
|
+
* DISAGREE about (`count(*) top 2`, `sum(x|trim) top 3`, `sum(x::number) top 3`,
|
|
967
|
+
* `sum(x|trim, top 3)`, `by y|trim top 3` — Rust accepts all five, Python refuses all five).
|
|
968
|
+
* Resolving those is an engine question. So this fires ONLY on shapes both engines refuse,
|
|
969
|
+
* and is silent everywhere else — it can never squiggle a query an engine accepts, which is
|
|
970
|
+
* the failure mode this parser keeps being repaired for.
|
|
971
|
+
*
|
|
972
|
+
* ## Why the group-by region is excluded
|
|
973
|
+
*
|
|
974
|
+
* `top` and `bottom` are ordinary identifiers, and `| stats sum(x) by top` is valid in BOTH
|
|
975
|
+
* engines — a group-by on a field named `top`. So "a `top` not followed by a number" is only
|
|
976
|
+
* unambiguous in the two AGGREGATION modifier positions: directly after the aggregation's
|
|
977
|
+
* `)`, or after a comma at the aggregation's own paren depth. `by y top` (no count) is
|
|
978
|
+
* refused by both engines and is deliberately NOT flagged here, because distinguishing it
|
|
979
|
+
* from `by top` needs the grammar this method is declining to reimplement.
|
|
980
|
+
*/
|
|
981
|
+
checkStatsModifiers() {
|
|
982
|
+
const e = (s) => s !== void 0 && s.type === "keyword" && (s.value.toLowerCase() === "top" || s.value.toLowerCase() === "bottom");
|
|
983
|
+
let t = this.tokens.length;
|
|
984
|
+
for (let s = 0; s < this.tokens.length; s++) {
|
|
985
|
+
const n = this.tokens[s];
|
|
986
|
+
if (n.type === "keyword" && n.value.toLowerCase() === "by" || n.value === "=>") {
|
|
987
|
+
t = s;
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
for (let s = 0; s < t; s++) {
|
|
992
|
+
if (this.tokens[s].type !== "aggregation") continue;
|
|
993
|
+
const n = s + 1;
|
|
994
|
+
if (n >= t || this.tokens[n].value !== "(") continue;
|
|
995
|
+
let i = 0, r = -1, u = -1, a;
|
|
996
|
+
for (let l = n; l < t; l++) {
|
|
997
|
+
const d = this.tokens[l];
|
|
998
|
+
if (d.value === "(") i++;
|
|
999
|
+
else if (d.value === ")") {
|
|
1000
|
+
if (i--, i === 0) {
|
|
1001
|
+
r = l;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
} else i === 1 && d.type === "comma" && (u === -1 && (u = l), e(this.tokens[l + 1]) && (a = this.tokens[l + 1]));
|
|
1005
|
+
}
|
|
1006
|
+
if (r === -1) continue;
|
|
1007
|
+
const h = this.tokens.slice(n + 1, u === -1 ? r : u), p = h.length === 0 || h.length === 1 && h[0].value === "*", c = e(this.tokens[r + 1]) ? this.tokens[r + 1] : void 0;
|
|
1008
|
+
a !== void 0 && c !== void 0 && this.addError(
|
|
1009
|
+
"The 'top'/'bottom' modifier is given twice on one aggregation",
|
|
1010
|
+
c,
|
|
1011
|
+
"INVALID_STATS_MODIFIER",
|
|
1012
|
+
"Write it once — either 'sum(x, top 10)' or 'sum(x) top 10'"
|
|
1013
|
+
);
|
|
1014
|
+
for (const l of [a, c]) {
|
|
1015
|
+
if (l === void 0) continue;
|
|
1016
|
+
const d = this.tokens.indexOf(l);
|
|
1017
|
+
this.tokens[d + 1]?.type !== "number" && this.addError(
|
|
1018
|
+
`Expected a count after '${l.value}'`,
|
|
1019
|
+
l,
|
|
1020
|
+
"INVALID_STATS_MODIFIER",
|
|
1021
|
+
`Give the modifier a number, like '${l.value.toLowerCase()} 10'`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
a !== void 0 && p && this.addError(
|
|
1025
|
+
"'*' cannot take an in-parens 'top'/'bottom' modifier",
|
|
1026
|
+
a,
|
|
1027
|
+
"INVALID_STATS_MODIFIER",
|
|
1028
|
+
"Name a field, or move the modifier outside the parentheses"
|
|
1029
|
+
), c !== void 0 && p && a === void 0 && this.addWarning(
|
|
1030
|
+
_,
|
|
1031
|
+
c,
|
|
1032
|
+
"Name a field instead of '*', which both engines accept",
|
|
1033
|
+
"ENGINE_DIVERGENCE"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
551
1037
|
/**
|
|
552
1038
|
* Check if the token stream represents a stats query.
|
|
553
1039
|
* Handles: "| stats ...", "stats ...", or starts with a filter before "| stats".
|
|
@@ -572,23 +1058,24 @@ class v {
|
|
|
572
1058
|
return t >= this.tokens.length ? !1 : this.tokens[t].type === "keyword" && this.tokens[t].value.toLowerCase() === "stats";
|
|
573
1059
|
}
|
|
574
1060
|
}
|
|
575
|
-
function
|
|
576
|
-
return
|
|
1061
|
+
function m(o) {
|
|
1062
|
+
return o ? new I(o).parse() : { ast: null, errors: [], warnings: [], tokens: [], success: !0 };
|
|
577
1063
|
}
|
|
578
|
-
function
|
|
579
|
-
return
|
|
1064
|
+
function b(o) {
|
|
1065
|
+
return m(o).success;
|
|
580
1066
|
}
|
|
581
|
-
function
|
|
582
|
-
return
|
|
1067
|
+
function x(o) {
|
|
1068
|
+
return m(o).errors;
|
|
583
1069
|
}
|
|
584
|
-
function
|
|
585
|
-
let e = `Error at line ${
|
|
586
|
-
return
|
|
1070
|
+
function M(o) {
|
|
1071
|
+
let e = `Error at line ${o.line}, column ${o.column}: ${o.message}`;
|
|
1072
|
+
return o.suggestion && (e += ` (${o.suggestion})`), e;
|
|
587
1073
|
}
|
|
588
1074
|
export {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1075
|
+
I as TqlParser,
|
|
1076
|
+
S as collectionOperatorSpelling,
|
|
1077
|
+
M as formatParseError,
|
|
1078
|
+
x as getTqlErrors,
|
|
1079
|
+
b as isValidTql,
|
|
1080
|
+
m as parseTql
|
|
594
1081
|
};
|