You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2016/10/20 16:13:01 UTC

calcite git commit: [CALCITE-1448] Add rules to flatten and prune Intersect and Minus

Repository: calcite
Updated Branches:
  refs/heads/master 97ccd6ded -> 9f44aea5b


[CALCITE-1448] Add rules to flatten and prune Intersect and Minus

New instances of UnionMergeRule flatten trees of Intersect and Minus
operators into nodes that have more than 2 inputs.

New rules in PruneEmptyRules act on Intersect and Minus that have
empty inputs, either converting them to the empty relational
expression or removing empty inputs.

RelBuilder now allows Minus to have more than 2 inputs.


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/9f44aea5
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/9f44aea5
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/9f44aea5

Branch: refs/heads/master
Commit: 9f44aea5b3168f8372717daa29021aca2030cd65
Parents: 97ccd6d
Author: Julian Hyde <jh...@apache.org>
Authored: Wed Oct 19 20:52:21 2016 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Oct 20 08:47:05 2016 -0700

----------------------------------------------------------------------
 .../org/apache/calcite/plan/RelOptUtil.java     |   6 +
 .../calcite/rel/rules/PruneEmptyRules.java      |  81 +++-
 .../calcite/rel/rules/UnionMergeRule.java       |  95 +++--
 .../org/apache/calcite/tools/RelBuilder.java    |  20 +-
 .../apache/calcite/test/RelOptRulesTest.java    | 164 ++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml | 380 +++++++++++++++++++
 6 files changed, 710 insertions(+), 36 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/9f44aea5/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index ebac3f0..28cf336 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -48,6 +48,7 @@ import org.apache.calcite.rel.rules.FilterMergeRule;
 import org.apache.calcite.rel.rules.MultiJoin;
 import org.apache.calcite.rel.rules.ProjectToWindowRule;
 import org.apache.calcite.rel.rules.PruneEmptyRules;
+import org.apache.calcite.rel.rules.UnionMergeRule;
 import org.apache.calcite.rel.rules.UnionPullUpConstantsRule;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
