@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/validator.js
CHANGED
|
@@ -1,22 +1,125 @@
|
|
|
1
|
-
import { parseTql as
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
is
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
},
|
|
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
|
|
340
|
+
class k {
|
|
154
341
|
options;
|
|
155
342
|
errors;
|
|
156
343
|
warnings;
|
|
157
|
-
constructor(
|
|
158
|
-
this.options =
|
|
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(
|
|
164
|
-
if (this.errors = [], this.warnings = [], !
|
|
350
|
+
validate(t) {
|
|
351
|
+
if (this.errors = [], this.warnings = [], !t)
|
|
165
352
|
return { valid: !0, errors: [], warnings: [] };
|
|
166
|
-
const
|
|
167
|
-
return this.errors.push(...
|
|
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(
|
|
177
|
-
switch (
|
|
363
|
+
validateExpression(t) {
|
|
364
|
+
switch (t.type) {
|
|
178
365
|
case "binary":
|
|
179
|
-
this.validateExpression(
|
|
366
|
+
this.validateExpression(t.left), this.validateExpression(t.right);
|
|
180
367
|
break;
|
|
181
368
|
case "unary":
|
|
182
|
-
this.validateExpression(
|
|
369
|
+
this.validateExpression(t.operand);
|
|
183
370
|
break;
|
|
184
371
|
case "group":
|
|
185
|
-
this.validateExpression(
|
|
372
|
+
this.validateExpression(t.expression);
|
|
186
373
|
break;
|
|
187
374
|
case "comparison":
|
|
188
|
-
this.validateComparison(
|
|
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(
|
|
196
|
-
const
|
|
197
|
-
if (this.options.availableFields && this.options.strictMode && !this.options.availableFields.includes(
|
|
198
|
-
`Unknown field '${
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
(
|
|
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(
|
|
205
|
-
), this.options.fieldTypes &&
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
`Operator '${
|
|
211
|
-
|
|
212
|
-
|
|
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 '${
|
|
490
|
+
`Use '${t.operator}' only with ${s.join(" or ")} fields`
|
|
217
491
|
);
|
|
218
492
|
}
|
|
219
493
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 ${
|
|
526
|
+
`Consider using a different operator for ${n} fields`
|
|
232
527
|
);
|
|
233
528
|
}
|
|
234
529
|
}
|
|
235
|
-
if (!Array.isArray(
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
`Operator '${
|
|
239
|
-
|
|
240
|
-
|
|
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 ((
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
`
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
"
|
|
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(
|
|
572
|
+
suggestField(t) {
|
|
265
573
|
if (!this.options.availableFields || this.options.availableFields.length === 0)
|
|
266
574
|
return "check available fields";
|
|
267
|
-
let
|
|
575
|
+
let e = this.options.availableFields[0], i = this.levenshteinDistance(t, e);
|
|
268
576
|
for (const n of this.options.availableFields) {
|
|
269
|
-
const
|
|
270
|
-
|
|
577
|
+
const s = this.levenshteinDistance(t, n);
|
|
578
|
+
s < i && (i = s, e = n);
|
|
271
579
|
}
|
|
272
|
-
return
|
|
580
|
+
return e;
|
|
273
581
|
}
|
|
274
582
|
/**
|
|
275
583
|
* Calculate Levenshtein distance between two strings
|
|
276
584
|
*/
|
|
277
|
-
levenshteinDistance(
|
|
278
|
-
const
|
|
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
|
-
|
|
283
|
-
for (let n =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
598
|
+
return i[e.length][t.length];
|
|
291
599
|
}
|
|
292
|
-
addError(
|
|
600
|
+
addError(t, e, i, n, s, a, d) {
|
|
293
601
|
this.errors.push({
|
|
294
|
-
message:
|
|
295
|
-
start:
|
|
296
|
-
end:
|
|
602
|
+
message: t,
|
|
603
|
+
start: e,
|
|
604
|
+
end: i,
|
|
297
605
|
line: n,
|
|
298
|
-
column:
|
|
606
|
+
column: s,
|
|
299
607
|
severity: "error",
|
|
300
|
-
code:
|
|
301
|
-
suggestion:
|
|
608
|
+
code: a,
|
|
609
|
+
suggestion: d
|
|
302
610
|
});
|
|
303
611
|
}
|
|
304
|
-
addWarning(
|
|
612
|
+
addWarning(t, e, i, n, s, a, d) {
|
|
305
613
|
this.warnings.push({
|
|
306
|
-
message:
|
|
307
|
-
start:
|
|
308
|
-
end:
|
|
614
|
+
message: t,
|
|
615
|
+
start: e,
|
|
616
|
+
end: i,
|
|
309
617
|
line: n,
|
|
310
|
-
column:
|
|
618
|
+
column: s,
|
|
311
619
|
severity: "warning",
|
|
312
|
-
code:
|
|
313
|
-
suggestion:
|
|
620
|
+
code: a,
|
|
621
|
+
suggestion: d
|
|
314
622
|
});
|
|
315
623
|
}
|
|
316
624
|
}
|
|
317
|
-
function
|
|
318
|
-
return new
|
|
625
|
+
function f(o, t) {
|
|
626
|
+
return new k(t).validate(o);
|
|
319
627
|
}
|
|
320
|
-
function
|
|
321
|
-
return
|
|
628
|
+
function $(o) {
|
|
629
|
+
return f(o).valid;
|
|
322
630
|
}
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
return [...
|
|
631
|
+
function A(o, t) {
|
|
632
|
+
const e = f(o, t);
|
|
633
|
+
return [...e.errors, ...e.warnings];
|
|
326
634
|
}
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
for (const
|
|
331
|
-
|
|
332
|
-
for (const
|
|
333
|
-
|
|
334
|
-
return
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
};
|