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 ee75345a7e7..e16ef277559 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 @@ -17,6 +17,7 @@ package org.apache.calcite.rel.rules; import org.apache.calcite.linq4j.function.Experimental; +import org.apache.calcite.plan.RelOptUtil.Exists; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.Aggregate; import org.apache.calcite.rel.core.Calc; @@ -354,6 +355,11 @@ private CoreRules() {} public static final IntersectToDistinctRule INTERSECT_TO_DISTINCT = IntersectToDistinctRule.Config.DEFAULT.toRule(); + /** Rule that translates a {@link Intersect} + * into a {@link Exists} subquery. */ + public static final IntersectToExistsRule INTERSECT_TO_EXISTS = + IntersectToExistsRule.Config.DEFAULT.toRule(); + /** Rule that translates a distinct * {@link Minus} into a group of operators * composed of {@link Union}, {@link Aggregate}, etc. */ diff --git a/core/src/main/java/org/apache/calcite/rel/rules/IntersectToExistsRule.java b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToExistsRule.java new file mode 100644 index 00000000000..c7f85fdee0f --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToExistsRule.java @@ -0,0 +1,158 @@ +/* + * 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.RelOptUtil.Exists; +import org.apache.calcite.plan.RelRule; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.CorrelationId; +import org.apache.calcite.rel.core.Intersect; +import org.apache.calcite.rel.logical.LogicalIntersect; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexSubQuery; +import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.tools.RelBuilder; +import org.apache.calcite.tools.RelBuilderFactory; +import org.apache.calcite.util.ImmutableBitSet; + +import com.google.common.collect.ImmutableSet; + +import org.immutables.value.Value; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Planner rule that translates a {@link Intersect} + * (all = false) + * into a {@link Exists}. + * + * @see CoreRules#INTERSECT_TO_EXISTS + */ +@Value.Enclosing +public class IntersectToExistsRule + extends RelRule + implements TransformationRule { + + /** Creates an IntersectToExistRule. */ + protected IntersectToExistsRule(Config config) { + super(config); + } + + @Deprecated // to be removed before 2.0 + public IntersectToExistsRule(Class intersectClass, + RelBuilderFactory relBuilderFactory) { + this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory) + .as(Config.class) + .withOperandFor(intersectClass)); + } + + //~ Methods ---------------------------------------------------------------- + + @Override public void onMatch(RelOptRuleCall call) { + final Intersect intersect = call.rel(0); + if (intersect.all) { + return; // nothing we can do + } + + final RelBuilder builder = call.builder(); + final RexBuilder rexBuilder = builder.getRexBuilder(); + + RelDataType rowType = intersect.getRowType(); + List inputs = intersect.getInputs(); + RelNode current = inputs.get(0); + + // get all column indices of intersect + ImmutableBitSet fieldIndices = ImmutableBitSet.of(rowType.getFieldList() + .stream().map(RelDataTypeField::getIndex) + .collect(Collectors.toList())); + + // iterate over the inputs and apply exists subquery + for (int i = 1; i < inputs.size(); i++) { + RelNode nextInput = inputs.get(i).stripped(); + + // create correlation + CorrelationId correlationId = intersect.getCluster().createCorrel(); + RexNode correl = + rexBuilder.makeCorrel(rowType, correlationId); + + // create condition in exists filter, and use correlation + List conditions = new ArrayList<>(); + for (int fieldIndex : fieldIndices) { + RexNode outerField = rexBuilder.makeInputRef(rowType, fieldIndex); + RexNode innerField = rexBuilder.makeFieldAccess(correl, fieldIndex); + conditions.add(builder.isNotDistinctFrom(innerField, outerField)); + } + RexNode condition = RexUtil.composeConjunction(rexBuilder, conditions); + + // build exists subquery + RelNode existsSubQuery = builder.push(nextInput) + .filter(condition) + .project(builder.fields(fieldIndices)) + .build(); + + // apply exists subquery to the current relation + current = builder.push(current) + .filter(ImmutableSet.of(correlationId), + RexSubQuery.exists(existsSubQuery)) + .build(); + } + + builder.push(current); + List projects = new ArrayList<>(); + for (int fieldIndex : fieldIndices) { + RexNode rexNode = builder.fields().get(fieldIndex); + RelDataType originalType = + rowType.getFieldList().get(projects.size()).getType(); + RexNode expr; + if (!originalType.equals(rexNode.getType())) { + expr = rexBuilder.makeCast(originalType, rexNode, true, false); + } else { + expr = rexNode; + } + projects.add(expr); + } + + RelNode result = builder.project(projects) + .distinct() + .build(); + + call.transformTo(result); + } + + /** Rule configuration. */ + @Value.Immutable + public interface Config extends RelRule.Config { + Config DEFAULT = ImmutableIntersectToExistsRule.Config.of() + .withOperandFor(LogicalIntersect.class); + + @Override default IntersectToExistsRule toRule() { + return new IntersectToExistsRule(this); + } + + /** Defines an operand tree for the given classes. */ + default Config withOperandFor(Class intersectClass) { + return withOperandSupplier(b -> b.operand(intersectClass).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 b53c71599d7..3032548b645 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -18,6 +18,7 @@ import org.apache.calcite.DataContexts; import org.apache.calcite.adapter.clone.CloneSchema; +import org.apache.calcite.adapter.enumerable.EnumerableRules; import org.apache.calcite.adapter.generate.RangeTable; import org.apache.calcite.adapter.java.AbstractQueryableTable; import org.apache.calcite.adapter.java.JavaTypeFactory; @@ -3925,6 +3926,30 @@ public void checkOrderBy(final boolean desc, .returnsUnordered("empid=150; name=Sebastian"); } + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExist() { + final String sql = "" + + "select \"empid\", \"name\" from \"hr\".\"emps\" where \"deptno\"=10\n" + + "intersect\n" + + "select \"empid\", \"name\" from \"hr\".\"emps\" where \"empid\">=150"; + CalciteAssert.hr() + .query(sql) + .withHook(Hook.PLANNER, (Consumer) + p -> { + p.removeRule(CoreRules.INTERSECT_TO_DISTINCT); + p.removeRule(EnumerableRules.ENUMERABLE_INTERSECT_RULE); + p.removeRule(EnumerableRules.ENUMERABLE_FILTER_RULE); + p.addRule(CoreRules.INTERSECT_TO_EXISTS); + p.addRule(CoreRules.FILTER_SUB_QUERY_TO_CORRELATE); + p.addRule(CoreRules.FILTER_TO_CALC); + }) + .explainContains("") + .returnsUnordered("empid=150; name=Sebastian"); + } + @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 e9a72270811..21fc13be13c 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -9986,4 +9986,66 @@ private void checkLoptOptimizeJoinRule(LoptOptimizeJoinRule rule) { .withRule(CoreRules.HYPER_GRAPH_OPTIMIZE, CoreRules.PROJECT_REMOVE, CoreRules.PROJECT_MERGE) .check(); } + + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExistsRuleOneField() { + String sql = "SELECT a.ename FROM emp AS a\n" + + "INTERSECT\n" + + "SELECT b.name FROM dept AS b"; + sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS) + .check(); + } + + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExistsRulePrimaryKey() { + String sql = "SELECT a.empno FROM emp AS a\n" + + "INTERSECT\n" + + "SELECT b.empno FROM emp AS b"; + sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS) + .check(); + } + + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExistsRuleMultiFields() { + String sql = "SELECT a.ename, a.job FROM emp AS a\n" + + "INTERSECT\n" + + "SELECT b.ename, b.job FROM emp AS b"; + sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS) + .check(); + } + + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExistsRuleMultiIntersect() { + String sql = "SELECT a.ename FROM emp AS a\n" + + "INTERSECT\n" + + "SELECT b.name FROM dept AS b\n" + + "INTERSECT\n" + + "SELECT c.ename FROM emp AS c"; + sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS) + .check(); + } + + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExistsRuleWithAll() { + String sql = "SELECT a.ename FROM emp AS a\n" + + "INTERSECT ALL\n" + + "SELECT b.name FROM dept AS b"; + sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS) + .checkUnchanged(); + } } 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 be8a8aeaadd..9c726fc5ae4 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -6068,6 +6068,143 @@ LogicalIntersect(all=[true]) LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8]) LogicalFilter(condition=[=($7, 30)]) LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +