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

xuzifu666 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 fd96e467c4 [CALCITE-7828] Aggregate whose grouping sets include () 
returns no rows on empty input
fd96e467c4 is described below

commit fd96e467c44e9f3c62c9e2ee5db14a8344553acb
Author: Yu Xu <[email protected]>
AuthorDate: Sun Oct 18 12:35:06 2026 +0800

    [CALCITE-7828] Aggregate whose grouping sets include () returns no rows on 
empty input
---
 .../adapter/enumerable/EnumerableAggregate.java    |  87 +++++++++++++---
 .../apache/calcite/interpreter/AggregateNode.java  |   9 ++
 .../apache/calcite/rel/rules/PruneEmptyRules.java  |   2 +-
 .../org/apache/calcite/util/BuiltInMethod.java     |   3 +
 .../org/apache/calcite/test/InterpreterTest.java   |  16 +++
 .../test/enumerable/EnumerableAggregateTest.java   | 111 +++++++++++++++++++++
 core/src/test/resources/sql/agg.iq                 |  42 ++++++++
 .../apache/calcite/linq4j/EnumerableDefaults.java  |  40 +++++++-
 8 files changed, 293 insertions(+), 17 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
index 0429168a8f..4130252d6f 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
@@ -26,6 +26,7 @@
 import org.apache.calcite.linq4j.tree.Expression;
 import org.apache.calcite.linq4j.tree.Expressions;
 import org.apache.calcite.linq4j.tree.ParameterExpression;
