Skip to content

Commit ed98960

Browse files
committed
[CALCITE-6836] Add Rule to convert INTERSECT to EXISTS
1 parent 0a4f00d commit ed98960

File tree

4 files changed

+292
-0
lines changed

4 files changed

+292
-0
lines changed

core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.calcite.rel.rules;
1818

19+
import org.apache.calcite.plan.RelOptUtil.Exists;
1920
import org.apache.calcite.rel.RelNode;
2021
import org.apache.calcite.rel.core.Aggregate;
2122
import org.apache.calcite.rel.core.Calc;
@@ -348,6 +349,11 @@ private CoreRules() {}
348349
public static final IntersectToDistinctRule INTERSECT_TO_DISTINCT =
349350
IntersectToDistinctRule.Config.DEFAULT.toRule();
350351

352+
/** Rule that translates a {@link Intersect}
353+
* into a {@link Exists} subquery. */
354+
public static final IntersectToExistsRule INTERSECT_TO_EXISTS =
355+
IntersectToExistsRule.Config.DEFAULT.toRule();
356+
351357
/** Rule that translates a distinct
352358
* {@link Minus} into a group of operators
353359
* composed of {@link Union}, {@link Aggregate}, etc. */
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.calcite.rel.rules;
18+
19+
import org.apache.calcite.plan.RelOptRuleCall;
20+
import org.apache.calcite.plan.RelOptUtil.Exists;
21+
import org.apache.calcite.plan.RelRule;
22+
import org.apache.calcite.rel.RelNode;
23+
import org.apache.calcite.rel.core.CorrelationId;
24+
import org.apache.calcite.rel.core.Intersect;
25+
import org.apache.calcite.rel.logical.LogicalIntersect;
26+
import org.apache.calcite.rel.type.RelDataTypeField;
27+
import org.apache.calcite.rex.RexBuilder;
28+
import org.apache.calcite.rex.RexNode;
29+
import org.apache.calcite.rex.RexSubQuery;
30+
import org.apache.calcite.rex.RexUtil;
31+
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
32+
import org.apache.calcite.tools.RelBuilder;
33+
import org.apache.calcite.tools.RelBuilderFactory;
34+
35+
import com.google.common.collect.ImmutableSet;
36+
37+
import org.immutables.value.Value;
38+
39+
import java.util.ArrayList;
40+
import java.util.List;
41+
import java.util.stream.Collectors;
42+
43+
/**
44+
* Planner rule that translates a {@link Intersect}
45+
* (<code>all</code> = <code>false</code>)
46+
* into a {@link Exists}.
47+
*
48+
* @see CoreRules#INTERSECT_TO_EXISTS
49+
*/
50+
@Value.Enclosing
51+
public class IntersectToExistsRule
52+
extends RelRule<IntersectToExistsRule.Config>
53+
implements TransformationRule {
54+
55+
/** Creates an IntersectToExistRule. */
56+
protected IntersectToExistsRule(Config config) {
57+
super(config);
58+
}
59+
60+
@Deprecated // to be removed before 2.0
61+
public IntersectToExistsRule(Class<? extends Intersect> intersectClass,
62+
RelBuilderFactory relBuilderFactory) {
63+
this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
64+
.as(Config.class)
65+
.withOperandFor(intersectClass));
66+
}
67+
68+
//~ Methods ----------------------------------------------------------------
69+
70+
@Override public void onMatch(RelOptRuleCall call) {
71+
final Intersect intersect = call.rel(0);
72+
if (intersect.all) {
73+
return; // nothing we can do
74+
}
75+
76+
final RelBuilder builder = call.builder();
77+
final RexBuilder rexBuilder = builder.getRexBuilder();
78+
79+
// get all column indices of intersect
80+
List<Integer> fieldIndices = intersect.getRowType().getFieldList()
81+
.stream().map(RelDataTypeField::getIndex)
82+
.collect(Collectors.toList());
83+
84+
List<RelNode> inputs = intersect.getInputs();
85+
RelNode current = inputs.get(0);
86+
87+
// iterate over the inputs and apply exists subquery
88+
for (int i = 1; i < inputs.size(); i++) {
89+
RelNode nextInput = inputs.get(i);
90+
91+
// create correlation
92+
CorrelationId correlationId = intersect.getCluster().createCorrel();
93+
RexNode correl =
94+
rexBuilder.makeCorrel(nextInput.getRowType(), correlationId);
95+
96+
// create condition in exists filter, and use correlation
97+
List<RexNode> conditions = new ArrayList<>();
98+
for (int fieldIndex : fieldIndices) {
99+
RexNode outerField = rexBuilder.makeInputRef(current, fieldIndex);
100+
RexNode innerField =
101+
rexBuilder.makeFieldAccess(correl, fieldIndex);
102+
conditions.add(
103+
rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
104+
outerField, innerField));
105+
}
106+
RexNode condition = RexUtil.composeConjunction(rexBuilder, conditions);
107+
108+
// build exists subquery
109+
RelNode existsSubQuery = builder.push(nextInput)
110+
.filter(condition)
111+
.project(builder.fields(fieldIndices))
112+
.build();
113+
114+
// apply exists subquery to the current relation
115+
current = builder.push(current)
116+
.filter(ImmutableSet.of(correlationId),
117+
RexSubQuery.exists(existsSubQuery))
118+
.build();
119+
}
120+
121+
RelNode result = builder.push(current)
122+
.project(builder.fields(fieldIndices))
123+
.distinct()
124+
.build();
125+
126+
call.transformTo(result);
127+
}
128+
129+
/** Rule configuration. */
130+
@Value.Immutable
131+
public interface Config extends RelRule.Config {
132+
Config DEFAULT = ImmutableIntersectToExistsRule.Config.of()
133+
.withOperandFor(LogicalIntersect.class);
134+
135+
@Override default IntersectToExistsRule toRule() {
136+
return new IntersectToExistsRule(this);
137+
}
138+
139+
/** Defines an operand tree for the given classes. */
140+
default Config withOperandFor(Class<? extends Intersect> intersectClass) {
141+
return withOperandSupplier(b -> b.operand(intersectClass).anyInputs())
142+
.as(Config.class);
143+
}
144+
}
145+
}

