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