+import org.apache.calcite.linq4j.tree.Primitive;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.InvalidRelException;
@@ -38,6 +39,7 @@
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
+import java.lang.reflect.Method;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.List;
@@ -265,10 +267,15 @@ public EnumerableAggregate(RelOptCluster cluster, 
RelTraitSet traitSet,
     resultBlock.add(physType.record(results));
     if (getGroupType() != Group.SIMPLE) {
       final List<Expression> list = new ArrayList<>();
+      boolean hasEmptyGroup = false;
       for (ImmutableBitSet set : groupSets) {
-        list.add(
+        final Expression keySelector =
             inputPhysType.generateSelector(parameter, groupSet.asList(),
-                set.asList(), keyPhysType.getFormat()));
+                set.asList(), keyPhysType.getFormat());
+        list.add(keySelector);
+        if (set.isEmpty()) {
+          hasEmptyGroup = true;
+        }
       }
       final Expression keySelectors_ =
           builder.append("keySelectors",
@@ -280,20 +287,72 @@ public EnumerableAggregate(RelOptCluster cluster, 
RelTraitSet traitSet,
                   resultBlock.toBlock(),
                   requireNonNull(key_, "key_"),
                   acc_));
+      final Expressions.FluentList<Expression> arguments =
+          Expressions.list(childExp,
+              keySelectors_,
+              Expressions.call(lambdaFactory,
+                  BuiltInMethod.AGG_LAMBDA_FACTORY_ACC_INITIALIZER.method),
+              Expressions.call(lambdaFactory,
+                  BuiltInMethod.AGG_LAMBDA_FACTORY_ACC_ADDER.method),
+              Expressions.call(lambdaFactory,
+                  BuiltInMethod.AGG_LAMBDA_FACTORY_ACC_RESULT_SELECTOR.method,
+                  resultSelector));
+      final Method groupByMultipleMethod;
+      if (hasEmptyGroup) {
+        // The key of the empty grouping set is a constant: each grouping
+        // column is defaulted (null, or the default value of its primitive
+        // type) and each indicator is true. It is built directly, mirroring
+        // the key that the selector generated for that grouping set
+        // computes for any input row. Passing it to groupByMultiple lets
+        // the empty grouping set produce its single row when the input is
+        // empty, just as a query with no GROUP BY produces a row over an
+        // empty input.
+        //
+        // For example, for the query
+        //
+        //   SELECT deptno, COUNT(*) AS c
+        //   FROM emp WHERE FALSE
+        //   GROUP BY ROLLUP(deptno)
+        //
+        // where deptno is an INT column, the generated code ends in a call
+        // to groupByMultiple whose last argument is the constant key of the
+        // empty grouping set (the key selectors, the lambda factory and the
+        // result selector are anonymous classes, elided here):
+        //
+        //   return EnumerableDefaults.groupByMultiple(
+        //       Linq4j.asEnumerable(new Integer[] {}),
+        //       Arrays.asList(keySelector0, keySelector1),
+        //       lambdaFactory.accumulatorInitializer(),
+        //       lambdaFactory.accumulatorAdder(),
+        //       lambdaFactory.resultSelector(resultSelector),
+        //       FlatLists.of(0, true));
+        //
+        // Here, '0' is the default value of the deptno column, and 'true'
+        // is its indicator, true because deptno is not in the empty
+        // grouping set.
+        final List<Expression> keyExpressions = new ArrayList<>();
+        for (int j = 0; j < groupCount; j++) {
+          final Primitive primitive =
+              Primitive.of(keyPhysType.fieldClass(j));
+          keyExpressions.add(
+              Expressions.constant(
+                  primitive != null ? primitive.defaultValue : null));
+        }
+        for (int j = 0; j < groupCount; j++) {
+          keyExpressions.add(Expressions.constant(true));
+        }
+        final Expression emptyGroupKey =
+            builder.append("emptyGroupKey",
+                keyPhysType.record(keyExpressions));
+        arguments.add(emptyGroupKey);
+        groupByMultipleMethod = BuiltInMethod.GROUP_BY_MULTIPLE2.method;
+      } else {
+        groupByMultipleMethod = BuiltInMethod.GROUP_BY_MULTIPLE.method;
+      }
       builder.add(
           Expressions.return_(null,
-              Expressions.call(
-                  BuiltInMethod.GROUP_BY_MULTIPLE.method,
-                  Expressions.list(childExp,
-                      keySelectors_,
-                      Expressions.call(lambdaFactory,
-                          
BuiltInMethod.AGG_LAMBDA_FACTORY_ACC_INITIALIZER.method),
-                      Expressions.call(lambdaFactory,
-                          BuiltInMethod.AGG_LAMBDA_FACTORY_ACC_ADDER.method),
-                      Expressions.call(lambdaFactory,
-                          
BuiltInMethod.AGG_LAMBDA_FACTORY_ACC_RESULT_SELECTOR.method,
-                          resultSelector))
-                      .appendIfNotNull(keyPhysType.comparer()))));
+              Expressions.call(groupByMultipleMethod,
+                  arguments.appendIfNotNull(keyPhysType.comparer()))));
     } else if (groupCount == 0) {
       final Expression resultSelector =
           builder.append(
diff --git 
a/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java 
b/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
index 1e4a97040c..10b6a823ed 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
@@ -395,6 +395,15 @@ public void send(Row row) {
     }
 
     public void end(Sink sink) throws InterruptedException {
+      if (accumulators.isEmpty() && grouping.isEmpty()) {
+        // The empty grouping set (the grand total) produces a row even when
+        // the input is empty, just as a query with no GROUP BY does.
+        final AccumulatorList list = new AccumulatorList();
+        for (AccumulatorFactory factory : accumulatorFactories) {
+          list.add(factory.get());
+        }
+        accumulators.put(Row.of(), list);
+      }
       for (Map.Entry<Row, AccumulatorList> e : accumulators.entrySet()) {
         final Row key = e.getKey();
         final AccumulatorList list = e.getValue();
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 95331c69a4..2cf5c1140a 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
@@ -395,7 +395,7 @@ public interface RemoveEmptySingleRuleConfig extends 
PruneEmptyRule.Config {
           .withOperandFor(Sort.class, singleRel -> true);
       RemoveEmptySingleRuleConfig AGGREGATE = 
ImmutableRemoveEmptySingleRuleConfig.of()
           .withDescription("PruneEmptyAggregate")
-          .withOperandFor(Aggregate.class, Aggregate::isNotGrandTotal);
+          .withOperandFor(Aggregate.class, aggregate -> 
!aggregate.hasEmptyGroup());
       RemoveEmptySingleRuleConfig WINDOW = 
ImmutableRemoveEmptySingleRuleConfig.of()
           .withDescription("PruneEmptyWindow")
           .withOperandFor(Window.class, singleRel -> true);
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index 5fd9d9a715..76579631be 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -296,6 +296,9 @@ public enum BuiltInMethod {
   GROUP_BY_MULTIPLE(EnumerableDefaults.class, "groupByMultiple",
       Enumerable.class, List.class, Function0.class, Function2.class,
       Function2.class),
+  GROUP_BY_MULTIPLE2(EnumerableDefaults.class, "groupByMultiple",
+      Enumerable.class, List.class, Function0.class, Function2.class,
+      Function2.class, Object.class),
   AGGREGATE(ExtendedEnumerable.class, "aggregate", Object.class,
       Function2.class, Function1.class),
   ORDER_BY(ExtendedEnumerable.class, "orderBy", Function1.class,
diff --git a/core/src/test/java/org/apache/calcite/test/InterpreterTest.java 
b/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
index c138056e72..ade1182ef1 100644
--- a/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
@@ -355,6 +355,22 @@ private static void assertRows(Interpreter interpreter,
             "[Ringo, 1]");
   }
 
+  /** Tests ROLLUP, which is GROUPING SETS with an empty grouping set. */
+  @Test void testAggregateRollup() {
+    rootSchema().add("beatles", new ScannableTableTest.BeatlesTable());
+    sql("select \"j\", count(*) from \"beatles\" group by rollup(\"j\")")
+        .returnsRowsUnordered("[John, 1]", "[Paul, 1]", "[Ringo, 1]",
+            "[George, 1]", "[null, 4]");
+  }
+
+  /** As {@link #testAggregateRollup()}, but the input is empty. The empty
+   * grouping set yields one row, as a query with no GROUP BY does. */
+  @Test void testAggregateRollupOnEmptyInput() {
+    rootSchema().add("beatles", new ScannableTableTest.BeatlesTable());
+    sql("select \"j\", count(*) from \"beatles\" where false group by 
rollup(\"j\")")
+        .returnsRows("[null, 0]");
+  }
+
   /** Tests a GROUP BY query that uses
    * {@link org.apache.calcite.sql.fun.SqlInternalOperators#LITERAL_AGG}. */
   @Test void testAggregateLiteralAgg() {
diff --git 
a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableAggregateTest.java
 
b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableAggregateTest.java
new file mode 100644
index 0000000000..0f3ba51b16
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableAggregateTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.test.enumerable;
+
+import org.apache.calcite.adapter.java.ReflectiveSchema;
+import org.apache.calcite.config.CalciteConnectionProperty;
+import org.apache.calcite.config.Lex;
+import org.apache.calcite.test.CalciteAssert;
+import org.apache.calcite.test.schemata.hr.HrSchema;
+
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link 
org.apache.calcite.adapter.enumerable.EnumerableAggregate}. */
+public class EnumerableAggregateTest {
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7828";>[CALCITE-7828]
+   * Aggregate whose grouping sets include () returns no rows on empty
+   * input</a>.
+   *
+   * <p>ROLLUP(deptno) is GROUPING SETS ((deptno), ()), and the () set yields
+   * one row even when the input is empty, as a query with no GROUP BY does
+   * (CALCITE-1059). */
+  @Test void rollupOnEmptyInput() {
+    tester()
+        .query("select deptno, count(*) as c\n"
+            + "from emps where false group by rollup(deptno)")
+        .explainContains("EnumerableAggregate(group=[{0}], groups=[[{0}, {}]], 
c=[COUNT()])\n"
+            + "  EnumerableValues(tuples=[[]])")
+        .returnsUnordered("deptno=null; c=0");
+  }
+
+  /** As {@link #rollupOnEmptyInput()}, but the input is empty at run time
+   * (not at plan time), so the plan still contains a filter. */
+  @Test void rollupOnEmptyRuntimeInput() {
+    tester()
+        .query("select deptno, count(*) as c\n"
+            + "from emps where deptno > 100 group by rollup(deptno)")
+        .returnsUnordered("deptno=null; c=0");
+  }
+
+  /** As {@link #rollupOnEmptyInput()}, with CUBE and more than one grouping
+   * column. CUBE(deptno, commission) is GROUPING SETS ((deptno, commission),
+   * (deptno), (commission), ()). */
+  @Test void cubeOnEmptyInput() {
+    tester()
+        .query("select deptno, commission, count(*) as c\n"
+            + "from emps where false group by cube(deptno, commission)")
+        .returnsUnordered("deptno=null; commission=null; c=0");
+  }
+
+  /** As {@link #rollupOnEmptyInput()}, with GROUPING SETS. */
+  @Test void groupingSetsWithEmptyGroupOnEmptyInput() {
+    tester()
+        .query("select deptno, count(*) as c\n"
+            + "from emps where false group by grouping sets ((deptno), ())")
+        .explainContains("EnumerableAggregate(group=[{0}], groups=[[{0}, {}]], 
c=[COUNT()])\n"
+            + "  EnumerableValues(tuples=[[]])")
+        .returnsUnordered("deptno=null; c=0");
+  }
+
+  /** A grouping set other than () yields no row when the input is empty. */
+  @Test void groupingSetsWithoutEmptyGroupOnEmptyInput() {
+    tester()
+        .query("select deptno, count(*) as c\n"
+            + "from emps where false group by grouping sets ((deptno), 
(commission))")
+        .returnsCount(0);
+  }
+
+  /** Sanity check: on non-empty input, ROLLUP produces a row per group,
+   * plus the grand-total row. */
+  @Test void rollupOnNonEmptyInput() {
+    tester()
+        .query("select deptno, count(*) as c\n"
+            + "from emps group by rollup(deptno)")
+        .returnsUnordered(
+            "deptno=10; c=3",
+            "deptno=20; c=1",
+            "deptno=null; c=4");
+  }
+
+  /** As {@link #rollupOnEmptyInput()}, with aggregate functions that return
+   * NULL on empty input (SUM), and GROUPING, which reads the indicator of
+   * the synthetic empty-group key. */
+  @Test void rollupOnEmptyInputSumGrouping() {
+    tester()
+        .query("select deptno, grouping(deptno) as g, sum(salary) as s\n"
+            + "from emps where false group by rollup(deptno)")
+        .returnsUnordered("deptno=null; g=1; s=null");
+  }
+
+  private CalciteAssert.AssertThat tester() {
+    return CalciteAssert.that()
+        .with(CalciteConnectionProperty.LEX, Lex.JAVA)
+        .withSchema("s", new ReflectiveSchema(new HrSchema()));
+  }
+}
diff --git a/core/src/test/resources/sql/agg.iq 
b/core/src/test/resources/sql/agg.iq
index 2b035304b8..9770d50a5e 100644
--- a/core/src/test/resources/sql/agg.iq
+++ b/core/src/test/resources/sql/agg.iq
@@ -497,6 +497,48 @@ group by rollup(gender);
 
 !ok
 
+# [CALCITE-7828] Aggregate whose grouping sets include () returns no rows on 
empty input
+# The empty grouping set, (), yields one row, as a query with no GROUP BY
+# These sql programs had been validated in PostgreSQL
+
+select gender, count(*) as c
+from emp
+where false
+group by rollup(gender);
++--------+---+
+| GENDER | C |
++--------+---+
+|        | 0 |
++--------+---+
+(1 row)
+
+!ok
+
+select gender, count(*) as c
+from emp
+where false
+group by grouping sets (gender, ());
++--------+---+
+| GENDER | C |
++--------+---+
+|        | 0 |
++--------+---+
+(1 row)
+
+!ok
+
+select gender, count(*) as c
+from emp
+where false
+group by grouping sets (gender, deptno);
++--------+---+
+| GENDER | C |
++--------+---+
++--------+---+
+(0 rows)
+
+!ok
+
 # ROLLUP plus ORDER BY
 select gender, count(*) as c
 from emp
diff --git 
a/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java 
b/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
index 2687c15131..dbfe7d3bd6 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
@@ -801,13 +801,40 @@ public static <TSource, TKey, TAccumulate, TResult> 
Enumerable<TResult> groupByM
       Function0<TAccumulate> accumulatorInitializer,
       Function2<TAccumulate, TSource, TAccumulate> accumulatorAdder,
       final Function2<TKey, TAccumulate, TResult> resultSelector) {
+    return groupByMultiple(enumerable, keySelectors, accumulatorInitializer,
+        accumulatorAdder, resultSelector, null);
+  }
+
+  /**
+   * Groups the elements of a sequence according to a list of
+   * specified key selector functions, initializing an accumulator for each
+   * group and adding to it each time an element with the same key is seen.
+   * Creates a result value from each accumulator and its key using a
+   * specified function.
+   *
+   * <p>This method exists to support SQL {@code GROUPING SETS}.
+   * It does not correspond to any method in {@link Enumerable}.
+   *
+   * <p>If {@code emptyGroupKey} is not null, it is the key of the empty
+   * grouping set (the one that aggregates over all input rows, like a query
+   * with no GROUP BY). In that case, a single row is produced for that key
+   * even when the input is empty, just as a query with no GROUP BY returns
+   * a row over empty input.
+   */
+  public static <TSource, TKey, TAccumulate, TResult> Enumerable<TResult> 
groupByMultiple(
+      Enumerable<TSource> enumerable, List<Function1<TSource, TKey>> 
keySelectors,
+      Function0<TAccumulate> accumulatorInitializer,
+      Function2<TAccumulate, TSource, TAccumulate> accumulatorAdder,
+      final Function2<TKey, TAccumulate, TResult> resultSelector,
+      @Nullable TKey emptyGroupKey) {
     return groupByMultiple_(
         new HashMap<>(),
         enumerable,
         keySelectors,
         accumulatorInitializer,
         accumulatorAdder,
-        resultSelector);
+        resultSelector,
+        emptyGroupKey);
   }
 
   /**
@@ -1192,9 +1219,12 @@ private static <TSource, TKey, TAccumulate, TResult> 
Enumerable<TResult> groupBy
       List<Function1<TSource, TKey>> keySelectors,
       Function0<TAccumulate> accumulatorInitializer,
       Function2<TAccumulate, TSource, TAccumulate> accumulatorAdder,
-      final Function2<TKey, TAccumulate, TResult> resultSelector) {
+      final Function2<TKey, TAccumulate, TResult> resultSelector,
+      @Nullable TKey emptyGroupKey) {
+    boolean seenRow = false;
     try (Enumerator<TSource> os = enumerable.enumerator()) {
       while (os.moveNext()) {
+        seenRow = true;
         for (Function1<TSource, TKey> keySelector : keySelectors) {
           TSource o = os.current();
           TKey key = keySelector.apply(o);
@@ -1214,6 +1244,12 @@ private static <TSource, TKey, TAccumulate, TResult> 
Enumerable<TResult> groupBy
         }
       }
     }
+    if (!seenRow && emptyGroupKey != null) {
+      // The empty grouping set (the grand total) produces a row even when the
+      // input is empty, just as a query with no GROUP BY does
+      // (see CALCITE-1059).
+      map.put(emptyGroupKey, accumulatorInitializer.apply());
+    }
     return new LookupResultEnumerable<>(map, resultSelector);
   }
 

Reply via email to