@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.
@@ -38,9 +38,25 @@ export interface TqlComparisonExpression {
38
38
  start: number;
39
39
  end: number;
40
40
  }
41
+ /**
42
+ * `field ANY value` and its relatives — a comparison applied across the ELEMENTS of an
43
+ * array-valued field rather than to the field as a whole.
44
+ *
45
+ * This type shipped for a long time with no parse path that could produce it: the tokenizer
46
+ * emitted `collectionOp` for `any` / `all` / `none`, the parser had no arm for that token
47
+ * type, and every collection query in the editor came back as
48
+ * "Expected operator after field". The type and `COLLECTION_OPERATORS` were exported the
49
+ * whole time, so the surface looked supported. `tests/collectionParity.spec.ts` now pins the
50
+ * accepted forms against both engine grammars.
51
+ *
52
+ * `negated` carries the `not any` / `!any` / `not_any` spellings, which both engines accept
53
+ * and which are otherwise indistinguishable from the positive form once `operator` has been
54
+ * normalised to its canonical upper-case name.
55
+ */
41
56
  export interface TqlCollectionExpression {
42
57
  type: "collection";
43
58
  operator: "ANY" | "ALL" | "NONE";
59
+ negated: boolean;
44
60
  field: TqlFieldReference;
45
61
  condition: TqlExpression;
46
62
  start: number;
@@ -88,21 +104,52 @@ export interface TqlLiteral {
88
104
  start: number;
89
105
  end: number;
90
106
  }
107
+ /**
108
+ * One argument inside a parameterised mutator call: `split(delimiter=",")`.
109
+ *
110
+ * Both engines accept named (`delimiter=","`), positional (`","`) and bare-identifier
111
+ * (`old=a`) arguments; neither accepts a list literal. See `mutator_arg` in
112
+ * tql/src/parser/grammar.pest and `_setup_mutators` in
113
+ * src/tql/parser_components/grammar.py.
114
+ */
115
+ export interface TqlMutatorArgument {
116
+ /** Present for `name=value` form; absent for a positional argument. */
117
+ name?: string;
118
+ valueType: "string" | "number" | "boolean" | "null" | "identifier";
119
+ value: string | number | boolean | null;
120
+ start: number;
121
+ end: number;
122
+ }
123
+ /**
124
+ * One link in a mutator chain, with the arguments it was called with.
125
+ *
126
+ * `TqlFieldReference.mutators` remains the flat list of names (unchanged for existing
127
+ * consumers); `mutatorCalls` carries the same chain with argument detail attached.
128
+ */
129
+ export interface TqlMutatorCall {
130
+ name: string;
131
+ args: TqlMutatorArgument[];
132
+ /** True when the call carried an explicit `(...)` list, even an empty one. */
133
+ parenthesized: boolean;
134
+ start: number;
135
+ end: number;
136
+ }
91
137
  export interface TqlFieldReference {
92
138
  type: "field";
93
139
  name: string;
94
140
  path: string[];
95
141
  mutators?: string[];
142
+ mutatorCalls?: TqlMutatorCall[];
96
143
  typeHint?: TqlTypeHint;
97
144
  start: number;
98
145
  end: number;
99
146
  }
100
- export type TqlComparisonOperator = "eq" | "ne" | "=" | "!=" | "==" | "eq_ci" | "gt" | "gte" | "lt" | "lte" | ">" | ">=" | "<" | "<=" | "contains" | "not_contains" | "!contains" | "startswith" | "not_startswith" | "!startswith" | "endswith" | "not_endswith" | "!endswith" | "contains_cs" | "not_contains_cs" | "!contains_cs" | "startswith_cs" | "not_startswith_cs" | "!startswith_cs" | "endswith_cs" | "not_endswith_cs" | "!endswith_cs" | "regexp" | "regex" | "not_regexp" | "not_regex" | "!regexp" | "!regex" | "matches" | "not_matches" | "!matches" | "between" | "not_between" | "!between" | "in" | "not_in" | "!in" | "in_cs" | "not_in_cs" | "!in_cs" | "exists" | "not_exists" | "!exists" | "is" | "is_not" | "!is" | "cidr" | "not_cidr" | "!cidr" | "any" | "all" | "none" | "not_any" | "not_all" | "not_none" | "!any" | "!all" | "!none";
147
+ export type TqlComparisonOperator = "eq" | "ne" | "=" | "!=" | "eq_ci" | "gt" | "gte" | "lt" | "lte" | ">" | ">=" | "<" | "<=" | "contains" | "not_contains" | "!contains" | "startswith" | "not_startswith" | "!startswith" | "endswith" | "not_endswith" | "!endswith" | "contains_cs" | "not_contains_cs" | "!contains_cs" | "startswith_cs" | "not_startswith_cs" | "!startswith_cs" | "endswith_cs" | "not_endswith_cs" | "!endswith_cs" | "regexp" | "regex" | "not_regexp" | "not_regex" | "!regexp" | "!regex" | "matches" | "not_matches" | "!matches" | "between" | "not_between" | "!between" | "in" | "not_in" | "!in" | "in_cs" | "not_in_cs" | "!in_cs" | "exists" | "not_exists" | "!exists" | "is" | "is_not" | "!is" | "cidr" | "not_cidr" | "!cidr" | "any" | "all" | "none" | "not_any" | "not_all" | "not_none" | "!any" | "!all" | "!none";
101
148
  export type TqlLogicalOperator = "AND" | "OR" | "NOT";
102
149
  export type TqlCollectionOperator = "ANY" | "ALL" | "NONE";
103
150
  export type TqlMutator = "lowercase" | "uppercase" | "trim" | "split" | "length" | "replace" | "b64encode" | "b64decode" | "urldecode" | "hexencode" | "hexdecode" | "md5" | "sha256" | "refang" | "defang" | "is_private" | "is_global" | "is_multicast" | "is_loopback" | "is_link_local" | "nslookup" | "geoip_lookup" | "geoip" | "geo" | "any" | "all" | "avg" | "average" | "sum" | "min" | "max";
104
151
  export type TqlAggregationFunction = "count" | "sum" | "avg" | "average" | "mean" | "median" | "med" | "min" | "max" | "percentile" | "percentiles" | "p" | "pct" | "percentile_rank" | "percentile_ranks" | "pct_rank" | "pct_ranks" | "std" | "standard_deviation" | "unique_count" | "cardinality" | "values" | "unique" | "distinct";
105
- export type TqlTypeHint = "string" | "str" | "number" | "int" | "integer" | "float" | "double" | "decimal" | "date" | "bool" | "boolean" | "array" | "list" | "object" | "geo";
152
+ export type TqlTypeHint = "string" | "str" | "number" | "int" | "integer" | "float" | "double" | "decimal" | "date" | "bool" | "boolean" | "array" | "list" | "object" | "geo" | "ip";
106
153
  export type TqlVisualizationType = "bar" | "barchart" | "horizontal_bar" | "line" | "area" | "stacked_area" | "pie" | "donut" | "scatter" | "heatmap" | "treemap" | "sunburst" | "table" | "number" | "gauge" | "map" | "grouped_bar" | "stacked_bar" | "nested_pie" | "nested_donut" | "chord" | "auto";
107
154
  /**
108
155
  * Visualization parameters (e.g., title="Events", stacked=true).
@@ -153,8 +200,75 @@ export interface TqlParseWarning {
153
200
  code: TqlWarningCode;
154
201
  suggestion?: string;
155
202
  }
156
- export type TqlErrorCode = "UNEXPECTED_TOKEN" | "UNEXPECTED_END" | "UNBALANCED_PARENS" | "UNBALANCED_BRACKETS" | "UNTERMINATED_STRING" | "INVALID_OPERATOR" | "INVALID_MUTATOR" | "INVALID_VALUE" | "MISSING_OPERAND" | "MISSING_OPERATOR" | "MISSING_VALUE" | "MISSING_FIELD" | "MISSING_MUTATOR" | "INVALID_SYNTAX" | "MAX_DEPTH_EXCEEDED" | "TYPE_MISMATCH";
157
- export type TqlWarningCode = "UNKNOWN_FIELD" | "TYPE_MISMATCH" | "DEPRECATED_OPERATOR" | "REDUNDANT_PARENS";
203
+ export type TqlErrorCode = "UNEXPECTED_TOKEN" | "UNEXPECTED_END" | "UNBALANCED_PARENS" | "UNBALANCED_BRACKETS" | "UNTERMINATED_STRING" | "INVALID_OPERATOR" | "INVALID_MUTATOR" | "INVALID_VALUE" | "MISSING_OPERAND" | "MISSING_OPERATOR" | "MISSING_VALUE" | "MISSING_FIELD" | "MISSING_MUTATOR" | "INVALID_SYNTAX" | "MAX_DEPTH_EXCEEDED" | "TYPE_MISMATCH"
204
+ /**
205
+ * `f::int | trim::str` — a type hint on BOTH sides of the mutator chain.
206
+ *
207
+ * Both engines accept the hint in either position and refuse it in both, structurally: each
208
+ * grammar is an ordered choice whose first alternative consumes `f::int | trim` and strands
209
+ * `::str`. There is no defensible answer for which hint would win.
210
+ */
211
+ | "DUPLICATE_TYPE_HINT"
212
+ /**
213
+ * `::` with a token after it that can never be a type name (`f::| trim`, `f:: eq 'a'`,
214
+ * `f | trim:: eq 'a'`, `f::lowercase`). All four are parse errors in both engines.
215
+ *
216
+ * `f::` at END of input is NOT this — that is a hint being typed, and stays a warning.
217
+ */
218
+ | "MISSING_TYPE_HINT"
219
+ /**
220
+ * A malformed `top N` / `bottom N` modifier on a stats aggregation.
221
+ *
222
+ * Reserved for the shapes BOTH engines refuse: the modifier given twice on one aggregation
223
+ * (`sum(x, top 10) top 5`), a modifier with no count (`sum(x) top by y`), and the in-parens
224
+ * form on `count(*)` / `count()` (`count(*, top 2)`). A spelling only one engine refuses is
225
+ * an ENGINE_DIVERGENCE warning instead — never this.
226
+ */
227
+ | "INVALID_STATS_MODIFIER";
228
+ export type TqlWarningCode = "UNKNOWN_FIELD" | "TYPE_MISMATCH" | "DEPRECATED_OPERATOR" | "REDUNDANT_PARENS"
229
+ /**
230
+ * The query parses, but only ONE of the two engines accepts it.
231
+ *
232
+ * Reserved for measured, currently-live divergences between `tql/src/parser/grammar.pest`
233
+ * and `src/tql/parser_components/grammar.py` — not for anything this package merely
234
+ * dislikes. The editor stays permissive (a red squiggle over syntax an engine accepts is
235
+ * the defect this parser keeps being fixed for) while still naming the engine that will
236
+ * refuse the query, because silently accepting it is the worse half of the same failure:
237
+ * the user finds out at execution instead of at the keystroke.
238
+ */
239
+ | "ENGINE_DIVERGENCE"
240
+ /**
241
+ * The query parses, but the field cannot answer the operator and the query engine refuses
242
+ * it at execution.
243
+ *
244
+ * Raised for the `_cs` (case-sensitive) operators, under TWO rules rather than one --
245
+ * they do not share a predicate, and describing only the first is how this docstring was
246
+ * wrong on 90 of its own 98 firing cells before review:
247
+ *
248
+ * * the WHOLE-VALUE six (`contains_cs`, `startswith_cs`, `endswith_cs` + negations)
249
+ * whenever the field has no case-preserving form at all. That includes analyzed `text`
250
+ * with no `keyword` subfield AND every non-string type -- a `long`, `ip`, `date`,
251
+ * `boolean`, `geo_point`, `nested` or `object` field has no un-analyzed STRING form,
252
+ * and a substring match against one is not imprecise, it is unanswerable.
253
+ * * the MEMBERSHIP two (`in_cs`, `not_in_cs`) only when the base field is analyzed
254
+ * `text`. Case-sensitivity is vacuous for a number or an instant, and the query engine
255
+ * answers `in_cs` on those perfectly well.
256
+ *
257
+ * See `caseSensitiveSupport` in `src/validator.ts` for the predicate and for what the
258
+ * schema has to carry for this to fire at all.
259
+ *
260
+ * A WARNING and never an error, deliberately. The verdict is only as good as the schema
261
+ * the host supplied: a converter that flattens field names but drops multifields makes a
262
+ * perfectly translatable query look unanswerable, and a red squiggle over a query the
263
+ * cluster serves is the worse failure.
264
+ *
265
+ * Distinct from `TYPE_MISMATCH` because the objection is to the MAPPING, not the type --
266
+ * `text` is an accepted type for every `_cs` operator. The two can coincide on a
267
+ * non-string field, where the type is also wrong; the validator suppresses the softer
268
+ * `TYPE_MISMATCH` there rather than reporting both, so a consumer filtering by code sees
269
+ * one diagnostic per clause and the more accurate one.
270
+ */
271
+ | "UNANSWERABLE_MAPPING";
158
272
  export interface TqlValidationResult {
159
273
  valid: boolean;
160
274
  errors: TqlParseError[];
@@ -168,6 +282,16 @@ export interface TqlValidationOptions {
168
282
  allowMutators?: boolean;
169
283
  maxDepth?: number;
170
284
  }
285
+ /**
286
+ * Whether a field can answer a case-SENSITIVE (`_cs`) operator, as far as a supplied schema shows.
287
+ *
288
+ * Three-valued because the engines' rule keys on the index MAPPING while this package is
289
+ * handed a flat name→type map: `"unknown"` is a real and common answer, distinct from
290
+ * `"supported"`, and conflating the two is what let the editor green-light a query the
291
+ * engines refuse (tql#206). Produced by `caseSensitiveSupport` in `src/validator.ts`, which
292
+ * documents the predicate and its two blind spots.
293
+ */
294
+ export type CaseSensitiveSupport = "supported" | "unsupported" | "unknown";
171
295
  export type TqlFieldType = "text" | "keyword" | "long" | "integer" | "short" | "byte" | "double" | "float" | "half_float" | "scaled_float" | "unsigned_long" | "date" | "boolean" | "ip" | "nested" | "object" | "geo_point";
172
296
  export interface TqlCompletionItem {
173
297
  label: string;
@@ -264,6 +388,10 @@ export interface TqlInputEmits {
264
388
  * Reactive reference type for Vue computed properties.
265
389
  * This is a simplified type that matches Vue's ComputedRef interface
266
390
  * without requiring Vue as a direct dependency.
391
+ *
392
+ * @deprecated No longer used by `TqlInputExpose`: Vue unwraps refs on a
393
+ * component's exposed instance, so a template ref never saw a `.value` here.
394
+ * Kept exported so existing imports keep compiling.
267
395
  */
268
396
  export interface ReactiveRef<T> {
269
397
  readonly value: T;
@@ -277,12 +405,12 @@ export interface TqlInputExpose {
277
405
  getTokenAtPosition(pos: number): TqlToken | null;
278
406
  getCursorPosition(): number;
279
407
  setCursorPosition(pos: number): void;
280
- /** Whether the current query is valid (no syntax errors). Access via .value */
281
- isValid: ReactiveRef<boolean>;
282
- /** Whether the current query has validation errors. Access via .value */
283
- hasErrors: ReactiveRef<boolean>;
284
- /** Array of current validation errors. Access via .value */
285
- errors: ReactiveRef<TqlParseError[]>;
408
+ /** Whether the current query is valid (no syntax errors). */
409
+ readonly isValid: boolean;
410
+ /** Whether the current query has validation errors. */
411
+ readonly hasErrors: boolean;
412
+ /** Array of current validation errors. */
413
+ readonly errors: TqlParseError[];
286
414
  }
287
415
  export type ComplexityScore = "low" | "medium" | "high";
288
416
  export interface ComplexityWarning {
@@ -1,4 +1,186 @@
1
- import { TqlParseError, TqlParseWarning, TqlValidationOptions, TqlValidationResult } from './types';
1
+ import { CaseSensitiveSupport, TqlComparisonOperator, TqlFieldType, TqlParseError, TqlParseWarning, TqlValidationOptions, TqlValidationResult } from './types';
2
+ /**
3
+ * Maximum regex pattern length TQL will compile, in characters (code points).
4
+ *
5
+ * A pattern longer than this is refused by every TQL runtime. It is flagged here
6
+ * -- in the editor, as the user types -- because the alternative is discovering
7
+ * it after the rule has been saved and deployed: before tql#185 the Python
8
+ * evaluator answered "no match" for an over-long pattern while the query parsed
9
+ * cleanly and reported health green, so a detection rule carrying one never
10
+ * fired and nothing anywhere said so.
11
+ *
12
+ * Mirrored by `MAX_REGEX_PATTERN_LENGTH` in `src/tql/regex_guard.py` and
13
+ * `tql/src/comparator.rs`. Changing it in one place only re-creates the
14
+ * divergence.
15
+ */
16
+ export declare const MAX_REGEX_PATTERN_LENGTH = 1000;
17
+ /**
18
+ * Every field type in the `TqlFieldType` union.
19
+ *
20
+ * Declared once so an operator that genuinely applies to everything says so by NAME rather
21
+ * than by a transcribed twelve-element list — and so a new field type cannot be added to the
22
+ * union while silently falling out of the operators it belongs to.
23
+ * `tests/collectionOperatorTypes.spec.ts` reads the union out of `src/types.ts` and asserts
24
+ * this array is exactly it, so a divergence is a red rather than a quiet narrowing.
25
+ */
26
+ export declare const ALL_FIELD_TYPES: TqlFieldType[];
27
+ /**
28
+ * Every numeric field type in the union — all nine widths, not the five that get typed out.
29
+ */
30
+ export declare const NUMERIC_FIELD_TYPES: TqlFieldType[];
31
+ /**
32
+ * The types an ORDERED comparison (`gt`/`gte`/`lt`/`lte`) is defined over.
33
+ *
34
+ * Numeric and date, plus `keyword`: both evaluators fall back to a lexicographic string
35
+ * comparison when neither side reads as a number, and Python's own validator lists all four
36
+ * operators in its `keyword_ops`. `text` is excluded on that same source's reasoning — a
37
+ * range over an analyzed field compares tokens, not the value the user typed.
38
+ */
39
+ export declare const ORDERED_FIELD_TYPES: TqlFieldType[];
40
+ /**
41
+ * The types `between` / `not_between` are defined over.
42
+ *
43
+ * `ORDERED_FIELD_TYPES` minus `keyword`. The four ordered operators and `between` are NOT
44
+ * the same set, and the difference is Python's, not this package's: `keyword_ops` in
45
+ * `_validate_simple_type_compatibility` lists `gt`/`gte`/`lt`/`lte` and excludes `between`.
46
+ * `tests/collectionAndNullSemantics.spec.ts` pins that exclusion against the Python source,
47
+ * so this constant self-retires if Python ever adds it.
48
+ */
49
+ export declare const BETWEEN_FIELD_TYPES: TqlFieldType[];
50
+ /**
51
+ * The case-SENSITIVE WHOLE-VALUE operators: every spelling the `TqlComparisonOperator` type
52
+ * admits.
53
+ *
54
+ * TWELVE spellings across both families, EIGHT operators. The engines' constants — same two
55
+ * names, in `tql/src/opensearch/field_mappings.rs` and
56
+ * `src/tql/opensearch_components/field_mapping.py` — carry eight, because the engines only
57
+ * ever see `not_contains_cs`.
58
+ *
59
+ * The four `!` members are unreachable from `validate()`: `normalizeOperator` in
60
+ * `src/parser.ts` maps every `!x` through `NEGATION_SYMBOL_OPERATORS` before an AST exists,
61
+ * and `TqlValidator` takes a query string and nothing else. They are kept because this array
62
+ * is exported public API and a host may match raw user-typed text against it — pinned by "the
63
+ * `!` spellings reach the validator normalised", which also goes red if normalisation is ever
64
+ * removed and the `!` forms become live.
65
+ *
66
+ * `tests/caseSensitiveOperatorMapping.spec.ts` reads both engine constants and asserts these
67
+ * arrays cover exactly them, family by family, once the `!` forms are normalised — so the two
68
+ * cannot drift in either direction, and neither can the SPLIT between the families, which is
69
+ * the part that actually changes the predicate.
70
+ */
71
+ export declare const CASE_SENSITIVE_WHOLE_VALUE_OPERATORS: TqlComparisonOperator[];
72
+ /**
73
+ * The case-SENSITIVE MEMBERSHIP operators, kept separate for the reason the engines keep them
74
+ * separate: their refusal predicate is NARROWER.
75
+ *
76
+ * `in_cs` is a keyword operator (an exact-match `terms` query), so it routes through a
77
+ * different branch of `get_field_for_operator` (Python) / `get_query_field` (Rust), and that branch refuses
78
+ * only when the base field is analyzed `text`. The whole-value six refuse whenever there is
79
+ * no un-analyzed form at all. Folding the two lists into one is what made
80
+ * `caseSensitiveSupport` wrong for six operators in this PR's first round.
81
+ */
82
+ export declare const CASE_SENSITIVE_MEMBERSHIP_OPERATORS: TqlComparisonOperator[];
83
+ /** Every spelling of every case-SENSITIVE operator — the union of the two families above. */
84
+ export declare const CASE_SENSITIVE_OPERATORS: TqlComparisonOperator[];
85
+ /**
86
+ * Whether a field offers a CASE-PRESERVING form, as far as the supplied schema shows.
87
+ *
88
+ * This is the field half of the engines' rule, on its own because the two `_cs` operator
89
+ * families consume it differently (see `caseSensitiveSupport`, which is the function you
90
+ * probably want).
91
+ *
92
+ * * `"present"` — the field itself is `keyword`, or it has a `keyword` subfield. One whole
93
+ * value, one indexed term, case intact.
94
+ * * `"absent"` — no such form. That covers analyzed `text` with no `keyword` sibling AND
95
+ * every non-string type: a `long`, `ip`, `date` or `boolean` field has no un-analyzed
96
+ * STRING form either. Whether that matters depends on the operator, which is why this
97
+ * function does not decide it.
98
+ * * `"unknown"` — no `fieldTypes` was supplied, or this field is not in it.
99
+ *
100
+ * Mirrors `FieldMappings::unanalyzed_form` (Rust) and `FieldMapping.unanalyzed_field`
101
+ * (Python) as closely as a flat name→type map allows. Two documented divergences, both of
102
+ * which fail quiet — see `caseSensitiveSupport`.
103
+ *
104
+ * ## The sibling test is on TYPE, not on the name `.keyword`
105
+ *
106
+ * Both engines scan a field's declared subfields for one whose TYPE is `keyword` (Rust:
107
+ * `subfield_of(field, |t| *t == FieldType::Keyword)`; Python: `keyword_field`, set for any
108
+ * subfield with `"type": "keyword"`). A multifield named `.raw` or `.kw` answers exactly as
109
+ * well as one named `.keyword`, and hard-coding the name would report `"absent"` on a working
110
+ * mapping. So this walks the map for `${field}.` prefixed entries and looks at their type.
111
+ *
112
+ * ## One level down, matching the engines
113
+ *
114
+ * A subfield is a DIRECT child: both engines read a declared `subfields` map, so
115
+ * `f.sub.deep` is not a subfield of `f` to either of them. Scanning descendants at any depth
116
+ * would report `"present"` for a mapping the engines refuse.
117
+ */
118
+ export declare function casePreservingForm(field: string, fieldTypes?: Record<string, TqlFieldType>): "present" | "absent" | "unknown";
119
+ /**
120
+ * Whether a field can answer a case-SENSITIVE operator, as far as the supplied schema shows.
121
+ *
122
+ * Three-valued on purpose. The engines' rule keys on the MAPPING and this package is handed a
123
+ * flat name→type map, so there are genuinely three answers and collapsing them to two is how
124
+ * this ended up unimplemented for a release (tql#206):
125
+ *
126
+ * * `"unsupported"` — both engines raise (`TQLTypeError` / `OpenSearchError::TypeError`)
127
+ * for this field-and-operator pair. That is now true of every type, which it was not
128
+ * when this check was written: Rust's `parse_field_type` had no arm for `geo_point` or
129
+ * `unsigned_long`, dropped such a field from its mappings, and permitted the query —
130
+ * emitting a `wildcard` that could never match. Fixed in tql#210, in this same PR, and
131
+ * `no_field_type_is_silently_dropped` in
132
+ * `tql/tests/opensearch_case_sensitive_unanswerable.rs` holds the property for type
133
+ * names neither engine lists, not just for the two that were noticed.
134
+ * * `"supported"` — the schema shows nothing that would stop the engines translating it.
135
+ * * `"unknown"` — no `fieldTypes` was supplied, or this field is not in it. Nothing is
136
+ * claimed and no diagnostic should be produced.
137
+ *
138
+ * ## Why the OPERATOR is a parameter and not an afterthought
139
+ *
140
+ * The two `_cs` families do NOT share a predicate, and a field-only version of this function
141
+ * got it wrong for six of the eight operators before review caught it:
142
+ *
143
+ * * **Whole-value** (`contains_cs`, `startswith_cs`, `endswith_cs` + negations) refuse
144
+ * whenever there is no un-analyzed form at all — `field_mapping.py`'s
145
+ * `if self.unanalyzed_field is None and operator in CASE_SENSITIVE_WHOLE_VALUE_OPERATORS`,
146
+ * and the unconditional `if CASE_SENSITIVE_WHOLE_VALUE_OPERATORS.contains(&operator)` in
147
+ * `field_mappings.rs`. Neither is gated on the base type. Measured against the Python
148
+ * engine at `{"f": {"type": t}}`: `contains_cs`, `startswith_cs` and `endswith_cs` all
149
+ * raise for `long`, `ip`, `date` and `boolean`.
150
+ * * **Membership** (`in_cs`, `not_in_cs`) refuse only when the base field is analyzed
151
+ * `text` — `operator in CASE_SENSITIVE_MEMBERSHIP_OPERATORS and
152
+ * self.field_types.get(self.base_field_name) == "text"`, and its Rust twin. The same
153
+ * measurement: `f in_cs ['a']` translates to `{"terms": …}` for all four of those types,
154
+ * because case-sensitivity is vacuous for a number, an address, an instant or a flag.
155
+ *
156
+ * So "no case-preserving form" is the whole answer for six operators and half of it for two.
157
+ * `test_a_non_text_field_without_a_keyword_form_is_not_caught` pins the membership half only;
158
+ * reading its NAME as covering all eight is exactly how the field-only version passed review
159
+ * of its own tests.
160
+ *
161
+ * ## Two blind spots, both of which fail SILENT rather than loud
162
+ *
163
+ * 1. **A `wildcard` sibling reads as absent.** The engines accept `wildcard` as a
164
+ * case-preserving form (`FieldType::is_unanalyzed_string`), but `wildcard` is not a member
165
+ * of `TqlFieldType`, so a host cannot express it and this cannot see it.
166
+ * 2. **An object CHILD is indistinguishable from a multifield.** `os` (`text`) beside
167
+ * `os.name` (`keyword`) is read as case-preserving though `os.name` is a sibling field,
168
+ * not a subfield of `os`. A flat map carries no way to tell them apart.
169
+ *
170
+ * Blind spot 2 under-reports (a real refusal goes unwarned — no worse than before this
171
+ * existed), and blind spot 1 over-reports, which is the whole reason the caller emits a
172
+ * WARNING and not an error. Neither justifies staying silent in the ordinary ECS case, which
173
+ * is the case this answers.
174
+ *
175
+ * @param field - the field name as written in the query
176
+ * @param operator - the `_cs` operator being applied. A non-`_cs` operator always returns
177
+ * `"supported"`: this function answers one rule and does not stand in for the type tables.
178
+ * @param fieldTypes - the flat schema, exactly as passed in `TqlValidationOptions.fieldTypes`.
179
+ * For this to return anything but `"unknown"` it must contain the field; for it to
180
+ * distinguish the two `text` shapes it must ALSO contain the multifield entries
181
+ * (`message.keyword`), which is what an index-mapping flattener naturally produces.
182
+ */
183
+ export declare function caseSensitiveSupport(field: string, operator: TqlComparisonOperator, fieldTypes?: Record<string, TqlFieldType>): CaseSensitiveSupport;
2
184
  export declare class TqlValidator {
3
185
  private options;
4
186
  private errors;
@@ -12,6 +194,58 @@ export declare class TqlValidator {
12
194
  * Validate an expression recursively
13
195
  */
14
196
  private validateExpression;
197
+ /**
198
+ * `f any ['a','b']` is ill-typed. Say so, and name `in`.
199
+ *
200
+ * ## The rule, as both engines implement it
201
+ *
202
+ * `any` / `none` apply a comparison to each ELEMENT of an array-valued field. The operand
203
+ * is therefore a SCALAR: `f any 'admin'` asks whether some element equals `'admin'`. A
204
+ * LIST operand asks whether some single element equals the whole two-element list, which
205
+ * no scalar element can. `in` is already the membership operator, and is what the author
206
+ * meant.
207
+ *
208
+ * Both engines now refuse it -- `ab55587` in `tql/src/opensearch/query_builder.rs`
209
+ * (`build_collection_op`) and `_refuse_list_operand` in
210
+ * `src/tql/opensearch_components/query_converter.py` -- raising a type error whose
211
+ * `valid_operators` are `in` / `not in`. Before that it emitted `{"term": {"f": [..]}}`,
212
+ * which OpenSearch answers with HTTP 400 `[term] query does not support array of values`,
213
+ * failing the WHOLE search including every unrelated clause; the intermediate fix emitted
214
+ * `match_none`, which executed cleanly and excluded nothing, silently.
215
+ *
216
+ * ## Three arities, and only the middle one is new
217
+ *
218
+ * The engines do NOT test `is_array`. They unwrap a single-element list FIRST
219
+ * (`unwrap_single` in Rust, `len(value) == 1` in Python) and refuse only what is still a
220
+ * list. Measured against the live Python translator at `ab55587`:
221
+ *
222
+ * | Query | Python translator |
223
+ * |--------------------|------------------------------------------------------|
224
+ * | `f any 'admin'` | `{"term": {"f": "admin"}}` |
225
+ * | `f any ['a']` | `{"term": {"f": "a"}}` -- unwrapped, NOT refused |
226
+ * | `f any ['a','b']` | `TQLTypeError`, valid operators `in`, `not in` |
227
+ *
228
+ * A naive `Array.isArray(value)` check would flag `f any ['a']`, which both engines
229
+ * translate perfectly well -- a red squiggle over a working query, which is the defect
230
+ * this parser keeps being fixed for. Hence `length >= 2`, and hence the two arities are
231
+ * tested separately in `tests/collectionListOperand.spec.ts`.
232
+ *
233
+ * The empty list is not handled here: `f any []` is already an `INVALID_VALUE` error from
234
+ * the parser (`tests/emptyValueList.spec.ts`), and Python does not even parse it
235
+ * (`TQLSyntaxError`). Reporting it twice would be noise.
236
+ *
237
+ * ## Why `ALL` is excluded, and why that is not an oversight
238
+ *
239
+ * `all` / `not_all` are answered by a Painless script that compares each element to
240
+ * `params.value`. A list operand there emits valid DSL that matches nothing -- which is
241
+ * what both in-memory evaluators also answer -- so both engines dispatch `all` BEFORE the
242
+ * refusal and leave it untouched. `f all ['a','b']` translating cleanly is a pinned
243
+ * negative case here, not a gap.
244
+ *
245
+ * Negation is irrelevant: in both engines the array check precedes the quantifier wrapper,
246
+ * so `not any` / `not none` refuse on the same footing as the positive spellings.
247
+ */
248
+ private validateCollectionOperand;
15
249
  /**
16
250
  * Validate a comparison expression
17
251
  */