diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java index 98fbbb781b9..a37284dfb78 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java +++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java @@ -657,6 +657,52 @@ private String simplifyLikeString(String content, char escape, char wildcard) { return simplifyMixedWildcards(builder.toString(), escape); } + /** Returns whether two nodes are the same comparison, considering the + * symmetry of comparison operators: "{@code a = b}" is equivalent to + * "{@code b = a}", and "{@code a < b}" is equivalent to + * "{@code b > a}". + * + *

This allows digest-based rewrites such as the absorption law + * ("{@code a AND (a OR b) => a}") to recognize comparison terms that + * differ only in the order of their operands. + * See [CALCITE-739] + * Extend RexUtil.pullFactors to recognize additional common factors. + */ + private static boolean equivalentComparison(RexNode a, RexNode b) { + if (a.equals(b)) { + return true; + } + if (!(a instanceof RexCall) || !(b instanceof RexCall)) { + return false; + } + final RexCall callA = (RexCall) a; + final RexCall callB = (RexCall) b; + final SqlKind kindA = callA.getKind(); + // Comparison operators are the only operators whose semantics is + // preserved when operands are reversed and the operator is reversed + // (for example, "a < b" becomes "b > a"). + if (!SqlKind.COMPARISON.contains(kindA) + || kindA.reverse() != callB.getKind() + || callA.getOperands().size() != 2 + || callB.getOperands().size() != 2) { + return false; + } + return callA.getOperands().get(0).equals(callB.getOperands().get(1)) + && callA.getOperands().get(1).equals(callB.getOperands().get(0)); + } + + /** Returns whether {@code nodes} contains a node that is the same + * comparison as {@code target}; see {@link #equivalentComparison}. */ + private static boolean containsEquivalentComparison(List nodes, + RexNode target) { + for (RexNode node : nodes) { + if (equivalentComparison(node, target)) { + return true; + } + } + return false; + } + // e must be a comparison (=, >, >=, <, <=, !=) private RexNode simplifyComparison(RexCall e, RexUnknownAs unknownAs) { //noinspection unchecked @@ -2461,8 +2507,8 @@ private static void absorb(List terms, SqlKind compositeKind) { ? RelOptUtil.disjunctions(term) : RelOptUtil.conjunctions(term); for (RexNode other : terms) { - if (other != term && components.contains(other) - && RexUtil.isDeterministic(other)) { + if (other != term && RexUtil.isDeterministic(other) + && containsEquivalentComparison(components, other)) { terms.remove(i); i--; break; diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java b/core/src/main/java/org/apache/calcite/rex/RexUtil.java index 99045f0a3cb..3220225b7f0 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java +++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java @@ -2967,7 +2967,7 @@ private RexNode pull(RexNode rex) { } return and(Iterables.concat(factors.values(), ImmutableList.of(or(list)))); default: - return rex; + return normalizeComparison(rex); } } @@ -2986,17 +2986,19 @@ private List pullList(List nodes) { return list; } - private static LinkedHashMap commonFactors(List nodes) { + private LinkedHashMap commonFactors(List nodes) { // make sure the result is in deterministic order final LinkedHashMap map = new LinkedHashMap<>(); int i = 0; for (RexNode node : nodes) { if (i++ == 0) { for (RexNode conjunction : RelOptUtil.conjunctions(node)) { - map.put(conjunction, conjunction); + RexNode normalized = normalizeComparison(conjunction); + map.put(normalized, normalized); } } else { - map.keySet().retainAll(RelOptUtil.conjunctions(node)); + map.keySet().retainAll( + Util.transform(RelOptUtil.conjunctions(node), this::normalizeComparison)); } } return map; @@ -3005,13 +3007,58 @@ private static LinkedHashMap commonFactors(List nodes private RexNode removeFactor(Map factors, RexNode node) { List list = new ArrayList<>(); for (RexNode operand : RelOptUtil.conjunctions(node)) { - if (!factors.containsKey(operand)) { + RexNode normalized = normalizeComparison(operand); + if (!factors.containsKey(normalized)) { list.add(operand); } } return and(list); } + /** + * Normalizes a comparison expression so that, when possible, an input ref + * appears on the left and a literal or higher-index input ref appears on + * the right. This exploits the symmetry of comparisons to help + * {@link #commonFactors} recognize equivalent terms. + * + *

Only comparisons that involve an input ref are normalized: if both + * operands are input refs, the lower-index one is placed on the left; + * if exactly one operand is an input ref, it is placed on the left. + * Comparisons whose operands are not input refs (for example, two + * CASTs) are returned unchanged. + */ + private RexNode normalizeComparison(RexNode rex) { + if (rex instanceof RexCall) { + RexCall call = (RexCall) rex; + switch (call.getKind()) { + case EQUALS: + case NOT_EQUALS: + case LESS_THAN: + case GREATER_THAN: + case LESS_THAN_OR_EQUAL: + case GREATER_THAN_OR_EQUAL: + final List operands = call.getOperands(); + final RexNode op0 = operands.get(0); + final RexNode op1 = operands.get(1); + final boolean op0IsInputRef = op0 instanceof RexInputRef; + final boolean op1IsInputRef = op1 instanceof RexInputRef; + if (op0IsInputRef && op1IsInputRef) { + final RexInputRef ref0 = (RexInputRef) op0; + final RexInputRef ref1 = (RexInputRef) op1; + if (ref0.getIndex() > ref1.getIndex()) { + return requireNonNull(invert(rexBuilder, call)); + } + } else if (!op0IsInputRef && op1IsInputRef) { + return requireNonNull(invert(rexBuilder, call)); + } + break; + default: + break; + } + } + return rex; + } + private RexNode and(Iterable nodes) { return composeConjunction(rexBuilder, nodes); } diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java index 0dbbcd9cdec..9e4e588b381 100644 --- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java +++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java @@ -889,6 +889,19 @@ private void checkExponentialCnf(int n) { + " 'MED PKG':CHAR(8)]:CHAR(8))), AND(=(?0.j, 'Brand#14'), " + ">=(?0.h, 20), <=(?0.h, 30), SEARCH(?0.k, Sarg['LG BOX':CHAR(7)," + " 'LG CASE', 'LG PACK', 'LG PKG':CHAR(7)]:CHAR(7)))))"); + + final RexNode ref3 = rexBuilder.makeInputRef(intType, 3); + final RexNode ref6 = rexBuilder.makeInputRef(intType, 6); + final RexNode bool0 = rexBuilder.makeInputRef(booleanType, 0); + final RexNode bool1 = rexBuilder.makeInputRef(booleanType, 1); + final RexNode seven = literal(7); + checkPullFactors(eq(ref6, ref3), "=($3, $6)"); + checkPullFactors(gt(ref6, ref3), "<($3, $6)"); + checkPullFactors(le(seven, ref3), ">=($3, 7)"); + checkPullFactors( + or(and(eq(ref6, ref3), bool0), + and(eq(ref3, ref6), bool1)), + "AND(=($3, $6), OR($0, $1))"); } @Test void testSimplify() { @@ -2342,6 +2355,28 @@ private void checkExponentialCnf(int n) { } } + /** Test case for + * [CALCITE-739] + * Extend RexUtil.pullFactors to recognize additional common factors. */ + @Test void testSimplifyComparisonSymmetry() { + final RexNode ref3 = rexBuilder.makeInputRef(tInt(), 3); + final RexNode ref6 = rexBuilder.makeInputRef(tInt(), 6); + final RexNode bool0 = rexBuilder.makeInputRef(tBool(), 0); + + // Contradiction detection: "$6 = $3 AND $3 != $6" is not satisfiable. + checkSimplifyFilter(and(eq(ref6, ref3), ne(ref3, ref6)), "false"); + + // Absorption law: "a AND (a OR b) => a" where "a" is written once as + // "$6 = $3" and once as "$3 = $6". The result keeps the first form. + checkSimplify(and(eq(ref6, ref3), or(eq(ref3, ref6), bool0)), + "=($6, $3)"); + + // Same, but with an order-sensitive operator: "a" is "$6 > $3", + // written inside the OR as "$3 < $6". + checkSimplify(and(gt(ref6, ref3), or(lt(ref3, ref6), bool0)), + ">($6, $3)"); + } + @Test void testSimplifyComparisonWithPredicates() { RelOptPredicateList relOptPredicateList = RelOptPredicateList.of(rexBuilder,