This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 150d9e6b0b [CALCITE-7901] UnionMergeRule merges EXCEPT ALL into an 
enclosing EXCEPT DISTINCT
150d9e6b0b is described below

commit 150d9e6b0b58e0714f0046e0d276cff540d4ee70
Author: Gian Merlino <[email protected]>
AuthorDate: Wed Oct 28 22:51:59 2026 -0700

    [CALCITE-7901] UnionMergeRule merges EXCEPT ALL into an enclosing EXCEPT 
DISTINCT
---
 .../apache/calcite/rel/rules/UnionMergeRule.java   | 20 +++++++++--------
 .../org/apache/calcite/test/RelOptRulesTest.java   | 14 ++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 25 ++++++++++++++++++++++
 core/src/test/resources/sql/set-op.iq              | 17 +++++++++++++++
 4 files changed, 67 insertions(+), 9 deletions(-)

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 09cef6fe26..e243e3dca9 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
@@ -85,11 +85,7 @@ public UnionMergeRule(Class<? extends Union> setOpClass,
       return false;
     }
 
-    if (topOp.all && !bottomOp.all) {
-      return false;
-    }
-
-    return true;
+    return canMerge(topOp, bottomOp);
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -127,10 +123,7 @@ public UnionMergeRule(Class<? extends Union> setOpClass,
       return;
     }
 
-    // Can only combine (1) if all operators are ALL,
-    // or (2) top operator is DISTINCT (i.e. not ALL).
-    // In case (2), all operators become DISTINCT.
-    if (topOp.all && !bottomOp.all) {
+    if (!canMerge(topOp, bottomOp)) {
       return;
     }
 
@@ -162,6 +155,15 @@ public UnionMergeRule(Class<? extends Union> setOpClass,
     call.transformTo(relBuilder.build());
   }
 
+  /** Returns whether {@code bottomOp} can be merged into {@code topOp}. */
+  private static boolean canMerge(SetOp topOp, SetOp bottomOp) {
+    if (topOp instanceof Minus) {
+      return topOp.all == bottomOp.all;
+    } else {
+      return !topOp.all || bottomOp.all;
+    }
+  }
+
   /** Rule configuration. */
   @Value.Immutable
   public interface Config extends RelRule.Config {
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 5b8854f19c..1d5224a0af 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -4455,6 +4455,20 @@ private void 
checkPushJoinThroughUnionOnRightDoesNotMatchSemiOrAntiJoin(JoinRelT
         .check();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7901";>[CALCITE-7901]
+   * UnionMergeRule merges EXCEPT ALL into an enclosing EXCEPT DISTINCT</a>. */
+  @Test void testMergeMinusAllUnderDistinct() {
+    final String sql = "select * from emp where deptno = 10\n"
+        + "except all\n"
+        + "select * from emp where deptno = 20\n"
+        + "except\n"
+        + "select * from emp where deptno = 30\n";
+    sql(sql)
+        .withRule(CoreRules.MINUS_MERGE)
+        .checkUnchanged();
+  }
+
   /** Tests {@link CoreRules#MINUS_MERGE}
    * does not merge {@code Minus(a, Minus(b, c))}
    * into {@code Minus(a, b, c)}, which would be incorrect. */
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 7fb1bbf347..06bb03975a 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -10672,6 +10672,31 @@ 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, 30)])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testMergeMinusAllUnderDistinct">
+    <Resource name="sql">
+      <![CDATA[select * from emp where deptno = 10
+except all
+select * from emp where deptno = 20
+except
+select * from emp where deptno = 30
+]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalMinus(all=[false])
+  LogicalMinus(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>
diff --git a/core/src/test/resources/sql/set-op.iq 
b/core/src/test/resources/sql/set-op.iq
index 1564650b1f..d1d998ea36 100644
--- a/core/src/test/resources/sql/set-op.iq
+++ b/core/src/test/resources/sql/set-op.iq
@@ -186,6 +186,23 @@ except
 
 !ok
 
+# [CALCITE-7901] UnionMergeRule merges EXCEPT ALL into an enclosing EXCEPT 
DISTINCT.
+# Postgres gives the same result.
+select x from (values (1), (1), (2)) as t(x)
+except all
+select x from (values (1)) as t2(x)
+except
+select x from (values (3)) as t3(x);
++---+
+| X |
++---+
+| 1 |
+| 2 |
++---+
+(2 rows)
+
+!ok
+
 # Except all with null value rows
 select * from
 (select x, y from (values (cast(NULL as int), cast(NULL as varchar(1))),

Reply via email to