Skip to content

Commit 655e3ce

Browse files
nik9000kanoshiou
andauthored
ESQL: Allow the data type of null in filters (elastic#127618)
* ESQL: Allow the data type of `null` in filters (elastic#118324) * Allow the data type of `null` in filters * ESQL: Fix `NULL` handling in `IN` clause (elastic#125832) This PR fixes elastic#119950 where an `IN` query includes `NULL` values with non-NULL `DataType` appearing within the query range. An expression is considered `NULL` when its `DataType` is `NULL` or it is a `Literal` with a value of `null`. * Revert formatting --------- Co-authored-by: kanoshiou <[email protected]>
1 parent 56e19e2 commit 655e3ce

File tree

7 files changed

+87
-1
lines changed

7 files changed

+87
-1
lines changed

docs/changelog/118324.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 118324
2+
summary: Allow the data type of `null` in filters
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 116351

docs/changelog/125832.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 125832
2+
summary: "ESQL: Fix `NULL` handling in `IN` clause"
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 119950

x-pack/plugin/esql/qa/testFixtures/src/main/resources/null.csv-spec

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,28 @@ emp_no:integer | languages:integer | height:double | x:double | y:double | z:dou
191191
10020 | null | 1.41 | 1.41 | 1.41 | 40031.0 | 40031
192192
10021 | null | 1.47 | 1.47 | 1.47 | 60408.0 | 60408
193193
;
194+
195+
whereNull
196+
FROM employees
197+
| WHERE NULL and emp_no <= 10021
198+
| SORT first_name, last_name
199+
| EVAL fullname = CONCAT(first_name, " ", last_name)
200+
| KEEP fullname, job_positions, salary, salary_change
201+
| limit 5
202+
;
203+
204+
fullname:keyword | job_positions:keyword | salary:integer | salary_change:double
205+
;
206+
207+
inConvertedNull
208+
required_capability: filter_in_converted_null
209+
FROM employees
210+
| WHERE emp_no in (10021, 10022, null::int)
211+
| KEEP emp_no, first_name, last_name
212+
| SORT emp_no
213+
;
214+
215+
emp_no:integer | first_name:keyword | last_name:keyword
216+
10021 | Ramzi | Erde
217+
10022 | Shahaf | Famili
218+
;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,12 @@ public enum Cap {
806806
*/
807807
AGGREGATE_METRIC_DOUBLE_SORTING(AGGREGATE_METRIC_DOUBLE_FEATURE_FLAG),
808808

809+
/**
810+
* Support for filter in converted null.
811+
* See <a href="https://github.com/elastic/elasticsearch/issues/125832"> ESQL: Fix `NULL` handling in `IN` clause #125832 </a>
812+
*/
813+
FILTER_IN_CONVERTED_NULL,
814+
809815
/**
810816
* When creating constant null blocks in {@link org.elasticsearch.compute.lucene.ValuesSourceReaderOperator}, we also handed off
811817
* the ownership of that block - but didn't account for the fact that the caller might close it, leading to double releases

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/In.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ private Query translate(TranslatorHandler handler) {
478478
List<Query> queries = new ArrayList<>();
479479

480480
for (Expression rhs : list()) {
481-
if (DataType.isNull(rhs.dataType()) == false) {
481+
if (Expressions.isGuaranteedNull(rhs) == false) {
482482
if (needsTypeSpecificValueHandling(attribute.dataType())) {
483483
// delegates to BinaryComparisons translator to ensure consistent handling of date and time values
484484
// TODO:

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,33 @@ public void testPeriodAndDurationInEval() {
962962

963963
public void testFilterNonBoolField() {
964964
assertEquals("1:19: Condition expression needs to be boolean, found [INTEGER]", error("from test | where emp_no"));
965+
966+
assertEquals(
967+
"1:19: Condition expression needs to be boolean, found [KEYWORD]",
968+
error("from test | where concat(first_name, \"foobar\")")
969+
);
970+
}
971+
972+
public void testFilterNullField() {
973+
// `where null` should return empty result set
974+
query("from test | where null");
975+
976+
// Value null of type `BOOLEAN`
977+
query("from test | where null::boolean");
978+
979+
// Provide `NULL` type in `EVAL`
980+
query("from t | EVAL x = null | where x");
981+
982+
// `to_string(null)` is of `KEYWORD` type null, resulting in `to_string(null) == "abc"` being of `BOOLEAN`
983+
query("from t | where to_string(null) == \"abc\"");
984+
985+
// Other DataTypes can contain null values
986+
assertEquals("1:19: Condition expression needs to be boolean, found [KEYWORD]", error("from test | where null::string"));
987+
assertEquals("1:19: Condition expression needs to be boolean, found [INTEGER]", error("from test | where null::integer"));
988+
assertEquals(
989+
"1:45: Condition expression needs to be boolean, found [DATETIME]",
990+
error("from test | EVAL x = null::datetime | where x")
991+
);
965992
}
966993

967994
public void testFilterDateConstant() {

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/InTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,25 @@
1313
import org.elasticsearch.geo.GeometryTestUtils;
1414
import org.elasticsearch.geo.ShapeTestUtils;
1515
import org.elasticsearch.xpack.esql.core.expression.Expression;
16+
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
1617
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
1718
import org.elasticsearch.xpack.esql.core.expression.Literal;
19+
import org.elasticsearch.xpack.esql.core.querydsl.query.TermsQuery;
1820
import org.elasticsearch.xpack.esql.core.tree.Source;
1921
import org.elasticsearch.xpack.esql.core.type.DataType;
22+
import org.elasticsearch.xpack.esql.core.type.EsField;
2023
import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
2124
import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
2225
import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLikeTests;
26+
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
2327
import org.junit.AfterClass;
2428

2529
import java.io.IOException;
2630
import java.util.ArrayList;
2731
import java.util.Arrays;
2832
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Set;
2935
import java.util.function.Supplier;
3036

3137
import static org.elasticsearch.xpack.esql.EsqlTestUtils.of;
@@ -79,6 +85,16 @@ private static Literal L(Object value) {
7985
return of(EMPTY, value);
8086
}
8187

88+
public void testConvertedNull() {
89+
In in = new In(
90+
EMPTY,
91+
new FieldAttribute(Source.EMPTY, "field", new EsField("suffix", DataType.KEYWORD, Map.of(), true)),
92+
Arrays.asList(ONE, new Literal(Source.EMPTY, null, randomFrom(DataType.types())), THREE)
93+
);
94+
var query = in.asQuery(TranslatorHandler.TRANSLATOR_HANDLER);
95+
assertEquals(new TermsQuery(EMPTY, "field", Set.of(1, 3)), query);
96+
}
97+
8298
@ParametersFactory
8399
public static Iterable<Object[]> parameters() {
84100
List<TestCaseSupplier> suppliers = new ArrayList<>();

0 commit comments

Comments
 (0)