@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,22 +1,125 @@
1
- import { parseTql as g } from "./parser.js";
2
- const c = {
3
- // Equality (all types)
4
- eq: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
5
- ne: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
6
- "=": ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
7
- "==": ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
8
- "!=": ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
1
+ import { parseTql as w } from "./parser.js";
2
+ const u = 1e3, r = [
3
+ "text",
4
+ "keyword",
5
+ "long",
6
+ "integer",
7
+ "short",
8
+ "byte",
9
+ "double",
10
+ "float",
11
+ "half_float",
12
+ "scaled_float",
13
+ "unsigned_long",
14
+ "date",
15
+ "boolean",
16
+ "ip",
17
+ "nested",
18
+ "object",
19
+ "geo_point"
20
+ ], h = [
21
+ "long",
22
+ "integer",
23
+ "short",
24
+ "byte",
25
+ "double",
26
+ "float",
27
+ "half_float",
28
+ "scaled_float",
29
+ "unsigned_long"
30
+ ], l = [...h, "date", "keyword"], c = [...h, "date"], y = {
31
+ // Equality (all types) — and now that is what it says.
32
+ //
33
+ // The transcribed twelve-element list this replaces claimed "all types" while omitting
34
+ // five: `short`, `byte`, `half_float`, `scaled_float` and `unsigned_long` are all in the
35
+ // `TqlFieldType` union and were all absent, so `port eq 443` on a `short` field warned
36
+ // that equality "may not work well" with it.
37
+ eq: r,
38
+ ne: r,
39
+ "=": r,
40
+ "!=": r,
9
41
  eq_ci: ["text", "keyword"],
10
- // Comparison (numeric and dates)
11
- gt: ["long", "integer", "double", "float", "date"],
12
- gte: ["long", "integer", "double", "float", "date"],
13
- lt: ["long", "integer", "double", "float", "date"],
14
- lte: ["long", "integer", "double", "float", "date"],
15
- ">": ["long", "integer", "double", "float", "date"],
16
- ">=": ["long", "integer", "double", "float", "date"],
17
- "<": ["long", "integer", "double", "float", "date"],
18
- "<=": ["long", "integer", "double", "float", "date"],
42
+ // Ordered comparison — numeric, date, AND keyword.
43
+ //
44
+ // `keyword` is not a courtesy. `compare_ordered` in `tql/src/comparator.rs` tries numbers
45
+ // first and falls back to a LEXICOGRAPHIC string comparison when both sides are strings,
46
+ // Python does the same, and Python's own validator lists all four in `keyword_ops`
47
+ // (`_validate_simple_type_compatibility`). Warning here was a false positive against every
48
+ // layer at once — `host.name gte "m"` is a query the editor flagged, the validator
49
+ // accepts, the evaluator answers and the cluster serves.
50
+ //
51
+ // `text` stays out, and that is the same source's judgement rather than an omission:
52
+ // `text_ops` excludes the four, because a range over an ANALYZED field compares tokens
53
+ // rather than the value the user typed. `between` also stays out — see the note on it.
54
+ //
55
+ // Every numeric width is listed via ALL_NUMERIC_FIELD_TYPES so the five narrow ones cannot
56
+ // fall out again the way they did from `eq`.
57
+ gt: l,
58
+ gte: l,
59
+ lt: l,
60
+ lte: l,
61
+ ">": l,
62
+ ">=": l,
63
+ "<": l,
64
+ "<=": l,
19
65
  // String operations
66
+ //
67
+ // ## The `_cs` operators list `text`, and that is deliberate — the rule lives in the caller
68
+ //
69
+ // The engines REFUSE every `_cs` operator when the field cannot answer it — an ANALYZED
70
+ // field with no case-preserving subfield, and for the whole-value six ALSO any field with
71
+ // no un-analyzed string form at all. The analyzer lowercased every token it indexed, so
72
+ // the case the query asks about is not in the index; before the refusal both engines fell
73
+ // back to the analyzed field and returned zero hits with no error.
74
+ //
75
+ // "An analyzed field" alone is the MEMBERSHIP half, and stating it as the whole rule is
76
+ // how this shipped wrong for six operators once already. See `caseSensitiveSupport`.
77
+ //
78
+ // ALL EIGHT are covered, and the set arrived in two commits rather than one, which is why
79
+ // it is stated as a rule and not as a list of six:
80
+ //
81
+ // * `a3971bd` — `contains_cs`, `not_contains_cs`, `startswith_cs`, `not_startswith_cs`,
82
+ // `endswith_cs`, `not_endswith_cs`, as `CASE_SENSITIVE_WHOLE_VALUE_OPERATORS`. Named
83
+ // one by one rather than as "and their negations": the shorthand is how the eighth
84
+ // operator went missing from this paragraph in the first place.
85
+ // * `c8e3e8a` — `in_cs`, `not_in_cs`, as `CASE_SENSITIVE_MEMBERSHIP_OPERATORS`. These
86
+ // escaped the first round for a classification reason, not a semantic one: `in_cs` is
87
+ // a keyword operator (an exact-match `terms` query), so it routed through a different
88
+ // branch of `get_query_field` whose fallback still landed on the analyzed field.
89
+ //
90
+ // Both constants live in `tql/src/opensearch/field_mappings.rs` and
91
+ // `src/tql/opensearch_components/field_mapping.py`. A note here naming only the first six
92
+ // would leave a reader correct about `contains_cs` and wrong about `in_cs`, silently —
93
+ // which is the exact failure the engines' own interim pin existed to prevent.
94
+ //
95
+ // ### Why the rule is not in THIS table (tql#206)
96
+ //
97
+ // It keys on the MAPPING, not on the field type. The engines refuse only when there is no
98
+ // case-preserving form — a `text` field WITH a `keyword` subfield translates fine, and
99
+ // that is the commonest shape in ECS. So `type === "text"` does not decide the question,
100
+ // and dropping `text` from the eight `_cs` rows below would be that wrong check by another
101
+ // name. The rows stay exactly as they are.
102
+ //
103
+ // NOTE the table they are in. These rows are in THIS table, `OPERATOR_TYPE_COMPATIBILITY`,
104
+ // which emits a WARNING. `OPERATOR_REQUIRED_FIELD_TYPES` further down emits an ERROR and
105
+ // holds exactly three rows — `cidr`, `not_cidr`, `!cidr`. tql#206 named the wrong one, and
106
+ // the mistake was copied into this file, its spec, `docs/developer/field-routing.md` and
107
+ // the CHANGELOG before review caught it; `tests/completionOperatorTableParity.spec.ts`
108
+ // had the distinction right the whole time. Moving these rows into the ERROR table is the
109
+ // OTHER wrong fix, and is guarded in the spec alongside the first one.
110
+ //
111
+ // What CHANGED at tql#206 is that the question turned out to be answerable from the schema
112
+ // this package already receives. `TqlValidationOptions.fieldTypes` is a FLAT
113
+ // `Record<name, type>`, and a host that flattens an index mapping puts each multifield in
114
+ // as its own entry — `message` → `text` alongside `message.keyword` → `keyword`. That is a
115
+ // sibling LOOKUP, not a schema change, and it is what `caseSensitiveSupport` below does.
116
+ // The verdict is three-valued (`supported` / `unsupported` / `unknown`) and only the middle
117
+ // one produces a diagnostic, which is why this table can stay permissive and the caller can
118
+ // still be specific. See that function for the predicate and its blind spots.
119
+ //
120
+ // `tests/caseSensitiveOperatorMapping.spec.ts` derives the engines' refusal set from their
121
+ // own sources and asserts this note names all of it — so a third `_cs` round cannot leave
122
+ // this paragraph quietly under-claiming the way the six-name version did.
20
123
  contains: ["text", "keyword"],
21
124
  not_contains: ["text", "keyword"],
22
125
  "!contains": ["text", "keyword"],
@@ -44,47 +147,131 @@ const c = {
44
147
  matches: ["text", "keyword"],
45
148
  not_matches: ["text", "keyword"],
46
149
  "!matches": ["text", "keyword"],
47
- // Range
48
- between: ["long", "integer", "double", "float", "date"],
49
- not_between: ["long", "integer", "double", "float", "date"],
50
- "!between": ["long", "integer", "double", "float", "date"],
51
- in: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip"],
52
- not_in: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip"],
53
- "!in": ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip"],
150
+ // Range. Numeric and date, via the named constant — the transcribed list this replaces
151
+ // dropped the same five narrow widths `eq` did (`short`, `byte`, `half_float`,
152
+ // `scaled_float`, `unsigned_long`), so `port between [1, 1024]` on a `short` field warned.
153
+ // Python's `numeric_ops` carries `between` and covers `short` and `byte`; the other three
154
+ // widths have no arm at all in `_validate_simple_type_compatibility` and fall through its
155
+ // `else: return`, which allows every operator.
156
+ //
157
+ // `keyword` and `text` stay out, and that is a decision rather than an omission: Python's
158
+ // `keyword_ops` excludes `between` while including `gte`, which is the divergence pinned
159
+ // in `tests/collectionAndNullSemantics.spec.ts`.
160
+ between: c,
161
+ not_between: c,
162
+ "!between": c,
163
+ // Membership — the same set as `eq`, because `in` IS a disjunction of equalities and both
164
+ // evaluators implement it that way. The transcribed list this replaces omitted the five
165
+ // narrow numeric widths plus `nested`, `object` and `geo_point`.
166
+ //
167
+ // This is deliberately WIDER than Python's validator, which is already the standing stance
168
+ // here rather than a new one: `date_ops` and `boolean_ops` both omit `in`, so
169
+ // `_validate_simple_type_compatibility` refuses `f in ['2024-01-01','2024-01-02']` on a
170
+ // date field — while its own evaluator answers it and the cluster serves the DSL. That is
171
+ // the same validator-versus-evaluator disagreement already pinned for `keyword between`
172
+ // and `keyword is null`, and this table has always sided with the evaluator on it (`date`
173
+ // and `boolean` were in the transcribed list too). Widening to the remaining types applies
174
+ // the existing stance consistently; it does not take a new position.
175
+ in: r,
176
+ not_in: r,
177
+ "!in": r,
54
178
  in_cs: ["text", "keyword"],
55
179
  not_in_cs: ["text", "keyword"],
56
180
  "!in_cs": ["text", "keyword"],
57
- // Existence (all types)
58
- exists: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
59
- not_exists: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
60
- "!exists": ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
61
- // Null checks (all types)
62
- is: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
63
- is_not: ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
64
- "!is": ["text", "keyword", "long", "integer", "double", "float", "date", "boolean", "ip", "nested", "object", "geo_point"],
181
+ // Existence and null checks — ONE predicate, so ONE list.
182
+ //
183
+ // `field is null` now means the field HAS NO VALUE (absent OR explicitly null), which is
184
+ // exactly `field not exists`; `is not null` is its complement and exactly `exists`. A
185
+ // product-owner decision landed in both engines as a breaking change. An empty array `[]`
186
+ // is a PRESENT value and satisfies neither `is null` nor `not exists` — complementarity is
187
+ // the reason, since `exists` answers true for `[]` and no record may satisfy both sides.
188
+ //
189
+ // Sharing the constant is what keeps the four honest. As four separate transcriptions they
190
+ // had already drifted from the union by five types apiece, and nothing would have caught
191
+ // two of them drifting from each other.
192
+ exists: r,
193
+ not_exists: r,
194
+ "!exists": r,
195
+ is: r,
196
+ is_not: r,
197
+ "!is": r,
65
198
  // IP-specific operators
66
199
  cidr: ["ip"],
67
200
  not_cidr: ["ip"],
68
201
  "!cidr": ["ip"],
69
- // Collection operators
70
- any: ["nested", "object", "keyword", "text"],
71
- all: ["nested", "object", "keyword", "text"],
72
- none: ["nested", "object", "keyword", "text"],
73
- not_any: ["nested", "object", "keyword", "text"],
74
- not_all: ["nested", "object", "keyword", "text"],
75
- not_none: ["nested", "object", "keyword", "text"],
76
- "!any": ["nested", "object", "keyword", "text"],
77
- "!all": ["nested", "object", "keyword", "text"],
78
- "!none": ["nested", "object", "keyword", "text"]
79
- }, l = {
202
+ // Collection operators — every type, and that is the engines' rule rather than a shrug.
203
+ //
204
+ // NOTE these rows are not reachable from this file: the parser builds a `collection` node
205
+ // whose `condition` carries the default inner operator `eq`, so `any`/`all`/`none` never
206
+ // arrive at `validateComparison`. They are corrected rather than left wrong because a
207
+ // stale rule gets copied -- the identical restriction WAS live in
208
+ // `completionProvider.ts`, where it withheld `any` from every `ip` and `long` field, and
209
+ // that is where `tests/collectionAndNullSemantics.spec.ts` measures it.
210
+ //
211
+ // Two things changed under this table. Both evaluators now treat a SCALAR field as a
212
+ // one-element collection (Rust returned false for every scalar before `1fe0a9f`), and both
213
+ // iterate array-valued fields across the comparison surface. Neither is conditional on the
214
+ // mapping type: OpenSearch has no array type, so `related.ip`, `host.ip`, `tags` and
215
+ // `dns.answers` are `ip` / `keyword` / `object` fields that ECS guarantees hold arrays.
216
+ //
217
+ // Restricting these six to `nested | object | keyword | text` therefore described neither
218
+ // engine. `related.ip any ["10.0.0.1"]` is the ordinary case for an IP field, not an
219
+ // abuse of one.
220
+ any: r,
221
+ all: r,
222
+ none: r,
223
+ not_any: r,
224
+ not_all: r,
225
+ not_none: r,
226
+ "!any": r,
227
+ "!all": r,
228
+ "!none": r
229
+ }, g = {
80
230
  cidr: ["ip"],
81
231
  not_cidr: ["ip"],
82
232
  "!cidr": ["ip"]
83
- }, u = {
233
+ }, p = [
234
+ "contains_cs",
235
+ "not_contains_cs",
236
+ "!contains_cs",
237
+ "startswith_cs",
238
+ "not_startswith_cs",
239
+ "!startswith_cs",
240
+ "endswith_cs",
241
+ "not_endswith_cs",
242
+ "!endswith_cs"
243
+ ], _ = [
244
+ "in_cs",
245
+ "not_in_cs",
246
+ "!in_cs"
247
+ ], b = [
248
+ ...p,
249
+ ..._
250
+ ];
251
+ function m(o, t) {
252
+ if (!t)
253
+ return "unknown";
254
+ const e = t[o];
255
+ if (!e)
256
+ return "unknown";
257
+ if (e === "keyword" || t[`${o}.keyword`] === "keyword")
258
+ return "present";
259
+ const i = `${o}.`;
260
+ for (const [n, s] of Object.entries(t))
261
+ if (!(s !== "keyword" || !n.startsWith(i)) && !n.slice(i.length).includes("."))
262
+ return "present";
263
+ return "absent";
264
+ }
265
+ function v(o, t, e) {
266
+ if (!b.includes(t))
267
+ return "supported";
268
+ const i = m(o, e);
269
+ return i !== "absent" ? i === "present" ? "supported" : "unknown" : _.includes(t) ? e?.[o] === "text" ? "unsupported" : "supported" : "unsupported";
270
+ }
271
+ const E = {
84
272
  eq: ["string", "number", "boolean"],
85
273
  ne: ["string", "number", "boolean"],
86
274
  "=": ["string", "number", "boolean"],
87
- "==": ["string", "number", "boolean"],
88
275
  "!=": ["string", "number", "boolean"],
89
276
  eq_ci: ["string"],
90
277
  gt: ["number", "string"],
@@ -150,21 +337,21 @@ const c = {
150
337
  "!all": ["string", "number", "boolean"],
151
338
  "!none": ["string", "number", "boolean"]
152
339
  };
153
- class b {
340
+ class k {
154
341
  options;
155
342
  errors;
156
343
  warnings;
157
- constructor(e = {}) {
158
- this.options = e, this.errors = [], this.warnings = [];
344
+ constructor(t = {}) {
345
+ this.options = t, this.errors = [], this.warnings = [];
159
346
  }
160
347
  /**
161
348
  * Validate a TQL query
162
349
  */
163
- validate(e) {
164
- if (this.errors = [], this.warnings = [], !e)
350
+ validate(t) {
351
+ if (this.errors = [], this.warnings = [], !t)
165
352
  return { valid: !0, errors: [], warnings: [] };
166
- const t = g(e);
167
- return this.errors.push(...t.errors), this.warnings.push(...t.warnings), t.ast && this.validateExpression(t.ast), {
353
+ const e = w(t);
354
+ return this.errors.push(...e.errors), this.warnings.push(...e.warnings), e.ast && this.validateExpression(e.ast), {
168
355
  valid: this.errors.length === 0,
169
356
  errors: this.errors,
170
357
  warnings: this.warnings
@@ -173,171 +360,302 @@ class b {
173
360
  /**
174
361
  * Validate an expression recursively
175
362
  */
176
- validateExpression(e) {
177
- switch (e.type) {
363
+ validateExpression(t) {
364
+ switch (t.type) {
178
365
  case "binary":
179
- this.validateExpression(e.left), this.validateExpression(e.right);
366
+ this.validateExpression(t.left), this.validateExpression(t.right);
180
367
  break;
181
368
  case "unary":
182
- this.validateExpression(e.operand);
369
+ this.validateExpression(t.operand);
183
370
  break;
184
371
  case "group":
185
- this.validateExpression(e.expression);
372
+ this.validateExpression(t.expression);
186
373
  break;
187
374
  case "comparison":
188
- this.validateComparison(e);
375
+ this.validateComparison(t);
376
+ break;
377
+ // `field ANY value`. The collection wrapper carries no validatable content of its
378
+ // own -- field, inner operator and value all live in `condition` -- but it must be
379
+ // walked explicitly. Falling through the switch would have made every collection
380
+ // query validate vacuously clean, which is the failure mode this node type was added
381
+ // out of, not into.
382
+ //
383
+ // The COLLECTION OPERATOR itself is deliberately not checked here. Every field type
384
+ // is compatible with it (see the note on `any` in OPERATOR_TYPE_COMPATIBILITY), so a
385
+ // check would be a code path that can never produce an answer -- and a mechanism with
386
+ // no reachable outcome reads as enforcement while enforcing nothing. The completion
387
+ // provider's copy of the table IS live, and is where the widened rule is observable.
388
+ //
389
+ // The OPERAND SHAPE is checked, though — see validateCollectionOperand.
390
+ case "collection":
391
+ this.validateCollectionOperand(t), this.validateExpression(t.condition);
189
392
  break;
190
393
  }
191
394
  }
395
+ /**
396
+ * `f any ['a','b']` is ill-typed. Say so, and name `in`.
397
+ *
398
+ * ## The rule, as both engines implement it
399
+ *
400
+ * `any` / `none` apply a comparison to each ELEMENT of an array-valued field. The operand
401
+ * is therefore a SCALAR: `f any 'admin'` asks whether some element equals `'admin'`. A
402
+ * LIST operand asks whether some single element equals the whole two-element list, which
403
+ * no scalar element can. `in` is already the membership operator, and is what the author
404
+ * meant.
405
+ *
406
+ * Both engines now refuse it -- `ab55587` in `tql/src/opensearch/query_builder.rs`
407
+ * (`build_collection_op`) and `_refuse_list_operand` in
408
+ * `src/tql/opensearch_components/query_converter.py` -- raising a type error whose
409
+ * `valid_operators` are `in` / `not in`. Before that it emitted `{"term": {"f": [..]}}`,
410
+ * which OpenSearch answers with HTTP 400 `[term] query does not support array of values`,
411
+ * failing the WHOLE search including every unrelated clause; the intermediate fix emitted
412
+ * `match_none`, which executed cleanly and excluded nothing, silently.
413
+ *
414
+ * ## Three arities, and only the middle one is new
415
+ *
416
+ * The engines do NOT test `is_array`. They unwrap a single-element list FIRST
417
+ * (`unwrap_single` in Rust, `len(value) == 1` in Python) and refuse only what is still a
418
+ * list. Measured against the live Python translator at `ab55587`:
419
+ *
420
+ * | Query | Python translator |
421
+ * |--------------------|------------------------------------------------------|
422
+ * | `f any 'admin'` | `{"term": {"f": "admin"}}` |
423
+ * | `f any ['a']` | `{"term": {"f": "a"}}` -- unwrapped, NOT refused |
424
+ * | `f any ['a','b']` | `TQLTypeError`, valid operators `in`, `not in` |
425
+ *
426
+ * A naive `Array.isArray(value)` check would flag `f any ['a']`, which both engines
427
+ * translate perfectly well -- a red squiggle over a working query, which is the defect
428
+ * this parser keeps being fixed for. Hence `length >= 2`, and hence the two arities are
429
+ * tested separately in `tests/collectionListOperand.spec.ts`.
430
+ *
431
+ * The empty list is not handled here: `f any []` is already an `INVALID_VALUE` error from
432
+ * the parser (`tests/emptyValueList.spec.ts`), and Python does not even parse it
433
+ * (`TQLSyntaxError`). Reporting it twice would be noise.
434
+ *
435
+ * ## Why `ALL` is excluded, and why that is not an oversight
436
+ *
437
+ * `all` / `not_all` are answered by a Painless script that compares each element to
438
+ * `params.value`. A list operand there emits valid DSL that matches nothing -- which is
439
+ * what both in-memory evaluators also answer -- so both engines dispatch `all` BEFORE the
440
+ * refusal and leave it untouched. `f all ['a','b']` translating cleanly is a pinned
441
+ * negative case here, not a gap.
442
+ *
443
+ * Negation is irrelevant: in both engines the array check precedes the quantifier wrapper,
444
+ * so `not any` / `not none` refuse on the same footing as the positive spellings.
445
+ */
446
+ validateCollectionOperand(t) {
447
+ if (t.operator === "ALL")
448
+ return;
449
+ const e = t.condition;
450
+ if (e.type !== "comparison")
451
+ return;
452
+ const i = e.value;
453
+ if (!Array.isArray(i) || i.length < 2)
454
+ return;
455
+ const n = t.operator.toLowerCase(), s = t.negated ? `not ${n}` : n, a = t.field.name;
456
+ this.addError(
457
+ `'${s}' takes a single value and tests it against each element of '${a}'; it does not take a list.`,
458
+ i[0].start,
459
+ i[i.length - 1].end,
460
+ 1,
461
+ 1,
462
+ "TYPE_MISMATCH",
463
+ `Use \`in\` for membership: \`${a} in [...]\` (or \`${a} not in [...]\` to exclude).`
464
+ );
465
+ }
192
466
  /**
193
467
  * Validate a comparison expression
194
468
  */
195
- validateComparison(e) {
196
- const t = e.field.type === "field" ? e.field.name : "computed";
197
- if (this.options.availableFields && this.options.strictMode && !this.options.availableFields.includes(t) && e.field.type === "field" && this.addWarning(
198
- `Unknown field '${t}'`,
199
- e.field.start,
200
- e.field.end,
201
- (e.field.type === "field", 1),
469
+ validateComparison(t) {
470
+ const e = t.field.type === "field" ? t.field.name : "computed";
471
+ if (this.options.availableFields && this.options.strictMode && !this.options.availableFields.includes(e) && t.field.type === "field" && this.addWarning(
472
+ `Unknown field '${e}'`,
473
+ t.field.start,
474
+ t.field.end,
475
+ (t.field.type === "field", 1),
202
476
  1,
203
477
  "UNKNOWN_FIELD",
204
- `Did you mean: ${this.suggestField(t)}`
205
- ), this.options.fieldTypes && e.field.type === "field") {
206
- const o = this.options.fieldTypes[t];
207
- if (o) {
208
- const n = l[e.operator];
209
- n && !n.includes(o) && this.addError(
210
- `Operator '${e.operator}' requires field type ${n.join(" or ")}, but '${t}' is type '${o}'`,
211
- e.start,
212
- e.end,
478
+ `Did you mean: ${this.suggestField(e)}`
479
+ ), this.options.fieldTypes && t.field.type === "field") {
480
+ const n = this.options.fieldTypes[e];
481
+ if (n) {
482
+ const s = g[t.operator];
483
+ s && !s.includes(n) && this.addError(
484
+ `Operator '${t.operator}' requires field type ${s.join(" or ")}, but '${e}' is type '${n}'`,
485
+ t.start,
486
+ t.end,
213
487
  1,
214
488
  1,
215
489
  "TYPE_MISMATCH",
216
- `Use '${e.operator}' only with ${n.join(" or ")} fields`
490
+ `Use '${t.operator}' only with ${s.join(" or ")} fields`
217
491
  );
218
492
  }
219
493
  }
220
- if (this.options.fieldTypes && e.field.type === "field") {
221
- const o = this.options.fieldTypes[t];
222
- if (o) {
223
- const n = c[e.operator];
224
- !l[e.operator] && n && !n.includes(o) && this.addWarning(
225
- `Operator '${e.operator}' may not work well with field type '${o}'`,
226
- e.start,
227
- e.end,
494
+ let i = !1;
495
+ if (t.field.type === "field" && v(e, t.operator, this.options.fieldTypes) === "unsupported") {
496
+ i = !0;
497
+ const n = t.operator.replace(/_cs$/, ""), s = this.options.fieldTypes?.[e], a = s === "text" ? `'${e}' is an analyzed text field with no case-preserving subfield in the supplied schema, so the analyzer lowercased every token it indexed and the case '${t.operator}' asks about is not in the index` : `'${e}' is type '${s}' and has no un-analyzed string form, which is what '${t.operator}' matches against`;
498
+ this.addWarning(
499
+ // "The query engine", singular and unqualified, and kept that way deliberately.
500
+ // It was written this way because "both engines" was FALSE in twelve cells —
501
+ // Rust permitted `_cs` on `geo_point` and `unsigned_long`. tql#210 fixed that in
502
+ // this same PR, so "both" would now be accurate. It stays singular anyway: the
503
+ // user does not choose the engine, the sentence is about what will go wrong
504
+ // rather than about who refuses, and a message that counts engines is a message
505
+ // that goes stale the next time one of them changes.
506
+ `${a}. The query engine refuses this at execution.`,
507
+ t.start,
508
+ t.end,
509
+ 1,
510
+ 1,
511
+ "UNANSWERABLE_MAPPING",
512
+ s === "text" ? `Use the case-insensitive '${n}', or query a keyword subfield (\`${e}.keyword\`) if the index has one this schema does not list.` : `Use the case-insensitive '${n}', or compare '${e}' with an operator suited to '${s}'.`
513
+ );
514
+ }
515
+ if (this.options.fieldTypes && t.field.type === "field") {
516
+ const n = this.options.fieldTypes[e];
517
+ if (n) {
518
+ const s = y[t.operator], a = g[t.operator];
519
+ !i && !a && s && !s.includes(n) && this.addWarning(
520
+ `Operator '${t.operator}' may not work well with field type '${n}'`,
521
+ t.start,
522
+ t.end,
228
523
  1,
229
524
  1,
230
525
  "TYPE_MISMATCH",
231
- `Consider using a different operator for ${o} fields`
526
+ `Consider using a different operator for ${n} fields`
232
527
  );
233
528
  }
234
529
  }
235
- if (!Array.isArray(e.value) && e.value.type === "literal") {
236
- const o = u[e.operator];
237
- o && !o.includes(e.value.valueType) && this.addWarning(
238
- `Operator '${e.operator}' expects ${o.join(" or ")} value`,
239
- e.value.start,
240
- e.value.end,
530
+ if (!Array.isArray(t.value) && t.value.type === "literal") {
531
+ const n = E[t.operator];
532
+ n && !n.includes(t.value.valueType) && this.addWarning(
533
+ `Operator '${t.operator}' expects ${n.join(" or ")} value`,
534
+ t.value.start,
535
+ t.value.end,
241
536
  1,
242
537
  1,
243
538
  "TYPE_MISMATCH"
244
539
  );
245
540
  }
246
- if ((e.operator === "regexp" || e.operator === "regex" || e.operator === "not_regexp" || e.operator === "not_regex" || e.operator === "matches" || e.operator === "not_matches") && !Array.isArray(e.value) && e.value.valueType === "string" && typeof e.value.value == "string")
247
- try {
248
- new RegExp(e.value.value);
249
- } catch {
541
+ if ((t.operator === "regexp" || t.operator === "regex" || t.operator === "not_regexp" || t.operator === "not_regex" || t.operator === "matches" || t.operator === "not_matches") && !Array.isArray(t.value) && t.value.valueType === "string" && typeof t.value.value == "string") {
542
+ const n = Array.from(t.value.value).length;
543
+ if (n > u)
250
544
  this.addError(
251
- `Invalid regular expression: ${e.value.value}`,
252
- e.value.start,
253
- e.value.end,
545
+ `Regex pattern is ${n} characters, which exceeds the maximum supported length of ${u}. The query cannot run as written.`,
546
+ t.value.start,
547
+ t.value.end,
254
548
  1,
255
549
  1,
256
550
  "INVALID_VALUE",
257
- "Check the regex syntax"
551
+ "Split the pattern into several shorter alternations combined with OR, or use an `in [...]` list"
258
552
  );
259
- }
553
+ else
554
+ try {
555
+ new RegExp(t.value.value);
556
+ } catch {
557
+ this.addError(
558
+ `Invalid regular expression: ${t.value.value}`,
559
+ t.value.start,
560
+ t.value.end,
561
+ 1,
562
+ 1,
563
+ "INVALID_VALUE",
564
+ "Check the regex syntax"
565
+ );
566
+ }
567
+ }
260
568
  }
261
569
  /**
262
570
  * Suggest a similar field name using Levenshtein distance
263
571
  */
264
- suggestField(e) {
572
+ suggestField(t) {
265
573
  if (!this.options.availableFields || this.options.availableFields.length === 0)
266
574
  return "check available fields";
267
- let t = this.options.availableFields[0], o = this.levenshteinDistance(e, t);
575
+ let e = this.options.availableFields[0], i = this.levenshteinDistance(t, e);
268
576
  for (const n of this.options.availableFields) {
269
- const i = this.levenshteinDistance(e, n);
270
- i < o && (o = i, t = n);
577
+ const s = this.levenshteinDistance(t, n);
578
+ s < i && (i = s, e = n);
271
579
  }
272
- return t;
580
+ return e;
273
581
  }
274
582
  /**
275
583
  * Calculate Levenshtein distance between two strings
276
584
  */
277
- levenshteinDistance(e, t) {
278
- const o = [];
279
- for (let n = 0; n <= t.length; n++)
280
- o[n] = [n];
585
+ levenshteinDistance(t, e) {
586
+ const i = [];
281
587
  for (let n = 0; n <= e.length; n++)
282
- o[0][n] = n;
283
- for (let n = 1; n <= t.length; n++)
284
- for (let i = 1; i <= e.length; i++)
285
- t.charAt(n - 1) === e.charAt(i - 1) ? o[n][i] = o[n - 1][i - 1] : o[n][i] = Math.min(
286
- o[n - 1][i - 1] + 1,
287
- o[n][i - 1] + 1,
288
- o[n - 1][i] + 1
588
+ i[n] = [n];
589
+ for (let n = 0; n <= t.length; n++)
590
+ i[0][n] = n;
591
+ for (let n = 1; n <= e.length; n++)
592
+ for (let s = 1; s <= t.length; s++)
593
+ e.charAt(n - 1) === t.charAt(s - 1) ? i[n][s] = i[n - 1][s - 1] : i[n][s] = Math.min(
594
+ i[n - 1][s - 1] + 1,
595
+ i[n][s - 1] + 1,
596
+ i[n - 1][s] + 1
289
597
  );
290
- return o[t.length][e.length];
598
+ return i[e.length][t.length];
291
599
  }
292
- addError(e, t, o, n, i, r, a) {
600
+ addError(t, e, i, n, s, a, d) {
293
601
  this.errors.push({
294
- message: e,
295
- start: t,
296
- end: o,
602
+ message: t,
603
+ start: e,
604
+ end: i,
297
605
  line: n,
298
- column: i,
606
+ column: s,
299
607
  severity: "error",
300
- code: r,
301
- suggestion: a
608
+ code: a,
609
+ suggestion: d
302
610
  });
303
611
  }
304
- addWarning(e, t, o, n, i, r, a) {
612
+ addWarning(t, e, i, n, s, a, d) {
305
613
  this.warnings.push({
306
- message: e,
307
- start: t,
308
- end: o,
614
+ message: t,
615
+ start: e,
616
+ end: i,
309
617
  line: n,
310
- column: i,
618
+ column: s,
311
619
  severity: "warning",
312
- code: r,
313
- suggestion: a
620
+ code: a,
621
+ suggestion: d
314
622
  });
315
623
  }
316
624
  }
317
- function d(s, e) {
318
- return new b(e).validate(s);
625
+ function f(o, t) {
626
+ return new k(t).validate(o);
319
627
  }
320
- function h(s) {
321
- return d(s).valid;
628
+ function $(o) {
629
+ return f(o).valid;
322
630
  }
323
- function y(s, e) {
324
- const t = d(s, e);
325
- return [...t.errors, ...t.warnings];
631
+ function A(o, t) {
632
+ const e = f(o, t);
633
+ return [...e.errors, ...e.warnings];
326
634
  }
327
- function f(s) {
328
- const e = [];
329
- s.valid ? e.push("Query is valid") : e.push("Query has errors:");
330
- for (const t of s.errors)
331
- e.push(` Error: ${t.message}`), t.suggestion && e.push(` Suggestion: ${t.suggestion}`);
332
- for (const t of s.warnings)
333
- e.push(` Warning: ${t.message}`), t.suggestion && e.push(` Suggestion: ${t.suggestion}`);
334
- return e.join(`
635
+ function I(o) {
636
+ const t = [];
637
+ o.valid ? t.push("Query is valid") : t.push("Query has errors:");
638
+ for (const e of o.errors)
639
+ t.push(` Error: ${e.message}`), e.suggestion && t.push(` Suggestion: ${e.suggestion}`);
640
+ for (const e of o.warnings)
641
+ t.push(` Warning: ${e.message}`), e.suggestion && t.push(` Suggestion: ${e.suggestion}`);
642
+ return t.join(`
335
643
  `);
336
644
  }
337
645
  export {
338
- b as TqlValidator,
339
- f as formatValidationResult,
340
- y as getTqlIssues,
341
- h as isValidTqlSyntax,
342
- d as validateTql
646
+ r as ALL_FIELD_TYPES,
647
+ c as BETWEEN_FIELD_TYPES,
648
+ _ as CASE_SENSITIVE_MEMBERSHIP_OPERATORS,
649
+ b as CASE_SENSITIVE_OPERATORS,
650
+ p as CASE_SENSITIVE_WHOLE_VALUE_OPERATORS,
651
+ u as MAX_REGEX_PATTERN_LENGTH,
652
+ h as NUMERIC_FIELD_TYPES,
653
+ l as ORDERED_FIELD_TYPES,
654
+ k as TqlValidator,
655
+ m as casePreservingForm,
656
+ v as caseSensitiveSupport,
657
+ I as formatValidationResult,
658
+ A as getTqlIssues,
659
+ $ as isValidTqlSyntax,
660
+ f as validateTql
343
661
  };