This is an automated email from the ASF dual-hosted git repository. jhyde pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 76f26b8319e66c30c6cbfdfba6d5b2839cdedfa2 Author: Julian Hyde <[email protected]> AuthorDate: Wed Sep 2 14:51:53 2020 -0700 [CALCITE-4220] In SqlToRelConverter, use RelBuilder for creating Aggregate Move the code added by "[CALCITE-1824] GROUP_ID returns wrong result" from SqlToRelConverter to RelBuilder. Now GROUP_ID is handled correctly if created via RelBuilder. Fix a bug where, in a query with multiple GROUPING SETS, RelBuilder incorrectly simplifed an Aggregate to a Project. That simplification is valid only with a single groupng set. --- .../sql/validate/AggregatingSelectScope.java | 25 +- .../apache/calcite/sql2rel/SqlToRelConverter.java | 121 +--------- .../org/apache/calcite/tools/PigRelBuilder.java | 15 +- .../java/org/apache/calcite/tools/RelBuilder.java | 268 +++++++++++++++++---- .../org/apache/calcite/test/RelBuilderTest.java | 38 +++ .../org/apache/calcite/test/RelOptRulesTest.java | 7 +- .../apache/calcite/test/SqlToRelConverterTest.java | 7 + .../org/apache/calcite/test/SqlToRelTestBase.java | 3 +- .../apache/calcite/test/SqlToRelConverterTest.xml | 20 ++ core/src/test/resources/sql/agg.iq | 15 ++ .../org/apache/calcite/piglet/PigRelBuilder.java | 5 +- 11 files changed, 333 insertions(+), 191 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java b/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java index b61fec5..279772d 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/AggregatingSelectScope.java @@ -30,11 +30,11 @@ import org.apache.calcite.util.Pair; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableSortedMultiset; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.SortedMap; import java.util.function.Supplier; import static org.apache.calcite.sql.SqlUtil.stripAs; @@ -96,21 +96,18 @@ public class AggregatingSelectScope } } - final SortedMap<ImmutableBitSet, Integer> flatGroupSetCount = - Maps.newTreeMap(ImmutableBitSet.COMPARATOR); + final List<ImmutableBitSet> flatGroupSets = new ArrayList<>(); for (List<ImmutableBitSet> groupSet : Linq4j.product(builder.build())) { - final ImmutableBitSet set = ImmutableBitSet.union(groupSet); - flatGroupSetCount.put(set, flatGroupSetCount.getOrDefault(set, 0) + 1); + flatGroupSets.add(ImmutableBitSet.union(groupSet)); } // For GROUP BY (), we need a singleton grouping set. - if (flatGroupSetCount.isEmpty()) { - flatGroupSetCount.put(ImmutableBitSet.of(), 1); + if (flatGroupSets.isEmpty()) { + flatGroupSets.add(ImmutableBitSet.of()); } return new Resolved(groupAnalyzer.extraExprs, groupAnalyzer.groupExprs, - flatGroupSetCount.keySet(), flatGroupSetCount, - groupAnalyzer.groupExprProjection); + flatGroupSets, groupAnalyzer.groupExprProjection); } finally { groupAnalyzer = null; } @@ -224,23 +221,21 @@ public class AggregatingSelectScope /** Information about an aggregating scope that can only be determined * after validation has occurred. Therefore it cannot be populated when * the scope is created. */ + @SuppressWarnings("UnstableApiUsage") public static class Resolved { public final ImmutableList<SqlNode> extraExprList; public final ImmutableList<SqlNode> groupExprList; public final ImmutableBitSet groupSet; - public final ImmutableList<ImmutableBitSet> groupSets; - public final Map<ImmutableBitSet, Integer> groupSetCount; + public final ImmutableSortedMultiset<ImmutableBitSet> groupSets; public final Map<Integer, Integer> groupExprProjection; Resolved(List<SqlNode> extraExprList, List<SqlNode> groupExprList, Iterable<ImmutableBitSet> groupSets, - Map<ImmutableBitSet, Integer> groupSetCount, Map<Integer, Integer> groupExprProjection) { this.extraExprList = ImmutableList.copyOf(extraExprList); this.groupExprList = ImmutableList.copyOf(groupExprList); this.groupSet = ImmutableBitSet.range(groupExprList.size()); - this.groupSets = ImmutableList.copyOf(groupSets); - this.groupSetCount = ImmutableMap.copyOf(groupSetCount); + this.groupSets = ImmutableSortedMultiset.copyOf(groupSets); this.groupExprProjection = ImmutableMap.copyOf(groupExprProjection); } diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java index af7c390..b941e72 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java @@ -181,7 +181,6 @@ import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; import org.slf4j.Logger; @@ -217,6 +216,7 @@ import static org.apache.calcite.sql.SqlUtil.stripAs; * <p>The public entry points are: {@link #convertQuery}, * {@link #convertExpression(SqlNode)}. */ +@SuppressWarnings("UnstableApiUsage") public class SqlToRelConverter { //~ Static fields/initializers --------------------------------------------- @@ -3020,12 +3020,10 @@ public class SqlToRelConverter { bb.scope.getMonotonicity(groupItem)); } - final RelNode relNode = aggConverter.containsGroupId() - ? rewriteAggregateWithGroupId(bb, r, aggConverter) - : createAggregate(bb, r.groupSet, r.groupSets, - aggConverter.getAggCalls()); - - bb.setRoot(relNode, false); + // Add the aggregator + bb.setRoot( + createAggregate(bb, r.groupSet, r.groupSets.asList(), + aggConverter.getAggCalls()), false); bb.mapRootRelToFieldProjection.put(bb.root, r.groupExprProjection); // Replace sub-queries in having here and modify having to use @@ -3100,104 +3098,6 @@ public class SqlToRelConverter { } /** - * The {@code GROUP_ID()} function is used to distinguish duplicate groups. - * However, as Aggregate normalizes group sets to canonical form (i.e., - * flatten, sorting, redundancy removal), this information is lost in RelNode. - * Therefore, it is impossible to implement the function in runtime. - * - * To fill this gap, an aggregation query that contains {@code GROUP_ID()} function - * will generally be rewritten into UNION when converting to RelNode. - * - * Also see the discussion in JIRA - * <a href="https://issues.apache.org/jira/browse/CALCITE-1824">[CALCITE-1824] - * GROUP_ID returns wrong result</a>. - */ - private RelNode rewriteAggregateWithGroupId(Blackboard bb, - AggregatingSelectScope.Resolved r, AggConverter converter) { - final List<AggregateCall> aggregateCalls = converter.getAggCalls(); - final ImmutableBitSet groupSet = r.groupSet; - final Map<ImmutableBitSet, Integer> groupSetCount = r.groupSetCount; - - final List<String> fieldNamesIfNoRewrite = createAggregate(bb, groupSet, - r.groupSets, aggregateCalls).getRowType().getFieldNames(); - - // If n duplicates exist for a particular grouping, the {@code GROUP_ID()} - // function produces values in the range 0 to n-1. For each value, - // we need to figure out the corresponding group sets. - // - // For example, "... GROUPING SETS (a, a, b, c, c, c, c)" - // (i) The max value of the GROUP_ID() function returns is 3 - // (ii) GROUPING SETS (a, b, c) produces value 0, - // GROUPING SETS (a, c) produces value 1, - // GROUPING SETS (c) produces value 2 - // GROUPING SETS (c) produces value 3 - final Map<Integer, Set<ImmutableBitSet>> groupIdToGroupSets = new HashMap<>(); - int maxGroupId = 0; - for (Map.Entry<ImmutableBitSet, Integer> entry: groupSetCount.entrySet()) { - int groupId = entry.getValue() - 1; - if (groupId > maxGroupId) { - maxGroupId = groupId; - } - for (int i = 0; i <= groupId; i++) { - groupIdToGroupSets.computeIfAbsent(i, - k -> Sets.newTreeSet(ImmutableBitSet.COMPARATOR)) - .add(entry.getKey()); - } - } - - // AggregateCall list without GROUP_ID function - final List<AggregateCall> aggregateCallsWithoutGroupId = new ArrayList<>(); - for (AggregateCall aggregateCall : aggregateCalls) { - if (aggregateCall.getAggregation().kind != SqlKind.GROUP_ID) { - aggregateCallsWithoutGroupId.add(aggregateCall); - } - } - final List<RelNode> projects = new ArrayList<>(); - // For each group id value , we first construct an Aggregate without - // GROUP_ID() function call, and then create a Project node on top of it. - // The Project adds literal value for group id in right position. - for (int groupId = 0; groupId <= maxGroupId; groupId++) { - // Create the Aggregate node without GROUP_ID() call - final ImmutableList<ImmutableBitSet> groupSets = - ImmutableList.copyOf(groupIdToGroupSets.get(groupId)); - final RelNode aggregate = createAggregate(bb, groupSet, - groupSets, aggregateCallsWithoutGroupId); - - // RexLiteral for each GROUP_ID, note the type should be BIGINT - final RelDataType groupIdType = typeFactory.createSqlType(SqlTypeName.BIGINT); - final RexNode groupIdLiteral = rexBuilder.makeExactLiteral( - BigDecimal.valueOf(groupId), groupIdType); - - relBuilder.push(aggregate); - final List<RexNode> selectList = new ArrayList<>(); - final int groupExprLength = r.groupExprList.size(); - // Project fields in group by expressions - for (int i = 0; i < groupExprLength; i++) { - selectList.add(relBuilder.field(i)); - } - // Project fields in aggregate calls - int groupIdCount = 0; - for (int i = 0; i < aggregateCalls.size(); i++) { - if (aggregateCalls.get(i).getAggregation().kind == SqlKind.GROUP_ID) { - selectList.add(groupIdLiteral); - groupIdCount++; - } else { - int ordinal = groupExprLength + i - groupIdCount; - selectList.add(relBuilder.field(ordinal)); - } - } - final RelNode project = relBuilder.project( - selectList, fieldNamesIfNoRewrite).build(); - projects.add(project); - } - // Skip to create Union when there is only one child, i.e., no duplicate group set. - if (projects.size() == 1) { - return projects.get(0); - } - return LogicalUnion.create(projects, true); - } - - /** * Creates an Aggregate. * * <p>In case the aggregate rel changes the order in which it projects @@ -3217,7 +3117,11 @@ public class SqlToRelConverter { */ protected RelNode createAggregate(Blackboard bb, ImmutableBitSet groupSet, ImmutableList<ImmutableBitSet> groupSets, List<AggregateCall> aggCalls) { - return LogicalAggregate.create(bb.root, ImmutableList.of(), groupSet, groupSets, aggCalls); + relBuilder.push(bb.root); + final RelBuilder.GroupKey groupKey = + relBuilder.groupKey(groupSet, (Iterable<ImmutableBitSet>) groupSets); + return relBuilder.aggregate(groupKey, aggCalls) + .build(); } public RexDynamicParam convertDynamicParam( @@ -5493,11 +5397,6 @@ public class SqlToRelConverter { return aggCalls; } - private boolean containsGroupId() { - return aggCalls.stream().anyMatch( - agg -> agg.getAggregation().kind == SqlKind.GROUP_ID); - } - public RelDataTypeFactory getTypeFactory() { return typeFactory; } diff --git a/core/src/main/java/org/apache/calcite/tools/PigRelBuilder.java b/core/src/main/java/org/apache/calcite/tools/PigRelBuilder.java index 0c46267..f41f151 100644 --- a/core/src/main/java/org/apache/calcite/tools/PigRelBuilder.java +++ b/core/src/main/java/org/apache/calcite/tools/PigRelBuilder.java @@ -122,13 +122,12 @@ public class PigRelBuilder extends RelBuilder { public PigRelBuilder group(GroupOption option, Partitioner partitioner, int parallel, Iterable<? extends GroupKey> groupKeys) { - @SuppressWarnings("unchecked") final List<GroupKeyImpl> groupKeyList = - ImmutableList.copyOf((Iterable) groupKeys); + final List<GroupKey> groupKeyList = ImmutableList.copyOf(groupKeys); validateGroupList(groupKeyList); - final int groupCount = groupKeyList.get(0).nodes.size(); + final int groupCount = groupKeyList.get(0).groupKeyCount(); final int n = groupKeyList.size(); - for (Ord<GroupKeyImpl> groupKey : Ord.reverse(groupKeyList)) { + for (Ord<GroupKey> groupKey : Ord.reverse(groupKeyList)) { RelNode r = null; if (groupKey.i < n - 1) { r = build(); @@ -152,13 +151,13 @@ public class PigRelBuilder extends RelBuilder { return this; } - protected void validateGroupList(List<GroupKeyImpl> groupKeyList) { + protected void validateGroupList(List<GroupKey> groupKeyList) { if (groupKeyList.isEmpty()) { throw new IllegalArgumentException("must have at least one group"); } - final int groupCount = groupKeyList.get(0).nodes.size(); - for (GroupKeyImpl groupKey : groupKeyList) { - if (groupKey.nodes.size() != groupCount) { + final int groupCount = groupKeyList.get(0).groupKeyCount(); + for (GroupKey groupKey : groupKeyList) { + if (groupKey.groupKeyCount() != groupCount) { throw new IllegalArgumentException("group key size mismatch"); } } diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java index af33cc4..bcd860b 100644 --- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java +++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java @@ -105,8 +105,11 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMultiset; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Multiset; +import com.google.common.collect.Sets; import java.math.BigDecimal; import java.util.AbstractList; @@ -1299,8 +1302,7 @@ public class RelBuilder { /** Creates a {@link Project} of all original fields, plus the given list of * expressions. */ public RelBuilder projectPlus(Iterable<RexNode> nodes) { - final ImmutableList.Builder<RexNode> builder = ImmutableList.builder(); - return project(builder.addAll(fields()).addAll(nodes).build()); + return project(Iterables.concat(fields(), nodes)); } /** Creates a {@link Project} of all original fields, except the given @@ -1652,8 +1654,7 @@ public class RelBuilder { .collect(Collectors.toList())); } - /** Creates an {@link Aggregate} with multiple - * calls. */ + /** Creates an {@link Aggregate} with multiple calls. */ public RelBuilder aggregate(GroupKey groupKey, Iterable<AggCall> aggCalls) { final Registrar registrar = new Registrar(fields(), peek().getRowType().getFieldNames()); @@ -1673,22 +1674,26 @@ public class RelBuilder { } if (registrar.extraNodes.size() == fields().size()) { final Boolean unique = mq.areColumnsUnique(peek(), groupSet); - if (unique != null && unique) { + if (unique != null && unique + && !config.aggregateUnique() + && groupKey_.isSimple()) { // Rel is already unique. return project(fields(groupSet)); } } final Double maxRowCount = mq.getMaxRowCount(peek()); - if (maxRowCount != null && maxRowCount <= 1D) { + if (maxRowCount != null && maxRowCount <= 1D + && !config.aggregateUnique() + && groupKey_.isSimple()) { // If there is at most one row, rel is already unique. return project(fields(groupSet)); } } + ImmutableList<ImmutableBitSet> groupSets; if (groupKey_.nodeLists != null) { final int sizeBefore = registrar.extraNodes.size(); - final SortedSet<ImmutableBitSet> groupSetSet = - new TreeSet<>(ImmutableBitSet.ORDERING); + final List<ImmutableBitSet> groupSetList = new ArrayList<>(); for (ImmutableList<RexNode> nodeList : groupKey_.nodeLists) { final ImmutableBitSet groupSet2 = ImmutableBitSet.of(registrar.registerExpressions(nodeList)); @@ -1696,9 +1701,16 @@ public class RelBuilder { throw new IllegalArgumentException("group set element " + nodeList + " must be a subset of group key"); } - groupSetSet.add(groupSet2); + groupSetList.add(groupSet2); + } + final ImmutableSortedMultiset<ImmutableBitSet> groupSetMultiset = + ImmutableSortedMultiset.copyOf(ImmutableBitSet.COMPARATOR, + groupSetList); + if (Iterables.any(aggCalls, RelBuilder::isGroupId)) { + return rewriteAggregateWithGroupId(groupSet, groupSetMultiset, + ImmutableList.copyOf(aggCalls)); } - groupSets = ImmutableList.copyOf(groupSetSet); + groupSets = ImmutableList.copyOf(groupSetMultiset.elementSet()); if (registrar.extraNodes.size() > sizeBefore) { throw new IllegalArgumentException( "group sets contained expressions not in group key: " @@ -1708,14 +1720,9 @@ public class RelBuilder { } else { groupSets = ImmutableList.of(groupSet); } + for (AggCall aggCall : aggCalls) { - if (aggCall instanceof AggCallImpl) { - final AggCallImpl aggCall1 = (AggCallImpl) aggCall; - registrar.registerExpressions(aggCall1.operands); - if (aggCall1.filter != null) { - registrar.registerExpression(aggCall1.filter); - } - } + ((AggCallPlus) aggCall).register(registrar); } project(registrar.extraNodes); rename(registrar.names); @@ -1723,35 +1730,8 @@ public class RelBuilder { RelNode r = frame.rel; final List<AggregateCall> aggregateCalls = new ArrayList<>(); for (AggCall aggCall : aggCalls) { - final AggregateCall aggregateCall; - if (aggCall instanceof AggCallImpl) { - final AggCallImpl aggCall1 = (AggCallImpl) aggCall; - final List<Integer> args = - registrar.registerExpressions(aggCall1.operands); - final int filterArg = aggCall1.filter == null ? -1 - : registrar.registerExpression(aggCall1.filter); - if (aggCall1.distinct && !aggCall1.aggFunction.isQuantifierAllowed()) { - throw new IllegalArgumentException("DISTINCT not allowed"); - } - if (aggCall1.filter != null && !aggCall1.aggFunction.allowsFilter()) { - throw new IllegalArgumentException("FILTER not allowed"); - } - RelCollation collation = - RelCollations.of(aggCall1.orderKeys - .stream() - .map(orderKey -> - collation(orderKey, RelFieldCollation.Direction.ASCENDING, - null, Collections.emptyList())) - .collect(Collectors.toList())); - aggregateCall = - AggregateCall.create(aggCall1.aggFunction, aggCall1.distinct, - aggCall1.approximate, - aggCall1.ignoreNulls, args, filterArg, collation, - groupSet.cardinality(), r, null, aggCall1.alias); - } else { - aggregateCall = ((AggCallImpl2) aggCall).aggregateCall; - } - aggregateCalls.add(aggregateCall); + aggregateCalls.add( + ((AggCallPlus) aggCall).aggregateCall(registrar, groupSet, r)); } assert ImmutableBitSet.ORDERING.isStrictlyOrdered(groupSets) : groupSets; @@ -1880,6 +1860,95 @@ public class RelBuilder { return this; } + /** + * The {@code GROUP_ID()} function is used to distinguish duplicate groups. + * However, as Aggregate normalizes group sets to canonical form (i.e., + * flatten, sorting, redundancy removal), this information is lost in RelNode. + * Therefore, it is impossible to implement the function in runtime. + * + * <p>To fill this gap, an aggregation query that contains {@code GROUP_ID()} + * function will generally be rewritten into UNION when converting to RelNode. + * + * <p>Also see the discussion in + * <a href="https://issues.apache.org/jira/browse/CALCITE-1824">[CALCITE-1824] + * GROUP_ID returns wrong result</a>. + */ + private RelBuilder rewriteAggregateWithGroupId(ImmutableBitSet groupSet, + ImmutableSortedMultiset<ImmutableBitSet> groupSets, + List<AggCall> aggregateCalls) { + final List<String> fieldNamesIfNoRewrite = + Aggregate.deriveRowType(getTypeFactory(), peek().getRowType(), false, + groupSet, groupSets.asList(), + aggregateCalls.stream().map(c -> ((AggCallPlus) c).aggregateCall()) + .collect(Util.toImmutableList())).getFieldNames(); + + // If n duplicates exist for a particular grouping, the {@code GROUP_ID()} + // function produces values in the range 0 to n-1. For each value, + // we need to figure out the corresponding group sets. + // + // For example, "... GROUPING SETS (a, a, b, c, c, c, c)" + // (i) The max value of the GROUP_ID() function returns is 3 + // (ii) GROUPING SETS (a, b, c) produces value 0, + // GROUPING SETS (a, c) produces value 1, + // GROUPING SETS (c) produces value 2 + // GROUPING SETS (c) produces value 3 + final Map<Integer, Set<ImmutableBitSet>> groupIdToGroupSets = new HashMap<>(); + int maxGroupId = 0; + for (Multiset.Entry<ImmutableBitSet> entry: groupSets.entrySet()) { + int groupId = entry.getCount() - 1; + if (groupId > maxGroupId) { + maxGroupId = groupId; + } + for (int i = 0; i <= groupId; i++) { + groupIdToGroupSets.computeIfAbsent(i, + k -> Sets.newTreeSet(ImmutableBitSet.COMPARATOR)) + .add(entry.getElement()); + } + } + + // AggregateCall list without GROUP_ID function + final List<AggCall> aggregateCallsWithoutGroupId = + new ArrayList<>(aggregateCalls); + aggregateCallsWithoutGroupId.removeIf(RelBuilder::isGroupId); + + // For each group id value, we first construct an Aggregate without + // GROUP_ID() function call, and then create a Project node on top of it. + // The Project adds literal value for group id in right position. + final Frame frame = stack.pop(); + for (int groupId = 0; groupId <= maxGroupId; groupId++) { + // Create the Aggregate node without GROUP_ID() call + stack.push(frame); + aggregate(groupKey(groupSet, groupIdToGroupSets.get(groupId)), + aggregateCallsWithoutGroupId); + + final List<RexNode> selectList = new ArrayList<>(); + final int groupExprLength = groupSet.cardinality(); + // Project fields in group by expressions + for (int i = 0; i < groupExprLength; i++) { + selectList.add(field(i)); + } + // Project fields in aggregate calls + int groupIdCount = 0; + for (int i = 0; i < aggregateCalls.size(); i++) { + if (isGroupId(aggregateCalls.get(i))) { + selectList.add( + getRexBuilder().makeExactLiteral(BigDecimal.valueOf(groupId), + getTypeFactory().createSqlType(SqlTypeName.BIGINT))); + groupIdCount++; + } else { + selectList.add(field(groupExprLength + i - groupIdCount)); + } + } + project(selectList, fieldNamesIfNoRewrite); + } + + return union(true, maxGroupId + 1); + } + + private static boolean isGroupId(AggCall c) { + return ((AggCallPlus) c).op().kind == SqlKind.GROUP_ID; + } + private RelBuilder setOp(boolean all, SqlKind kind, int n) { List<RelNode> inputs = new LinkedList<>(); for (int i = 0; i < n; i++) { @@ -2787,6 +2856,24 @@ public class RelBuilder { AggCall distinct(); } + /** Internal methods shared by all implementations of {@link AggCall}. */ + private interface AggCallPlus extends AggCall { + /** Returns the aggregate function. */ + SqlAggFunction op(); + + /** Returns an {@link AggregateCall} that is approximately equivalent + * to this {@code AggCall} and is good for certain things, such as deriving + * field names. */ + AggregateCall aggregateCall(); + + /** Converts this {@code AggCall} to a good {@link AggregateCall}. */ + AggregateCall aggregateCall(Registrar registrar, ImmutableBitSet groupSet, + RelNode r); + + /** Registers expressions in operands and filters. */ + void register(Registrar registrar); + } + /** Information necessary to create the GROUP BY clause of an Aggregate. * * @see RelBuilder#groupKey */ @@ -2795,13 +2882,16 @@ public class RelBuilder { * * <p>Used to assign field names in the {@code group} operation. */ GroupKey alias(String alias); + + /** Returns the number of columns in the group key. */ + int groupKeyCount(); } /** Implementation of {@link RelBuilder.GroupKey}. */ - public static class GroupKeyImpl implements GroupKey { - public final ImmutableList<RexNode> nodes; - public final ImmutableList<ImmutableList<RexNode>> nodeLists; - public final String alias; + static class GroupKeyImpl implements GroupKey { + final ImmutableList<RexNode> nodes; + final ImmutableList<ImmutableList<RexNode>> nodeLists; + final String alias; GroupKeyImpl(ImmutableList<RexNode> nodes, ImmutableList<ImmutableList<RexNode>> nodeLists, String alias) { @@ -2814,15 +2904,23 @@ public class RelBuilder { return alias == null ? nodes.toString() : nodes + " as " + alias; } + @Override public int groupKeyCount() { + return nodes.size(); + } + public GroupKey alias(String alias) { return Objects.equals(this.alias, alias) ? this : new GroupKeyImpl(nodes, nodeLists, alias); } + + boolean isSimple() { + return nodeLists == null || nodeLists.size() == 1; + } } /** Implementation of {@link AggCall}. */ - private class AggCallImpl implements AggCall { + private class AggCallImpl implements AggCallPlus { private final SqlAggFunction aggFunction; private final boolean distinct; private final boolean approximate; @@ -2879,6 +2977,46 @@ public class RelBuilder { return b.toString(); } + @Override public SqlAggFunction op() { + return aggFunction; + } + + @Override public AggregateCall aggregateCall() { + return AggregateCall.create(aggFunction, distinct, approximate, + ignoreNulls, ImmutableList.of(), -1, null, null, alias); + } + + @Override public AggregateCall aggregateCall(Registrar registrar, + ImmutableBitSet groupSet, RelNode r) { + final List<Integer> args = + registrar.registerExpressions(this.operands); + final int filterArg = this.filter == null ? -1 + : registrar.registerExpression(this.filter); + if (this.distinct && !this.aggFunction.isQuantifierAllowed()) { + throw new IllegalArgumentException("DISTINCT not allowed"); + } + if (this.filter != null && !this.aggFunction.allowsFilter()) { + throw new IllegalArgumentException("FILTER not allowed"); + } + RelCollation collation = + RelCollations.of(this.orderKeys + .stream() + .map(orderKey -> + collation(orderKey, RelFieldCollation.Direction.ASCENDING, + null, Collections.emptyList())) + .collect(Collectors.toList())); + return AggregateCall.create(aggFunction, distinct, approximate, + ignoreNulls, args, filterArg, collation, groupSet.cardinality(), r, + null, alias); + } + + @Override public void register(Registrar registrar) { + registrar.registerExpressions(operands); + if (filter != null) { + registrar.registerExpression(filter); + } + } + public AggCall sort(Iterable<RexNode> orderKeys) { final ImmutableList<RexNode> orderKeyList = ImmutableList.copyOf(orderKeys); @@ -2934,7 +3072,7 @@ public class RelBuilder { /** Implementation of {@link AggCall} that wraps an * {@link AggregateCall}. */ - private static class AggCallImpl2 implements AggCall { + private static class AggCallImpl2 implements AggCallPlus { private final AggregateCall aggregateCall; AggCallImpl2(AggregateCall aggregateCall) { @@ -2945,6 +3083,23 @@ public class RelBuilder { return aggregateCall.toString(); } + @Override public SqlAggFunction op() { + return aggregateCall.getAggregation(); + } + + @Override public AggregateCall aggregateCall() { + return aggregateCall; + } + + @Override public AggregateCall aggregateCall(Registrar registrar, + ImmutableBitSet groupSet, RelNode r) { + return aggregateCall; + } + + @Override public void register(Registrar registrar) { + // nothing to do + } + public AggCall sort(Iterable<RexNode> orderKeys) { throw new UnsupportedOperationException(); } @@ -3231,6 +3386,15 @@ public class RelBuilder { /** Sets {@link #simplify}. */ Config withSimplify(boolean simplify); + + /** Whether to create an Aggregate even if we know that the input is + * already unique; default false. */ + @ImmutableBeans.Property + @ImmutableBeans.BooleanDefault(false) + boolean aggregateUnique(); + + /** Sets {@link #aggregateUnique()}. */ + Config withAggregateUnique(boolean aggregateUnique); } /** Creates a {@link RelBuilder.Config}. diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java index 610905e..ea74078 100644 --- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java @@ -1569,6 +1569,44 @@ public class RelBuilderTest { } } + @Test void testAggregateOneRow() { + final Function<RelBuilder, RelNode> f = builder -> + builder.values(new String[] {"a", "b"}, 1, 2) + .aggregate(builder.groupKey(1)) + .build(); + final String plan = "LogicalProject(b=[$1])\n" + + " LogicalValues(tuples=[[{ 1, 2 }]])\n"; + assertThat(f.apply(createBuilder()), hasTree(plan)); + + final String plan2 = "LogicalAggregate(group=[{1}])\n" + + " LogicalValues(tuples=[[{ 1, 2 }]])\n"; + assertThat(f.apply(createBuilder(c -> c.withAggregateUnique(true))), + hasTree(plan2)); + } + + /** Tests that we do not convert an Aggregate to a Project if there are + * multiple group sets. */ + @Test void testAggregateGroupingSetsOneRow() { + final Function<RelBuilder, RelNode> f = builder -> { + final List<Integer> list01 = Arrays.asList(0, 1); + final List<Integer> list0 = Collections.singletonList(0); + final List<Integer> list1 = Collections.singletonList(1); + return builder.values(new String[] {"a", "b"}, 1, 2) + .aggregate( + builder.groupKey(builder.fields(list01), + ImmutableList.of(builder.fields(list0), + builder.fields(list1), + builder.fields(list01)))) + .build(); + }; + final String plan = "" + + "LogicalAggregate(group=[{0, 1}], groups=[[{0, 1}, {0}, {1}]])\n" + + " LogicalValues(tuples=[[{ 1, 2 }]])\n"; + assertThat(f.apply(createBuilder()), hasTree(plan)); + assertThat(f.apply(createBuilder(c -> c.withAggregateUnique(true))), + hasTree(plan)); + } + @Test void testDistinct() { // Equivalent SQL: // SELECT DISTINCT deptno diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java index 42a3c87..b049154 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -241,6 +241,7 @@ class RelOptRulesTest extends RelOptTestBase { + " group by empno, deptno))\n" + "or deptno < 40 + 60"; checkSubQuery(sql) + .withRelBuilderConfig(b -> b.withAggregateUnique(true)) .withRule(CoreRules.FILTER_REDUCE_EXPRESSIONS) .check(); } @@ -4488,6 +4489,7 @@ class RelOptRulesTest extends RelOptTestBase { + "left outer join sales.dept as d on e.empno < d.deptno\n" + "group by e.empno,d.deptno"; sql(sql) + .withRelBuilderConfig(b -> b.withAggregateUnique(true)) .withPreRule(CoreRules.AGGREGATE_PROJECT_MERGE) .withRule(CoreRules.AGGREGATE_JOIN_TRANSPOSE_EXTENDED) .checkUnchanged(); @@ -4577,6 +4579,7 @@ class RelOptRulesTest extends RelOptTestBase { + "join sales.dept as d on e.empno < d.deptno\n" + "group by e.empno,d.deptno"; sql(sql) + .withRelBuilderConfig(b -> b.withAggregateUnique(true)) .withPreRule(CoreRules.AGGREGATE_PROJECT_MERGE) .withRule(CoreRules.AGGREGATE_JOIN_TRANSPOSE_EXTENDED) .checkUnchanged(); @@ -5022,6 +5025,7 @@ class RelOptRulesTest extends RelOptTestBase { @Test void testAggregateRemove2() { final String sql = "select distinct empno, deptno from sales.emp\n"; sql(sql) + .withRelBuilderConfig(b -> b.withAggregateUnique(true)) .withRule(CoreRules.AGGREGATE_REMOVE, CoreRules.PROJECT_MERGE) .check(); @@ -5988,7 +5992,8 @@ class RelOptRulesTest extends RelOptTestBase { @Test void testWhereInCorrelated() { final String sql = "select sal from emp where empno IN (\n" + " select deptno from dept where emp.job = dept.name)"; - checkSubQuery(sql).withLateDecorrelation(true).check(); + checkSubQuery(sql).withLateDecorrelation(true) + .check(); } @Test void testWhereExpressionInCorrelated() { diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java index 44d46ef..0f76773 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java @@ -537,6 +537,13 @@ class SqlToRelConverterTest extends SqlToRelTestBase { sql(sql).ok(); } + @Test void testGroupingSetsRepeated() { + final String sql = "select deptno, group_id()\n" + + "from emp\n" + + "group by grouping sets (deptno, (), deptno)"; + sql(sql).ok(); + } + @Test void testGroupingSetsWith() { final String sql = "with t(a, b, c, d) as (values (1, 2, 3, 4))\n" + "select 1 from t\n" diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java index 68bef38..e74b67d 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java +++ b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java @@ -115,7 +115,8 @@ public abstract class SqlToRelTestBase { c.withTrimUnusedFields(true) .withExpand(true) .addRelBuilderConfigTransform(b -> - b.withPruneInputOfAggregate(false))); + b.withAggregateUnique(true) + .withPruneInputOfAggregate(false))); } protected Tester getTesterWithDynamicTable() { diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml index adb27d2..46fdbd0 100644 --- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml @@ -1406,6 +1406,26 @@ LogicalProject(EXPR$0=[JSON_TYPE(ROW($2.TYPE, $2.DESC, ROW($2.OTHERS.A, $2.OTHER ]]> </Resource> </TestCase> + <TestCase name="testGroupingSetsRepeated"> + <Resource name="sql"> + <![CDATA[select deptno, group_id() +from emp +group by grouping sets (deptno, (), deptno)]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalUnion(all=[true]) + LogicalProject(DEPTNO=[$0], EXPR$1=[0:BIGINT]) + LogicalAggregate(group=[{0}], groups=[[{0}, {}]]) + LogicalProject(DEPTNO=[$7]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) + LogicalProject(DEPTNO=[$0], EXPR$1=[1:BIGINT]) + LogicalAggregate(group=[{0}]) + LogicalProject(DEPTNO=[$7]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + </TestCase> <TestCase name="testOrder"> <Resource name="sql"> <![CDATA[select empno from emp order by empno, empno desc]]> diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index c3573e6..16cb93f 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -351,6 +351,21 @@ select deptno + 1, count(*) as c from emps group by grouping sets ((), (deptno + !ok +# GROUPING SETS on single-row relation returns multiple rows +select 1 as c +from (values ('a', 'b')) as t (a, b) +group by grouping sets ((a), (b), (b, a)); ++---+ +| C | ++---+ +| 1 | +| 1 | +| 1 | ++---+ +(3 rows) + +!ok + # CUBE select deptno + 1, count(*) as c from emp group by cube(deptno, gender); +--------+---+ diff --git a/piglet/src/main/java/org/apache/calcite/piglet/PigRelBuilder.java b/piglet/src/main/java/org/apache/calcite/piglet/PigRelBuilder.java index 6af2f24..93afa88 100644 --- a/piglet/src/main/java/org/apache/calcite/piglet/PigRelBuilder.java +++ b/piglet/src/main/java/org/apache/calcite/piglet/PigRelBuilder.java @@ -384,9 +384,8 @@ public class PigRelBuilder extends RelBuilder { * @return This builder */ public RelBuilder cogroup(Iterable<? extends GroupKey> groupKeys) { - @SuppressWarnings("unchecked") final List<GroupKeyImpl> groupKeyList = - ImmutableList.copyOf((Iterable) groupKeys); - final int groupCount = groupKeyList.get(0).nodes.size(); + final List<GroupKey> groupKeyList = ImmutableList.copyOf(groupKeys); + final int groupCount = groupKeyList.get(0).groupKeyCount(); // Pull out all relations needed for the group final int numRels = groupKeyList.size();
