You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by xu...@apache.org on 2025/07/07 11:36:14 UTC
(flink) branch master updated: [FLINK-37591][table] Add some PruneEmptyRules (#25826)
This is an automated email from the ASF dual-hosted git repository.
xuyangzhong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 324ec83fdb3 [FLINK-37591][table] Add some PruneEmptyRules (#25826)
324ec83fdb3 is described below
commit 324ec83fdb398d6f3d8e72532358d7ad5b60b09c
Author: Jacky Lau <li...@gmail.com>
AuthorDate: Mon Jul 7 19:36:08 2025 +0800
[FLINK-37591][table] Add some PruneEmptyRules (#25826)
---
.../plan/rules/logical/FlinkPruneEmptyRules.java | 194 +++++++++++++++++++++
.../planner/plan/rules/FlinkBatchRuleSets.scala | 12 +-
.../planner/plan/rules/FlinkStreamRuleSets.scala | 12 +-
.../rules/logical/FlinkPruneEmptyRulesTest.xml | 183 +++++++++++++++++++
.../rules/logical/FlinkPruneEmptyRulesTest.scala | 92 +++++++++-
5 files changed, 482 insertions(+), 11 deletions(-)
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRules.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRules.java
new file mode 100644
index 00000000000..c1bead2fa25
--- /dev/null
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRules.java
@@ -0,0 +1,194 @@
+/*
+ * 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.flink.table.planner.plan.rules.logical;
+
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.plan.hep.HepRelVertex;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Minus;
+import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.rules.SubstitutionRule;
+import org.apache.calcite.tools.RelBuilder;
+import org.immutables.value.Value;
+
+import java.util.List;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * This rule is copied from Calcite's {@link org.apache.calcite.rel.rules.PruneEmptyRules} because
+ * of * CALCITE-6955 * and should be removed after upgraded to calcite-1.40.0.
+ */
+public class FlinkPruneEmptyRules {
+
+ public static final RelOptRule UNION_INSTANCE =
+ FlinkPruneEmptyRules.UnionEmptyPruneRuleConfig.DEFAULT.toRule();
+
+ public static final RelOptRule MINUS_INSTANCE =
+ FlinkPruneEmptyRules.MinusEmptyPruneRuleConfig.DEFAULT.toRule();
+
+ /** Configuration for a rule that prunes empty inputs from a Minus. */
+ @Value.Immutable
+ public interface UnionEmptyPruneRuleConfig
+ extends FlinkPruneEmptyRules.FlinkPruneEmptyRule.Config {
+ FlinkPruneEmptyRules.UnionEmptyPruneRuleConfig DEFAULT =
+ ImmutableUnionEmptyPruneRuleConfig.builder()
+ .build()
+ .withOperandSupplier(
+ b0 ->
+ b0.operand(Union.class)
+ .unorderedInputs(
+ b1 ->
+ b1.operand(Values.class)
+ .predicate(Values::isEmpty)
+ .noInputs()))
+ .withDescription("Union");
+
+ @Override
+ default FlinkPruneEmptyRules.FlinkPruneEmptyRule toRule() {
+ return new FlinkPruneEmptyRules.FlinkPruneEmptyRule(this) {
+ @Override
+ public void onMatch(RelOptRuleCall call) {
+ final Union union = call.rel(0);
+ final List<RelNode> inputs = requireNonNull(union.getInputs());
+ final RelBuilder relBuilder = call.builder();
+ int nonEmptyInputs = 0;
+ for (RelNode input : inputs) {
+ if (!isEmpty(input)) {
+ relBuilder.push(input);
+ nonEmptyInputs++;
+ }
+ }
+ assert nonEmptyInputs < inputs.size()
+ : "planner promised us at least one Empty child: "
+ + RelOptUtil.toString(union);
+ if (nonEmptyInputs == 0) {
+ relBuilder.push(union).empty();
+ } else if (nonEmptyInputs == 1 && !union.all) {
+ relBuilder.distinct();
+ relBuilder.convert(union.getRowType(), true);
+ } else {
+ relBuilder.union(union.all, nonEmptyInputs);
+ relBuilder.convert(union.getRowType(), true);
+ }
+ call.transformTo(relBuilder.build());
+ }
+ };
+ }
+ }
+
+ /** Configuration for a rule that prunes empty inputs from a Minus. */
+ @Value.Immutable
+ public interface MinusEmptyPruneRuleConfig
+ extends FlinkPruneEmptyRules.FlinkPruneEmptyRule.Config {
+ FlinkPruneEmptyRules.MinusEmptyPruneRuleConfig DEFAULT =
+ ImmutableMinusEmptyPruneRuleConfig.builder()
+ .build()
+ .withOperandSupplier(
+ b0 ->
+ b0.operand(Minus.class)
+ .unorderedInputs(
+ b1 ->
+ b1.operand(Values.class)
+ .predicate(Values::isEmpty)
+ .noInputs()))
+ .withDescription("Minus");
+
+ @Override
+ default FlinkPruneEmptyRules.FlinkPruneEmptyRule toRule() {
+ return new FlinkPruneEmptyRules.FlinkPruneEmptyRule(this) {
+ @Override
+ public void onMatch(RelOptRuleCall call) {
+ final Minus minus = call.rel(0);
+ final List<RelNode> inputs = requireNonNull(minus.getInputs());
+ int nonEmptyInputs = 0;
+ final RelBuilder relBuilder = call.builder();
+ for (RelNode input : inputs) {
+ if (!isEmpty(input)) {
+ relBuilder.push(input);
+ nonEmptyInputs++;
+ } else if (nonEmptyInputs == 0) {
+ // If the first input of Minus is empty, the whole thing is
+ // empty.
+ break;
+ }
+ }
+ assert nonEmptyInputs < inputs.size()
+ : "planner promised us at least one Empty child: "
+ + RelOptUtil.toString(minus);
+ if (nonEmptyInputs == 0) {
+ relBuilder.push(minus).empty();
+ } else if (nonEmptyInputs == 1 && !minus.all) {
+ relBuilder.distinct();
+ relBuilder.convert(minus.getRowType(), true);
+ } else {
+ relBuilder.minus(minus.all, nonEmptyInputs);
+ relBuilder.convert(minus.getRowType(), true);
+ }
+ call.transformTo(relBuilder.build());
+ }
+ };
+ }
+ }
+
+ private static boolean isEmpty(RelNode node) {
+ if (node instanceof Values) {
+ return ((Values) node).getTuples().isEmpty();
+ }
+ if (node instanceof HepRelVertex) {
+ return isEmpty(((HepRelVertex) node).getCurrentRel());
+ }
+ // Note: relation input might be a RelSubset, so we just iterate over the relations
+ // in order to check if the subset is equivalent to an empty relation.
+ if (!(node instanceof RelSubset)) {
+ return false;
+ }
+ RelSubset subset = (RelSubset) node;
+ for (RelNode rel : subset.getRels()) {
+ if (isEmpty(rel)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Abstract prune empty rule that implements SubstitutionRule interface. */
+ protected abstract static class FlinkPruneEmptyRule
+ extends RelRule<FlinkPruneEmptyRules.FlinkPruneEmptyRule.Config>
+ implements SubstitutionRule {
+ protected FlinkPruneEmptyRule(FlinkPruneEmptyRule.Config config) {
+ super(config);
+ }
+
+ @Override
+ public boolean autoPruneOld() {
+ return true;
+ }
+
+ /** Rule configuration. */
+ public interface Config extends RelRule.Config {
+ @Override
+ FlinkPruneEmptyRules.FlinkPruneEmptyRule toRule();
+ }
+ }
+}
diff --git a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkBatchRuleSets.scala b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkBatchRuleSets.scala
index b5760b2bdf0..31e033ebd7f 100644
--- a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkBatchRuleSets.scala
+++ b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkBatchRuleSets.scala
@@ -176,13 +176,15 @@ object FlinkBatchRuleSets {
/** RuleSet to prune empty results rules */
val PRUNE_EMPTY_RULES: RuleSet = RuleSets.ofList(
- PruneEmptyRules.AGGREGATE_INSTANCE,
- PruneEmptyRules.FILTER_INSTANCE,
- PruneEmptyRules.JOIN_LEFT_INSTANCE,
- PruneEmptyRules.JOIN_RIGHT_INSTANCE,
+ FlinkPruneEmptyRules.UNION_INSTANCE,
+ PruneEmptyRules.INTERSECT_INSTANCE,
+ FlinkPruneEmptyRules.MINUS_INSTANCE,
PruneEmptyRules.PROJECT_INSTANCE,
+ PruneEmptyRules.FILTER_INSTANCE,
PruneEmptyRules.SORT_INSTANCE,
- PruneEmptyRules.UNION_INSTANCE
+ PruneEmptyRules.AGGREGATE_INSTANCE,
+ PruneEmptyRules.JOIN_LEFT_INSTANCE,
+ PruneEmptyRules.JOIN_RIGHT_INSTANCE
)
/** RuleSet about project */
diff --git a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala
index c6347cdc5c1..228f8147e71 100644
--- a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala
+++ b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala
@@ -182,13 +182,15 @@ object FlinkStreamRuleSets {
/** RuleSet to prune empty results rules */
val PRUNE_EMPTY_RULES: RuleSet = RuleSets.ofList(
- PruneEmptyRules.AGGREGATE_INSTANCE,
- PruneEmptyRules.FILTER_INSTANCE,
- PruneEmptyRules.JOIN_LEFT_INSTANCE,
- PruneEmptyRules.JOIN_RIGHT_INSTANCE,
+ FlinkPruneEmptyRules.UNION_INSTANCE,
+ PruneEmptyRules.INTERSECT_INSTANCE,
+ FlinkPruneEmptyRules.MINUS_INSTANCE,
PruneEmptyRules.PROJECT_INSTANCE,
+ PruneEmptyRules.FILTER_INSTANCE,
PruneEmptyRules.SORT_INSTANCE,
- PruneEmptyRules.UNION_INSTANCE
+ PruneEmptyRules.AGGREGATE_INSTANCE,
+ PruneEmptyRules.JOIN_LEFT_INSTANCE,
+ PruneEmptyRules.JOIN_RIGHT_INSTANCE
)
/** RuleSet about project */
diff --git a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.xml b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.xml
index de3b51cfba2..9bd547f9a53 100644
--- a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.xml
+++ b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.xml
@@ -57,6 +57,189 @@ LogicalProject(d=[$0])
<![CDATA[
LogicalProject(a=[$0], b=[$1], c=[$2])
+- LogicalTableScan(table=[[default_catalog, default_database, T1]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testEmptyFilterProjectUnion">
+ <Resource name="sql">
+ <![CDATA[
+SELECT * FROM (
+SELECT * FROM (VALUES (10, 1), (30, 3)) AS T (x, y)
+UNION ALL
+SELECT * FROM (VALUES (20, 2))
+)
+WHERE x + y > 30
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalProject(x=[$0], y=[$1])
++- LogicalFilter(condition=[>(+($0, $1), 30)])
+ +- LogicalUnion(all=[true])
+ :- LogicalProject(x=[$0], y=[$1])
+ : +- LogicalValues(tuples=[[{ 10, 1 }, { 30, 3 }]])
+ +- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+ +- LogicalValues(tuples=[[{ 20, 2 }]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+LogicalProject(x=[$0], y=[$1])
++- LogicalValues(tuples=[[{ 30, 3 }]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testEmptyFilterProjectUnion2">
+ <Resource name="sql">
+ <![CDATA[
+SELECT * FROM (
+SELECT * FROM (VALUES (10, 1), (30, 3), (30, 3)) AS T (X, Y)
+UNION
+SELECT * FROM (VALUES (20, 2))
+)
+WHERE X + Y > 30
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalProject(X=[$0], Y=[$1])
++- LogicalFilter(condition=[>(+($0, $1), 30)])
+ +- LogicalUnion(all=[false])
+ :- LogicalProject(X=[$0], Y=[$1])
+ : +- LogicalValues(tuples=[[{ 10, 1 }, { 30, 3 }, { 30, 3 }]])
+ +- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+ +- LogicalValues(tuples=[[{ 20, 2 }]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+LogicalProject(X=[$0], Y=[$1])
++- LogicalAggregate(group=[{0, 1}])
+ +- LogicalValues(tuples=[[{ 30, 3 }, { 30, 3 }]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testEmptyIntersect">
+ <Resource name="sql">
+ <![CDATA[
+SELECT * FROM (VALUES (30, 3))
+INTERSECT
+SELECT * FROM (VALUES (10, 1), (30, 3)) AS T (x, y) WHERE x > 50
+INTERSECT
+SELECT * FROM (VALUES (30, 3))
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalIntersect(all=[false])
+:- LogicalIntersect(all=[false])
+: :- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+: : +- LogicalValues(tuples=[[{ 30, 3 }]])
+: +- LogicalProject(x=[$0], y=[$1])
+: +- LogicalFilter(condition=[>($0, 50)])
+: +- LogicalValues(tuples=[[{ 10, 1 }, { 30, 3 }]])
++- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+ +- LogicalValues(tuples=[[{ 30, 3 }]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+LogicalValues(tuples=[[]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testEmptyMinus">
+ <Resource name="sql">
+ <![CDATA[
+SELECT * FROM (VALUES (30, 3)) AS T (x, y)
+WHERE x > 30
+EXCEPT
+SELECT * FROM (VALUES (20, 2))
+EXCEPT
+SELECT * FROM (VALUES (40, 4))
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalMinus(all=[false])
+:- LogicalMinus(all=[false])
+: :- LogicalProject(x=[$0], y=[$1])
+: : +- LogicalFilter(condition=[>($0, 30)])
+: : +- LogicalValues(tuples=[[{ 30, 3 }]])
+: +- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+: +- LogicalValues(tuples=[[{ 20, 2 }]])
++- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+ +- LogicalValues(tuples=[[{ 40, 4 }]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+LogicalValues(tuples=[[]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testEmptyMinus2">
+ <Resource name="sql">
+ <![CDATA[
+SELECT * FROM (VALUES (30, 3)) AS T (x, y)
+EXCEPT
+SELECT * FROM (VALUES (20, 2)) AS T (x, y) WHERE x > 30
+EXCEPT
+SELECT * FROM (VALUES (40, 4))
+EXCEPT
+SELECT * FROM (VALUES (50, 5)) AS T (x, y) WHERE x > 50
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalMinus(all=[false])
+:- LogicalMinus(all=[false])
+: :- LogicalMinus(all=[false])
+: : :- LogicalProject(x=[$0], y=[$1])
+: : : +- LogicalValues(tuples=[[{ 30, 3 }]])
+: : +- LogicalProject(x=[$0], y=[$1])
+: : +- LogicalFilter(condition=[>($0, 30)])
+: : +- LogicalValues(tuples=[[{ 20, 2 }]])
+: +- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+: +- LogicalValues(tuples=[[{ 40, 4 }]])
++- LogicalProject(x=[$0], y=[$1])
+ +- LogicalFilter(condition=[>($0, 50)])
+ +- LogicalValues(tuples=[[{ 50, 5 }]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+LogicalMinus(all=[false])
+:- LogicalProject(x=[$0], y=[$1])
+: +- LogicalValues(tuples=[[{ 30, 3 }]])
++- LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
+ +- LogicalValues(tuples=[[{ 40, 4 }]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testEmptyMinus3">
+ <Resource name="sql">
+ <![CDATA[
+SELECT * FROM (VALUES (30, 3), (30, 3)) AS T (X, Y)
+EXCEPT
+SELECT * FROM (VALUES (20, 2)) AS T (X, Y) WHERE X > 30
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalMinus(all=[false])
+:- LogicalProject(X=[$0], Y=[$1])
+: +- LogicalValues(tuples=[[{ 30, 3 }, { 30, 3 }]])
++- LogicalProject(X=[$0], Y=[$1])
+ +- LogicalFilter(condition=[>($0, 30)])
+ +- LogicalValues(tuples=[[{ 20, 2 }]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+LogicalAggregate(group=[{0, 1}])
++- LogicalProject(X=[$0], Y=[$1])
+ +- LogicalValues(tuples=[[{ 30, 3 }, { 30, 3 }]])
]]>
</Resource>
</TestCase>
diff --git a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.scala b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.scala
index c61b003cf50..74e9ad050fe 100644
--- a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.scala
+++ b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/rules/logical/FlinkPruneEmptyRulesTest.scala
@@ -46,8 +46,18 @@ class FlinkPruneEmptyRulesTest extends TableTestBase {
FlinkSubQueryRemoveRule.FILTER,
CoreRules.FILTER_REDUCE_EXPRESSIONS,
CoreRules.PROJECT_REDUCE_EXPRESSIONS,
- PruneEmptyRules.FILTER_INSTANCE,
+ CoreRules.FILTER_SET_OP_TRANSPOSE,
+ CoreRules.FILTER_PROJECT_TRANSPOSE,
+ CoreRules.PROJECT_MERGE,
+ CoreRules.PROJECT_FILTER_VALUES_MERGE,
+ FlinkPruneEmptyRules.UNION_INSTANCE,
+ PruneEmptyRules.INTERSECT_INSTANCE,
+ FlinkPruneEmptyRules.MINUS_INSTANCE,
PruneEmptyRules.PROJECT_INSTANCE,
+ PruneEmptyRules.FILTER_INSTANCE,
+ PruneEmptyRules.SORT_INSTANCE,
+ PruneEmptyRules.AGGREGATE_INSTANCE,
+ PruneEmptyRules.JOIN_LEFT_INSTANCE,
PruneEmptyRules.JOIN_RIGHT_INSTANCE
))
.build()
@@ -68,4 +78,84 @@ class FlinkPruneEmptyRulesTest extends TableTestBase {
util.verifyRelPlan("SELECT * FROM T1 WHERE a NOT IN (SELECT d FROM T2 WHERE 1=0)")
}
+ @Test
+ def testEmptyFilterProjectUnion(): Unit = {
+ val sqlQuery =
+ s"""
+ |SELECT * FROM (
+ |SELECT * FROM (VALUES (10, 1), (30, 3)) AS T (x, y)
+ |UNION ALL
+ |SELECT * FROM (VALUES (20, 2))
+ |)
+ |WHERE x + y > 30
+ """.stripMargin
+ util.verifyRelPlan(sqlQuery)
+ }
+
+ @Test
+ def testEmptyFilterProjectUnion2(): Unit = {
+ val sqlQuery =
+ s"""
+ |SELECT * FROM (
+ |SELECT * FROM (VALUES (10, 1), (30, 3), (30, 3)) AS T (X, Y)
+ |UNION
+ |SELECT * FROM (VALUES (20, 2))
+ |)
+ |WHERE X + Y > 30
+ """.stripMargin
+ util.verifyRelPlan(sqlQuery)
+ }
+
+ @Test
+ def testEmptyIntersect(): Unit = {
+ val sqlQuery =
+ s"""
+ |SELECT * FROM (VALUES (30, 3))
+ |INTERSECT
+ |SELECT * FROM (VALUES (10, 1), (30, 3)) AS T (x, y) WHERE x > 50
+ |INTERSECT
+ |SELECT * FROM (VALUES (30, 3))
+ """.stripMargin
+ util.verifyRelPlan(sqlQuery)
+ }
+
+ @Test
+ def testEmptyMinus(): Unit = {
+ val sqlQuery =
+ s"""
+ |SELECT * FROM (VALUES (30, 3)) AS T (x, y)
+ |WHERE x > 30
+ |EXCEPT
+ |SELECT * FROM (VALUES (20, 2))
+ |EXCEPT
+ |SELECT * FROM (VALUES (40, 4))
+ """.stripMargin
+ util.verifyRelPlan(sqlQuery)
+ }
+
+ @Test
+ def testEmptyMinus2(): Unit = {
+ val sqlQuery =
+ s"""
+ |SELECT * FROM (VALUES (30, 3)) AS T (x, y)
+ |EXCEPT
+ |SELECT * FROM (VALUES (20, 2)) AS T (x, y) WHERE x > 30
+ |EXCEPT
+ |SELECT * FROM (VALUES (40, 4))
+ |EXCEPT
+ |SELECT * FROM (VALUES (50, 5)) AS T (x, y) WHERE x > 50
+ """.stripMargin
+ util.verifyRelPlan(sqlQuery)
+ }
+
+ @Test
+ def testEmptyMinus3(): Unit = {
+ val sqlQuery =
+ s"""
+ |SELECT * FROM (VALUES (30, 3), (30, 3)) AS T (X, Y)
+ |EXCEPT
+ |SELECT * FROM (VALUES (20, 2)) AS T (X, Y) WHERE X > 30
+ """.stripMargin
+ util.verifyRelPlan(sqlQuery)
+ }
}