diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Expressions.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Expressions.java
index 8baffbf887e47..4e4338aad3704 100644
--- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Expressions.java
+++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Expressions.java
@@ -132,8 +132,16 @@ public static String name(Expression e) {
return e instanceof NamedExpression ne ? ne.name() : e.sourceText();
}
- public static boolean isNull(Expression e) {
- return e.dataType() == DataType.NULL || (e.foldable() && e.fold() == null);
+ /**
+ * Is this {@linkplain Expression} guaranteed to have
+ * only the {@code null} value. {@linkplain Expression}s that
+ * {@link Expression#fold()} to {@code null} may
+ * return {@code false} here, but should eventually be folded
+ * into a {@link Literal} containing {@code null} which will return
+ * {@code true} from here.
+ */
+ public static boolean isGuaranteedNull(Expression e) {
+ return e.dataType() == DataType.NULL || (e instanceof Literal lit && lit.value() == null);
}
public static List names(Collection extends Expression> e) {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/In.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/In.java
index eda6aadccc86a..f6c23304c189b 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/In.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/In.java
@@ -151,14 +151,14 @@ public Expression replaceChildren(List newChildren) {
public boolean foldable() {
// QL's In fold()s to null, if value() is null, but isn't foldable() unless all children are
// TODO: update this null check in QL too?
- return Expressions.isNull(value)
+ return Expressions.isGuaranteedNull(value)
|| Expressions.foldable(children())
- || (Expressions.foldable(list) && list.stream().allMatch(Expressions::isNull));
+ || (Expressions.foldable(list) && list.stream().allMatch(Expressions::isGuaranteedNull));
}
@Override
public Object fold() {
- if (Expressions.isNull(value) || list.stream().allMatch(Expressions::isNull)) {
+ if (Expressions.isGuaranteedNull(value) || list.stream().allMatch(Expressions::isGuaranteedNull)) {
return null;
}
return super.fold();
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java
index 638fa1b8db456..4f97bf60bd863 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java
@@ -30,7 +30,7 @@ public Expression rule(Expression e) {
// perform this early to prevent the rule from converting the null filter into nullifying the whole expression
// P.S. this could be done inside the Aggregate but this place better centralizes the logic
if (e instanceof AggregateFunction agg) {
- if (Expressions.isNull(agg.filter())) {
+ if (Expressions.isGuaranteedNull(agg.filter())) {
return agg.withFilter(Literal.of(agg.filter(), false));
}
}
@@ -38,13 +38,13 @@ public Expression rule(Expression e) {
if (result != e) {
return result;
} else if (e instanceof In in) {
- if (Expressions.isNull(in.value())) {
+ if (Expressions.isGuaranteedNull(in.value())) {
return Literal.of(in, null);
}
} else if (e instanceof Alias == false
&& e.nullable() == Nullability.TRUE
&& e instanceof Categorize == false
- && Expressions.anyMatch(e.children(), Expressions::isNull)) {
+ && Expressions.anyMatch(e.children(), Expressions::isGuaranteedNull)) {
return Literal.of(e, null);
}
return e;
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PruneFilters.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PruneFilters.java
index b6f7ac9e464f4..00698d009ea23 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PruneFilters.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PruneFilters.java
@@ -29,7 +29,7 @@ protected LogicalPlan rule(Filter filter) {
if (TRUE.equals(condition)) {
return filter.child();
}
- if (FALSE.equals(condition) || Expressions.isNull(condition)) {
+ if (FALSE.equals(condition) || Expressions.isGuaranteedNull(condition)) {
return PruneEmptyPlans.skipPlan(filter);
}
}
@@ -42,8 +42,8 @@ protected LogicalPlan rule(Filter filter) {
private static Expression foldBinaryLogic(BinaryLogic binaryLogic) {
if (binaryLogic instanceof Or or) {
- boolean nullLeft = Expressions.isNull(or.left());
- boolean nullRight = Expressions.isNull(or.right());
+ boolean nullLeft = Expressions.isGuaranteedNull(or.left());
+ boolean nullRight = Expressions.isGuaranteedNull(or.right());
if (nullLeft && nullRight) {
return new Literal(binaryLogic.source(), null, DataType.NULL);
}
@@ -55,7 +55,7 @@ private static Expression foldBinaryLogic(BinaryLogic binaryLogic) {
}
}
if (binaryLogic instanceof And and) {
- if (Expressions.isNull(and.left()) || Expressions.isNull(and.right())) {
+ if (Expressions.isGuaranteedNull(and.left()) || Expressions.isGuaranteedNull(and.right())) {
return new Literal(binaryLogic.source(), null, DataType.NULL);
}
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SplitInWithFoldableValue.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SplitInWithFoldableValue.java
index 930b485dbd374..9e9ae6a9a559d 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SplitInWithFoldableValue.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SplitInWithFoldableValue.java
@@ -30,7 +30,7 @@ public Expression rule(In in) {
List foldables = new ArrayList<>(in.list().size());
List nonFoldables = new ArrayList<>(in.list().size());
in.list().forEach(e -> {
- if (e.foldable() && Expressions.isNull(e) == false) { // keep `null`s, needed for the 3VL
+ if (e.foldable() && Expressions.isGuaranteedNull(e) == false) { // keep `null`s, needed for the 3VL
foldables.add(e);
} else {
nonFoldables.add(e);
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java
index 4c2ad531f3f1c..737bb2eb23a6f 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java
@@ -4820,7 +4820,7 @@ private static boolean oneLeaveIsNull(Expression e) {
e.forEachUp(node -> {
if (node.children().size() == 0) {
- result.set(result.get() || Expressions.isNull(node));
+ result.set(result.get() || Expressions.isGuaranteedNull(node));
}
});