From ca2016d406ffd5017cb4e4699f9669cb4763bd88 Mon Sep 17 00:00:00 2001 From: xiedeyantu Date: Thu, 10 Apr 2025 22:45:08 +0800 Subject: [PATCH] [CALCITE-6948] Implement MinusToAntiJoinRule --- .../apache/calcite/rel/rules/CoreRules.java | 4 + .../rel/rules/MinusToAntiJoinRule.java | 108 ++++++++++++++++++ .../org/apache/calcite/test/JdbcTest.java | 28 +++++ .../apache/calcite/test/RelOptRulesTest.java | 10 ++ .../apache/calcite/test/RelOptRulesTest.xml | 31 +++++ 5 files changed, 181 insertions(+) create mode 100644 core/src/main/java/org/apache/calcite/rel/rules/MinusToAntiJoinRule.java 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 31b47aedf9ca..717e919b2dfd 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 @@ -375,6 +375,10 @@ private CoreRules() {} public static final MinusToDistinctRule MINUS_TO_DISTINCT = MinusToDistinctRule.Config.DEFAULT.toRule(); + /** Rule to translates a {@link Minus} to {@link Join} anti-join}. */ + public static final MinusToAntiJoinRule MINUS_TO_ANTI_JOIN_RULE = + MinusToAntiJoinRule.Config.DEFAULT.toRule(); + /** Rule that converts a {@link LogicalMatch} to the result of calling * {@link LogicalMatch#copy}. */ public static final MatchRule MATCH = MatchRule.Config.DEFAULT.toRule(); diff --git a/core/src/main/java/org/apache/calcite/rel/rules/MinusToAntiJoinRule.java b/core/src/main/java/org/apache/calcite/rel/rules/MinusToAntiJoinRule.java new file mode 100644 index 000000000000..050d8be6b96d --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rules/MinusToAntiJoinRule.java @@ -0,0 +1,108 @@ +/* + * 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.RelOptRuleCall; +import org.apache.calcite.plan.RelRule; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.JoinRelType; +import org.apache.calcite.rel.core.Minus; +import org.apache.calcite.rel.logical.LogicalMinus; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.tools.RelBuilder; + +import org.immutables.value.Value; + +import java.util.ArrayList; +import java.util.List; + +/** + * Planner rule that translates a {@link Minus} + * to a series of {@link org.apache.calcite.rel.core.Join} that type is + * {@link JoinRelType#ANTI}. + */ +@Value.Enclosing +public class MinusToAntiJoinRule + extends RelRule + implements TransformationRule { + + /** Creates an MinusToAntiJoinRule. */ + protected MinusToAntiJoinRule(Config config) { + super(config); + } + + //~ Methods ---------------------------------------------------------------- + + @Override public void onMatch(RelOptRuleCall call) { + final Minus minus = call.rel(0); + if (minus.all) { + return; // nothing we can do + } + + List inputs = minus.getInputs(); + if (inputs.size() != 2) { + return; + } + + final RelBuilder relBuilder = call.builder(); + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + RelNode left = inputs.get(0); + RelNode right = inputs.get(1); + + List conditions = new ArrayList<>(); + int fieldCount = left.getRowType().getFieldCount(); + + for (int i = 0; i < fieldCount; i++) { + RelDataType leftFieldType = left.getRowType().getFieldList().get(i).getType(); + RelDataType rightFieldType = right.getRowType().getFieldList().get(i).getType(); + + conditions.add( + relBuilder.isNotDistinctFrom( + rexBuilder.makeInputRef(leftFieldType, i), + rexBuilder.makeInputRef(rightFieldType, i + fieldCount))); + } + RexNode condition = RexUtil.composeConjunction(rexBuilder, conditions); + + relBuilder.push(left) + .push(right) + .join(JoinRelType.ANTI, condition) + .distinct(); + + call.transformTo(relBuilder.build()); + } + + /** Rule configuration. */ + @Value.Immutable + public interface Config extends RelRule.Config { + Config DEFAULT = ImmutableMinusToAntiJoinRule.Config.of() + .withOperandFor(LogicalMinus.class); + + @Override default MinusToAntiJoinRule toRule() { + return new MinusToAntiJoinRule(this); + } + + /** Defines an operand tree for the given classes. */ + default Config withOperandFor(Class minusClass) { + return withOperandSupplier(b -> b.operand(minusClass).anyInputs()) + .as(Config.class); + } + } +} diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java index 578deea95af1..f17a8f57fcf7 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -4015,6 +4015,34 @@ public void checkOrderBy(final boolean desc, "commission=null"); } + /** Test case for [CALCITE-6948] + * Implement IntersectToSemiJoinRule. */ + @Test void testMinusToAntiJoinRule() { + final String sql = "" + + "select \"commission\" from \"hr\".\"emps\"\n" + + "except\n" + + "select \"commission\" from \"hr\".\"emps\" where \"empid\">=150"; + + final String[] returns = new String[] { + "commission=1000", + "commission=250"}; + + CalciteAssert.hr() + .query(sql) + .returnsUnordered(returns); + + CalciteAssert.hr() + .query(sql) + .withHook(Hook.PLANNER, (Consumer) + p -> { + p.removeRule(CoreRules.MINUS_TO_DISTINCT); + p.removeRule(ENUMERABLE_MINUS_RULE); + p.addRule(CoreRules.MINUS_TO_ANTI_JOIN_RULE); + }) + .explainContains("joinType=[anti]") + .returnsUnordered(returns); + } + @Test void testExcept() { final String sql = "" + "select \"empid\", \"name\" from \"hr\".\"emps\" where \"deptno\"=10\n" 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 887f93c9a2e8..4d56ea2b3323 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -3612,6 +3612,16 @@ private void checkPushJoinThroughUnionOnRightDoesNotMatchSemiOrAntiJoin(JoinRelT .check(); } + /** Test case for [CALCITE-6948] + * Implement MinusToAntiJoinRule. */ + @Test void testMinusToAntiJoinRule() { + final String sql = "select ename from emp where deptno = 10\n" + + "except\n" + + "select ename from emp where deptno = 20\n"; + sql(sql).withRule(CoreRules.MINUS_TO_ANTI_JOIN_RULE) + .check(); + } + /** Tests {@link CoreRules#MINUS_MERGE}, which merges 2 * {@link Minus} operators into a single {@code Minus} with 3 * inputs. */ 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 c1f7b3c1a453..5b2f91af91b0 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -8703,6 +8703,37 @@ LogicalProject(NAME=[$0], DEPTNO=[$1]) LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) LogicalProject(NAME=[$1], DEPTNO=[$0]) LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) +]]> + + + + + + + + + + +