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))),