Repository navigation
[CALCITE-7828] Aggregate whose grouping sets include () returns no rows on empty input #5339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 @@ | |
| 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 @@ | |
| 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)); | ||
|
Check warning on line 328 in core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregate.java
|
||
| // | ||
| // 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -395,6 +395,15 @@ public void send(Row row) { | |
| } | ||
|
|
||
| public void end(Sink sink) throws InterruptedException { | ||
| if (accumulators.isEmpty() && grouping.isEmpty()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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(); | ||
|
|
||
| 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())); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.