core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9585,4 +9585,38 @@ private void checkJoinAssociateRuleWithTopAlwaysTrueCondition(boolean allowAlway
95859585
.withRule(CoreRules.MULTI_JOIN_OPTIMIZE)
95869586
.check();
95879587
}
9588+
9589+
@Test void testIntersectToExistsRuleOneField() {
9590+
String sql = "SELECT a.deptno FROM dept AS a\n"
9591+
+ "INTERSECT\n"
9592+
+ "SELECT b.empno FROM emp AS b";
9593+
sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS)
9594+
.check();
9595+
}
9596+
9597+
@Test void testIntersectToExistsRuleMultiFields() {
9598+
String sql = "SELECT a.empno, a.ename FROM emp AS a\n"
9599+
+ "INTERSECT\n"
9600+
+ "SELECT b.empno, b.ename FROM emp AS b";
9601+
sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS)
9602+
.check();
9603+
}
9604+
9605+
@Test void testIntersectToExistsRuleMultiIntersect() {
9606+
String sql = "SELECT a.deptno FROM dept AS a\n"
9607+
+ "INTERSECT\n"
9608+
+ "SELECT b.empno FROM emp AS b\n"
9609+
+ "INTERSECT\n"
9610+
+ "SELECT a.deptno FROM dept AS a";
9611+
sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS)
9612+
.check();
9613+
}
9614+
9615+
@Test void testIntersectToExistsRuleWithAll() {
9616+
String sql = "SELECT a.deptno FROM dept AS a\n"
9617+
+ "INTERSECT ALL\n"
9618+
+ "SELECT b.empno FROM emp AS b";
9619+
sql(sql).withRule(CoreRules.INTERSECT_TO_EXISTS)
9620+
.checkUnchanged();
9621+
}
95889622
}

