Skip to content

Commit 6abddd8

Browse files
authored
SQL: Optimizer rule for folding nullable expressions (#35080)
Add optimization for folding nullable expressions with a NULL argument. This is a variant of folding for the NULL case. Fix #34826
1 parent 512319c commit 6abddd8

File tree

9 files changed

+110
-7
lines changed

9 files changed

+110
-7
lines changed

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expression.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public Object fold() {
7878
throw new SqlIllegalArgumentException("Should not fold expression");
7979
}
8080

81+
// whether the expression becomes null if at least one param/input is null
8182
public abstract boolean nullable();
8283

8384
// the references/inputs/leaves of the expression tree

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ public static Literal of(String name, Expression foldable) {
161161
if (name == null) {
162162
name = foldable instanceof NamedExpression ? ((NamedExpression) foldable).name() : String.valueOf(fold);
163163
}
164-
165164
return new Literal(foldable.location(), name, fold, foldable.dataType());
166165
}
167-
}
166+
167+
public static Literal of(Expression source, Object value) {
168+
String name = source instanceof NamedExpression ? ((NamedExpression) source).name() : String.valueOf(value);
169+
return new Literal(source.location(), name, value, source.dataType());
170+
}
171+
}

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/Function.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public String name() {
4646

4747
@Override
4848
public boolean nullable() {
49-
return false;
49+
return Expressions.nullable(children());
5050
}
5151

5252
@Override

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ protected Pipe makePipe() {
4949
return new ConcatFunctionPipe(location(), this, Expressions.pipe(left()), Expressions.pipe(right()));
5050
}
5151

52+
@Override
53+
public boolean nullable() {
54+
return left().nullable() && right().nullable();
55+
}
56+
5257
@Override
5358
public boolean foldable() {
5459
return left().foldable() && right().foldable();
@@ -80,4 +85,4 @@ public ScriptTemplate scriptWithField(FieldAttribute field) {
8085
public DataType dataType() {
8186
return DataType.KEYWORD;
8287
}
83-
}
88+
}

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/predicate/logical/BinaryLogic.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ protected TypeResolution resolveInputType(Expression e, Expressions.ParamOrdinal
3333
protected Pipe makePipe() {
3434
return new BinaryLogicPipe(location(), this, Expressions.pipe(left()), Expressions.pipe(right()), function());
3535
}
36+
37+
@Override
38+
public boolean nullable() {
39+
return left().nullable() && right().nullable();
40+
}
3641
}

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import org.elasticsearch.xpack.sql.expression.predicate.BinaryOperator;
4040
import org.elasticsearch.xpack.sql.expression.predicate.BinaryOperator.Negateable;
4141
import org.elasticsearch.xpack.sql.expression.predicate.BinaryPredicate;
42+
import org.elasticsearch.xpack.sql.expression.predicate.In;
43+
import org.elasticsearch.xpack.sql.expression.predicate.IsNotNull;
4244
import org.elasticsearch.xpack.sql.expression.predicate.Predicates;
4345
import org.elasticsearch.xpack.sql.expression.predicate.Range;
4446
import org.elasticsearch.xpack.sql.expression.predicate.logical.And;
@@ -63,6 +65,7 @@
6365
import org.elasticsearch.xpack.sql.rule.RuleExecutor;
6466
import org.elasticsearch.xpack.sql.session.EmptyExecutable;
6567
import org.elasticsearch.xpack.sql.session.SingletonExecutable;
68+
import org.elasticsearch.xpack.sql.type.DataType;
6669
import org.elasticsearch.xpack.sql.util.CollectionUtils;
6770

6871
import java.util.ArrayList;
@@ -122,6 +125,7 @@ protected Iterable<RuleExecutor<LogicalPlan>.Batch> batches() {
122125
new CombineProjections(),
123126
// folding
124127
new ReplaceFoldableAttributes(),
128+
new FoldNull(),
125129
new ConstantFolding(),
126130
// boolean
127131
new BooleanSimplification(),
@@ -682,8 +686,7 @@ protected LogicalPlan rule(Filter filter) {
682686
if (TRUE.equals(filter.condition())) {
683687
return filter.child();
684688
}
685-
// TODO: add comparison with null as well
686-
if (FALSE.equals(filter.condition())) {
689+
if (FALSE.equals(filter.condition()) || FoldNull.isNull(filter.condition())) {
687690
return new LocalRelation(filter.location(), new EmptyExecutable(filter.output()));
688691
}
689692
}
@@ -1112,6 +1115,41 @@ private boolean canPropagateFoldable(LogicalPlan p) {
11121115
}
11131116
}
11141117

1118+
static class FoldNull extends OptimizerExpressionRule {
1119+
1120+
FoldNull() {
1121+
super(TransformDirection.UP);
1122+
}
1123+
1124+
private static boolean isNull(Expression ex) {
1125+
return DataType.NULL == ex.dataType() || (ex.foldable() && ex.fold() == null);
1126+
}
1127+
1128+
@Override
1129+
protected Expression rule(Expression e) {
1130+
if (e instanceof IsNotNull) {
1131+
if (((IsNotNull) e).field().nullable() == false) {
1132+
return new Literal(e.location(), Expressions.name(e), Boolean.TRUE, DataType.BOOLEAN);
1133+
}
1134+
}
1135+
// see https://github.com/elastic/elasticsearch/issues/34876
1136+
// similar for IsNull once it gets introduced
1137+
1138+
if (e instanceof In) {
1139+
In in = (In) e;
1140+
if (isNull(in.value())) {
1141+
return Literal.of(in, null);
1142+
}
1143+
}
1144+
1145+
if (e.nullable() && Expressions.anyMatch(e.children(), FoldNull::isNull)) {
1146+
return Literal.of(e, null);
1147+
}
1148+
1149+
return e;
1150+
}
1151+
}
1152+
11151153
static class ConstantFolding extends OptimizerExpressionRule {
11161154

11171155
ConstantFolding() {

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction;
2020
import org.elasticsearch.xpack.sql.expression.function.aggregate.Count;
2121
import org.elasticsearch.xpack.sql.expression.function.scalar.Cast;
22+
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayName;
2223
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfMonth;
2324
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfYear;
2425
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.MonthOfYear;
@@ -28,8 +29,11 @@
2829
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ASin;
2930
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ATan;
3031
import org.elasticsearch.xpack.sql.expression.function.scalar.math.Abs;
32+
import org.elasticsearch.xpack.sql.expression.function.scalar.math.Cos;
3133
import org.elasticsearch.xpack.sql.expression.function.scalar.math.E;
3234
import org.elasticsearch.xpack.sql.expression.function.scalar.math.Floor;
35+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Ascii;
36+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Repeat;
3337
import org.elasticsearch.xpack.sql.expression.predicate.BinaryOperator;
3438
import org.elasticsearch.xpack.sql.expression.predicate.In;
3539
import org.elasticsearch.xpack.sql.expression.predicate.IsNotNull;
@@ -56,6 +60,7 @@
5660
import org.elasticsearch.xpack.sql.optimizer.Optimizer.CombineBinaryComparisons;
5761
import org.elasticsearch.xpack.sql.optimizer.Optimizer.CombineProjections;
5862
import org.elasticsearch.xpack.sql.optimizer.Optimizer.ConstantFolding;
63+
import org.elasticsearch.xpack.sql.optimizer.Optimizer.FoldNull;
5964
import org.elasticsearch.xpack.sql.optimizer.Optimizer.PropagateEquals;
6065
import org.elasticsearch.xpack.sql.optimizer.Optimizer.PruneDuplicateFunctions;
6166
import org.elasticsearch.xpack.sql.optimizer.Optimizer.PruneSubqueryAliases;
@@ -374,10 +379,36 @@ private static Object foldOperator(BinaryOperator<?, ?, ?, ?> b) {
374379
return ((Literal) new ConstantFolding().rule(b)).value();
375380
}
376381

382+
public void testNullFoldingIsNotNull() {
383+
assertEquals(Literal.TRUE, new FoldNull().rule(new IsNotNull(EMPTY, Literal.TRUE)));
384+
}
385+
386+
public void testGenericNullableExpression() {
387+
FoldNull rule = new FoldNull();
388+
// date-time
389+
assertNullLiteral(rule.rule(new DayName(EMPTY, Literal.NULL, randomTimeZone())));
390+
// math function
391+
assertNullLiteral(rule.rule(new Cos(EMPTY, Literal.NULL)));
392+
// string function
393+
assertNullLiteral(rule.rule(new Ascii(EMPTY, Literal.NULL)));
394+
assertNullLiteral(rule.rule(new Repeat(EMPTY, getFieldAttribute(), Literal.NULL)));
395+
// arithmetic
396+
assertNullLiteral(rule.rule(new Add(EMPTY, getFieldAttribute(), Literal.NULL)));
397+
// comparison
398+
assertNullLiteral(rule.rule(new GreaterThan(EMPTY, getFieldAttribute(), Literal.NULL)));
399+
// regex
400+
assertNullLiteral(rule.rule(new RLike(EMPTY, getFieldAttribute(), Literal.NULL)));
401+
}
402+
377403
//
378404
// Logical simplifications
379405
//
380406

407+
private void assertNullLiteral(Expression expression) {
408+
assertEquals(Literal.class, expression.getClass());
409+
assertNull(((Literal) expression).fold());
410+
}
411+
381412
public void testBinaryComparisonSimplification() {
382413
assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new Equals(EMPTY, FIVE, FIVE)));
383414
assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new GreaterThanOrEqual(EMPTY, FIVE, FIVE)));

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.xpack.sql.expression.function.FunctionRegistry;
1313
import org.elasticsearch.xpack.sql.optimizer.Optimizer;
1414
import org.elasticsearch.xpack.sql.parser.SqlParser;
15+
import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec;
1516
import org.elasticsearch.xpack.sql.plan.physical.LocalExec;
1617
import org.elasticsearch.xpack.sql.plan.physical.PhysicalPlan;
1718
import org.elasticsearch.xpack.sql.session.EmptyExecutable;
@@ -64,6 +65,24 @@ public void testFoldingToLocalExecWithProject() {
6465
assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#"));
6566
}
6667

68+
public void testFoldingOfIsNotNull() {
69+
PhysicalPlan p = plan("SELECT keyword FROM test WHERE (keyword IS NULL) IS NOT NULL");
70+
assertEquals(EsQueryExec.class, p.getClass());
71+
EsQueryExec ee = (EsQueryExec) p;
72+
assertEquals(1, ee.output().size());
73+
assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#"));
74+
}
75+
76+
public void testFoldingToLocalExecWithNullFilter() {
77+
PhysicalPlan p = plan("SELECT keyword FROM test WHERE null IN (1, 2)");
78+
assertEquals(LocalExec.class, p.getClass());
79+
LocalExec le = (LocalExec) p;
80+
assertEquals(EmptyExecutable.class, le.executable().getClass());
81+
EmptyExecutable ee = (EmptyExecutable) le.executable();
82+
assertEquals(1, ee.output().size());
83+
assertThat(ee.output().get(0).toString(), startsWith("keyword{f}#"));
84+
}
85+
6786
public void testFoldingToLocalExecWithProject_FoldableIn() {
6887
PhysicalPlan p = plan("SELECT keyword FROM test WHERE int IN (null, null)");
6988
assertEquals(LocalExec.class, p.getClass());

x-pack/qa/sql/src/main/resources/nulls.csv-spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44

55
nullDate
6-
SELECT YEAR(CAST(NULL AS DATE)) d;
6+
SELECT YEAR(CAST(NULL AS DATE)) AS d;
77

88
d:i
99
null

0 commit comments

Comments
 (0)