Skip to content

Commit 8c1f5ab

Browse files
authored
ES|QL: Skip CASE function from InferIsNotNull rule checks (#113123) (#113252)
* Skip CASE function from InferIsNotNull rule checks (cherry picked from commit 2cccf13)
1 parent 8f29e40 commit 8c1f5ab

File tree

5 files changed

+108
-41
lines changed

5 files changed

+108
-41
lines changed

docs/changelog/113123.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 113123
2+
summary: "ES|QL: Skip CASE function from `InferIsNotNull` rule checks"
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 112704

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,23 @@ warning:Line 1:38: java.lang.IllegalArgumentException: single-value function enc
185185

186186
a:integer | b:integer | same:boolean
187187
;
188+
189+
caseOnTheValue_NotOnTheField
190+
required_capability: fixed_wrong_is_not_null_check_on_case
191+
192+
FROM employees
193+
| WHERE emp_no < 10022 AND emp_no > 10012
194+
| KEEP languages, emp_no
195+
| EVAL eval = CASE(languages == 1, null, languages == 2, "bilingual", languages > 2, "multilingual", languages IS NULL, "languages is null")
196+
| SORT languages, emp_no
197+
| WHERE eval IS NOT NULL;
198+
199+
languages:integer| emp_no:integer|eval:keyword
200+
2 |10016 |bilingual
201+
2 |10017 |bilingual
202+
2 |10018 |bilingual
203+
5 |10014 |multilingual
204+
5 |10015 |multilingual
205+
null |10020 |languages is null
206+
null |10021 |languages is null
207+
;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,13 @@ public enum Cap {
171171
/**
172172
* Changed error messages for fields with conflicting types in different indices.
173173
*/
174-
SHORT_ERROR_MESSAGES_FOR_UNSUPPORTED_FIELDS;
174+
SHORT_ERROR_MESSAGES_FOR_UNSUPPORTED_FIELDS,
175+
176+
/**
177+
* Don't optimize CASE IS NOT NULL function by not requiring the fields to be not null as well.
178+
* https://github.com/elastic/elasticsearch/issues/112704
179+
*/
180+
FIXED_WRONG_IS_NOT_NULL_CHECK_ON_CASE;
175181

176182
private final boolean snapshotOnly;
177183

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.elasticsearch.xpack.esql.core.util.CollectionUtils;
3434
import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction;
3535
import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
36+
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
3637
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
3738
import org.elasticsearch.xpack.esql.optimizer.rules.PropagateEmptyRelation;
3839
import org.elasticsearch.xpack.esql.plan.logical.Aggregate;
@@ -181,8 +182,7 @@ else if (plan instanceof Project project) {
181182
}
182183

183184
/**
184-
* Simplify IsNotNull targets by resolving the underlying expression to its root fields with unknown
185-
* nullability.
185+
* Simplify IsNotNull targets by resolving the underlying expression to its root fields.
186186
* e.g.
187187
* (x + 1) / 2 IS NOT NULL --> x IS NOT NULL AND (x+1) / 2 IS NOT NULL
188188
* SUBSTRING(x, 3) > 4 IS NOT NULL --> x IS NOT NULL AND SUBSTRING(x, 3) > 4 IS NOT NULL
@@ -242,7 +242,7 @@ protected Set<Expression> resolveExpressionAsRootAttributes(Expression exp, Attr
242242

243243
private boolean doResolve(Expression exp, AttributeMap<Expression> aliases, Set<Expression> resolvedExpressions) {
244244
boolean changed = false;
245-
// check if the expression can be skipped or is not nullabe
245+
// check if the expression can be skipped
246246
if (skipExpression(exp)) {
247247
resolvedExpressions.add(exp);
248248
} else {
@@ -263,7 +263,14 @@ private boolean doResolve(Expression exp, AttributeMap<Expression> aliases, Set<
263263
}
264264

265265
private static boolean skipExpression(Expression e) {
266-
return e instanceof Coalesce;
266+
// These two functions can have a complex set of expressions as arguments that can mess up the simplification we are trying
267+
// to add. If there is a "case(f is null, null, ...) is not null" expression,
268+
// assuming that "case(f is null.....) is not null AND f is not null" (what this rule is doing) is a wrong assumption because
269+
// the "case" function will want both null "f" and not null "f". Doing it like this contradicts the condition inside case, so we
270+
// must avoid these cases.
271+
// We could be smarter and look inside "case" and "coalesce" to see if there is any comparison of fields with "null" but,
272+
// the complexity is too high to warrant an attempt _now_.
273+
return e instanceof Coalesce || e instanceof Case;
267274
}
268275
}
269276

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.elasticsearch.xpack.esql.core.type.DataType;
3434
import org.elasticsearch.xpack.esql.core.type.EsField;
3535
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
36+
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
3637
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
3738
import org.elasticsearch.xpack.esql.expression.function.scalar.string.StartsWith;
3839
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
@@ -56,8 +57,11 @@
5657

5758
import static java.util.Collections.emptyMap;
5859
import static org.elasticsearch.xpack.esql.EsqlTestUtils.L;
60+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.ONE;
5961
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_SEARCH_STATS;
6062
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_VERIFIER;
63+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.THREE;
64+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TWO;
6165
import static org.elasticsearch.xpack.esql.EsqlTestUtils.as;
6266
import static org.elasticsearch.xpack.esql.EsqlTestUtils.getFieldAttribute;
6367
import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping;
@@ -79,10 +83,6 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase {
7983
private static LogicalPlanOptimizer logicalOptimizer;
8084
private static Map<String, EsField> mapping;
8185

82-
private static final Literal ONE = L(1);
83-
private static final Literal TWO = L(2);
84-
private static final Literal THREE = L(3);
85-
8686
@BeforeClass
8787
public static void init() {
8888
parser = new EsqlParser();
@@ -365,38 +365,6 @@ public void testMissingFieldInFilterNoProjection() {
365365
);
366366
}
367367

368-
public void testIsNotNullOnCoalesce() {
369-
var plan = localPlan("""
370-
from test
371-
| where coalesce(emp_no, salary) is not null
372-
""");
373-
374-
var limit = as(plan, Limit.class);
375-
var filter = as(limit.child(), Filter.class);
376-
var inn = as(filter.condition(), IsNotNull.class);
377-
var coalesce = as(inn.children().get(0), Coalesce.class);
378-
assertThat(Expressions.names(coalesce.children()), contains("emp_no", "salary"));
379-
var source = as(filter.child(), EsRelation.class);
380-
}
381-
382-
public void testIsNotNullOnExpression() {
383-
var plan = localPlan("""
384-
from test
385-
| eval x = emp_no + 1
386-
| where x is not null
387-
""");
388-
389-
var limit = as(plan, Limit.class);
390-
var filter = as(limit.child(), Filter.class);
391-
var inn = as(filter.condition(), IsNotNull.class);
392-
assertThat(Expressions.names(inn.children()), contains("x"));
393-
var eval = as(filter.child(), Eval.class);
394-
filter = as(eval.child(), Filter.class);
395-
inn = as(filter.condition(), IsNotNull.class);
396-
assertThat(Expressions.names(inn.children()), contains("emp_no"));
397-
var source = as(filter.child(), EsRelation.class);
398-
}
399-
400368
public void testSparseDocument() throws Exception {
401369
var query = """
402370
from large
@@ -495,6 +463,66 @@ public void testIsNotNullOnFunctionWithTwoFields() {
495463
assertEquals(expected, new LocalLogicalPlanOptimizer.InferIsNotNull().apply(f));
496464
}
497465

466+
public void testIsNotNullOnCoalesce() {
467+
var plan = localPlan("""
468+
from test
469+
| where coalesce(emp_no, salary) is not null
470+
""");
471+
472+
var limit = as(plan, Limit.class);
473+
var filter = as(limit.child(), Filter.class);
474+
var inn = as(filter.condition(), IsNotNull.class);
475+
var coalesce = as(inn.children().get(0), Coalesce.class);
476+
assertThat(Expressions.names(coalesce.children()), contains("emp_no", "salary"));
477+
var source = as(filter.child(), EsRelation.class);
478+
}
479+
480+
public void testIsNotNullOnExpression() {
481+
var plan = localPlan("""
482+
from test
483+
| eval x = emp_no + 1
484+
| where x is not null
485+
""");
486+
487+
var limit = as(plan, Limit.class);
488+
var filter = as(limit.child(), Filter.class);
489+
var inn = as(filter.condition(), IsNotNull.class);
490+
assertThat(Expressions.names(inn.children()), contains("x"));
491+
var eval = as(filter.child(), Eval.class);
492+
filter = as(eval.child(), Filter.class);
493+
inn = as(filter.condition(), IsNotNull.class);
494+
assertThat(Expressions.names(inn.children()), contains("emp_no"));
495+
var source = as(filter.child(), EsRelation.class);
496+
}
497+
498+
public void testIsNotNullOnCase() {
499+
var plan = localPlan("""
500+
from test
501+
| where case(emp_no > 10000, "1", salary < 50000, "2", first_name) is not null
502+
""");
503+
504+
var limit = as(plan, Limit.class);
505+
var filter = as(limit.child(), Filter.class);
506+
var inn = as(filter.condition(), IsNotNull.class);
507+
var caseF = as(inn.children().get(0), Case.class);
508+
assertThat(Expressions.names(caseF.children()), contains("emp_no > 10000", "\"1\"", "salary < 50000", "\"2\"", "first_name"));
509+
var source = as(filter.child(), EsRelation.class);
510+
}
511+
512+
public void testIsNotNullOnCase_With_IS_NULL() {
513+
var plan = localPlan("""
514+
from test
515+
| where case(emp_no IS NULL, "1", salary IS NOT NULL, "2", first_name) is not null
516+
""");
517+
518+
var limit = as(plan, Limit.class);
519+
var filter = as(limit.child(), Filter.class);
520+
var inn = as(filter.condition(), IsNotNull.class);
521+
var caseF = as(inn.children().get(0), Case.class);
522+
assertThat(Expressions.names(caseF.children()), contains("emp_no IS NULL", "\"1\"", "salary IS NOT NULL", "\"2\"", "first_name"));
523+
var source = as(filter.child(), EsRelation.class);
524+
}
525+
498526
private IsNotNull isNotNull(Expression field) {
499527
return new IsNotNull(EMPTY, field);
500528
}

0 commit comments

Comments
 (0)