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]]) +]]> + + + + + + + + + + + + + +