Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -265,10 +267,15 @@
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",
Expand All @@ -280,20 +287,72 @@
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be much better to explain what happens here without references to calcite issues closed long ago.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I had added comments to explain the behavior on its own: the key of the empty grouping set is a constant, built the same way as the key the selector for that grouping set computes for any input row, and passed to groupByMultiple so that the empty grouping set produces its single row when the input is empty, just as a query with no GROUP BY produces a row over an empty input.

// 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));

Check warning on line 328 in core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This block of commented-out lines of code should be removed.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AaFZx7D-88wxWMPxKoyF&open=AaFZx7D-88wxWMPxKoyF&pullRequest=5339
//
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would help a lot the reviewers if you showed in a comment the generated Java code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,15 @@ public void send(Row row) {
}

public void end(Sink sink) throws InterruptedException {
if (accumulators.isEmpty() && grouping.isEmpty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the interpreter is hopelessly unmaintained, but it's fine to fix bugs here and there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and I wouldn't want to invest much in the interpreter either.
The fix is only a few lines in AggregateNode.Grouping.end, and it keeps the two runtimes consistent now that PruneEmptyRules no longer prunes an aggregate with an empty grouping set: without it, the interpreter would still return 0 rows while the enumerable path returns the grand-total row. I've also dropped the stale issue reference from the comment there.

// 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions core/src/test/java/org/apache/calcite/test/InterpreterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()));
}
}
42 changes: 42 additions & 0 deletions core/src/test/resources/sql/agg.iq
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading