Skip to content

Commit e8a0716

Browse files
committed
Added strict range type checks for ENRICH
1 parent 3bb20e3 commit e8a0716

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ public enum Cap {
255255
*/
256256
RANGEQUERY_FOR_DATETIME,
257257

258+
/**
259+
* Enforce strict type checking on ENRICH range types.
260+
*/
261+
ENRICH_STRICT_RANGE_TYPES,
262+
258263
/**
259264
* Fix for non-unique attribute names in ROW and logical plans.
260265
* https://github.com/elastic/elasticsearch/issues/110541

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/EnrichLookupService.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import org.elasticsearch.core.Releasables;
4747
import org.elasticsearch.index.mapper.BlockLoader;
4848
import org.elasticsearch.index.mapper.MappedFieldType;
49+
import org.elasticsearch.index.mapper.RangeFieldMapper;
50+
import org.elasticsearch.index.mapper.RangeType;
4951
import org.elasticsearch.index.query.SearchExecutionContext;
5052
import org.elasticsearch.index.shard.ShardId;
5153
import org.elasticsearch.search.SearchService;
@@ -294,6 +296,7 @@ private void doLookup(
294296
releasables.add(mergePositionsOperator);
295297
SearchExecutionContext searchExecutionContext = searchContext.getSearchExecutionContext();
296298
MappedFieldType fieldType = searchExecutionContext.getFieldType(matchField);
299+
validateTypes(inputDataType, fieldType);
297300
var queryList = switch (matchType) {
298301
case "match", "range" -> QueryList.termQueryList(fieldType, searchExecutionContext, inputBlock, inputDataType);
299302
case "geo_match" -> QueryList.geoShapeQuery(fieldType, searchExecutionContext, inputBlock, inputDataType);
@@ -354,6 +357,30 @@ private void doLookup(
354357
}
355358
}
356359

360+
private static void validateTypes(DataType inputDataType, MappedFieldType fieldType) {
361+
if (inputDataType == DataType.UNSUPPORTED) {
362+
throw new EsqlIllegalArgumentException("ENRICH cannot match on unsupported input data type");
363+
}
364+
if (fieldType == null) {
365+
throw new EsqlIllegalArgumentException("ENRICH cannot match on non-existent field");
366+
}
367+
if (fieldType instanceof RangeFieldMapper.RangeFieldType rangeType) {
368+
if (rangeTypesCompatible(rangeType.rangeType(), inputDataType) == false) {
369+
throw new EsqlIllegalArgumentException(
370+
"ENRICH range and input types are incompatible: range[" + rangeType.rangeType() + "], input[" + inputDataType + "]"
371+
);
372+
}
373+
}
374+
}
375+
376+
private static boolean rangeTypesCompatible(RangeType rangeType, DataType inputDataType) {
377+
return switch (rangeType) {
378+
case INTEGER, LONG -> inputDataType.isWholeNumber();
379+
case IP -> inputDataType == DataType.IP;
380+
default -> rangeType.isNumeric() == inputDataType.isNumeric();
381+
};
382+
}
383+
357384
private static Operator extractFieldsOperator(
358385
SearchContext searchContext,
359386
DriverContext driverContext,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
---
2+
setup:
3+
- requires:
4+
capabilities:
5+
- method: POST
6+
path: /_query
7+
parameters: [method, path, parameters, capabilities]
8+
capabilities: [enrich_strict_range_types]
9+
reason: "Strict range type checking was added"
10+
test_runner_features: [capabilities, allowed_warnings_regex, warnings_regex]
11+
12+
- do:
13+
indices.create:
14+
index: ages
15+
body:
16+
settings:
17+
index.number_of_shards: 1
18+
index.routing.rebalance.enable: "none"
19+
mappings:
20+
properties:
21+
age_range:
22+
type: "integer_range"
23+
description:
24+
type: "keyword"
25+
26+
- do:
27+
bulk:
28+
index: ages
29+
refresh: true
30+
body:
31+
- { "index": { } }
32+
- { "age_range": { "gte": 0, "lt": 2 }, "description": "Baby" }
33+
- { "index": { } }
34+
- { "age_range": { "gte": 2, "lt": 4 }, "description": "Toddler" }
35+
- { "index": { } }
36+
- { "age_range": { "gte": 3, "lt": 5 }, "description": "Preschooler" }
37+
- { "index": { } }
38+
- { "age_range": { "gte": 5, "lt": 12 }, "description": "Child" }
39+
- { "index": { } }
40+
- { "age_range": { "gte": 13, "lt": 20 }, "description": "Adolescent" }
41+
- { "index": { } }
42+
- { "age_range": { "gte": 20, "lt": 40 }, "description": "Young Adult" }
43+
- { "index": { } }
44+
- { "age_range": { "gte": 40, "lt": 60 }, "description": "Middle-aged" }
45+
- { "index": { } }
46+
- { "age_range": { "gte": 60, "lt": 80 }, "description": "Senior" }
47+
- { "index": { } }
48+
- { "age_range": { "gte": 80, "lt": 100 }, "description": "Elderly" }
49+
- { "index": { } }
50+
- { "age_range": { "gte": 100, "lt": 200 }, "description": "Incredible" }
51+
- do:
52+
cluster.health:
53+
wait_for_no_initializing_shards: true
54+
wait_for_events: languid
55+
56+
- do:
57+
enrich.put_policy:
58+
name: ages-policy
59+
body:
60+
range:
61+
indices: [ "ages" ]
62+
match_field: "age_range"
63+
enrich_fields: [ "description" ]
64+
65+
- do:
66+
enrich.execute_policy:
67+
name: ages-policy
68+
69+
- do:
70+
indices.create:
71+
index: employees
72+
body:
73+
mappings:
74+
properties:
75+
name:
76+
type: keyword
77+
age:
78+
type: integer
79+
ak:
80+
type: keyword
81+
salary:
82+
type: double
83+
84+
- do:
85+
bulk:
86+
index: employees
87+
refresh: true
88+
body:
89+
- { "index": { } }
90+
- { "name": "Joe Soap", "age": 36, "ak": "36", "salary": 55.55 }
91+
- { "index": { } }
92+
- { "name": "Jane Doe", "age": 31, "ak": "31", "salary": 55.55 }
93+
- { "index": { } }
94+
- { "name": "Magic Mike", "age": 44, "ak": "44", "salary": 55.55 }
95+
- { "index": { } }
96+
- { "name": "Anon Ymous", "age": 61, "ak": "61", "salary": 55.55 }
97+
98+
---
99+
teardown:
100+
- do:
101+
enrich.delete_policy:
102+
name: ages-policy
103+
104+
---
105+
"ages":
106+
- do:
107+
allowed_warnings_regex:
108+
- "No limit defined, adding default limit of \\[.*\\]"
109+
esql.query:
110+
body:
111+
query: 'FROM employees | ENRICH ages-policy ON age | STATS count=COUNT(*) BY description | SORT count DESC, description ASC'
112+
113+
- match: { columns.0.name: "count" }
114+
- match: { columns.0.type: "long" }
115+
- match: { columns.1.name: "description" }
116+
- match: { columns.1.type: "keyword" }
117+
118+
- length: { values: 3 }
119+
- match: { values.0: [ 2, "Young Adult" ] }
120+
- match: { values.1: [ 1, "Middle-aged" ] }
121+
- match: { values.2: [ 1, "Senior" ] }
122+
123+
---
124+
"ages as typecast keywords":
125+
- do:
126+
allowed_warnings_regex:
127+
- "No limit defined, adding default limit of \\[.*\\]"
128+
esql.query:
129+
body:
130+
query: 'FROM employees | EVAL aki = ak::integer | ENRICH ages-policy ON aki | STATS count=COUNT(*) BY description | SORT count DESC, description ASC'
131+
132+
- match: { columns.0.name: "count" }
133+
- match: { columns.0.type: "long" }
134+
- match: { columns.1.name: "description" }
135+
- match: { columns.1.type: "keyword" }
136+
137+
- length: { values: 3 }
138+
- match: { values.0: [ 2, "Young Adult" ] }
139+
- match: { values.1: [ 1, "Middle-aged" ] }
140+
- match: { values.2: [ 1, "Senior" ] }
141+
142+
---
143+
"ages as keywords":
144+
- do:
145+
catch: /ENRICH range and input types are incompatible. range\[INTEGER\], input\[KEYWORD\]/
146+
esql.query:
147+
body:
148+
query: 'FROM employees | ENRICH ages-policy ON ak | STATS count=COUNT(*) BY description | SORT count DESC, description ASC'
149+
150+
---
151+
"Invalid age as keyword":
152+
- requires:
153+
cluster_features: [ "gte_v8.14.0" ]
154+
reason: "IP range ENRICH support was added in 8.14.0"
155+
156+
- do:
157+
catch: /ENRICH range and input types are incompatible. range\[INTEGER\], input\[KEYWORD\]/
158+
esql.query:
159+
body:
160+
query: 'FROM employees | ENRICH ages-policy ON name | STATS count=COUNT(*) BY description | SORT count DESC, description ASC'
161+
162+
---
163+
"Invalid age as double":
164+
- requires:
165+
cluster_features: [ "gte_v8.14.0" ]
166+
reason: "IP range ENRICH support was added in 8.14.0"
167+
168+
- do:
169+
catch: /ENRICH range and input types are incompatible. range\[INTEGER\], input\[DOUBLE\]/
170+
esql.query:
171+
body:
172+
query: 'FROM employees | ENRICH ages-policy ON salary | STATS count=COUNT(*) BY description | SORT count DESC, description ASC'

0 commit comments

Comments
 (0)