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 dd067b6f92 [CALCITE-7702] JoinAggregateTransposeRule produces a 
non-equivalent plan when the aggregate with empty input and empty group set
dd067b6f92 is described below

commit dd067b6f9292eb828c5bb1c6d9c134872d3ffae9
Author: zzwqqq <[email protected]>
AuthorDate: Tue Aug 11 10:22:33 2026 +0800

    [CALCITE-7702] JoinAggregateTransposeRule produces a non-equivalent plan 
when the aggregate with empty input and empty group set
---
 .../rel/rules/JoinAggregateTransposeRule.java      |  5 ++
 .../test/JoinAggregateTransposeRuleTest.java       | 33 ++++++++++
 .../test/JoinAggregateTransposeRuleTest.xml        | 70 ++++++++++++++++++++++
 core/src/test/resources/sql/join-agg-transpose.iq  | 14 +++++
 4 files changed, 122 insertions(+)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/JoinAggregateTransposeRule.java
 
b/core/src/main/java/org/apache/calcite/rel/rules/JoinAggregateTransposeRule.java
index 99cf46141d..39393e879a 100644
--- 
a/core/src/main/java/org/apache/calcite/rel/rules/JoinAggregateTransposeRule.java
+++ 
b/core/src/main/java/org/apache/calcite/rel/rules/JoinAggregateTransposeRule.java
@@ -106,6 +106,11 @@ protected JoinAggregateTransposeRule(Config config) {
         // in case we decide to extend this rule for lazy aggregation.
         && isAggregateSupported(left, true)
         && groupOutput.contains(info.leftSet())
+        // An aggregate returns one row on empty input if its group set is 
empty.
+        // Pull-up adds group keys and may lose that row. Require the input to 
be
+        // known non-empty.
+        && (!left.getGroupSet().isEmpty()
+            || Boolean.FALSE.equals(mq.isEmpty(left.getInput())))
         // The right side must be unique on its join keys (no row duplication)
         && Boolean.TRUE.equals(mq.areColumnsUnique(right, info.rightSet()));
   }
diff --git 
a/core/src/test/java/org/apache/calcite/test/JoinAggregateTransposeRuleTest.java
 
b/core/src/test/java/org/apache/calcite/test/JoinAggregateTransposeRuleTest.java
index 97f784522a..fd1a9ad568 100644
--- 
a/core/src/test/java/org/apache/calcite/test/JoinAggregateTransposeRuleTest.java
+++ 
b/core/src/test/java/org/apache/calcite/test/JoinAggregateTransposeRuleTest.java
@@ -161,6 +161,39 @@ private static RelOptFixture sql(String sql) {
     sql(sql).withRule(CoreRules.JOIN_AGGREGATE_TRANSPOSE).checkUnchanged();
   }
 