core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5440,6 +5440,113 @@ LogicalIntersect(all=[true])
54405440
LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
54415441
LogicalFilter(condition=[=($7, 30)])
54425442
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5443+
]]>
5444+
</Resource>
5445+
</TestCase>
5446+
<TestCase name="testIntersectToExistsRuleMultiFields">
5447+
<Resource name="sql">
5448+
<![CDATA[SELECT a.empno, a.ename FROM emp AS a
5449+
INTERSECT
5450+
SELECT b.empno, b.ename FROM emp AS b]]>
5451+
</Resource>
5452+
<Resource name="planBefore">
5453+
<![CDATA[
5454+
LogicalIntersect(all=[false])
5455+
LogicalProject(EMPNO=[$0], ENAME=[$1])
5456+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5457+
LogicalProject(EMPNO=[$0], ENAME=[$1])
5458+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5459+
]]>
5460+
</Resource>
5461+
<Resource name="planAfter">
5462+
<![CDATA[
5463+
LogicalFilter(condition=[EXISTS({
5464+
LogicalFilter(condition=[AND(=($0, $cor0.EMPNO), =($1, $cor0.ENAME))])
5465+
LogicalProject(EMPNO=[$0], ENAME=[$1])
5466+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5467+
})], variablesSet=[[$cor0]])
5468+
LogicalProject(EMPNO=[$0], ENAME=[$1])
5469+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5470+
]]>
5471+
</Resource>
5472+
</TestCase>
5473+
<TestCase name="testIntersectToExistsRuleMultiIntersect">
5474+
<Resource name="sql">
5475+
<![CDATA[SELECT a.deptno FROM dept AS a
5476+
INTERSECT
5477+
SELECT b.empno FROM emp AS b
5478+
INTERSECT
5479+
SELECT a.deptno FROM dept AS a]]>
5480+
</Resource>
5481+
<Resource name="planBefore">
5482+
<![CDATA[
5483+
LogicalIntersect(all=[false])
5484+
LogicalIntersect(all=[false])
5485+
LogicalProject(DEPTNO=[$0])
5486+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5487+
LogicalProject(EMPNO=[$0])
5488+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5489+
LogicalProject(DEPTNO=[$0])
5490+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5491+
]]>
5492+
</Resource>
5493+
<Resource name="planAfter">
5494+
<![CDATA[
5495+
LogicalFilter(condition=[EXISTS({
5496+
LogicalFilter(condition=[=($0, $cor0.DEPTNO)])
5497+
LogicalProject(DEPTNO=[$0])
5498+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5499+
})], variablesSet=[[$cor0]])
5500+
LogicalFilter(condition=[EXISTS({
5501+
LogicalFilter(condition=[=($0, $cor1.EMPNO)])
5502+
LogicalProject(EMPNO=[$0])
5503+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5504+
})], variablesSet=[[$cor1]])
5505+
LogicalProject(DEPTNO=[$0])
5506+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5507+
]]>
5508+
</Resource>
5509+
</TestCase>
5510+
<TestCase name="testIntersectToExistsRuleOneField">
5511+
<Resource name="sql">
5512+
<![CDATA[SELECT a.deptno FROM dept AS a
5513+
INTERSECT
5514+
SELECT b.empno FROM emp AS b]]>
5515+
</Resource>
5516+
<Resource name="planBefore">
5517+
<![CDATA[
5518+
LogicalIntersect(all=[false])
5519+
LogicalProject(DEPTNO=[$0])
5520+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5521+
LogicalProject(EMPNO=[$0])
5522+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5523+
]]>
5524+
</Resource>
5525+
<Resource name="planAfter">
5526+
<![CDATA[
5527+
LogicalFilter(condition=[EXISTS({
5528+
LogicalFilter(condition=[=($0, $cor0.EMPNO)])
5529+
LogicalProject(EMPNO=[$0])
5530+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
5531+
})], variablesSet=[[$cor0]])
5532+
LogicalProject(DEPTNO=[$0])
5533+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5534+
]]>
5535+
</Resource>
5536+
</TestCase>
5537+
<TestCase name="testIntersectToExistsRuleWithAll">
5538+
<Resource name="sql">
5539+
<![CDATA[SELECT a.deptno FROM dept AS a
5540+
INTERSECT ALL
5541+
SELECT b.empno FROM emp AS b]]>
5542+
</Resource>
5543+
<Resource name="planBefore">
5544+
<![CDATA[
5545+
LogicalIntersect(all=[true])
5546+
LogicalProject(DEPTNO=[$0])
5547+
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
5548+
LogicalProject(EMPNO=[$0])
5549+
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
54435550
]]>
54445551
</Resource>
54455552
</TestCase>

0 commit comments

Comments
 (0)