@@ -1714,6 +1715,8 @@ public abstract class RelOptUtil {
     planner.addRule(AggregateProjectPullUpConstantsRule.INSTANCE2);
     planner.addRule(UnionPullUpConstantsRule.INSTANCE);
     planner.addRule(PruneEmptyRules.UNION_INSTANCE);
+    planner.addRule(PruneEmptyRules.INTERSECT_INSTANCE);
+    planner.addRule(PruneEmptyRules.MINUS_INSTANCE);
     planner.addRule(PruneEmptyRules.PROJECT_INSTANCE);
     planner.addRule(PruneEmptyRules.FILTER_INSTANCE);
     planner.addRule(PruneEmptyRules.SORT_INSTANCE);
@@ -1721,6 +1724,9 @@ public abstract class RelOptUtil {
     planner.addRule(PruneEmptyRules.JOIN_LEFT_INSTANCE);
     planner.addRule(PruneEmptyRules.JOIN_RIGHT_INSTANCE);
     planner.addRule(PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE);
+    planner.addRule(UnionMergeRule.INSTANCE);
+    planner.addRule(UnionMergeRule.INTERSECT_INSTANCE);
+    planner.addRule(UnionMergeRule.MINUS_INSTANCE);
     planner.addRule(ProjectToWindowRule.PROJECT);
     planner.addRule(FilterMergeRule.INSTANCE);
     planner.addRule(DateRangeRules.FILTER_INSTANCE);

http://git-wip-us.apache.org/repos/asf/calcite/blob/9f44aea5/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
index 8af045a..acd1a1c 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
@@ -28,6 +28,8 @@ import org.apache.calcite.rel.core.Project;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.logical.LogicalIntersect;
+import org.apache.calcite.rel.logical.LogicalMinus;
 import org.apache.calcite.rel.logical.LogicalUnion;
 import org.apache.calcite.rel.logical.LogicalValues;
 import org.apache.calcite.rex.RexLiteral;
@@ -76,7 +78,7 @@ public abstract class PruneEmptyRules {
               unordered(operand(Values.class, null, Values.IS_EMPTY, none()))),
           "Union") {
         public void onMatch(RelOptRuleCall call) {
-          LogicalUnion union = call.rel(0);
+          final LogicalUnion union = call.rel(0);
           final List<RelNode> inputs = call.getChildRels(union);
           assert inputs != null;
           final List<RelNode> newInputs = new ArrayList<>();
@@ -107,6 +109,83 @@ public abstract class PruneEmptyRules {
         }
       };
 
+  /**
+   * Rule that removes empty children of a
+   * {@link org.apache.calcite.rel.logical.LogicalMinus}.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   * <li>Minus(Rel, Empty, Rel2) becomes Minus(Rel, Rel2)
+   * <li>Minus(Empty, Rel) becomes Empty
+   * </ul>
+   */
+  public static final RelOptRule MINUS_INSTANCE =
+      new RelOptRule(
+          operand(LogicalMinus.class,
+              unordered(operand(Values.class, null, Values.IS_EMPTY, none()))),
+          "Minus") {
+        public void onMatch(RelOptRuleCall call) {
+          final LogicalMinus minus = call.rel(0);
+          final List<RelNode> inputs = call.getChildRels(minus);
+          assert inputs != null;
+          final List<RelNode> newInputs = new ArrayList<>();
+          for (RelNode input : inputs) {
+            if (!isEmpty(input)) {
+              newInputs.add(input);
+            } else if (newInputs.isEmpty()) {
+              // If the first input of Minus is empty, the whole thing is
+              // empty.
+              break;
+            }
+          }
+          assert newInputs.size() < inputs.size()
+              : "planner promised us at least one Empty child";
+          final RelBuilder builder = call.builder();
+          switch (newInputs.size()) {
+          case 0:
+            builder.push(minus).empty();
+            break;
+          case 1:
+            builder.push(
+                RelOptUtil.createCastRel(
+                    newInputs.get(0),
+                    minus.getRowType(),
+                    true));
+            break;
+          default:
+            builder.push(LogicalMinus.create(newInputs, minus.all));
+            break;
+          }
+          call.transformTo(builder.build());
+        }
+      };
+
+  /**
+   * Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalIntersect} to
+   * empty if any of its children are empty.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   * <li>Intersect(Rel, Empty, Rel2) becomes Empty
+   * <li>Intersect(Empty, Rel) becomes Empty
+   * </ul>
+   */
+  public static final RelOptRule INTERSECT_INSTANCE =
+      new RelOptRule(
+          operand(LogicalIntersect.class,
+              unordered(operand(Values.class, null, Values.IS_EMPTY, none()))),
+          "Intersect") {
+        public void onMatch(RelOptRuleCall call) {
+          LogicalIntersect intersect = call.rel(0);
+          final RelBuilder builder = call.builder();
+          builder.push(intersect).empty();
+          call.transformTo(builder.build());
+        }
+      };
+
   private static boolean isEmpty(RelNode node) {
     return node instanceof Values
         && ((Values) node).getTuples().isEmpty();

http://git-wip-us.apache.org/repos/asf/calcite/blob/9f44aea5/core/src/main/java/org/apache/calcite/rel/rules/UnionMergeRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/UnionMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/UnionMergeRule.java
index fbd3e76..e1fc0ac 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/UnionMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/UnionMergeRule.java
@@ -19,8 +19,13 @@ package org.apache.calcite.rel.rules;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Intersect;
+import org.apache.calcite.rel.core.Minus;
 import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.core.SetOp;
 import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rel.logical.LogicalIntersect;
+import org.apache.calcite.rel.logical.LogicalMinus;
 import org.apache.calcite.rel.logical.LogicalUnion;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
@@ -28,70 +33,104 @@ import org.apache.calcite.util.Util;
 
 /**
  * UnionMergeRule implements the rule for combining two
- * non-distinct {@link org.apache.calcite.rel.core.Union}s
- * into a single {@link org.apache.calcite.rel.core.Union}.
+ * non-distinct {@link org.apache.calcite.rel.core.SetOp}s
+ * into a single {@link org.apache.calcite.rel.core.SetOp}.
+ *
+ * <p>Originally written for {@link Union} (hence the name),
+ * but now also applies to {@link Intersect}.
  */
 public class UnionMergeRule extends RelOptRule {
   public static final UnionMergeRule INSTANCE =
-      new UnionMergeRule(LogicalUnion.class, RelFactories.LOGICAL_BUILDER);
+      new UnionMergeRule(LogicalUnion.class, "UnionMergeRule",
+          RelFactories.LOGICAL_BUILDER);
+  public static final UnionMergeRule INTERSECT_INSTANCE =
+      new UnionMergeRule(LogicalIntersect.class, "IntersectMergeRule",
+          RelFactories.LOGICAL_BUILDER);
+  public static final UnionMergeRule MINUS_INSTANCE =
+      new UnionMergeRule(LogicalMinus.class, "MinusMergeRule",
+          RelFactories.LOGICAL_BUILDER);
 
   //~ Constructors -----------------------------------------------------------
 
   /** Creates a UnionMergeRule. */
-  public UnionMergeRule(Class<? extends Union> unionClazz,
+  public UnionMergeRule(Class<? extends SetOp> unionClazz, String description,
       RelBuilderFactory relBuilderFactory) {
     super(
         operand(unionClazz,
             operand(RelNode.class, any()),
             operand(RelNode.class, any())),
-        relBuilderFactory, null);
+        relBuilderFactory, description);
   }
 
   @Deprecated // to be removed before 2.0
   public UnionMergeRule(Class<? extends Union> unionClazz,
       RelFactories.SetOpFactory setOpFactory) {
-    this(unionClazz, RelBuilder.proto(setOpFactory));
+    this(unionClazz, null, RelBuilder.proto(setOpFactory));
   }
 
   //~ Methods ----------------------------------------------------------------
 
   public void onMatch(RelOptRuleCall call) {
-    final Union topUnion = call.rel(0);
+    final SetOp topOp = call.rel(0);
+    @SuppressWarnings("unchecked") final Class<? extends SetOp> setOpClass =
+        (Class) operands.get(0).getMatchedClass();
 
-    // We want to combine the Union that's in the second input first.
-    // Hence, that's why the rule pattern matches on generic RelNodes
-    // rather than explicit UnionRels.  By doing so, and firing this rule
+    // For Union and Intersect, we want to combine the set-op that's in the
+    // second input first.
+    //
+    // For example, we reduce
+    //    Union(Union(a, b), Union(c, d))
+    // to
+    //    Union(Union(a, b), c, d)
+    // in preference to
+    //    Union(a, b, Union(c, d))
+    //
+    // But for Minus, we can only reduce the left input. It is not valid to
+    // reduce
+    //    Minus(a, Minus(b, c))
+    // to
+    //    Minus(a, b, c)
+    //
+    // Hence, that's why the rule pattern matches on generic RelNodes rather
+    // than explicit sub-classes of SetOp.  By doing so, and firing this rule
     // in a bottom-up order, it allows us to only specify a single
     // pattern for this rule.
-    final Union bottomUnion;
-    if (call.rel(2) instanceof Union) {
-      bottomUnion = call.rel(2);
-    } else if (call.rel(1) instanceof Union) {
-      bottomUnion = call.rel(1);
+    final SetOp bottomOp;
+    if (setOpClass.isInstance(call.rel(2))
+        && !Minus.class.isAssignableFrom(setOpClass)) {
+      bottomOp = call.rel(2);
+    } else if (setOpClass.isInstance(call.rel(1))) {
+      bottomOp = call.rel(1);
     } else {
       return;
     }
 
-    // If distincts haven't been removed yet, defer invoking this rule
-    if (!topUnion.all || !bottomUnion.all) {
+    // Can only combine if both are ALL or both are DISTINCT (i.e. not ALL).
+    if (topOp.all != bottomOp.all) {
       return;
     }
 
-    // Combine the inputs from the bottom union with the other inputs from
-    // the top union
+    // Combine the inputs from the bottom set-op with the other inputs from
+    // the top set-op.
     final RelBuilder relBuilder = call.builder();
-    if (call.rel(2) instanceof Union) {
-      assert topUnion.getInputs().size() == 2;
-      relBuilder.push(topUnion.getInput(0));
-      relBuilder.pushAll(bottomUnion.getInputs());
+    if (setOpClass.isInstance(call.rel(2))) {
+      assert topOp.getInputs().size() == 2;
+      relBuilder.push(topOp.getInput(0));
+      relBuilder.pushAll(bottomOp.getInputs());
     } else {
-      relBuilder.pushAll(bottomUnion.getInputs());
-      relBuilder.pushAll(Util.skip(topUnion.getInputs()));
+      relBuilder.pushAll(bottomOp.getInputs());
+      relBuilder.pushAll(Util.skip(topOp.getInputs()));
     }
-    int n = bottomUnion.getInputs().size()
-        + topUnion.getInputs().size()
+    int n = bottomOp.getInputs().size()
+        + topOp.getInputs().size()
         - 1;
-    relBuilder.union(true, n);
+    if (topOp instanceof Union) {
+      relBuilder.union(topOp.all, n);
+    } else if (topOp instanceof Intersect) {
+      relBuilder.intersect(topOp.all, n);
+    } else if (topOp instanceof Minus) {
+      relBuilder.minus(topOp.all, n);
+    }
     call.transformTo(relBuilder.build());
   }
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/9f44aea5/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 2973a2f..9608059 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -1016,13 +1016,10 @@ public class RelBuilder {
     switch (kind) {
     case UNION:
     case INTERSECT:
-      if (n < 1) {
-        throw new IllegalArgumentException("bad INTERSECT/UNION input count");
-      }
-      break;
     case EXCEPT:
-      if (n != 2) {
-        throw new AssertionError("bad EXCEPT input count");
+      if (n < 1) {
+        throw new IllegalArgumentException(
+            "bad INTERSECT/UNION/EXCEPT input count");
       }
       break;
     default:
@@ -1080,7 +1077,16 @@ public class RelBuilder {
    * @param all Whether to create EXCEPT ALL
    */
   public RelBuilder minus(boolean all) {
-    return setOp(all, SqlKind.EXCEPT, 2);
+    return minus(all, 2);
+  }
+
+  /** Creates a {@link org.apache.calcite.rel.core.Minus} of the {@code n}
+   * most recent relational expressions on the stack.
+   *
+   * @param all Whether to create EXCEPT ALL
+   */
+  public RelBuilder minus(boolean all, int n) {
+    return setOp(all, SqlKind.EXCEPT, n);
   }
 
   /** Creates a {@link org.apache.calcite.rel.core.Join}. */

http://git-wip-us.apache.org/repos/asf/calcite/blob/9f44aea5/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
----------------------------------------------------------------------
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 221dca2..2fd89f0 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -29,8 +29,11 @@ import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.rel.core.Intersect;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.core.Minus;
+import org.apache.calcite.rel.core.Union;
 import org.apache.calcite.rel.logical.LogicalTableModify;
 import org.apache.calcite.rel.metadata.CachingRelMetadataProvider;
 import org.apache.calcite.rel.metadata.ChainedRelMetadataProvider;
@@ -81,6 +84,7 @@ import org.apache.calcite.rel.rules.SortProjectTransposeRule;
 import org.apache.calcite.rel.rules.SortUnionTransposeRule;
 import org.apache.calcite.rel.rules.SubQueryRemoveRule;
 import org.apache.calcite.rel.rules.TableScanRule;
+import org.apache.calcite.rel.rules.UnionMergeRule;
 import org.apache.calcite.rel.rules.UnionPullUpConstantsRule;
 import org.apache.calcite.rel.rules.UnionToDistinctRule;
 import org.apache.calcite.rel.rules.ValuesReduceRule;
@@ -762,6 +766,117 @@ public class RelOptRulesTest extends RelOptTestBase {
             + "where deptno = 10\n");
   }
 
+  /** Tests {@link UnionMergeRule}, which merges 2 {@link Union} operators into
+   * a single {@code Union} with 3 inputs. */
+  @Test public void testMergeUnionAll() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "union all\n"
+        + "select * from emp where deptno = 20\n"
+        + "union all\n"
+        + "select * from emp where deptno = 30\n";
+    sql(sql).with(program).check();
+  }
+
+  /** Tests {@link UnionMergeRule}, which merges 2 {@link Union}
+   * {@code DISTINCT} (not {@code ALL}) operators into a single
+   * {@code Union} with 3 inputs. */
+  @Test public void testMergeUnionDistinct() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "union distinct\n"
+        + "select * from emp where deptno = 20\n"
+        + "union\n" // same as 'union distinct'
+        + "select * from emp where deptno = 30\n";
+    sql(sql).with(program).check();
+  }
+
+  /** Tests that {@link UnionMergeRule} does nothing if its arguments have
+   * different {@code ALL} settings. */
+  @Test public void testMergeUnionMixed() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "union all\n"
+        + "select * from emp where deptno = 20\n"
+        + "union\n"
+        + "select * from emp where deptno = 30\n";
+    sql(sql).with(program).checkUnchanged();
+  }
+
+  /** Tests that {@link UnionMergeRule} does nothing if its arguments have
+   * are different set operators, {@link Union} and {@link Intersect}. */
+  @Test public void testMergeSetOpMixed() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.INSTANCE)
+        .addRuleInstance(UnionMergeRule.INTERSECT_INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "union\n"
+        + "select * from emp where deptno = 20\n"
+        + "intersect\n"
+        + "select * from emp where deptno = 30\n";
+    sql(sql).with(program).checkUnchanged();
+  }
+
+  /** Tests {@link UnionMergeRule#INTERSECT_INSTANCE}, which merges 2
+   * {@link Intersect} operators into a single {@code Intersect} with 3
+   * inputs. */
+  @Test public void testMergeIntersect() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.INTERSECT_INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "intersect\n"
+        + "select * from emp where deptno = 20\n"
+        + "intersect\n"
+        + "select * from emp where deptno = 30\n";
+    sql(sql).with(program).check();
+  }
+
+  /** Tests {@link UnionMergeRule#MINUS_INSTANCE}, which merges 2
+   * {@link Minus} operators into a single {@code Minus} with 3
+   * inputs. */
+  @Test public void testMergeMinus() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.MINUS_INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "except\n"
+        + "select * from emp where deptno = 20\n"
+        + "except\n"
+        + "select * from emp where deptno = 30\n";
+    sql(sql).with(program).check();
+  }
+
+  /** Tests {@link UnionMergeRule#MINUS_INSTANCE}
+   * does not merge {@code Minus(a, Minus(b, c))}
+   * into {@code Minus(a, b, c)}, which would be incorrect. */
+  @Test public void testMergeMinusRightDeep() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(UnionMergeRule.MINUS_INSTANCE)
+        .build();
+
+    final String sql = "select * from emp where deptno = 10\n"
+        + "except\n"
+        + "select * from (\n"
+        + "  select * from emp where deptno = 20\n"
+        + "  except\n"
+        + "  select * from emp where deptno = 30)";
+    sql(sql).with(program).checkUnchanged();
+  }
+
   @Ignore("cycles")
   @Test public void testHeterogeneousConversion() throws Exception {
     // This one tests the planner's ability to correctly
@@ -1343,6 +1458,55 @@ public class RelOptRulesTest extends RelOptTestBase {
             + "where x + y > 30");
   }
 
+  @Test public void testEmptyIntersect() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(ValuesReduceRule.PROJECT_FILTER_INSTANCE)
+        .addRuleInstance(PruneEmptyRules.PROJECT_INSTANCE)
+        .addRuleInstance(PruneEmptyRules.INTERSECT_INSTANCE)
+        .build();
+
+    final String sql = "select * from (values (30, 3))"
+        + "intersect\n"
+        + "select *\nfrom (values (10, 1), (30, 3)) as t (x, y) where x > 50\n"
+        + "intersect\n"
+        + "select * from (values (30, 3))";
+    sql(sql).with(program).check();
+  }
+
+  @Test public void testEmptyMinus() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(ValuesReduceRule.PROJECT_FILTER_INSTANCE)
+        .addRuleInstance(PruneEmptyRules.PROJECT_INSTANCE)
+        .addRuleInstance(PruneEmptyRules.MINUS_INSTANCE)
+        .build();
+
+    // First input is empty; therefore whole expression is empty
+    final String sql = "select * from (values (30, 3)) as t (x, y) where x > 30"
+        + "except\n"
+        + "select * from (values (20, 2))\n"
+        + "except\n"
+        + "select * from (values (40, 4))";
+    sql(sql).with(program).check();
+  }
+
+  @Test public void testEmptyMinus2() throws Exception {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(ValuesReduceRule.PROJECT_FILTER_INSTANCE)
+        .addRuleInstance(PruneEmptyRules.PROJECT_INSTANCE)
+        .addRuleInstance(PruneEmptyRules.MINUS_INSTANCE)
+        .build();
+
+    // Second and fourth inputs are empty; they are removed
+    final String sql = "select * from (values (30, 3)) as t (x, y)\n"
+        + "except\n"
+        + "select * from (values (20, 2)) as t (x, y) where x > 30\n"
+        + "except\n"
+        + "select * from (values (40, 4))\n"
+        + "except\n"
+        + "select * from (values (50, 5)) as t (x, y) where x > 50";
+    sql(sql).with(program).check();
+  }
+
   @Test public void testEmptyJoin() {
     HepProgram program = new HepProgramBuilder()
         .addRuleInstance(ReduceExpressionsRule.FILTER_INSTANCE)

http://git-wip-us.apache.org/repos/asf/calcite/blob/9f44aea5/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
----------------------------------------------------------------------
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 e773160..d3ccca6 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -208,6 +208,53 @@ LogicalAggregate(group=[{}], EXPR$0=[SUM($0)])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testEmptyExcept">
+        <Resource name="sql">
+            <![CDATA[select * from (values (30, 3)) as t (x, y) where x > 30except
+select * from (values (20, 2))except
+select * from (values (40, 4))]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![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>
+    </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="planBefore">
+            <![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="planAfter">
+            <![CDATA[
+LogicalValues(tuples=[[]])
+]]>
+        </Resource>
+    </TestCase>
     <TestCase name="testPushFilterThroughOuterJoin">
         <Resource name="sql">
             <![CDATA[select 1 from sales.dept d left outer join sales.emp e on d.deptno = e.deptno where d.name = 'Charlie']]>
@@ -717,6 +764,68 @@ LogicalProject(EXPR$0=[$0], EXPR$1=[$1])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testEmptyMinus">
+        <Resource name="sql">
+            <![CDATA[select * from (values (30, 3)) as t (x, y) where x > 30except
+select * from (values (20, 2))except
+select * from (values (40, 4))]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![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="planAfter">
+            <![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="planBefore">
+            <![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="planAfter">
+            <![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="testReduceValuesUnderFilter">
         <Resource name="sql">
             <![CDATA[select a, b from (values (10, 'x'), (20, 'y')) as t(a, b) where a < 15]]>
@@ -1273,6 +1382,277 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testMergeIntersect">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+intersect
+select * from emp where deptno = 20
+intersect
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalIntersect(all=[false])
+  LogicalIntersect(all=[false])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 10)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalIntersect(all=[false])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 20)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeMinus">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+except
+select * from emp where deptno = 20
+except
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalMinus(all=[false])
+  LogicalMinus(all=[false])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 10)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalMinus(all=[false])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 20)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeMinusRightDeep">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+except
+select * from (
+  select * from emp where deptno = 20
+  except
+  select * from emp where deptno = 30)]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalMinus(all=[false])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalMinus(all=[false])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+        LogicalFilter(condition=[=($7, 20)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeSetOpMixed">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+union
+select * from emp where deptno = 20
+intersect
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalUnion(all=[false])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalIntersect(all=[false])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeUnion">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+union all
+select * from emp where deptno = 20
+union all
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalUnion(all=[true])
+  LogicalUnion(all=[true])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 10)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalUnion(all=[true])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 20)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeUnionAll">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+union all
+select * from emp where deptno = 20
+union all
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalUnion(all=[true])
+  LogicalUnion(all=[true])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 10)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalUnion(all=[true])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 20)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeUnionDistinct">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+union distinct
+select * from emp where deptno = 20
+union
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalUnion(all=[false])
+  LogicalUnion(all=[false])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 10)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalUnion(all=[false])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 10)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[=($7, 20)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testMergeUnionMixed">
+        <Resource name="sql">
+            <![CDATA[select * from emp where deptno = 10
+union all
+select * from emp where deptno = 20
+union
+select * from emp where deptno = 30
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalUnion(all=[false])
+  LogicalUnion(all=[true])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 10)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+      LogicalFilter(condition=[=($7, 20)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  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]])
+]]>
+        </Resource>
+    </TestCase>
     <TestCase name="testNestedAggregates">
         <Resource name="sql">
             <![CDATA[SELECT