+  /**
+   * Tests that the rule does not pull an aggregate with an empty group set 
when
+   * its input is empty.
+   */
+  @Test void testNoPullAggregateWithEmptyGroupSetOnEmptyInput() {
+    final String sql = "select g.emp_count, d.deptno\n"
+        + "from (select count(*) as emp_count from emp where false) g\n"
+        + "join (select deptno from dept where deptno = 10) d on true";
+    sql(sql).withRule(CoreRules.JOIN_AGGREGATE_TRANSPOSE).checkUnchanged();
+  }
+
+  /**
+   * Tests that the rule does not pull an aggregate with an empty group set 
when
+   * its input may be empty.
+   */
+  @Test void testNoPullAggregateWithEmptyGroupSetOnPotentiallyEmptyInput() {
+    final String sql = "select g.emp_count, d.deptno\n"
+        + "from (select count(*) as emp_count from emp) g\n"
+        + "join (select deptno from dept where deptno = 10) d on true";
+    sql(sql).withRule(CoreRules.JOIN_AGGREGATE_TRANSPOSE).checkUnchanged();
+  }
+
+  /**
+   * Tests that the rule pulls an aggregate with an empty group set when its
+   * input is known to be non-empty.
+   */
+  @Test void testPullAggregateWithEmptyGroupSetOnNonEmptyInput() {
+    final String sql = "select g.emp_count, d.deptno\n"
+        + "from (select count(*) as emp_count\n"
+        + "      from (values (1)) as v(n)) g\n"
+        + "join (select deptno from dept where deptno = 10) d on true";
+    sql(sql).withRule(CoreRules.JOIN_AGGREGATE_TRANSPOSE).check();
+  }
 
   @AfterAll static void checkActualAndReferenceFiles() {
     fixture().diffRepos.checkActualAndReferenceFiles();
diff --git 
a/core/src/test/resources/org/apache/calcite/test/JoinAggregateTransposeRuleTest.xml
 
b/core/src/test/resources/org/apache/calcite/test/JoinAggregateTransposeRuleTest.xml
index 8c7c7c5964..26aa8d629f 100644
--- 
a/core/src/test/resources/org/apache/calcite/test/JoinAggregateTransposeRuleTest.xml
+++ 
b/core/src/test/resources/org/apache/calcite/test/JoinAggregateTransposeRuleTest.xml
@@ -16,6 +16,45 @@
   ~ limitations under the License.
   -->
 <Root>
+  <TestCase name="testNoPullAggregateWithEmptyGroupSetOnEmptyInput">
+    <Resource name="sql">
+      <![CDATA[select g.emp_count, d.deptno
+from (select count(*) as emp_count from emp where false) g
+join (select deptno from dept where deptno = 10) d on true]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMP_COUNT=[$0], DEPTNO=[$1])
+  LogicalJoin(condition=[true], joinType=[inner])
+    LogicalAggregate(group=[{}], EMP_COUNT=[COUNT()])
+      LogicalProject($f0=[0])
+        LogicalFilter(condition=[false])
+          LogicalTableScan(table=[[scott, EMP]])
+    LogicalProject(DEPTNO=[$0])
+      LogicalFilter(condition=[=(CAST($0):INTEGER NOT NULL, 10)])
+        LogicalTableScan(table=[[scott, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNoPullAggregateWithEmptyGroupSetOnPotentiallyEmptyInput">
+    <Resource name="sql">
+      <![CDATA[select g.emp_count, d.deptno
+from (select count(*) as emp_count from emp) g
+join (select deptno from dept where deptno = 10) d on true]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMP_COUNT=[$0], DEPTNO=[$1])
+  LogicalJoin(condition=[true], joinType=[inner])
+    LogicalAggregate(group=[{}], EMP_COUNT=[COUNT()])
+      LogicalProject($f0=[0])
+        LogicalTableScan(table=[[scott, EMP]])
+    LogicalProject(DEPTNO=[$0])
+      LogicalFilter(condition=[=(CAST($0):INTEGER NOT NULL, 10)])
+        LogicalTableScan(table=[[scott, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
   <TestCase name="testNoPullGroupByAboveNonEquiJoin">
     <Resource name="sql">
       <![CDATA[select g.deptno
@@ -48,6 +87,37 @@ LogicalProject(JOB=[$0], CNT=[$1])
       LogicalProject(JOB=[$2])
         LogicalTableScan(table=[[scott, EMP]])
     LogicalTableScan(table=[[scott, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPullAggregateWithEmptyGroupSetOnNonEmptyInput">
+    <Resource name="sql">
+      <![CDATA[select g.emp_count, d.deptno
+from (select count(*) as emp_count
+      from (values (1)) as v(n)) g
+join (select deptno from dept where deptno = 10) d on true]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMP_COUNT=[$0], DEPTNO=[$1])
+  LogicalJoin(condition=[true], joinType=[inner])
+    LogicalAggregate(group=[{}], EMP_COUNT=[COUNT()])
+      LogicalValues(tuples=[[{ 0 }]])
+    LogicalProject(DEPTNO=[$0])
+      LogicalFilter(condition=[=(CAST($0):INTEGER NOT NULL, 10)])
+        LogicalTableScan(table=[[scott, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMP_COUNT=[$0], DEPTNO=[$1])
+  LogicalProject(EMP_COUNT=[$1], DEPTNO=[$0])
+    LogicalAggregate(group=[{1}], EMP_COUNT=[COUNT()])
+      LogicalJoin(condition=[true], joinType=[inner])
+        LogicalValues(tuples=[[{ 0 }]])
+        LogicalProject(DEPTNO=[$0])
+          LogicalFilter(condition=[=(CAST($0):INTEGER NOT NULL, 10)])
+            LogicalTableScan(table=[[scott, DEPT]])
 ]]>
     </Resource>
   </TestCase>
diff --git a/core/src/test/resources/sql/join-agg-transpose.iq 
b/core/src/test/resources/sql/join-agg-transpose.iq
index 3cccb32da2..756b13b233 100644
--- a/core/src/test/resources/sql/join-agg-transpose.iq
+++ b/core/src/test/resources/sql/join-agg-transpose.iq
@@ -75,4 +75,18 @@ EnumerableCalc(expr#0..3=[{inputs}], DEPTNO=[$t0], 
TOTAL_SAL=[$t3], DNAME=[$t2])
         EnumerableTableScan(table=[[scott, DEPT]])
 !plan
 
+# Tests that the rule preserves a row from an aggregate with an empty group set
+
+select g.emp_count, d.deptno
+from (select count(*) as emp_count from emp where false) g
+join (select deptno from dept where deptno = 10) d on true;
++-----------+--------+
+| EMP_COUNT | DEPTNO |
++-----------+--------+
+|         0 |     10 |
++-----------+--------+
+(1 row)
+
+!ok
+
 # End join-agg-transpose.iq

Reply via email to