@tellaro/tql 1.4.3 → 2.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,110 @@
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 c = 1e3, i = [
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
+ ], u = [
21
+ "long",
22
+ "integer",
23
+ "short",
24
+ "byte",
25
+ "double",
26
+ "float",
27
+ "half_float",
28
+ "scaled_float",
29
+ "unsigned_long"
30
+ ], a = [...u, "date", "keyword"], g = [...u, "date"], f = {
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: i,
38
+ ne: i,
39
+ "=": i,
40
+ "!=": i,
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: a,
58
+ gte: a,
59
+ lt: a,
60
+ lte: a,
61
+ ">": a,
62
+ ">=": a,
63
+ "<": a,
64
+ "<=": a,
19
65
  // String operations
66
+ //
67
+ // ## The `_cs` operators list `text`, and that is deliberate — see below
68
+ //
69
+ // Both engines now REFUSE every `_cs` operator when the field cannot answer it: an
70
+ // ANALYZED field with no case-preserving subfield. The analyzer lowercased every token it
71
+ // indexed, so the case the query asks about is not in the index at all; before the refusal
72
+ // both engines fell back to the analyzed field and returned zero hits with no error.
73
+ //
74
+ // ALL EIGHT are covered, and the set arrived in two commits rather than one, which is why
75
+ // it is stated as a rule and not as a list of six:
76
+ //
77
+ // * `a3971bd` — `contains_cs`, `not_contains_cs`, `startswith_cs`, `not_startswith_cs`,
78
+ // `endswith_cs`, `not_endswith_cs`, as `CASE_SENSITIVE_WHOLE_VALUE_OPERATORS`. Named
79
+ // one by one rather than as "and their negations": the shorthand is how the eighth
80
+ // operator went missing from this paragraph in the first place.
81
+ // * `c8e3e8a` — `in_cs`, `not_in_cs`, as `CASE_SENSITIVE_MEMBERSHIP_OPERATORS`. These
82
+ // escaped the first round for a classification reason, not a semantic one: `in_cs` is
83
+ // a keyword operator (an exact-match `terms` query), so it routed through a different
84
+ // branch of `get_query_field` whose fallback still landed on the analyzed field.
85
+ //
86
+ // Both constants live in `tql/src/opensearch/field_mappings.rs` and
87
+ // `src/tql/opensearch_components/field_mapping.py`. A note here naming only the first six
88
+ // would leave a reader correct about `contains_cs` and wrong about `in_cs`, silently —
89
+ // which is the exact failure the engines' own interim pin existed to prevent.
90
+ //
91
+ // That rule is NOT expressible in this package, and the reason is specific rather than a
92
+ // shrug: it keys on the MAPPING, not on the field type. The engines refuse only when there
93
+ // is no case-preserving form — a `text` field WITH a `.keyword` subfield translates fine,
94
+ // and that is the most common shape in ECS. This package's field model
95
+ // (`TqlValidationOptions.fieldTypes`, `TqlFieldSchema`) carries one `TqlFieldType` per
96
+ // name and nothing about analysis or multifields: `TqlFieldSchema.nested` is object
97
+ // children, not subfields. So `type === "text"` does not decide the question, and a check
98
+ // keyed on it would be a false positive on exactly the mapping shape that works.
99
+ //
100
+ // Dropping `text` from the eight `_cs` rows — the six below and `in_cs` / `not_in_cs`
101
+ // further down — would be that check by another name. It is left in on all eight.
102
+ // Closing this properly means teaching the schema about multifields, at which point the
103
+ // rule belongs in `OPERATOR_REQUIRED_FIELD_TYPES`'s caller rather than in the table.
104
+ //
105
+ // `tests/caseSensitiveOperatorAbsence.spec.ts` derives the engines' refusal set from their
106
+ // own sources and asserts this note names all of it — so a third `_cs` round cannot leave
107
+ // this paragraph quietly under-claiming the way the six-name version did.
20
108
  contains: ["text", "keyword"],
21
109
  not_contains: ["text", "keyword"],
22
110
  "!contains": ["text", "keyword"],
@@ -44,47 +132,93 @@ const c = {
44
132
  matches: ["text", "keyword"],
45
133
  not_matches: ["text", "keyword"],
46
134
  "!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"],
135
+ // Range. Numeric and date, via the named constant — the transcribed list this replaces
136
+ // dropped the same five narrow widths `eq` did (`short`, `byte`, `half_float`,
137
+ // `scaled_float`, `unsigned_long`), so `port between [1, 1024]` on a `short` field warned.
138
+ // Python's `numeric_ops` carries `between` and covers `short` and `byte`; the other three
139
+ // widths have no arm at all in `_validate_simple_type_compatibility` and fall through its
140
+ // `else: return`, which allows every operator.
141
+ //
142
+ // `keyword` and `text` stay out, and that is a decision rather than an omission: Python's
143
+ // `keyword_ops` excludes `between` while including `gte`, which is the divergence pinned
144
+ // in `tests/collectionAndNullSemantics.spec.ts`.
145
+ between: g,
146
+ not_between: g,
147
+ "!between": g,
148
+ // Membership — the same set as `eq`, because `in` IS a disjunction of equalities and both
149
+ // evaluators implement it that way. The transcribed list this replaces omitted the five
150
+ // narrow numeric widths plus `nested`, `object` and `geo_point`.
151
+ //
152
+ // This is deliberately WIDER than Python's validator, which is already the standing stance
153
+ // here rather than a new one: `date_ops` and `boolean_ops` both omit `in`, so
154
+ // `_validate_simple_type_compatibility` refuses `f in ['2024-01-01','2024-01-02']` on a
155
+ // date field — while its own evaluator answers it and the cluster serves the DSL. That is
156
+ // the same validator-versus-evaluator disagreement already pinned for `keyword between`
157
+ // and `keyword is null`, and this table has always sided with the evaluator on it (`date`
158
+ // and `boolean` were in the transcribed list too). Widening to the remaining types applies
159
+ // the existing stance consistently; it does not take a new position.
160
+ in: i,
161
+ not_in: i,
162
+ "!in": i,
54
163
  in_cs: ["text", "keyword"],
55
164
  not_in_cs: ["text", "keyword"],
56
165
  "!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"],
