diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
index aca746905d47..d8f97d50527b 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
@@ -590,6 +590,12 @@ private CoreRules() {}
public static final JoinDeriveIsNotNullFilterRule JOIN_DERIVE_IS_NOT_NULL_FILTER_RULE =
JoinDeriveIsNotNullFilterRule.Config.DEFAULT.toRule();
+ /** Rule that derives more equivalent predicates from inner {@link Join} and creates
+ * {@link Filter}s with those predicates. See {@link JoinDeriveEquivalenceFilterRule}
+ * for details.*/
+ public static final JoinDeriveEquivalenceFilterRule JOIN_DERIVE_EQUIVALENCE_FILTER_RULE =
+ JoinDeriveEquivalenceFilterRule.Config.DEFAULT.toRule();
+
/** Rule that reduces constants inside a {@link Join}.
*
* @see #FILTER_REDUCE_EXPRESSIONS
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinDeriveEquivalenceFilterRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinDeriveEquivalenceFilterRule.java
new file mode 100644
index 000000000000..0ce70a58d6a0
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinDeriveEquivalenceFilterRule.java
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.rel.rules;
+
+import org.apache.calcite.plan.RelOptPredicateList;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexSimplify;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+import org.immutables.value.Value;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Planner rule that derives more equivalent predicates from inner
+ * {@link Join} and creates {@link Filter} with those predicates.
+ * Then {@link FilterJoinRule} will try to push these new predicates down.
+ * (So if you enable this rule, please make sure to enable {@link FilterJoinRule} also).
+ *
The basic idea is that, for example, in the query
+ *
SELECT * FROM ta INNER JOIN tb ON ta.x = tb.y WHERE ta.x > 10
+ * we can infer condition tb.y > 10 and push it down to the table tb.
+ * In this way, maybe we can reduce the amount of data involved in the {@link Join}.
+ * For example, the query plan
+ *
{@code
+ * LogicalJoin(condition=[=($1, $5)], joinType=[inner])
+ * LogicalTableScan(table=[[hr, emps]])
+ * LogicalFilter(condition=[>($0, 10)])
+ * LogicalTableScan(table=[[hr, depts]])
+ * }
+ * will convert to
+ *
{@code
+ * LogicalJoin(condition=[=($1, $5)], joinType=[inner])
+ * LogicalFilter(condition=[>($1, 20)])
+ * LogicalTableScan(table=[[hr, emps]])
+ * LogicalFilter(condition=[>($0, 20)])
+ * LogicalTableScan(table=[[hr, depts]])
+ * }
+ * the query plan
+ *
{@code
+ * LogicalJoin(condition=[=($1, $5)], joinType=[inner])
+ * LogicalFilter(condition=[SEARCH($1, Sarg[(10..30)])])
+ * LogicalTableScan(table=[[hr, emps]])
+ * LogicalFilter(condition=[SEARCH($0, Sarg[(20..40)])])
+ * LogicalTableScan(table=[[hr, depts]])
+ * }
+ * will convert to
+ *
{@code
+ * LogicalJoin(condition=[=($1, $5)], joinType=[inner])
+ * LogicalFilter(condition=[SEARCH($1, Sarg[(20..30)])])
+ * LogicalTableScan(table=[[hr, emps]])
+ * LogicalFilter(condition=[SEARCH($0, Sarg[(20..30)])])
+ * LogicalTableScan(table=[[hr, depts]])
+ * }
+ * Currently, the rule has some limitations:
+ *
1. only handle partial predicates (comparison), but this can be extended to
+ * support more predicates such as 'LIKE', 'RLIKE' and 'SIMILAR' in the future.
+ *
2. only support simple condition inference, such as: {$1 = $2, $2 = 10} => {$1 = 10},
+ * can not handle complex condition inference, such as conditions with functions, like
+ * {a = b, b = abs(c), c = 1} => {a = abs(1)}
+ *
3. only support discomposed literal, for example
+ * it can infer {$1 = $2, $1 = 10} => {$2 = 10}
+ * it can not infer {$1 = $2, $1 = 10 + 10} => {$2 = 10 + 10}
+ */
+
+@Value.Enclosing
+public class JoinDeriveEquivalenceFilterRule
+ extends RelRule implements TransformationRule {
+
+ public JoinDeriveEquivalenceFilterRule(Config config) {
+ super(config);
+ }
+
+ @Override public void onMatch(RelOptRuleCall call) {
+ final Filter filter = call.rel(0);
+ final Join join = call.rel(1);
+
+ final RexBuilder rexBuilder = join.getCluster().getRexBuilder();
+ final RexSimplify simplify =
+ new RexSimplify(rexBuilder, RelOptPredicateList.EMPTY, RexUtil.EXECUTOR);
+
+ final RexNode originalCondition =
+ prepare(rexBuilder, filter.getCondition(), join.getCondition());
+
+ final RexNode newCondition =
+ deriveEquivalenceCondition(simplify, rexBuilder, originalCondition);
+
+ if (arePredicatesEquivalent(rexBuilder, simplify, originalCondition, newCondition)) {
+ // if originalCondition and newCondition are equivalent, it means that the current
+ // Filter has either been derived or there is no room for derivation. if so,
+ // then we can stop.
+ return;
+ }
+
+ final Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+
+ call.transformTo(newFilter);
+
+ // after derivation, the original filter can be pruned
+ call.getPlanner().prune(filter);
+ }
+
+ /**
+ * normalized expressions are easier to compare. so here try to normalize conditions.
+ */
+ private RexNode prepare(final RexBuilder rexBuilder,
+ final RexNode filterCondition, final RexNode joinCondition) {
+ final RexNode condition =
+ RexUtil.composeConjunction(rexBuilder,
+ ImmutableList.of(filterCondition, joinCondition));
+
+ // 1. reorder operands to make sure that
+ // a. literal/constant is always in right, such as: 10 > $1 -> $1 < 10
+ // b. input ref with smaller index is in left, such as: $1 = $0 -> $0 = $1
+ // 2. expand search to comparison predicates, such as:
+ // SEARCH($1, Sarg[(10..30)]) -> $1 > 10 AND $1 < 30
+ // DO NOT simply expression now
+ return RexUtil.canonizeNode(rexBuilder, condition);
+ }
+
+ /**
+ * determine whether two predicate expressions are equivalent.
+ */
+ private boolean arePredicatesEquivalent(final RexBuilder rexBuilder,
+ final RexSimplify simplify, final RexNode left, final RexNode right) {
+ // simplify expression first to avoid redundancy
+ final RexNode simplifiedLeftPredicate = simplify.simplify(left);
+ final RexNode simplifiedRightPredicate = simplify.simplify(right);
+
+ // reorder operands and expand Search
+ final RexNode canonizedLeftPredicate =
+ RexUtil.canonizeNode(rexBuilder, simplifiedLeftPredicate);
+ final RexNode canonizedRightPredicate =
+ RexUtil.canonizeNode(rexBuilder, simplifiedRightPredicate);
+
+ // split into conjunctions to avoid (A AND B) not equals (B AND A)
+ final List leftPredicates = RelOptUtil.conjunctions(canonizedLeftPredicate);
+ final List rightPredicates = RelOptUtil.conjunctions(canonizedRightPredicate);
+
+ if (leftPredicates.size() != rightPredicates.size()) {
+ return false;
+ }
+ return Sets.newHashSet(leftPredicates).containsAll(rightPredicates);
+ }
+
+ /**
+ * derive more conditions based on inputRef-inputRef equality and inputRef-value equality.
+ */
+ private RexNode deriveEquivalenceCondition(final RexSimplify simplify,
+ final RexBuilder rexBuilder, final RexNode originalCondition) {
+ // map for inputRef to corresponding predicate such as: $1 -> [$1 > 10, $1 < 20, $1 = $2]
+ final Multimap predicateMultimap
+ = LinkedHashMultimap.create();
+ // map for inputRef to corresponding equivalent values or inputRefs such as: $1 -> [$2, 1]
+ final Multimap equivalenceMultimap
+ = LinkedHashMultimap.create();
+
+ // 1. construct predicate map and equivalence map
+ final List originalConjunctions = RelOptUtil.conjunctions(originalCondition);
+ for (RexNode rexNode : originalConjunctions) {
+ if (rexNode instanceof RexCall) {
+ // only handle partial predicates, will try to handle more predicates such as
+ // 'LIKE', 'RLIKE' or 'SIMILAR' later
+ if (!rexNode.isA(SqlKind.COMPARISON) && !rexNode.isA(SqlKind.OR)) {
+ continue;
+ }
+
+ final RexNode operand0 = ((RexCall) rexNode).getOperands().get(0);
+ final RexNode operand1 = ((RexCall) rexNode).getOperands().get(1);
+ final List leftInputRefs = RexUtil.gatherRexInputReferences(operand0);
+ final List rightInputRefs = RexUtil.gatherRexInputReferences(operand1);
+
+ // only handle inputRef-inputRef predicate like $1 = $2
+ // or inputRef-literal predicate like $1 > 10
+ if (rexNode.isA(SqlKind.COMPARISON)
+ && (leftInputRefs.size() != 1 || rightInputRefs.size() > 1)) {
+ continue;
+ }
+ // only handle single-inputRef disjunctions like {$0 = 10 or $0 = 20}
+ // can't handle multi-inputRef disjunctions like {$0 = 10 or $1 = 20} now
+ if (rexNode.isA(SqlKind.OR)
+ && RexUtil.gatherRexInputReferences(rexNode).size() > 1) {
+ continue;
+ }
+
+ // record equivalence relation
+ if (rexNode.isA(SqlKind.EQUALS)
+ && RexUtil.isInputReference(operand0, /* allowCast= */true)
+ && operand1.isA(ImmutableList.of(SqlKind.INPUT_REF, SqlKind.LITERAL))) {
+ equivalenceMultimap.put(leftInputRefs.get(0), operand1);
+ if (operand1.isA(SqlKind.INPUT_REF)) {
+ equivalenceMultimap.put(rightInputRefs.get(0), leftInputRefs.get(0));
+ }
+ }
+
+ // record predicate
+ predicateMultimap.put(leftInputRefs.get(0), rexNode);
+ }
+ }
+
+ // 2. search map and rewrite predicates with equivalent inputRefs or literals
+ //
+ // first, find all inputRefs that are equivalent to the current inputRef, and then
+ // rewrite all predicates involving equivalent inputRefs using inputRef, such as:
+ // if we have inputRef $1 = equivInputRef $2, then we can rewrite {$2 = 10} to {$1 = 10}
+ //
+ // then, find all predicates involving current inputRef. If any predicate refers
+ // to another inputRef, rewrite the predicate with the literal/constant equivalent
+ // to that inputRef, such as: if we have inputRef {$1 > $2} and {$2 = 10} then we
+ // can infer new condition {$1 > 10}
+ //
+ // finally, derive new predicates based on equivalence relation in equivalenceMultimap
+ //
+ // all derived predicates need to be canonized before recorded in predicateMultimap
+
+ final Set allInputRefs =
+ Sets.union(equivalenceMultimap.keySet(), predicateMultimap.keySet());
+
+ // derive new equivalence condition
+ for (RexInputRef inputRef : allInputRefs) {
+ for (RexInputRef equiv : getEquivalentInputRefs(inputRef, equivalenceMultimap)) {
+ equivalenceMultimap.putAll(inputRef, equivalenceMultimap.get(equiv));
+ }
+ }
+
+ // rewrite predicate with new inputRef
+ for (RexInputRef inputRef : allInputRefs) {
+ for (RexInputRef equiv : getEquivalentInputRefs(inputRef, equivalenceMultimap)) {
+ for (RexNode predicate : predicateMultimap.get(equiv)) {
+ RexNode newPredicate =
+ rewriteWithNewInputRef(rexBuilder, predicate, equiv, inputRef);
+ newPredicate = RexUtil.canonizeNode(rexBuilder, newPredicate);
+ predicateMultimap.put(inputRef, newPredicate);
+ }
+ }
+ }
+
+ // rewrite predicate with new value
+ for (RexInputRef inputRef : allInputRefs) {
+ for (RexNode predicate : ImmutableList.copyOf(predicateMultimap.get(inputRef))) {
+ final List inputRefs = RexUtil.gatherRexInputReferences(predicate);
+ inputRefs.remove(inputRef);
+ if (inputRefs.isEmpty()) {
+ continue;
+ }
+ final RexInputRef relatedInputRef = inputRefs.get(0);
+ for (RexLiteral literal : getEquivalentLiterals(relatedInputRef,
+ equivalenceMultimap)) {
+ RexNode newPredicate =
+ rewriteWithNewValue(rexBuilder, predicate, relatedInputRef, literal);
+ newPredicate = RexUtil.canonizeNode(rexBuilder, newPredicate);
+ predicateMultimap.put(inputRef, newPredicate);
+ }
+ }
+ }
+
+ // derive new equivalence predicates
+ for (RexInputRef inputRef : allInputRefs) {
+ for (RexNode rexNode : equivalenceMultimap.get(inputRef)) {
+ RexNode newPredicate =
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, inputRef, rexNode);
+ newPredicate = RexUtil.canonizeNode(rexBuilder, newPredicate);
+ predicateMultimap.put(inputRef, newPredicate);
+ }
+ }
+
+ // 3. compose all original predicates and derived predicates with AND.
+ //
+ // currently some predicates can not be handled, so we need to compose with
+ // original conditions with AND to avoid missing any conditions
+ final Set predicates = Sets.newHashSet(originalConjunctions);
+ predicates.addAll(predicateMultimap.values());
+ final RexNode composeConjunction = RexUtil.composeConjunction(rexBuilder, predicates);
+
+ // 4. simplify expression such as range merging, like {$1 > 10, $1 > 20} => {$1 > 20}
+ return simplify.simplify(composeConjunction);
+ }
+
+ private Set getEquivalentInputRefs(final RexInputRef inputRef,
+ final Multimap equivalenceMultimap) {
+ return equivalenceMultimap.get(inputRef).stream()
+ .filter(rexNode -> rexNode.isA(SqlKind.INPUT_REF))
+ .map(rexNode -> (RexInputRef) rexNode)
+ .collect(Collectors.toSet());
+ }
+
+ private Set getEquivalentLiterals(final RexInputRef inputRef,
+ final Multimap equivalenceMultimap) {
+ return equivalenceMultimap.get(inputRef).stream()
+ .filter(rexNode -> rexNode.isA(SqlKind.LITERAL))
+ .map(rexNode -> (RexLiteral) rexNode)
+ .collect(Collectors.toSet());
+ }
+
+
+ /**
+ * rewrite expression with the equivalent inputRef such as:
+ * based on {$1 = $2}, rewrite {$1 = 10} to {$2 = 10}.
+ * This operation will modify the original expression, so always use a copy.
+ */
+ private RexNode rewriteWithNewInputRef(final RexBuilder rexBuilder, final RexNode rexNode,
+ final RexInputRef originalInputRef, final RexInputRef newInputRef) {
+ return rexBuilder.copy(rexNode).accept(new RexShuttle() {
+ @Override public RexNode visitInputRef(RexInputRef inputRef) {
+ if (originalInputRef.equals(inputRef)) {
+ return newInputRef;
+ }
+ return super.visitInputRef(inputRef);
+ }
+ });
+ }
+
+ /**
+ * rewrite expression with the equivalent value such as:
+ * based on {$1 = 10}, rewrite {$1 > $2} to> {$2 < 10}.
+ * This operation will modify the original expression, so always use a copy.
+ */
+ private RexNode rewriteWithNewValue(final RexBuilder rexBuilder, final RexNode rexNode,
+ final RexInputRef originalInputRef, final RexLiteral newValue) {
+ return rexBuilder.copy(rexNode).accept(new RexShuttle() {
+ @Override public RexNode visitInputRef(RexInputRef inputRef) {
+ if (originalInputRef.equals(inputRef)) {
+ return newValue;
+ }
+ return super.visitInputRef(inputRef);
+ }
+ });
+ }
+
+ /**
+ * Rule configuration.
+ */
+ @Value.Immutable public interface Config extends RelRule.Config {
+ ImmutableJoinDeriveEquivalenceFilterRule.Config DEFAULT =
+ ImmutableJoinDeriveEquivalenceFilterRule.Config
+ .of().withOperandSupplier(
+ b0 -> b0.operand(LogicalFilter.class)
+ .oneInput(b1 -> b1.operand(LogicalJoin.class)
+ .predicate(join -> join.getJoinType() == JoinRelType.INNER).anyInputs()));
+
+ @Override default JoinDeriveEquivalenceFilterRule toRule() {
+ return new JoinDeriveEquivalenceFilterRule(this);
+ }
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/rex/RexNormalize.java b/core/src/main/java/org/apache/calcite/rex/RexNormalize.java
index 95ffc9fb63b9..7266bc524e88 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexNormalize.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexNormalize.java
@@ -145,7 +145,7 @@ public static int hashCode(
* @return non-negative (>=0) if {@code operand0} should be in the front,
* negative if {@code operand1} should be in the front
*/
- private static int reorderOperands(RexNode operand0, RexNode operand1) {
+ public static int reorderOperands(RexNode operand0, RexNode operand1) {
// Reorder the operands based on the SqlKind enumeration sequence,
// smaller is in the behind, e.g. the literal is behind of input ref and AND, OR.
int x = operand0.getKind().compareTo(operand1.getKind());
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 e18043609185..a1aa227b0522 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -20,6 +20,7 @@
import org.apache.calcite.linq4j.function.Predicate1;
import org.apache.calcite.plan.RelOptPredicateList;
import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.SubstitutionVisitor;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelFieldCollation;
@@ -66,8 +67,10 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.NavigableMap;
import java.util.Objects;
import java.util.Set;
+import java.util.TreeMap;
import java.util.function.Predicate;
import static com.google.common.collect.ImmutableList.toImmutableList;
@@ -297,6 +300,26 @@ public static boolean isReferenceOrAccess(RexNode node, boolean allowCast) {
return false;
}
+ /**
+ * Returns whether a node represents an input reference.
+ *
+ * @param node The node, never null.
+ * @param allowCast whether to regard CAST(x) as true
+ * @return Whether the node is a reference
+ */
+ public static boolean isInputReference(RexNode node, boolean allowCast) {
+ if (node.isA(SqlKind.INPUT_REF)) {
+ return true;
+ }
+ if (allowCast) {
+ if (node.isA(SqlKind.CAST)) {
+ RexCall call = (RexCall) node;
+ return isInputReference(call.operands.get(0), false);
+ }
+ }
+ return false;
+ }
+
/** Returns whether an expression is a cast just for the purposes of
* nullability, not changing any other aspect of the type. */
public static boolean isNullabilityCast(RelDataTypeFactory typeFactory,
@@ -2372,6 +2395,23 @@ public static Set gatherTableReferences(final List nodes)
return occurrences;
}
+ /**
+ * Gather all input references in input expression.
+ *
+ * @param rexNode expression
+ * @return list of input references, no duplicated element
+ */
+ public static List gatherRexInputReferences(final RexNode rexNode) {
+ final Set rexInputRefs = new HashSet<>();
+ new RexFinder() {
+ @Override public Void visitInputRef(RexInputRef inputRef) {
+ rexInputRefs.add(inputRef);
+ return super.visitInputRef(inputRef);
+ }
+ }.visitEach(ImmutableList.of(rexNode));
+ return Lists.newArrayList(rexInputRefs);
+ }
+
/**
* Given some expressions, gets the indices of the non-constant ones.
*/
@@ -3191,4 +3231,75 @@ private static class SearchExpandingShuttle extends RexShuttle {
}
}
}
+
+ /**
+ * Reorders some of the operands in this expression so structural comparison.
+ *
+ * This is mostly the same as {@link SubstitutionVisitor#canonizeNode}, except
+ * that comparator is based on the {@link SqlKind}'s ordinal rather than string
+ * representation lexicographic order, which allows constant to always be on the
+ * right side of the expression. See {@link RexNormalize#reorderOperands} for details.
+ */
+ public static RexNode canonizeNode(RexBuilder rexBuilder, RexNode expression) {
+ switch (expression.getKind()) {
+ case AND:
+ case OR: {
+ RexCall call = (RexCall) expression;
+ NavigableMap newOperands = new TreeMap<>();
+ for (RexNode operand : call.operands) {
+ operand = canonizeNode(rexBuilder, operand);
+ newOperands.put(operand.toString(), operand);
+ }
+ if (newOperands.size() < 2) {
+ return newOperands.values().iterator().next();
+ }
+ return rexBuilder.makeCall(call.getOperator(),
+ ImmutableList.copyOf(newOperands.values()));
+ }
+ case EQUALS:
+ case NOT_EQUALS:
+ case LESS_THAN:
+ case GREATER_THAN:
+ case LESS_THAN_OR_EQUAL:
+ case GREATER_THAN_OR_EQUAL: {
+ RexCall call = (RexCall) expression;
+ RexNode left = canonizeNode(rexBuilder, call.getOperands().get(0));
+ RexNode right = canonizeNode(rexBuilder, call.getOperands().get(1));
+ call = (RexCall) rexBuilder.makeCall(call.getOperator(), left, right);
+
+ if (RexNormalize.reorderOperands(left, right) >= 0) {
+ return call;
+ }
+
+ final RexNode result = RexUtil.invert(rexBuilder, call);
+ if (result == null) {
+ throw new NullPointerException("RexUtil.invert returned null for " + call);
+ }
+ return result;
+ }
+ case SEARCH: {
+ final RexNode e = RexUtil.expandSearch(rexBuilder, null, expression);
+ return canonizeNode(rexBuilder, e);
+ }
+ case PLUS:
+ case TIMES: {
+ RexCall call = (RexCall) expression;
+ RexNode left = canonizeNode(rexBuilder, call.getOperands().get(0));
+ RexNode right = canonizeNode(rexBuilder, call.getOperands().get(1));
+
+ if (RexNormalize.reorderOperands(left, right) >= 0) {
+ return rexBuilder.makeCall(call.getOperator(), left, right);
+ }
+
+ RexNode newCall = rexBuilder.makeCall(call.getOperator(), right, left);
+ // new call should not be used if its inferred type is not same as old
+ if (!newCall.getType().equals(call.getType())) {
+ return call;
+ }
+ return newCall;
+ }
+ default:
+ return expression;
+ }
+ }
}
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 7e58a5b77513..ba278f18626f 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -8869,4 +8869,62 @@ private void checkJoinAssociateRuleWithTopAlwaysTrueCondition(boolean allowAlway
.check();
}
+ /** Test case for
+ * [CALCITE-6363]
+ * Introduce a rule to derive more filters from inner join condition.
+ */
+ @Test void testJoinDeriveEquivalenceFilterRule1() {
+ final String sql = "SELECT *\n"
+ + "FROM sales.dept d INNER JOIN sales.emp e\n"
+ + "ON d.deptno = e.deptno\n"
+ + "WHERE e.deptno < 20 AND d.deptno > 10";
+
+ sql(sql)
+ .withRule(
+ CoreRules.JOIN_DERIVE_EQUIVALENCE_FILTER_RULE,
+ CoreRules.FILTER_INTO_JOIN)
+ .check();
+ }
+
+ @Test void testJoinDeriveEquivalenceFilterRule2() {
+ final String sql = "SELECT *\n"
+ + "FROM sales.dept d INNER JOIN sales.emp e\n"
+ + "ON d.deptno > e.deptno\n"
+ + "WHERE e.deptno = 20";
+
+ sql(sql)
+ .withRule(
+ CoreRules.JOIN_DERIVE_EQUIVALENCE_FILTER_RULE,
+ CoreRules.FILTER_INTO_JOIN)
+ .check();
+ }
+
+ @Test void testJoinDeriveEquivalenceFilterRule3() {
+ // test range merging
+ final String sql = "SELECT *\n"
+ + "FROM sales.dept d INNER JOIN sales.emp e\n"
+ + "ON d.deptno = e.deptno\n"
+ + "WHERE e.deptno < 40 AND e.deptno > 20 AND d.deptno > 10 AND d.deptno < 30";
+
+ sql(sql)
+ .withRule(
+ CoreRules.JOIN_DERIVE_EQUIVALENCE_FILTER_RULE,
+ CoreRules.FILTER_INTO_JOIN)
+ .check();
+ }
+
+ @Test void testJoinDeriveEquivalenceFilterRule4() {
+ // test 'in' predicates
+ final String sql = "SELECT *\n"
+ + "FROM sales.dept d INNER JOIN sales.emp e\n"
+ + "ON d.deptno = e.deptno\n"
+ + "WHERE e.deptno in (20, 30, 40)";
+
+ sql(sql)
+ .withRule(
+ CoreRules.JOIN_DERIVE_EQUIVALENCE_FILTER_RULE,
+ CoreRules.FILTER_INTO_JOIN)
+ .check();
+ }
+
}
diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 855a6d56895a..7ec8ff9f9884 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -5462,6 +5462,114 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$
LogicalFilter(condition=[=($7, $0)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+
+
+
+
+ 10]]>
+
+
+ ($0, 10))])
+ LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+
+
+
+
+
+
+
+ e.deptno
+WHERE e.deptno = 20]]>
+
+
+ ($0, $9)], joinType=[inner])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+
+
+ ($0, $9)], joinType=[inner])
+ LogicalFilter(condition=[>($0, 20)])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalFilter(condition=[=($7, 20)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+
+
+
+
+ 20 AND d.deptno > 10 AND d.deptno < 30]]>
+
+
+ ($9, 20), >($0, 10), <($0, 30))])
+ LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+