@tellaro/tql 1.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +73 -65
- package/dist/src/builder.d.ts +6 -0
- package/dist/src/completionProvider.js +276 -195
- package/dist/src/complexityAnalyzer.js +41 -32
- package/dist/src/index.d.ts +3 -3
- package/dist/src/parser.d.ts +193 -1
- package/dist/src/parser.js +614 -127
- package/dist/src/semanticPreview.d.ts +23 -0
- package/dist/src/semanticPreview.js +111 -41
- package/dist/src/tokenizer.d.ts +134 -5
- package/dist/src/tokenizer.js +47 -30
- package/dist/src/types.d.ts +138 -10
- package/dist/src/validator.d.ts +235 -1
- package/dist/src/validator.js +468 -150
- package/dist/tql.css +1 -1
- package/dist/vue/TqlAutocomplete.vue.d.ts +24 -1
- package/dist/vue/TqlChart.vue.d.ts +19 -1
- package/dist/vue/TqlChart.vue.js +1 -1
- package/dist/vue/TqlInput.vue.d.ts +92 -1
- package/dist/vue/TqlInput.vue.js +1 -1
- package/dist/vue/TqlInput.vue2.js +282 -256
- package/dist/vue/components/TqlPreview.vue.d.ts +89 -2
- package/dist/vue/components/TqlPreview.vue.js +4 -4
- package/dist/vue/index.d.ts +1 -1
- package/dist/vue/useTqlInput.d.ts +38 -29
- package/package.json +3 -5
package/dist/src/types.d.ts
CHANGED
|
@@ -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" | "=" | "!=" | "
|
|
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
|
-
|
|
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).
|
|
281
|
-
isValid:
|
|
282
|
-
/** Whether the current query has validation errors.
|
|
283
|
-
hasErrors:
|
|
284
|
-
/** Array of current validation errors.
|
|
285
|
-
errors:
|
|
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 {
|
package/dist/src/validator.d.ts
CHANGED
|
@@ -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
|
*/
|