166
+ // Existence and null checks — ONE predicate, so ONE list.
167
+ //
168
+ // `field is null` now means the field HAS NO VALUE (absent OR explicitly null), which is
169
+ // exactly `field not exists`; `is not null` is its complement and exactly `exists`. A
170
+ // product-owner decision landed in both engines as a breaking change. An empty array `[]`
171
+ // is a PRESENT value and satisfies neither `is null` nor `not exists` — complementarity is
172
+ // the reason, since `exists` answers true for `[]` and no record may satisfy both sides.
173
+ //
174
+ // Sharing the constant is what keeps the four honest. As four separate transcriptions they
175
+ // had already drifted from the union by five types apiece, and nothing would have caught
176
+ // two of them drifting from each other.
177
+ exists: i,
178
+ not_exists: i,
179
+ "!exists": i,
180
+ is: i,
181
+ is_not: i,
182
+ "!is": i,
65
183
  // IP-specific operators
66
184
  cidr: ["ip"],
67
185
  not_cidr: ["ip"],
68
186
  "!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 = {
187
+ // Collection operators — every type, and that is the engines' rule rather than a shrug.
188
+ //
189
+ // NOTE these rows are not reachable from this file: the parser builds a `collection` node
190
+ // whose `condition` carries the default inner operator `eq`, so `any`/`all`/`none` never
191
+ // arrive at `validateComparison`. They are corrected rather than left wrong because a
192
+ // stale rule gets copied -- the identical restriction WAS live in
193
+ // `completionProvider.ts`, where it withheld `any` from every `ip` and `long` field, and
194
+ // that is where `tests/collectionAndNullSemantics.spec.ts` measures it.
195
+ //
196
+ // Two things changed under this table. Both evaluators now treat a SCALAR field as a
197
+ // one-element collection (Rust returned false for every scalar before `1fe0a9f`), and both
198
+ // iterate array-valued fields across the comparison surface. Neither is conditional on the
199
+ // mapping type: OpenSearch has no array type, so `related.ip`, `host.ip`, `tags` and
200
+ // `dns.answers` are `ip` / `keyword` / `object` fields that ECS guarantees hold arrays.
201
+ //
202
+ // Restricting these six to `nested | object | keyword | text` therefore described neither
203
+ // engine. `related.ip any ["10.0.0.1"]` is the ordinary case for an IP field, not an
204
+ // abuse of one.
205
+ any: i,
206
+ all: i,
207
+ none: i,
208
+ not_any: i,
209
+ not_all: i,
210
+ not_none: i,
211
+ "!any": i,
212
+ "!all": i,
213
+ "!none": i
214
+ }, h = {
80
215
  cidr: ["ip"],
81
216
  not_cidr: ["ip"],
82
217
  "!cidr": ["ip"]
83
- }, u = {
218
+ }, y = {
84
219
  eq: ["string", "number", "boolean"],
85
220
  ne: ["string", "number", "boolean"],
86
221
  "=": ["string", "number", "boolean"],
87
- "==": ["string", "number", "boolean"],
88
222
  "!=": ["string", "number", "boolean"],
89
223
  eq_ci: ["string"],
90
224
  gt: ["number", "string"],
@@ -154,17 +288,17 @@ class b {
154
288
  options;
155
289
  errors;
156
290
  warnings;
157
- constructor(e = {}) {
158
- this.options = e, this.errors = [], this.warnings = [];
291
+ constructor(t = {}) {
292
+ this.options = t, this.errors = [], this.warnings = [];
159
293
  }
160
294
  /**
161
295
  * Validate a TQL query
162
296
  */
163
- validate(e) {
164
- if (this.errors = [], this.warnings = [], !e)
297
+ validate(t) {
298
+ if (this.errors = [], this.warnings = [], !t)
165
299
  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), {
300
+ const e = w(t);
301
+ return this.errors.push(...e.errors), this.warnings.push(...e.warnings), e.ast && this.validateExpression(e.ast), {
168
302
  valid: this.errors.length === 0,
169
303
  errors: this.errors,
170
304
  warnings: this.warnings
@@ -173,171 +307,276 @@ class b {
173
307
  /**
174
308
  * Validate an expression recursively
175
309
  */
176
- validateExpression(e) {
177
- switch (e.type) {
310
+ validateExpression(t) {
311
+ switch (t.type) {
178
312
  case "binary":
179
- this.validateExpression(e.left), this.validateExpression(e.right);
313
+ this.validateExpression(t.left), this.validateExpression(t.right);
180
314
  break;
181
315
  case "unary":
182
- this.validateExpression(e.operand);
316
+ this.validateExpression(t.operand);
183
317
  break;
184
318
  case "group":
185
- this.validateExpression(e.expression);
319
+ this.validateExpression(t.expression);
186
320
  break;
187
321
  case "comparison":
188
- this.validateComparison(e);
322
+ this.validateComparison(t);
323
+ break;
324
+ // `field ANY value`. The collection wrapper carries no validatable content of its
325
+ // own -- field, inner operator and value all live in `condition` -- but it must be
326
+ // walked explicitly. Falling through the switch would have made every collection
327
+ // query validate vacuously clean, which is the failure mode this node type was added
328
+ // out of, not into.
329
+ //
330
+ // The COLLECTION OPERATOR itself is deliberately not checked here. Every field type
331
+ // is compatible with it (see the note on `any` in OPERATOR_TYPE_COMPATIBILITY), so a
332
+ // check would be a code path that can never produce an answer -- and a mechanism with
333
+ // no reachable outcome reads as enforcement while enforcing nothing. The completion
334
+ // provider's copy of the table IS live, and is where the widened rule is observable.
335
+ //
336
+ // The OPERAND SHAPE is checked, though — see validateCollectionOperand.
337
+ case "collection":
338
+ this.validateCollectionOperand(t), this.validateExpression(t.condition);
189
339
  break;
190
340
  }
191
341
  }
342
+ /**
343
+ * `f any ['a','b']` is ill-typed. Say so, and name `in`.
344
+ *
345
+ * ## The rule, as both engines implement it
346
+ *
347
+ * `any` / `none` apply a comparison to each ELEMENT of an array-valued field. The operand
348
+ * is therefore a SCALAR: `f any 'admin'` asks whether some element equals `'admin'`. A
349
+ * LIST operand asks whether some single element equals the whole two-element list, which
350
+ * no scalar element can. `in` is already the membership operator, and is what the author
351
+ * meant.
352
+ *
353
+ * Both engines now refuse it -- `ab55587` in `tql/src/opensearch/query_builder.rs`
354
+ * (`build_collection_op`) and `_refuse_list_operand` in
355
+ * `src/tql/opensearch_components/query_converter.py` -- raising a type error whose
356
+ * `valid_operators` are `in` / `not in`. Before that it emitted `{"term": {"f": [..]}}`,
357
+ * which OpenSearch answers with HTTP 400 `[term] query does not support array of values`,
358
+ * failing the WHOLE search including every unrelated clause; the intermediate fix emitted
359
+ * `match_none`, which executed cleanly and excluded nothing, silently.
360
+ *
361
+ * ## Three arities, and only the middle one is new
362
+ *
363
+ * The engines do NOT test `is_array`. They unwrap a single-element list FIRST
364
+ * (`unwrap_single` in Rust, `len(value) == 1` in Python) and refuse only what is still a
365
+ * list. Measured against the live Python translator at `ab55587`:
366
+ *
367
+ * | Query | Python translator |
368
+ * |--------------------|------------------------------------------------------|
369
+ * | `f any 'admin'` | `{"term": {"f": "admin"}}` |
370
+ * | `f any ['a']` | `{"term": {"f": "a"}}` -- unwrapped, NOT refused |
371
+ * | `f any ['a','b']` | `TQLTypeError`, valid operators `in`, `not in` |
372
+ *
373
+ * A naive `Array.isArray(value)` check would flag `f any ['a']`, which both engines
374
+ * translate perfectly well -- a red squiggle over a working query, which is the defect
375
+ * this parser keeps being fixed for. Hence `length >= 2`, and hence the two arities are
376
+ * tested separately in `tests/collectionListOperand.spec.ts`.
377
+ *
378
+ * The empty list is not handled here: `f any []` is already an `INVALID_VALUE` error from
379
+ * the parser (`tests/emptyValueList.spec.ts`), and Python does not even parse it
380
+ * (`TQLSyntaxError`). Reporting it twice would be noise.
381
+ *
382
+ * ## Why `ALL` is excluded, and why that is not an oversight
383
+ *
384
+ * `all` / `not_all` are answered by a Painless script that compares each element to
385
+ * `params.value`. A list operand there emits valid DSL that matches nothing -- which is
386
+ * what both in-memory evaluators also answer -- so both engines dispatch `all` BEFORE the
387
+ * refusal and leave it untouched. `f all ['a','b']` translating cleanly is a pinned
388
+ * negative case here, not a gap.
389
+ *
390
+ * Negation is irrelevant: in both engines the array check precedes the quantifier wrapper,
391
+ * so `not any` / `not none` refuse on the same footing as the positive spellings.
392
+ */
393
+ validateCollectionOperand(t) {
394
+ if (t.operator === "ALL")
395
+ return;
396
+ const e = t.condition;
397
+ if (e.type !== "comparison")
398
+ return;
399
+ const n = e.value;
400
+ if (!Array.isArray(n) || n.length < 2)
401
+ return;
402
+ const s = t.operator.toLowerCase(), r = t.negated ? `not ${s}` : s, l = t.field.name;
403
+ this.addError(
404
+ `'${r}' takes a single value and tests it against each element of '${l}'; it does not take a list.`,
405
+ n[0].start,
406
+ n[n.length - 1].end,
407
+ 1,
408
+ 1,
409
+ "TYPE_MISMATCH",
410
+ `Use \`in\` for membership: \`${l} in [...]\` (or \`${l} not in [...]\` to exclude).`
411
+ );
412
+ }
192
413
  /**
193
414
  * Validate a comparison expression
194
415
  */
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),
416
+ validateComparison(t) {
417
+ const e = t.field.type === "field" ? t.field.name : "computed";
418
+ if (this.options.availableFields && this.options.strictMode && !this.options.availableFields.includes(e) && t.field.type === "field" && this.addWarning(
419
+ `Unknown field '${e}'`,
420
+ t.field.start,
421
+ t.field.end,
422
+ (t.field.type === "field", 1),
202
423
  1,
203
424
  "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,
425
+ `Did you mean: ${this.suggestField(e)}`
426
+ ), this.options.fieldTypes && t.field.type === "field") {
427
+ const n = this.options.fieldTypes[e];
428
+ if (n) {
429
+ const s = h[t.operator];
430
+ s && !s.includes(n) && this.addError(
431
+ `Operator '${t.operator}' requires field type ${s.join(" or ")}, but '${e}' is type '${n}'`,
432
+ t.start,
433
+ t.end,
213
434
  1,
214
435
  1,
215
436
  "TYPE_MISMATCH",
216
- `Use '${e.operator}' only with ${n.join(" or ")} fields`
437
+ `Use '${t.operator}' only with ${s.join(" or ")} fields`
217
438
  );
218
439
  }
219
440
  }
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,
441
+ if (this.options.fieldTypes && t.field.type === "field") {
442
+ const n = this.options.fieldTypes[e];
443
+ if (n) {
444
+ const s = f[t.operator];
445
+ !h[t.operator] && s && !s.includes(n) && this.addWarning(
446
+ `Operator '${t.operator}' may not work well with field type '${n}'`,
447
+ t.start,
448
+ t.end,
228
449
  1,
229
450
  1,
230
451
  "TYPE_MISMATCH",
231
- `Consider using a different operator for ${o} fields`
452
+ `Consider using a different operator for ${n} fields`
232
453
  );
233
454
  }
234
455
  }
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,
456
+ if (!Array.isArray(t.value) && t.value.type === "literal") {
457
+ const n = y[t.operator];
458
+ n && !n.includes(t.value.valueType) && this.addWarning(
459
+ `Operator '${t.operator}' expects ${n.join(" or ")} value`,
460
+ t.value.start,
461
+ t.value.end,
241
462
  1,
242
463
  1,
243
464
  "TYPE_MISMATCH"
244
465
  );
245
466
  }
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 {
467
+ 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") {
468
+ const n = Array.from(t.value.value).length;
469
+ if (n > c)
250
470
  this.addError(
251
- `Invalid regular expression: ${e.value.value}`,
252
- e.value.start,
253
- e.value.end,
471
+ `Regex pattern is ${n} characters, which exceeds the maximum supported length of ${c}. The query cannot run as written.`,
472
+ t.value.start,
473
+ t.value.end,
254
474
  1,
255
475
  1,
256
476
  "INVALID_VALUE",
257
- "Check the regex syntax"
477
+ "Split the pattern into several shorter alternations combined with OR, or use an `in [...]` list"
258
478
  );
259
- }
479
+ else
480
+ try {
481
+ new RegExp(t.value.value);
482
+ } catch {
483
+ this.addError(
484
+ `Invalid regular expression: ${t.value.value}`,
485
+ t.value.start,
486
+ t.value.end,
487
+ 1,
488
+ 1,
489
+ "INVALID_VALUE",
490
+ "Check the regex syntax"
491
+ );
492
+ }
493
+ }
260
494
  }
261
495
  /**
262
496
  * Suggest a similar field name using Levenshtein distance
263
497
  */
264
- suggestField(e) {
498
+ suggestField(t) {
265
499
  if (!this.options.availableFields || this.options.availableFields.length === 0)
266
500
  return "check available fields";
267
- let t = this.options.availableFields[0], o = this.levenshteinDistance(e, t);
268
- for (const n of this.options.availableFields) {
269
- const i = this.levenshteinDistance(e, n);
270
- i < o && (o = i, t = n);
501
+ let e = this.options.availableFields[0], n = this.levenshteinDistance(t, e);
502
+ for (const s of this.options.availableFields) {
503
+ const r = this.levenshteinDistance(t, s);
504
+ r < n && (n = r, e = s);
271
505
  }
272
- return t;
506
+ return e;
273
507
  }
274
508
  /**
275
509
  * Calculate Levenshtein distance between two strings
276
510
  */
277
- levenshteinDistance(e, t) {
278
- const o = [];
279
- for (let n = 0; n <= t.length; n++)
280
- o[n] = [n];
281
- 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
511
+ levenshteinDistance(t, e) {
512
+ const n = [];
513
+ for (let s = 0; s <= e.length; s++)
514
+ n[s] = [s];
515
+ for (let s = 0; s <= t.length; s++)
516
+ n[0][s] = s;
517
+ for (let s = 1; s <= e.length; s++)
518
+ for (let r = 1; r <= t.length; r++)
519
+ e.charAt(s - 1) === t.charAt(r - 1) ? n[s][r] = n[s - 1][r - 1] : n[s][r] = Math.min(
520
+ n[s - 1][r - 1] + 1,
521
+ n[s][r - 1] + 1,
522
+ n[s - 1][r] + 1
289
523
  );
290
- return o[t.length][e.length];
524
+ return n[e.length][t.length];
291
525
  }
292
- addError(e, t, o, n, i, r, a) {
526
+ addError(t, e, n, s, r, l, d) {
293
527
  this.errors.push({
294
- message: e,
295
- start: t,
296
- end: o,
297
- line: n,
298
- column: i,
528
+ message: t,
529
+ start: e,
530
+ end: n,
531
+ line: s,
532
+ column: r,
299
533
  severity: "error",
300
- code: r,
301
- suggestion: a
534
+ code: l,
535
+ suggestion: d
302
536
  });
303
537
  }
304
- addWarning(e, t, o, n, i, r, a) {
538
+ addWarning(t, e, n, s, r, l, d) {
305
539
  this.warnings.push({
306
- message: e,
307
- start: t,
308
- end: o,
309
- line: n,
310
- column: i,
540
+ message: t,
541
+ start: e,
542
+ end: n,
543
+ line: s,
544
+ column: r,
311
545
  severity: "warning",
312
- code: r,
313
- suggestion: a
546
+ code: l,
547
+ suggestion: d
314
548
  });
315
549
  }
316
550
  }
317
- function d(s, e) {
318
- return new b(e).validate(s);
551
+ function _(o, t) {
552
+ return new b(t).validate(o);
319
553
  }
320
- function h(s) {
321
- return d(s).valid;
554
+ function m(o) {
555
+ return _(o).valid;
322
556
  }
323
- function y(s, e) {
324
- const t = d(s, e);
325
- return [...t.errors, ...t.warnings];
557
+ function v(o, t) {
558
+ const e = _(o, t);
559
+ return [...e.errors, ...e.warnings];
326
560
  }
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(`
561
+ function E(o) {
562
+ const t = [];
563
+ o.valid ? t.push("Query is valid") : t.push("Query has errors:");
564
+ for (const e of o.errors)
565
+ t.push(` Error: ${e.message}`), e.suggestion && t.push(` Suggestion: ${e.suggestion}`);
566
+ for (const e of o.warnings)
567
+ t.push(` Warning: ${e.message}`), e.suggestion && t.push(` Suggestion: ${e.suggestion}`);
568
+ return t.join(`
335
569
  `);
336
570
  }
337
571
  export {
572
+ i as ALL_FIELD_TYPES,
573
+ g as BETWEEN_FIELD_TYPES,
574
+ c as MAX_REGEX_PATTERN_LENGTH,
575
+ u as NUMERIC_FIELD_TYPES,
576
+ a as ORDERED_FIELD_TYPES,
338
577
  b as TqlValidator,
339
- f as formatValidationResult,
340
- y as getTqlIssues,
341
- h as isValidTqlSyntax,
342
- d as validateTql
578
+ E as formatValidationResult,
579
+ v as getTqlIssues,
580
+ m as isValidTqlSyntax,
581
+ _ as validateTql
343
582
  };