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 b88e1459b2e..0062c1f455c 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 @@ -16,6 +16,7 @@ */ package org.apache.calcite.rel.rules; +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; @@ -348,6 +349,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..a1c2be1eb93 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToExistsRule.java @@ -0,0 +1,161 @@ +/* + * 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.sql.fun.SqlStdOperatorTable; +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( + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + outerField, innerField)); + } + 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/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 6d59536d231..d9ca61ef87b 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -37,6 +37,7 @@ import org.apache.calcite.rel.rules.AggregateProjectMergeRule; import org.apache.calcite.rel.rules.CoreRules; import org.apache.calcite.rel.rules.FilterJoinRule; +import org.apache.calcite.rel.rules.IntersectToExistsRule; import org.apache.calcite.rel.rules.ProjectOverSumToSum0Rule; import org.apache.calcite.rel.rules.ProjectToWindowRule; import org.apache.calcite.rel.rules.PruneEmptyRules; @@ -244,6 +245,33 @@ private static String toSql(RelNode root, SqlDialect dialect, sql(query).withMysql().ok(expected); } + /** + * Test case of + * [CALCITE-6836] + * Add Rule to convert INTERSECT to EXISTS. */ + @Test void testIntersectToExistsRule() { + String query = "SELECT \"product_name\"\n" + + "FROM \"foodmart\".\"product\"\n" + + "INTERSECT\n" + + "SELECT \"product_name\"\n" + + "FROM \"foodmart\".\"product\""; + String expected = "SELECT `product_name`\n" + + "FROM (SELECT `product_name`\n" + + "FROM `foodmart`.`product`) AS `t`\n" + + "WHERE EXISTS (SELECT *\n" + + "FROM (SELECT `product_name`\n" + + "FROM `foodmart`.`product`) AS `t0`\n" + + "WHERE `product_name` IS NOT DISTINCT FROM `t`.`product_name`)\n" + + "GROUP BY `product_name`"; + HepProgramBuilder builder = new HepProgramBuilder(); + builder.addRuleClass(IntersectToExistsRule.class); + HepPlanner hepPlanner = new HepPlanner(builder.build()); + RuleSet rules = + RuleSets.ofList(CoreRules.INTERSECT_TO_EXISTS); + + sql(query).withMysql().optimize(rules, hepPlanner).ok(expected); + } + @Test void testGroupByBooleanLiteral() { String query = "select avg(\"salary\") from \"employee\" group by true"; String expectedRedshift = "SELECT AVG(\"employee\".\"salary\")\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 cd15f88ac87..3da590970d4 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -9716,4 +9716,50 @@ private void checkJoinAssociateRuleWithTopAlwaysTrueCondition(boolean allowAlway fixture().withRelBuilderConfig(a -> a.withBloat(-1)) .relFn(relFn).withPlanner(planner).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 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 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 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 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 1cb6c346791..da6d4c300c1 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -5840,6 +5840,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]]) +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +