@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.
@@ -1,6 +1,31 @@
1
- import { tokenizeTql as u, COMPARISON_OPERATORS as c } from "./tokenizer.js";
2
- const p = 50, d = /* @__PURE__ */ new Set(["logicalOp", "paren"]);
3
- class v {
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 = u(e), this.tokens = this.allTokens.filter((t) => t.type !== "whitespace"), this.current = 0, this.errors = [], this.warnings = [], this.depth = 0;
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: !0
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 > p)
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 r = this.parseAnd();
117
+ const s = this.parseAnd();
93
118
  e = {
94
119
  type: "binary",
95
120
  operator: "OR",
96
121
  left: e,
97
- right: r,
122
+ right: s,
98
123
  start: e.start,
99
- end: r.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 r = this.parseUnary();
143
+ const s = this.parseUnary();
119
144
  e = {
120
145
  type: "binary",
121
146
  operator: "AND",
122
147
  left: e,
123
- right: r,
148
+ right: s,
124
149
  start: e.start,
125
- end: r.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
- if (this.check("doubleColon"))
191
- if (this.advance(), this.check("typeHint") || this.check("field")) {
192
- const s = this.advance();
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 s = this.advance();
210
- r.push(s.value.toLowerCase());
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 s = this.advance();
213
- r.push(s.value.toLowerCase()), this.addWarning(`Unknown mutator '${s.value}'`, s, "Check the mutator name spelling");
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 s = this.isAtEnd() ? this.previous() : this.peek();
239
+ const r = this.isAtEnd() ? this.previous() : this.peek();
216
240
  this.addError(
217
241
  "Expected mutator name after '|'",
218
- s,
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 (r.length > 0 && (t = {
248
+ if (n.length > 0 && (t = {
225
249
  ...t,
226
- mutators: r,
250
+ mutators: n,
251
+ mutatorCalls: i,
227
252
  end: this.previous().end
228
- }), !this.check("operator")) {
229
- const s = this.isAtEnd() ? this.previous() : this.peek(), o = r.length > 0 ? `'${e.value}' with mutator` : `'${e.value}'`;
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 operator after field ${o}`,
232
- s,
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', '==', etc."
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 a = this.advance(), n = this.normalizeOperator(a.value);
251
- if (n === "exists" || n === "not_exists")
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: n,
467
+ operator: r,
256
468
  value: {
257
469
  type: "literal",
258
470
  valueType: "boolean",
259
471
  value: !0,
260
- start: a.end,
261
- end: a.end
472
+ start: i.end,
473
+ end: i.end
262
474
  },
263
475
  start: t.start,
264
- end: a.end
476
+ end: i.end
265
477
  };
266
- if ((n === "is" || n === "is_not") && this.check("null")) {
267
- const s = this.advance();
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: n,
483
+ operator: r,
272
484
  value: {
273
485
  type: "literal",
274
486
  valueType: "null",
275
487
  value: null,
276
- start: s.start,
277
- end: s.end
488
+ start: a.start,
489
+ end: a.end
278
490
  },
279
491
  start: t.start,
280
- end: s.end
492
+ end: a.end
281
493
  };
282
494
  }
283
- if (n === "in" || n === "not_in") {
284
- const s = this.parseValueList();
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: n,
289
- value: s,
500
+ operator: r,
501
+ value: a,
290
502
  start: t.start,
291
503
  end: this.previous().end
292
504
  };
293
505
  }
294
- if (n === "between" || n === "not_between") {
295
- const s = this.parseValue();
296
- this.matchLogical("AND") || this.addError(
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 o = this.parseValue();
530
+ const h = this.parseValue();
303
531
  return {
304
532
  type: "comparison",
305
533
  field: t,
306
- operator: n,
307
- value: [s, o],
534
+ operator: r,
535
+ value: [a, h],
308
536
  start: t.start,
309
- end: o.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 '${n}'`,
315
- a,
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: n,
549
+ operator: r,
322
550
  value: {
323
551
  type: "literal",
324
552
  valueType: "string",
325
553
  value: "",
326
- start: a.end,
327
- end: a.end
554
+ start: i.end,
555
+ end: i.end
328
556
  },
329
557
  start: t.start,
330
- end: a.end
558
+ end: i.end
331
559
  };
332
- const h = this.parseValue();
560
+ const u = this.parseValue();
333
561
  return {
334
562
  type: "comparison",
335
563
  field: t,
336
- operator: n,
337
- value: h,
564
+ operator: r,
565
+ value: u,
338
566
  start: t.start,
339
- end: h.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
- this.check("bracket") && this.peek().value === "[" ? this.advance() : this.check("paren") && this.peek().value === "(" && this.advance();
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
- return this.check("bracket") && this.peek().value === "]" ? this.advance() : this.check("paren") && this.peek().value === ")" && this.advance(), e;
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(), r = {
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 r)
440
- return r[e];
441
- const a = {
442
- "!in": "not_in",
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] : (c.includes(t), 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.peek().value.toUpperCase() === e ? (this.advance(), !0) : !1;
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
- addError(e, t, r, a) {
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: r,
499
- suggestion: a
902
+ code: s,
903
+ suggestion: n
500
904
  });
501
905
  }
502
- addWarning(e, t, r) {
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: "TYPE_MISMATCH",
511
- suggestion: r
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 (d.has(this.peek().type) || this.peek().type === "paren" && this.peek().value === ")")
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 l(i) {
576
- return i ? new v(i).parse() : { ast: null, errors: [], warnings: [], tokens: [], success: !0 };
1061
+ function m(o) {
1062
+ return o ? new I(o).parse() : { ast: null, errors: [], warnings: [], tokens: [], success: !0 };
577
1063
  }
578
- function f(i) {
579
- return l(i).success;
1064
+ function b(o) {
1065
+ return m(o).success;
580
1066
  }
581
- function y(i) {
582
- return l(i).errors;
1067
+ function x(o) {
1068
+ return m(o).errors;
583
1069
  }
584
- function g(i) {
585
- let e = `Error at line ${i.line}, column ${i.column}: ${i.message}`;
586
- return i.suggestion && (e += ` (${i.suggestion})`), e;
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
- v as TqlParser,
590
- g as formatParseError,
591
- y as getTqlErrors,
592
- f as isValidTql,
593
- l as parseTql
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
  };