Skip to content

Commit 3a625c9

Browse files
authored
[7.x] QL: Make canonical form take into account children (#71266) (#71287)
Currently the canonical form takes into account only the node itself and not its children. For commutative cases this creates subtle issues in that the children are swapped based on their canonical form but not canonicalize and thus semantic comparison fail. This PR fixes that by taking into account the canonical children and, for commutative expressions, applies semantic ordering. In the process, improve handling of nested negated expressions.
1 parent 6410638 commit 3a625c9

File tree

23 files changed

+420
-103
lines changed

23 files changed

+420
-103
lines changed

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/predicate/operator/comparison/InsensitiveBinaryComparison.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@ protected TypeResolution resolveInputType(Expression e, ParamOrdinal paramOrdina
5252
*/
5353
protected abstract String regularOperatorSymbol();
5454

55-
@Override
56-
protected Expression canonicalize() {
57-
return left().hashCode() > right().hashCode() ? swapLeftAndRight() : this;
58-
}
59-
6055
@Override
6156
public DataType dataType() {
6257
return DataTypes.BOOLEAN;

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/Attribute.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ public boolean semanticEquals(Expression other) {
116116
return other instanceof Attribute ? id().equals(((Attribute) other).id()) : false;
117117
}
118118

119+
@Override
120+
protected Expression canonicalize() {
121+
return clone(Source.EMPTY, name(), dataType(), qualifier, nullability, id(), synthetic());
122+
}
123+
119124
@Override
120125
public int hashCode() {
121126
return Objects.hash(super.hashCode(), qualifier, nullability);

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,15 @@ public final Expression canonical() {
114114
}
115115

116116
protected Expression canonicalize() {
117-
return this;
117+
if (children().isEmpty()) {
118+
return this;
119+
}
120+
List<Expression> canonicalChildren = Expressions.canonicalize(children());
121+
// check if replacement is really needed
122+
if (children().equals(canonicalChildren)) {
123+
return this;
124+
}
125+
return replaceChildrenSameSize(canonicalChildren);
118126
}
119127

120128
public boolean semanticEquals(Expression other) {

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/Expressions.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ public static Nullability nullable(List<? extends Expression> exps) {
118118
return value;
119119
}
120120

121+
public static List<Expression> canonicalize(List<? extends Expression> exps) {
122+
List<Expression> canonical = new ArrayList<>(exps.size());
123+
for (Expression exp : exps) {
124+
canonical.add(exp.canonical());
125+
}
126+
return canonical;
127+
}
128+
121129
public static boolean foldable(List<? extends Expression> exps) {
122130
for (Expression exp : exps) {
123131
if (exp.foldable() == false) {

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/LeafExpression.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@ public final Expression replaceChildren(List<Expression> newChildren) {
2626
public AttributeSet references() {
2727
return AttributeSet.EMPTY;
2828
}
29+
30+
@Override
31+
protected Expression canonicalize() {
32+
return this;
33+
}
2934
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ public Object fold() {
6868

6969
@Override
7070
public int hashCode() {
71-
return Objects.hash(value, dataType);
71+
return Objects.hash(dataType, value);
72+
}
73+
74+
@Override
75+
protected Expression canonicalize() {
76+
return this;
7277
}
7378

7479
@Override

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/BinaryOperator.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
import org.elasticsearch.xpack.ql.expression.Expressions.ParamOrdinal;
1212
import org.elasticsearch.xpack.ql.tree.Source;
1313

14+
import java.util.ArrayList;
15+
import java.util.Iterator;
16+
import java.util.List;
17+
1418
/**
1519
* Operator is a specialized binary predicate where both sides have the compatible types
1620
* (it's up to the analyzer to do any conversion if needed).
@@ -38,8 +42,56 @@ protected TypeResolution resolveType() {
3842
return resolveInputType(right(), ParamOrdinal.SECOND);
3943
}
4044

45+
protected boolean isCommutative() {
46+
return false;
47+
}
48+
4149
@Override
4250
protected Expression canonicalize() {
43-
return left().semanticHash() > right().semanticHash() ? swapLeftAndRight() : this;
51+
// fast check
52+
if (isCommutative() == false) {
53+
Expression exp = left().semanticHash() > right().semanticHash() ? swapLeftAndRight() : this;
54+
// swap is not guaranteed to return a different expression, in which case simply delegate to super to avoid a cycle
55+
return exp != this ? exp.canonical() : super.canonicalize();
56+
}
57+
// break down all connected commutative operators
58+
// in order to sort all their children at once
59+
// then reassemble/reduce back the expression
60+
List<Expression> commutativeChildren = new ArrayList<>(2);
61+
collectCommutative(commutativeChildren, this);
62+
// sort
63+
commutativeChildren.sort((l, r) -> Integer.compare(l.semanticHash(), r.semanticHash()));
64+
65+
// reduce all children using the current operator - this method creates a balanced tree
66+
while (commutativeChildren.size() > 1) {
67+
// combine (in place) expressions in pairs
68+
// NB: this loop modifies the list (just like an array)
69+
for (int i = 0; i < commutativeChildren.size() - 1; i++) {
70+
// reduce two children into one and moves to the next pair
71+
Expression current = commutativeChildren.get(i);
72+
Expression next = commutativeChildren.remove(i + 1);
73+
// do the update in place to minimize the amount of array modifications
74+
commutativeChildren.set(i, replaceChildren(current, next));
75+
76+
}
77+
}
78+
Iterator<Expression> iterator = commutativeChildren.iterator();
79+
Expression last = iterator.next();
80+
while (iterator.hasNext()) {
81+
last = replaceChildren(last, iterator.next());
82+
}
83+
return last;
84+
}
85+
86+
protected void collectCommutative(List<Expression> commutative, Expression expression) {
87+
// keep digging for same binary operator
88+
if (getClass() == expression.getClass()) {
89+
BinaryOperator<?, ?, ?, ?> bi = (BinaryOperator<?, ?, ?, ?>) expression;
90+
collectCommutative(commutative, bi.left());
91+
collectCommutative(commutative, bi.right());
92+
} else {
93+
// not same operation - no ordering possible
94+
commutative.add(expression.canonical());
95+
}
4496
}
4597
}

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/Negatable.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
*/
77
package org.elasticsearch.xpack.ql.expression.predicate;
88

9-
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
9+
import org.elasticsearch.xpack.ql.expression.Expression;
1010

11-
public interface Negatable<T extends ScalarFunction> {
11+
public interface Negatable<T extends Expression> {
1212

1313
T negate();
1414

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/Predicates.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ private static Expression combine(List<Expression> exps, BiFunction<Expression,
7676
// combine (in place) expressions in pairs
7777
// NB: this loop modifies the list (just like an array)
7878
for (int i = 0; i < result.size() - 1; i++) {
79-
Expression l = result.remove(i);
80-
Expression r = result.remove(i);
81-
result.add(i, combiner.apply(l, r));
79+
// keep the current element to update it in place
80+
Expression l = result.get(i);
81+
// remove the next element due to combining
82+
Expression r = result.remove(i + 1);
83+
result.set(i, combiner.apply(l, r));
8284
}
8385
}
8486

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/expression/predicate/logical/And.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public And swapLeftAndRight() {
3636

3737
@Override
3838
public Or negate() {
39-
return new Or(source(), new Not(source(), left()), new Not(source(), right()));
39+
return new Or(source(), Not.negate(left()), Not.negate(right()));
4040
}
4141

4242
@Override

0 commit comments

Comments
 (0)