Skip to content

Commit aaf7bac

Browse files
committed
Added strict range type checks for ENRICH
1 parent e2c29f5 commit aaf7bac

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-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
@@ -273,6 +273,11 @@ public enum Cap {
273273
*/
274274
RANGEQUERY_FOR_DATETIME,
275275

276+
/**
277+
* Enforce strict type checking on ENRICH range types.
278+
*/
279+
ENRICH_STRICT_RANGE_TYPES,
280+
276281
/**
277282
* Fix for non-unique attribute names in ROW and logical plans.
278283
* https://github.com/elastic/elasticsearch/issues/110541

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import org.elasticsearch.compute.data.Page;
1919
import org.elasticsearch.compute.operator.lookup.QueryList;
2020
import org.elasticsearch.index.mapper.MappedFieldType;
21+
import org.elasticsearch.index.mapper.RangeFieldMapper;
22+
import org.elasticsearch.index.mapper.RangeType;
2123
import org.elasticsearch.index.query.SearchExecutionContext;
2224
import org.elasticsearch.index.shard.ShardId;
2325
import org.elasticsearch.search.SearchService;
@@ -85,6 +87,30 @@ protected QueryList queryList(TransportRequest request, SearchExecutionContext c
8587
};
8688
}
8789

90+
private static void validateTypes(DataType inputDataType, MappedFieldType fieldType) {
91+
if (inputDataType == DataType.UNSUPPORTED) {
92+
throw new EsqlIllegalArgumentException("ENRICH cannot match on unsupported input data type");
93+
}
94+
if (fieldType == null) {
95+
throw new EsqlIllegalArgumentException("ENRICH cannot match on non-existent field");
96+
}
97+
if (fieldType instanceof RangeFieldMapper.RangeFieldType rangeType) {
98+
if (rangeTypesCompatible(rangeType.rangeType(), inputDataType) == false) {
99+
throw new EsqlIllegalArgumentException(
100+
"ENRICH range and input types are incompatible: range[" + rangeType.rangeType() + "], input[" + inputDataType + "]"
101+
);
102+
}
103+
}
104+
}
105+
106+
private static boolean rangeTypesCompatible(RangeType rangeType, DataType inputDataType) {
107+
return switch (rangeType) {
108+
case INTEGER, LONG -> inputDataType.isWholeNumber();
109+
case IP -> inputDataType == DataType.IP;
110+
default -> rangeType.isNumeric() == inputDataType.isNumeric();
111+
};
112+
}
113+
88114
public static class Request extends AbstractLookupService.Request {
89115
private final String matchType;
90116
private final String matchField;
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)