Skip to content

Commit 1a110a5

Browse files
authored
ESQL: Do not fold in Range.foldable (elastic#119766) (elastic#119964)
The upper and lower bounds can be incompatible, in which case the range is foldable to FALSE independently of the foldability of the field. But we shouldn't try and fold the bounds to find out. Calling `foldable` really shouldn't already perform folding. Instead, only perform the check for the invalid bounds if the bounds are already literals. This means that folding will still take place, but it requires the range's child expressions to be folded first.
1 parent 2daacef commit 1a110a5

File tree

2 files changed

+67
-1
lines changed
  • x-pack/plugin
    • esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate
    • esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical

2 files changed

+67
-1
lines changed

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/predicate/Range.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.elasticsearch.common.io.stream.StreamOutput;
1010
import org.elasticsearch.xpack.esql.core.expression.Expression;
11+
import org.elasticsearch.xpack.esql.core.expression.Literal;
1112
import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction;
1213
import org.elasticsearch.xpack.esql.core.expression.predicate.operator.comparison.BinaryComparison;
1314
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
@@ -85,10 +86,22 @@ public ZoneId zoneId() {
8586
return zoneId;
8687
}
8788

89+
/**
90+
* In case that the range is empty due to foldable, invalid bounds, but the bounds themselves are not yet folded, the optimizer will
91+
* need two passes to fold this.
92+
* That's because we shouldn't perform folding when trying to determine foldability.
93+
*/
8894
@Override
8995
public boolean foldable() {
9096
if (lower.foldable() && upper.foldable()) {
91-
return areBoundariesInvalid() || value.foldable();
97+
if (value().foldable()) {
98+
return true;
99+
}
100+
101+
// We cannot fold the bounds here; but if they're already literals, we can check if the range is always empty.
102+
if (lower() instanceof Literal && upper() instanceof Literal) {
103+
return areBoundariesInvalid();
104+
}
92105
}
93106

94107
return false;

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.xpack.esql.core.expression.Literal;
1515
import org.elasticsearch.xpack.esql.core.expression.Nullability;
1616
import org.elasticsearch.xpack.esql.core.expression.predicate.BinaryOperator;
17+
import org.elasticsearch.xpack.esql.core.expression.predicate.Range;
1718
import org.elasticsearch.xpack.esql.core.expression.predicate.logical.And;
1819
import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Not;
1920
import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Or;
@@ -27,11 +28,16 @@
2728
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mod;
2829
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul;
2930
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Sub;
31+
import org.elasticsearch.xpack.esql.plan.logical.Filter;
32+
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
3033

3134
import static org.elasticsearch.xpack.esql.EsqlTestUtils.FIVE;
3235
import static org.elasticsearch.xpack.esql.EsqlTestUtils.THREE;
3336
import static org.elasticsearch.xpack.esql.EsqlTestUtils.TWO;
37+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.as;
38+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.emptySource;
3439
import static org.elasticsearch.xpack.esql.EsqlTestUtils.equalsOf;
40+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.fieldAttribute;
3541
import static org.elasticsearch.xpack.esql.EsqlTestUtils.greaterThanOf;
3642
import static org.elasticsearch.xpack.esql.EsqlTestUtils.greaterThanOrEqualOf;
3743
import static org.elasticsearch.xpack.esql.EsqlTestUtils.lessThanOf;
@@ -111,6 +117,53 @@ public void testArithmeticFolding() {
111117
assertEquals(1, foldOperator(new Mod(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE)));
112118
}
113119

120+
public void testFoldRange() {
121+
// 1 + 9 < value AND value < 20-1
122+
// with value = 12 and randomly replacing the `<` by `<=`
123+
Expression lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 9, DataType.INTEGER));
124+
Expression upperBound = new Sub(EMPTY, new Literal(EMPTY, 20, DataType.INTEGER), new Literal(EMPTY, 1, DataType.INTEGER));
125+
Expression value = new Literal(EMPTY, 12, DataType.INTEGER);
126+
Range range = new Range(EMPTY, value, lowerBound, randomBoolean(), upperBound, randomBoolean(), randomZone());
127+
128+
Expression folded = new ConstantFolding().rule(range);
129+
assertTrue((Boolean) as(folded, Literal.class).value());
130+
}
131+
132+
public void testFoldRangeWithInvalidBoundaries() {
133+
// 1 + 9 < value AND value <= 11 - 1
134+
// This is always false. We also randomly test versions with `<=`.
135+
Expression lowerBound;
136+
boolean includeLowerBound = randomBoolean();
137+
if (includeLowerBound) {
138+
// 1 + 10 <= value
139+
lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 10, DataType.INTEGER));
140+
} else {
141+
// 1 + 9 < value
142+
lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 9, DataType.INTEGER));
143+
}
144+
145+
boolean includeUpperBound = randomBoolean();
146+
// value < 11 - 1
147+
// or
148+
// value <= 11 - 1
149+
Expression upperBound = new Sub(EMPTY, new Literal(EMPTY, 11, DataType.INTEGER), new Literal(EMPTY, 1, DataType.INTEGER));
150+
151+
Expression value = fieldAttribute();
152+
153+
Range range = new Range(EMPTY, value, lowerBound, includeLowerBound, upperBound, includeUpperBound, randomZone());
154+
155+
// We need to test this as part of a logical plan, to correctly simulate how we traverse down the expression tree.
156+
// Just applying this to the range directly won't perform a transformDown.
157+
LogicalPlan filter = new Filter(EMPTY, emptySource(), range);
158+
159+
Filter foldedOnce = as(new ConstantFolding().apply(filter), Filter.class);
160+
// We need to run the rule twice, because during the first run only the boundaries can be folded - the range doesn't know it's
161+
// foldable, yet.
162+
Filter foldedTwice = as(new ConstantFolding().apply(foldedOnce), Filter.class);
163+
164+
assertFalse((Boolean) as(foldedTwice.condition(), Literal.class).value());
165+
}
166+
114167
private static Object foldOperator(BinaryOperator<?, ?, ?, ?> b) {
115168
return ((Literal) new ConstantFolding().rule(b)).value();
116169
}

0 commit comments

Comments
 (0)