Skip to content

Commit 4cc21b6

Browse files
authored
ESQL: Fix NULL handling in IN clause (#125832)
This PR fixes #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`.
1 parent 489a388 commit 4cc21b6

File tree

5 files changed

+42
-1
lines changed

5 files changed

+42
-1
lines changed

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,16 @@ FROM employees
203203

204204
fullname:keyword | job_positions:keyword | salary:integer | salary_change:double
205205
;
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
@@ -954,6 +954,12 @@ public enum Cap {
954954
*/
955955
FIX_REPLACE_MISSING_FIELD_WITH_NULL_DUPLICATE_NAME_ID_IN_LAYOUT,
956956

957+
/**
958+
* Support for filter in converted null.
959+
* See <a href="https://github.com/elastic/elasticsearch/issues/125832"> ESQL: Fix `NULL` handling in `IN` clause #125832 </a>
960+
*/
961+
FILTER_IN_CONVERTED_NULL,
962+
957963
/**
958964
* When creating constant null blocks in {@link org.elasticsearch.compute.lucene.ValuesSourceReaderOperator}, we also handed off
959965
* 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/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;
25+
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
2226
import org.junit.AfterClass;
2327

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

@@ -81,6 +87,16 @@ private static Literal L(Object value) {
8187
return of(EMPTY, value);
8288
}
8389

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

0 commit comments

Comments
 (0)