This is an automated email from the ASF dual-hosted git repository. amashenkov pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 8f6a1f99e71 IGNITE-25872 Sql. Support GROUPING aggregate function (#6479) 8f6a1f99e71 is described below commit 8f6a1f99e71c4d743a73e627610831343fdf8f9c Author: Andrew V. Mashenkov <amashen...@users.noreply.github.com> AuthorDate: Wed Sep 3 10:42:11 2025 +0300 IGNITE-25872 Sql. Support GROUPING aggregate function (#6479) --- .../internal/sql/engine/ItAggregatesTest.java | 72 +++++++- .../group1/aggregate/group/test_grouping_sets.test | 149 ++++++++++++++++ .../group/test_multicolumn_grouping_sets.test | 106 ++++++++++++ .../engine/exec/exp/agg/AccumulatorWrapper.java | 3 + .../sql/engine/exec/exp/agg/Accumulators.java | 65 +++++++ .../engine/exec/exp/agg/AccumulatorsFactory.java | 19 ++- .../sql/engine/exec/exp/agg/AggregateRow.java | 21 ++- .../sql/engine/exec/rel/HashAggregateNode.java | 4 +- .../sql/engine/exec/rel/SortAggregateNode.java | 2 +- .../sql/engine/prepare/IgniteSqlValidator.java | 8 +- .../sql/engine/rel/agg/MapReduceAggregates.java | 27 ++- .../sql/engine/sql/fun/IgniteSqlOperatorTable.java | 1 + .../internal/sql/engine/util/IgniteResource.java | 5 + .../ignite/internal/sql/docs/OperatorListTest.java | 1 + .../exec/exp/agg/GroupingAccumulatorTest.java | 189 +++++++++++++++++++++ .../planner/AbstractAggregatePlannerTest.java | 35 +++- .../sql/engine/planner/AggregatePlannerTest.java | 12 ++ .../planner/ColocatedHashAggregatePlannerTest.java | 12 ++ .../planner/ColocatedSortAggregatePlannerTest.java | 12 ++ .../planner/MapReduceHashAggregatePlannerTest.java | 12 ++ .../planner/MapReduceSortAggregatePlannerTest.java | 12 ++ .../sql/engine/planner/TpcdsQueryPlannerTest.java | 4 +- .../src/test/resources/docs/operator_list.txt | 3 + .../src/test/resources/tpcds/plan/q27.plan | 68 ++++++++ .../test/resources/tpcds/plan/q27_colocated.plan | 68 ++++++++ 25 files changed, 887 insertions(+), 23 deletions(-) diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItAggregatesTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItAggregatesTest.java index ea3973dec7a..c5f9d9f11f4 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItAggregatesTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItAggregatesTest.java @@ -29,7 +29,10 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; +import org.apache.ignite.internal.lang.IgniteStringFormatter; import org.apache.ignite.internal.sql.BaseSqlIntegrationTest; import org.apache.ignite.internal.sql.engine.hint.IgniteHint; import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem; @@ -37,6 +40,7 @@ import org.apache.ignite.internal.sql.engine.util.HintUtils; import org.apache.ignite.internal.sql.engine.util.QueryChecker; import org.apache.ignite.internal.testframework.WithSystemProperty; import org.apache.ignite.lang.IgniteException; +import org.apache.ignite.sql.SqlException; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -515,20 +519,82 @@ public class ItAggregatesTest extends BaseSqlIntegrationTest { assertQuery("SELECT str_col, SUM(int_col), COUNT(str_col) FROM test_str_int_real_dec GROUP BY GROUPING SETS " + "( (str_col, int_col), (str_col), (int_col), () ) HAVING SUM(int_col) > 0") .disableRules(rules) + // empty group .returns(null, 80L, 4L) + // group (str_col, int_col) .returns("s1", 10L, 1L) - .returns("s3", 40L, 1L) .returns("s1", 20L, 1L) .returns("s2", 10L, 1L) - .returns("s2", 10L, 1L) .returns("s3", 40L, 1L) + // group (str_col) .returns("s1", 30L, 2L) + .returns("s2", 10L, 1L) + .returns("s3", 40L, 1L) + // group (int_col) .returns(null, 40L, 1L) .returns(null, 20L, 2L) .returns(null, 20L, 1L) .check(); } + @ParameterizedTest + @MethodSource("rulesForGroupingSets") + public void testGroupingFunction(String[] rules) { + sql("DELETE FROM test_str_int_real_dec"); + + sql("DELETE FROM test_str_int_real_dec"); + + sql("INSERT INTO test_str_int_real_dec(id, str_col, int_col) VALUES (1, 's1', 10)"); + sql("INSERT INTO test_str_int_real_dec(id, str_col, int_col) VALUES (2, 's1', 20)"); + sql("INSERT INTO test_str_int_real_dec(id, str_col, int_col) VALUES (3, 's2', 10)"); + sql("INSERT INTO test_str_int_real_dec(id, str_col, int_col) VALUES (4, 's3', 40)"); + + assertQuery("SELECT GROUPING(str_col), str_col FROM test_str_int_real_dec GROUP BY GROUPING SETS ((str_col))") + .disableRules(rules) + .returns(1L, "s1") + .returns(1L, "s2") + .returns(1L, "s3") + .check(); + + assertQuery("SELECT GROUPING(str_col), str_col FROM test_str_int_real_dec GROUP BY GROUPING SETS ((str_col), (str_col))") + .disableRules(rules) + .returns(1L, "s1") + .returns(1L, "s1") + .returns(1L, "s2") + .returns(1L, "s2") + .returns(1L, "s3") + .returns(1L, "s3") + .check(); + + assertQuery("SELECT GROUPING(int_col, str_col), GROUPING(int_col)," + + "str_col, SUM(int_col), COUNT(str_col) FROM test_str_int_real_dec GROUP BY GROUPING SETS " + + "( (str_col, int_col), (str_col), (int_col), () ) HAVING SUM(int_col) > 0") + .disableRules(rules) + // group (str_col, int_col) + .returns(3L, 1L, "s1", 10L, 1L) + .returns(3L, 1L, "s1", 20L, 1L) + .returns(3L, 1L, "s2", 10L, 1L) + .returns(3L, 1L, "s3", 40L, 1L) + // group (str_col) + .returns(1L, 0L, "s1", 30L, 2L) + .returns(1L, 0L, "s2", 10L, 1L) + .returns(1L, 0L, "s3", 40L, 1L) + // group (int_col) + .returns(2L, 1L, null, 20L, 2L) + .returns(2L, 1L, null, 20L, 1L) + .returns(2L, 1L, null, 40L, 1L) + // empty group + .returns(0L, 0L, null, 80L, 4L) + .check(); + + String invalidQuery = IgniteStringFormatter.format( + "SELECT GROUPING({}), str_col FROM test_str_int_real_dec GROUP BY GROUPING SETS ((str_col))", + IntStream.rangeClosed(1, 64).mapToObj(i -> "str_col").collect(Collectors.joining(","))); + assertThrows(SqlException.class, () -> sql(invalidQuery), + "Invalid number of arguments to function ''GROUPING''. Was expecting number of agruments in range [1, 63]"); + + } + @ParameterizedTest @MethodSource("rulesForGroupingSets") public void testDuplicateGroupingSets(String[] rules) { @@ -760,7 +826,7 @@ public class ItAggregatesTest extends BaseSqlIntegrationTest { new String[]{"ColocatedHashAggregateConverterRule", "ColocatedSortAggregateConverterRule"}, // Use colocated aggregates grouping sets - new String[]{"MapReduceHashAggregateConverterRule", "ColocatedSortAggregateConverterRule"} + new String[]{"MapReduceHashAggregateConverterRule", "MapReduceSortAggregateConverterRule"} ); return rules.stream().map(Object.class::cast).map(Arguments::of); diff --git a/modules/sql-engine/src/integrationTest/sql/group1/aggregate/group/test_grouping_sets.test b/modules/sql-engine/src/integrationTest/sql/group1/aggregate/group/test_grouping_sets.test new file mode 100644 index 00000000000..051533ad1fb --- /dev/null +++ b/modules/sql-engine/src/integrationTest/sql/group1/aggregate/group/test_grouping_sets.test @@ -0,0 +1,149 @@ +# name: test/sql/aggregate/group/test_grouping_sets.test +# description: Test group by with rollup and cube +# feature: T431 (GROUPING operation), T433 (Multiargument GROUPING function) +# group: [group] + +# Single group +query I +SELECT GROUPING(a) +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a)) +---- +1 +1 + +query II rowsort +SELECT GROUPING(a), a +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a)) +---- +1 1 +1 4 + +query II rowsort +SELECT GROUPING(a), SUM(c) +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a)) +---- +1 9 +1 6 + +# Multiple groups +query III rowsort +SELECT GROUPING(a), GROUPING(b), GROUPING(c) +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a), (b), (c)) +---- +1 0 0 +1 0 0 +0 1 0 +0 1 0 +0 0 1 +0 0 1 + +query IIII +SELECT GROUPING(a), GROUPING(b), a, b +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a), (b)) +---- +1 0 1 NULL +1 0 4 NULL +0 1 NULL 2 +0 1 NULL NULL + +query IIII rowsort +SELECT GROUPING(a), SUM(a), GROUPING(b), SUM(c) +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a), (b)) +---- +0 1 1 3 +0 5 1 12 +1 2 0 9 +1 4 0 6 + +# Duplicate groups +query I +SELECT GROUPING(b) as g1 +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((b), (b)) +---- +1 +1 +1 +1 + +query II rowsort +SELECT GROUPING(b), b +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((b), (b)) +---- +1 2 +1 2 +1 NULL +1 NULL + +query II rowsort +SELECT GROUPING(b) as g1, SUM(c) as g2 +FROM (VALUES (1, 2, 3), (4, NULL, 6), (1, NULL, 6)) AS t (a, b, c) +GROUP BY GROUPING SETS ((b), (b)) +---- +1 3 +1 3 +1 12 +1 12 + +# Empty group +query IIRR rowsort +SELECT GROUPING(city_id), GROUPING(category_id), min(price), max(price) +FROM (VALUES +(2, 6, 21500.00), +(2, 6, 22900.00), +(2, 6, 23500.00), +(2, 7, 17800.00), +(4, 6, 20000.00), +(4, 8, 37000.00), +(4, 9, 75600.00) +) AS t (city_id, category_id, price) +GROUP BY GROUPING SETS ((city_id), (category_id), ()) +---- +1 0 17800.00 23500.00 +1 0 20000.00 75600.00 +0 1 17800.00 17800.00 +0 1 20000.00 23500.00 +0 1 37000.00 37000.00 +0 1 75600.00 75600.00 +0 0 17800.00 75600.00 + +skipif ignite3 +# https://issues.apache.org/jira/browse/IGNITE-26335 Conversion to relational algebra failed to preserve datatypes +query IIT rowsort +SELECT GROUPING(id), GROUPING(id), id +FROM ( VALUES +('c4a0327c-44be-416d-ae90-75c05079789f'::UUID, 1), +('367fc6f1-40c3-4237-8545-3fd102d29134'::UUID, 2) +) as t(val, id) +GROUP BY ALL GROUPING SETS (id) +---- +1 1 c4a0327c-44be-416d-ae90-75c05079789f +1 1 c4a0327c-44be-416d-ae90-75c05079789f +1 1 367fc6f1-40c3-4237-8545-3fd102d29134 +1 1 367fc6f1-40c3-4237-8545-3fd102d29134 + +skipif ignite3 +# https://issues.apache.org/jira/browse/IGNITE-26335 Conversion to relational algebra failed while rewriting GROUPING SETS to UNION +# Duplicate groups +query IIT rowsort +SELECT GROUPING(val), GROUPING(val), val +FROM ( VALUES +(1, 1), +(1, 2), +(1, 3) +) as t(id, val) +GROUP BY ALL GROUPING SETS ((val), (val)) +---- +1 1 1 +1 1 1 +1 1 2 +1 1 2 +1 1 3 +1 1 3 diff --git a/modules/sql-engine/src/integrationTest/sql/group1/aggregate/group/test_multicolumn_grouping_sets.test b/modules/sql-engine/src/integrationTest/sql/group1/aggregate/group/test_multicolumn_grouping_sets.test new file mode 100644 index 00000000000..8b20715393d --- /dev/null +++ b/modules/sql-engine/src/integrationTest/sql/group1/aggregate/group/test_multicolumn_grouping_sets.test @@ -0,0 +1,106 @@ +# name: test/sql/aggregate/group/test_grouping_sets.test +# description: Test group by with rollup and cube +# feature: T431 (GROUPING operation), T433 (Multiargument GROUPING function) +# group: [group] + +query IIII rowsort +SELECT GROUPING(a), GROUPING(b), GROUPING(c), GROUPING(a, b, c) +FROM (VALUES (1, 2, 3), (1, NULL, 3)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a), (a, b), (a, b, c)) +---- +1 0 0 4 +1 1 0 6 +1 1 0 6 +1 1 1 7 +1 1 1 7 + +query IIII rowsort +SELECT GROUPING(a), GROUPING(b), GROUPING(c), GROUPING(a, b, c) +FROM (VALUES (1, 2, 3), (1, NULL, 3)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a), (b, c), ()) +---- +1 0 0 4 +0 1 1 3 +0 1 1 3 +0 0 0 0 + +query III rowsort +SELECT GROUPING(a), GROUPING(a, b), GROUPING(a, b, c) +FROM (VALUES (1, 2, 3), (1, NULL, 3)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a, b), (a, c), (b, c)) +---- +1 3 6 +1 3 6 +1 2 5 +0 1 3 +0 1 3 + +query IIIII rowsort +SELECT GROUPING(a, b), GROUPING(a, c), GROUPING(b, c), GROUPING(a), GROUPING(a, b, c) +FROM (VALUES (1, 2, 3), (1, NULL, 3)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a, b), (a, c), (b, c)) +---- +3 2 2 1 6 +3 2 2 1 6 +2 3 1 1 5 +1 1 3 0 3 +1 1 3 0 3 + +query IIIII rowsort +SELECT GROUPING(a, b), GROUPING(a, b, c), a, b, c +FROM (VALUES (1, 2, 3), (1, NULL, 3)) AS t (a, b, c) +GROUP BY GROUPING SETS ((a), (a, b), (a, c), (a, b, c)) +---- +2 4 1 NULL NULL +3 6 1 2 NULL +3 6 1 NULL NULL +2 5 1 NULL 3 +3 7 1 2 3 +3 7 1 NULL 3 + +query IIRR rowsort +SELECT GROUPING(category_id, city_id) as city, GROUPING(city_id, category_id), min(price) as price_min, max(price) as price_max +FROM (VALUES +(2, 6, 21500.00), +(2, 6, 22900.00), +(2, 6, 23500.00), +(2, 7, 17800.00), +(4, 6, 20000.00), +(4, 8, 37000.00), +(4, 9, 75600.00) +) AS t (city_id, category_id, price) +GROUP BY GROUPING SETS ((city_id, category_id)) +---- +3 3 17800.00 17800.00 +3 3 20000.00 20000.00 +3 3 21500.00 23500.00 +3 3 37000.00 37000.00 +3 3 75600.00 75600.00 + +skipif ignite3 +# https://issues.apache.org/jira/browse/IGNITE-26335 Conversion to relational algebra failed to preserve datatypes +query IIIT rowsort +SELECT GROUPING(val), GROUPING(id, val), id, val +FROM ( VALUES +('c4a0327c-44be-416d-ae90-75c05079789f'::UUID, 1), +('367fc6f1-40c3-4237-8545-3fd102d29134'::UUID, 2) +) as t(val, id) +GROUP BY ALL GROUPING SETS ((val, id)) +---- +1 3 1 c4a0327c-44be-416d-ae90-75c05079789f +1 3 2 367fc6f1-40c3-4237-8545-3fd102d29134 + +skipif ignite3 +# https://issues.apache.org/jira/browse/IGNITE-26335 Rewriting GROUPING SETS to UNION failed with AssertionError during conversion from AST +query IIIT rowsort +SELECT GROUPING(val), GROUPING(id, val), id, val +FROM ( VALUES +('c4a0327c-44be-416d-ae90-75c05079789f'::UUID, 1), +('367fc6f1-40c3-4237-8545-3fd102d29134'::UUID, 2) +) as t(val, id) +GROUP BY ALL GROUPING SETS ((val, id), (id, val)) +---- +1 3 1 c4a0327c-44be-416d-ae90-75c05079789f +1 3 2 367fc6f1-40c3-4237-8545-3fd102d29134 +1 3 1 c4a0327c-44be-416d-ae90-75c05079789f +1 3 2 367fc6f1-40c3-4237-8545-3fd102d29134 diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorWrapper.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorWrapper.java index a5802ca9dd2..0502bb51be2 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorWrapper.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorWrapper.java @@ -27,6 +27,9 @@ public interface AccumulatorWrapper<RowT> { /** Returns {@code true} if the accumulator function should be applied to distinct elements. */ boolean isDistinct(); + /** Returns {@code true} if the accumulator function should be applied to groupset keys. */ + boolean isGrouping(); + /** Returns the accumulator function. */ Accumulator accumulator(); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/Accumulators.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/Accumulators.java index 6780c762878..2ff21e0e03a 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/Accumulators.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/Accumulators.java @@ -30,6 +30,7 @@ import java.util.Comparator; import java.util.List; import java.util.function.IntFunction; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.calcite.DataContext; import org.apache.calcite.avatica.util.ByteString; import org.apache.calcite.rel.core.AggregateCall; @@ -38,6 +39,7 @@ import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.sql.fun.SqlLiteralAggFunction; +import org.apache.calcite.util.ImmutableBitSet; import org.apache.ignite.internal.catalog.commands.CatalogUtils; import org.apache.ignite.internal.sql.engine.exec.exp.IgniteSqlFunctions; import org.apache.ignite.internal.sql.engine.type.IgniteCustomType; @@ -100,6 +102,8 @@ public class Accumulators { assert lit instanceof RexLiteral : "Non-literal argument for LiteralAgg: " + call + ", argument: " + lit; return LiteralVal.newAccumulator(context, (RexLiteral) lit); + case "GROUPING": + return groupingFactory(call); default: throw new AssertionError(call.getAggregation().getName()); } @@ -217,6 +221,67 @@ public class Accumulators { return AnyVal.newAccumulator(type); } + private Supplier<Accumulator> groupingFactory(AggregateCall call) { + RelDataType type = call.getType(); + + if (type.getSqlTypeName() == ANY && !(type instanceof IgniteCustomType)) { + throw unsupportedAggregateFunction(call); + } + + return GroupingAccumulator.newAccumulator(call.getArgList()); + } + + /** + * {@code GROUPING(column [, ...])} accumulator. Pseudo accumulator that accepts group key columns set. + */ + public static class GroupingAccumulator implements Accumulator { + private final List<Integer> argList; + + public static Supplier<Accumulator> newAccumulator(List<Integer> argList) { + return () -> new GroupingAccumulator(argList); + } + + private GroupingAccumulator(List<Integer> argList) { + assert !argList.isEmpty() : "GROUPING function must have at least one argument."; + assert argList.size() < Long.SIZE : "GROUPING function with more than 63 arguments is not supported."; + this.argList = argList; + } + + @Override + public void add(AccumulatorsState state, Object[] args) { + assert false; + } + + @Override + public void end(AccumulatorsState state, AccumulatorsState result) { + ImmutableBitSet groupKey = (ImmutableBitSet) state.get(); + + assert groupKey != null; + + long res = 0; + long bit = 1L << (argList.size() - 1); + for (Integer col : argList) { + res += groupKey.get(col) ? bit : 0L; + bit >>= 1; + } + + result.set(res); + state.set(null); + } + + @Override + public List<RelDataType> argumentTypes(IgniteTypeFactory typeFactory) { + return argList.stream() + .map(i -> typeFactory.createTypeWithNullability(typeFactory.createSqlType(ANY), true)) + .collect(Collectors.toList()); + } + + @Override + public RelDataType returnType(IgniteTypeFactory typeFactory) { + return typeFactory.createSqlType(BIGINT); + } + } + /** * {@code SINGLE_VALUE(SUBQUERY)} accumulator. Pseudo accumulator that returns a first value produced by a subquery and an error if a * subquery returns more than one row. diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorsFactory.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorsFactory.java index 39386c6885a..853d8d2f1c2 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorsFactory.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AccumulatorsFactory.java @@ -17,7 +17,6 @@ package org.apache.ignite.internal.sql.engine.exec.exp.agg; -import static org.apache.calcite.sql.fun.SqlInternalOperators.LITERAL_AGG; import static org.apache.ignite.internal.sql.engine.util.TypeUtils.createRowType; import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty; @@ -45,6 +44,7 @@ import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexNode; import org.apache.calcite.rex.RexProgram; import org.apache.calcite.rex.RexProgramBuilder; +import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.calcite.util.Pair; @@ -259,6 +259,8 @@ public class AccumulatorsFactory<RowT> { private static final class AccumulatorWrapperImpl<RowT> implements AccumulatorWrapper<RowT> { static final IntList SINGLE_ARG_LIST = IntList.of(0); + private static final Object[] NO_ARGUMENTS = {null}; + private final Accumulator accumulator; private final Function<Object[], Object[]> inAdapter; @@ -277,6 +279,8 @@ public class AccumulatorsFactory<RowT> { private final boolean distinct; + private final boolean grouping; + AccumulatorWrapperImpl( ExecutionContext<RowT> ctx, Accumulator accumulator, @@ -290,7 +294,8 @@ public class AccumulatorsFactory<RowT> { this.outAdapter = outAdapter; this.distinct = call.isDistinct(); - literalAgg = call.getAggregation() == LITERAL_AGG; + grouping = call.getAggregation().getKind() == SqlKind.GROUPING; + literalAgg = call.getAggregation().getKind() == SqlKind.LITERAL_AGG; ignoreNulls = call.ignoreNulls(); filterArg = call.hasFilter() ? call.filterArg : -1; @@ -302,6 +307,11 @@ public class AccumulatorsFactory<RowT> { return distinct; } + @Override + public boolean isGrouping() { + return grouping; + } + @Override public Accumulator accumulator() { return accumulator; @@ -313,9 +323,10 @@ public class AccumulatorsFactory<RowT> { return null; } - if (literalAgg) { + if (literalAgg || grouping) { // LiteralAgg has a constant as its argument. - return new Object[]{null}; + // Grouping aggregate doesn't accepts rows. + return NO_ARGUMENTS; } if (IgniteUtils.assertionsEnabled() && argList == SINGLE_ARG_LIST) { diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AggregateRow.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AggregateRow.java index bf7be3cb594..836d3118e43 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AggregateRow.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/AggregateRow.java @@ -61,7 +61,7 @@ public class AggregateRow<RowT> { } /** Updates this row by using data of the given row. */ - public void update(List<AccumulatorWrapper<RowT>> accs, ImmutableBitSet allFields, RowHandler<RowT> handler, RowT row) { + public void update(List<AccumulatorWrapper<RowT>> accs, ImmutableBitSet grpFields, RowHandler<RowT> handler, RowT row) { for (int i = 0; i < accs.size(); i++) { AccumulatorWrapper<RowT> acc = accs.get(i); @@ -72,7 +72,9 @@ public class AggregateRow<RowT> { state.setIndex(i); - if (acc.isDistinct()) { + if (acc.isGrouping()) { + state.set(grpFields); + } else if (acc.isDistinct()) { Set<Object> distinctSet = distinctSets.get(i); distinctSet.add(args[0]); } else { @@ -92,9 +94,14 @@ public class AggregateRow<RowT> { } /** Writes aggregate state of the given row to given array. */ - public void writeTo(AggregateType type, List<AccumulatorWrapper<RowT>> accs, Object[] output, ImmutableBitSet allFields, byte groupId) { - int cardinality = allFields.cardinality(); - + public void writeTo( + AggregateType type, + List<AccumulatorWrapper<RowT>> accs, + Object[] output, + int offset, + ImmutableBitSet groupFields, + byte groupId + ) { AccumulatorsState result = new AccumulatorsState(accs.size()); for (int i = 0; i < accs.size(); i++) { @@ -104,6 +111,8 @@ public class AggregateRow<RowT> { result.setIndex(i); if (acc.isDistinct()) { + assert !acc.isGrouping(); + Set<Object> distinctSet = distinctSets.get(i); for (var arg : distinctSet) { @@ -113,7 +122,7 @@ public class AggregateRow<RowT> { acc.accumulator().end(state, result); - output[i + cardinality] = acc.convertResult(result.get()); + output[i + offset] = acc.convertResult(result.get()); state.resetIndex(); result.resetIndex(); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateNode.java index 5183d53f88c..7aecd02af47 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateNode.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateNode.java @@ -257,7 +257,7 @@ public class HashAggregateNode<RowT> extends AbstractNode<RowT> implements Singl GroupKey grpKey = b.build(); AggregateRow<RowT> aggRow = groups.computeIfAbsent(grpKey, k -> create()); - aggRow.update(accs, allFields, handler, row); + aggRow.update(accs, grpFields, handler, row); } /** @@ -287,7 +287,7 @@ public class HashAggregateNode<RowT> extends AbstractNode<RowT> implements Singl fields[j++] = grpFields.get(field) ? grpKey.field(k++) : null; } - aggRow.writeTo(type, accs, fields, allFields, grpId); + aggRow.writeTo(type, accs, fields, allFields.cardinality(), grpFields, grpId); RowT row = rowFactory.create(fields); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateNode.java index 20b69855f6b..9d6606de4ba 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateNode.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateNode.java @@ -277,7 +277,7 @@ public class SortAggregateNode<RowT> extends AbstractNode<RowT> implements Singl fields[i++] = grpKey; } - aggRow.writeTo(type, accs, fields, grpSet, AggregateRow.NO_GROUP_ID); + aggRow.writeTo(type, accs, fields, grpSet.cardinality(), grpSet, AggregateRow.NO_GROUP_ID); return rowFactory.create(fields); } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java index c777e66bdbc..bb5bb0d9f8f 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java @@ -1229,7 +1229,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl { } private void validateAggregateFunction(SqlCall call, SqlAggFunction aggFunction) { - if (!SqlKind.AGGREGATE.contains(aggFunction.kind)) { + if (!aggFunction.isAggregator()) { throw newValidationError(call, IgniteResource.INSTANCE.unsupportedAggregationFunction(aggFunction.getName())); } @@ -1247,6 +1247,12 @@ public class IgniteSqlValidator extends SqlValidatorImpl { case MAX: case ANY_VALUE: + return; + case GROUPING: + if (call.operandCount() > 63) { + // Function result of BIGINT can fit only bitmask of length less than 64; + throw newValidationError(call, IgniteResource.INSTANCE.invalidArgCount(aggFunction.getName(), 1, 63)); + } return; default: throw newValidationError(call, diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/agg/MapReduceAggregates.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/agg/MapReduceAggregates.java index b3d0914aebc..a2b430c0b2f 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/agg/MapReduceAggregates.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/agg/MapReduceAggregates.java @@ -71,7 +71,8 @@ public class MapReduceAggregates { "ANY", "AVG", "SINGLE_VALUE", - "ANY_VALUE" + "ANY_VALUE", + "GROUPING" ); /** @@ -364,6 +365,8 @@ public class MapReduceAggregates { return createCountAgg(call, reduceArgumentOffset); case "AVG": return createAvgAgg(cluster, call, reduceArgumentOffset, input, canBeNull); + case "GROUPING": + return createGroupingAgg(call, reduceArgumentOffset); default: return createSimpleAgg(call, reduceArgumentOffset); } @@ -487,6 +490,28 @@ public class MapReduceAggregates { return new MapReduceAgg(argList, call, reduceCall, USE_INPUT_FIELD); } + private static MapReduceAgg createGroupingAgg(AggregateCall call, int reduceArgumentOffset) { + IntList argList = IntList.of(reduceArgumentOffset); + + AggregateCall reduceCall = AggregateCall.create( + SqlStdOperatorTable.SINGLE_VALUE, + call.isDistinct(), + call.isApproximate(), + call.ignoreNulls(), + ImmutableList.of(), + argList, + // there is no filtering on REDUCE phase + -1, + null, + call.collation, + call.type, + "GROUPING" + reduceArgumentOffset); + + // For aggregate that use the same aggregate function for both MAP and REDUCE phases + // use the result of an aggregate as is. + return new MapReduceAgg(argList, call, reduceCall, USE_INPUT_FIELD); + } + /** * Produces intermediate expressions that modify results of MAP/REDUCE aggregate. * For example: after splitting a function into a MAP aggregate and REDUCE aggregate it is necessary to add casts to diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/fun/IgniteSqlOperatorTable.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/fun/IgniteSqlOperatorTable.java index f88f433de2a..dd8edff3533 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/fun/IgniteSqlOperatorTable.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/fun/IgniteSqlOperatorTable.java @@ -544,6 +544,7 @@ public class IgniteSqlOperatorTable extends ReflectiveSqlOperatorTable { definedOperatorsBuilder.add(SqlStdOperatorTable.ANY_VALUE); definedOperatorsBuilder.add(SqlStdOperatorTable.SINGLE_VALUE); definedOperatorsBuilder.add(SqlStdOperatorTable.FILTER); + definedOperatorsBuilder.add(SqlStdOperatorTable.GROUPING); definedOperatorsBuilder.add(EVERY); definedOperatorsBuilder.add(SOME); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java index 54029aa695d..e3840f40445 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java @@ -114,6 +114,11 @@ public interface IgniteResource { @BaseMessage("Timestamp literal ''{0}'' out of range.") ExInst<SqlValidatorException> timestampLiteralOutOfRange(String typeName); + @BaseMessage( + "Invalid number of arguments to function ''{0}''. Was expecting number of arguments in range [{1,number,#}, {2,number,#}]." + ) + ExInst<SqlValidatorException> invalidArgCount(String functionName, int min, int max); + /** Constructs a signature string to use in error messages. */ static String makeSignature(SqlCallBinding binding, RelDataType... operandTypes) { return makeSignature(binding, Arrays.asList(operandTypes)); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/docs/OperatorListTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/docs/OperatorListTest.java index 42e88c8e9bf..812563e8bb7 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/docs/OperatorListTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/docs/OperatorListTest.java @@ -271,6 +271,7 @@ public class OperatorListTest extends BaseIgniteAbstractTest { ops.add(SqlStdOperatorTable.EVERY); ops.add(SqlStdOperatorTable.SOME); + ops.add(SqlStdOperatorTable.GROUPING); ops.internal(SqlInternalOperators.LITERAL_AGG); return ops; diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/GroupingAccumulatorTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/GroupingAccumulatorTest.java new file mode 100644 index 00000000000..85476a6fda7 --- /dev/null +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/agg/GroupingAccumulatorTest.java @@ -0,0 +1,189 @@ +/* + * 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.ignite.internal.sql.engine.exec.exp.agg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.util.ImmutableBitSet; +import org.apache.ignite.internal.sql.engine.exec.exp.agg.Accumulators.GroupingAccumulator; +import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@code GROUPING} accumulator function. + */ +public class GroupingAccumulatorTest { + + @Test + public void testFactory() { + assertThrows(AssertionError.class, + () -> GroupingAccumulator.newAccumulator(List.of()).get(), + "GROUPING function must have at least one argument" + ); + assertThrows(AssertionError.class, + () -> GroupingAccumulator.newAccumulator(IntStream.range(0, 64).boxed().collect(Collectors.toList())).get(), + "GROUPING function with more than 63 arguments is not supported" + ); + + assertNotNull(GroupingAccumulator.newAccumulator(IntStream.range(0, 63).boxed().collect(Collectors.toList())).get()); + } + + @Test + public void emptyState() { + Accumulator acc = newCall(List.of(0)); + AccumulatorsState result = newState(); + + assertThrows(AssertionError.class, () -> acc.end(newState(), result)); + + assertNull(result.get()); + } + + @Test + public void accumulatorIgnoresValues() { + Accumulator acc = newCall(List.of(0)); + + AccumulatorsState state = newState(); + + assertEquals(IgniteTypeFactory.INSTANCE.createSqlType(SqlTypeName.BIGINT), acc.returnType(IgniteTypeFactory.INSTANCE)); + assertThrows(AssertionError.class, () -> acc.add(state, new Object[]{1})); + + assertFalse(state.hasValue()); + } + + @Test + public void callWithSingleArgument() { + Accumulator acc = newCall(List.of(1)); + + // Empty group + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of()), result); + + assertTrue(result.hasValue()); + assertEquals(0L, result.get()); + } + + // Aggregate by single column + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(1)), result); + + assertTrue(result.hasValue()); + assertEquals(1L, result.get()); + } + + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(0)), result); + + assertTrue(result.hasValue()); + assertEquals(0L, result.get()); + } + + // Aggregate by multiple columns + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(0, 1)), result); + + assertTrue(result.hasValue()); + assertEquals(1L, result.get()); + } + } + + @Test + public void callWithMultipleArguments() { + Accumulator acc = newCall(List.of(3, 2)); + + // Empty group + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of()), result); + + assertTrue(result.hasValue()); + assertEquals(0L, result.get()); + } + + // Aggregate by single column + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(1)), result); + + assertTrue(result.hasValue()); + assertEquals(0L, result.get()); + } + + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(2)), result); + + assertTrue(result.hasValue()); + assertEquals(1L, result.get()); + } + + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(3)), result); + + assertTrue(result.hasValue()); + assertEquals(2L, result.get()); + } + + // Aggregate by multiple columns + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(1, 2)), result); + + assertTrue(result.hasValue()); + assertEquals(1L, result.get()); + } + + { + AccumulatorsState result = newState(); + acc.end(newState(ImmutableBitSet.of(2, 3)), result); + + assertTrue(result.hasValue()); + assertEquals(3L, result.get()); + } + } + + private Accumulator newCall(List<Integer> columns) { + return GroupingAccumulator.newAccumulator(columns).get(); + } + + private static AccumulatorsState newState() { + AccumulatorsState state = new AccumulatorsState(1); + state.setIndex(0); + return state; + } + + private static AccumulatorsState newState(Object arg) { + AccumulatorsState state = new AccumulatorsState(1); + state.setIndex(0); + state.set(arg); + return state; + } +} diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractAggregatePlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractAggregatePlannerTest.java index 5c425342f6d..c916cd48e68 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractAggregatePlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractAggregatePlannerTest.java @@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.planner; import static org.apache.ignite.internal.sql.engine.trait.IgniteDistributions.single; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import it.unimi.dsi.fastutil.ints.IntList; @@ -948,6 +949,34 @@ public abstract class AbstractAggregatePlannerTest extends AbstractPlannerTest { */ CASE_27C("MERGE INTO test as t0 USING test as t1 ON t0.id = t1.id " + "WHEN MATCHED THEN UPDATE SET val1 = (SELECT val0 FROM test)", schema(single())), + + /** + * Query: SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0)). + * + * <p>Distribution single() + */ + CASE_28_1A("SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0))", schema(single())), + + /** + * Query: SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0), (grp1)). + * + * <p>Distribution single() + */ + CASE_28_1B("SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0), (grp1))", schema(single())), + + /** + * Query: SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0)). + * + * <p>Distribution hash(1) + */ + CASE_28_2A("SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0))", schema(hash(1))), + + /** + * Query: SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0), (grp1)). + * + * <p>Distribution hash(1) + */ + CASE_28_2B("SELECT GROUPING(grp0) from test GROUP BY GROUPING SETS ((grp0), (grp1))", schema(hash(1))), ; final String query; @@ -1000,8 +1029,10 @@ public abstract class AbstractAggregatePlannerTest extends AbstractPlannerTest { assertPlan(testCase.query, Collections.singleton(testCase.schema), predicate, List.of(), rulesToDisable); } - protected void assumeRun(String methodName, TestCase testCase) { - missedCases.remove(testCase); + protected void assumeRun(TestCase testCase) { + boolean removed = missedCases.remove(testCase); + + assertTrue(removed, "Unapplicable/Ignored test case was unexpectedly run."); } @SafeVarargs diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AggregatePlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AggregatePlannerTest.java index cfbf3d58469..52830a5db49 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AggregatePlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AggregatePlannerTest.java @@ -577,6 +577,18 @@ public class AggregatePlannerTest extends AbstractAggregatePlannerTest { checkDerivedCollationWithOrderBySubsetOfGroupColumnsHash(TestCase.CASE_26A); } + /** + * Validates a plan for a query with GROUPING aggregate. + */ + @Test + public void groupsWithGroupingAggregate() throws Exception { + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1A); + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1B); + + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2A); + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2B); + } + private void checkSimpleAggSingle(TestCase testCase) throws Exception { checkSimpleAggSingle(testCase, hasAggregate()); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedHashAggregatePlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedHashAggregatePlannerTest.java index c70c505d273..29ae9c01366 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedHashAggregatePlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedHashAggregatePlannerTest.java @@ -527,6 +527,18 @@ public class ColocatedHashAggregatePlannerTest extends AbstractAggregatePlannerT checkDerivedCollationWithOrderBySubsetOfGroupColumnsHash(TestCase.CASE_26A); } + /** + * Validates a plan for a query with GROUPING aggregate. + */ + @Test + public void groupsWithGroupingAggregate() throws Exception { + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1A); + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1B); + + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2A); + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2B); + } + private void checkSimpleAggSingle(TestCase testCase) throws Exception { checkSimpleAggSingle(testCase, hasAggregate()); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedSortAggregatePlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedSortAggregatePlannerTest.java index 587cc13b35b..d5bade7f839 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedSortAggregatePlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ColocatedSortAggregatePlannerTest.java @@ -521,6 +521,18 @@ public class ColocatedSortAggregatePlannerTest extends AbstractAggregatePlannerT checkDerivedCollationWithOrderBySubsetOfGroupColumnsHash(TestCase.CASE_26A); } + /** + * Validates a plan for a query with GROUPING aggregate. + */ + @Test + public void groupsWithGroupingAggregate() throws Exception { + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1A); + assumeRun(TestCase.CASE_28_1B); // No collation can be utilized for the case if multiple group sets. + + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2A); + assumeRun(TestCase.CASE_28_2B); // No collation can be utilized for the case if multiple group sets. + } + private void checkSimpleAggSingle(TestCase testCase) throws Exception { checkSimpleAggSingle(testCase, hasAggregate()); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceHashAggregatePlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceHashAggregatePlannerTest.java index 4a114fd5c41..6b4f846600a 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceHashAggregatePlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceHashAggregatePlannerTest.java @@ -573,6 +573,18 @@ public class MapReduceHashAggregatePlannerTest extends AbstractAggregatePlannerT checkDerivedCollationWithOrderBySubsetOfGroupColumnsHash(TestCase.CASE_26A); } + /** + * Validates a plan for a query with GROUPING aggregate. + */ + @Test + public void groupsWithGroupingAggregate() throws Exception { + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1A); + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1B); + + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2A); + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2B); + } + private void checkSimpleAggSingle(TestCase testCase) throws Exception { checkSimpleAggSingle(testCase, hasAggregate()); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceSortAggregatePlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceSortAggregatePlannerTest.java index f8754c671e7..464ccb27f48 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceSortAggregatePlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/MapReduceSortAggregatePlannerTest.java @@ -581,6 +581,18 @@ public class MapReduceSortAggregatePlannerTest extends AbstractAggregatePlannerT checkDerivedCollationWithOrderBySubsetOfGroupColumnsHash(TestCase.CASE_26A); } + /** + * Validates a plan for a query with GROUPING aggregate. + */ + @Test + public void groupsWithGroupingAggregate() throws Exception { + checkSimpleAggWithGroupBySingle(TestCase.CASE_28_1A); + assumeRun(TestCase.CASE_28_1B); // No collation can be utilized for the case if multiple group sets. + + checkSimpleAggWithGroupByHash(TestCase.CASE_28_2A); + assumeRun(TestCase.CASE_28_2B); // No collation can be utilized for the case if multiple group sets. + } + private void checkSimpleAggSingle(TestCase testCase) throws Exception { checkSimpleAggSingle(testCase, hasAggregate()); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TpcdsQueryPlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TpcdsQueryPlannerTest.java index d1f989a4207..4b3905a4fce 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TpcdsQueryPlannerTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TpcdsQueryPlannerTest.java @@ -45,10 +45,8 @@ public class TpcdsQueryPlannerTest extends AbstractTpcQueryPlannerTest { private static final IntSet UNSUPPORTED_TESTS = IntSet.of( // TODO https://issues.apache.org/jira/browse/IGNITE-14642 Support STDDEV_SAMP function and unmute tests. 17, 29, 35, 39, - // TODO https://issues.apache.org/jira/browse/IGNITE-25872 Support GROUPING function and unmute tests. - 27, 36, 86, // TODO https://issues.apache.org/jira/browse/IGNITE-25873 Support aggregate window function RANK and unmute tests. - 44, 47, 49, 57, 67, 70 + 44, 47, 49, 57, 67, 70, 36, 86 ); @ParameterizedTest diff --git a/modules/sql-engine/src/test/resources/docs/operator_list.txt b/modules/sql-engine/src/test/resources/docs/operator_list.txt index 51aac5d03d1..9411a7a2b84 100644 --- a/modules/sql-engine/src/test/resources/docs/operator_list.txt +++ b/modules/sql-engine/src/test/resources/docs/operator_list.txt @@ -378,6 +378,9 @@ EVERY(<boolean>) SOME(<boolean>) #014b53be23fc2009080420e94b78d52bf2b09dbd +GROUPING(...) +#c19a178a82716bcc5f313ca5259637dd60c8ba0e + [internal] LITERAL_AGG() #6d021f1dd7329dc18c22767552769bc90f27b7ac diff --git a/modules/sql-engine/src/test/resources/tpcds/plan/q27.plan b/modules/sql-engine/src/test/resources/tpcds/plan/q27.plan new file mode 100644 index 00000000000..1ce5258a1f7 --- /dev/null +++ b/modules/sql-engine/src/test/resources/tpcds/plan/q27.plan @@ -0,0 +1,68 @@ +Sort + collation: [I_ITEM_ID ASC, S_STATE ASC] + fetch: 100 + est: (rows=100) + ColocatedHashAggregate + fieldNames: [I_ITEM_ID, S_STATE, G_STATE, AGG1, AGG2, AGG3, AGG4] + group: [I_ITEM_ID, S_STATE] + groupSets: [[I_ITEM_ID, S_STATE], [I_ITEM_ID], []] + aggregation: [GROUPING(S_STATE), AVG(SS_QUANTITY), AVG(SS_LIST_PRICE), AVG(SS_COUPON_AMT), AVG(SS_SALES_PRICE)] + est: (rows=12751) + Project + fieldNames: [I_ITEM_ID, S_STATE, SS_QUANTITY, SS_LIST_PRICE, SS_COUPON_AMT, SS_SALES_PRICE] + projection: [I_ITEM_ID, S_STATE, SS_QUANTITY, SS_LIST_PRICE, SS_COUPON_AMT, SS_SALES_PRICE] + est: (rows=35419) + HashJoin + predicate: =(SS_STORE_SK, S_STORE_SK) + type: inner + est: (rows=35419) + HashJoin + predicate: =(SS_ITEM_SK, I_ITEM_SK) + type: inner + est: (rows=35419) + HashJoin + predicate: =(SS_SOLD_DATE_SK, D_DATE_SK) + type: inner + est: (rows=35419) + HashJoin + predicate: =(SS_CDEMO_SK, CD_DEMO_SK) + type: inner + est: (rows=106362) + Exchange + distribution: single + est: (rows=2880404) + TableScan + table: PUBLIC.STORE_SALES + fieldNames: [SS_SOLD_DATE_SK, SS_ITEM_SK, SS_CDEMO_SK, SS_STORE_SK, SS_QUANTITY, SS_LIST_PRICE, SS_SALES_PRICE, SS_COUPON_AMT] + est: (rows=2880404) + Exchange + distribution: single + est: (rows=70928) + TableScan + table: PUBLIC.CUSTOMER_DEMOGRAPHICS + predicate: AND(=(CD_GENDER, _UTF-8'F'), =(CD_MARITAL_STATUS, _UTF-8'U'), =(CD_EDUCATION_STATUS, _UTF-8'2 yr Degree')) + fieldNames: [CD_DEMO_SK, CD_GENDER, CD_MARITAL_STATUS, CD_EDUCATION_STATUS] + est: (rows=70928) + Exchange + distribution: single + est: (rows=24325) + TableScan + table: PUBLIC.DATE_DIM + predicate: =(D_YEAR, 2000) + fieldNames: [D_DATE_SK, D_YEAR] + est: (rows=24325) + Exchange + distribution: single + est: (rows=18000) + TableScan + table: PUBLIC.ITEM + fieldNames: [I_ITEM_SK, I_ITEM_ID] + est: (rows=18000) + Exchange + distribution: single + est: (rows=12) + TableScan + table: PUBLIC.STORE + predicate: SEARCH(S_STATE, Sarg[_UTF-8'AL':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'FL':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'IN':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'NY':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'OH':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'SC':VARCHAR(2) CHARACTER SET "UTF-8"]:VARCHAR(2) CHARACTER SET "UTF-8") + fieldNames: [S_STORE_SK, S_STATE] + est: (rows=12) diff --git a/modules/sql-engine/src/test/resources/tpcds/plan/q27_colocated.plan b/modules/sql-engine/src/test/resources/tpcds/plan/q27_colocated.plan new file mode 100644 index 00000000000..1ce5258a1f7 --- /dev/null +++ b/modules/sql-engine/src/test/resources/tpcds/plan/q27_colocated.plan @@ -0,0 +1,68 @@ +Sort + collation: [I_ITEM_ID ASC, S_STATE ASC] + fetch: 100 + est: (rows=100) + ColocatedHashAggregate + fieldNames: [I_ITEM_ID, S_STATE, G_STATE, AGG1, AGG2, AGG3, AGG4] + group: [I_ITEM_ID, S_STATE] + groupSets: [[I_ITEM_ID, S_STATE], [I_ITEM_ID], []] + aggregation: [GROUPING(S_STATE), AVG(SS_QUANTITY), AVG(SS_LIST_PRICE), AVG(SS_COUPON_AMT), AVG(SS_SALES_PRICE)] + est: (rows=12751) + Project + fieldNames: [I_ITEM_ID, S_STATE, SS_QUANTITY, SS_LIST_PRICE, SS_COUPON_AMT, SS_SALES_PRICE] + projection: [I_ITEM_ID, S_STATE, SS_QUANTITY, SS_LIST_PRICE, SS_COUPON_AMT, SS_SALES_PRICE] + est: (rows=35419) + HashJoin + predicate: =(SS_STORE_SK, S_STORE_SK) + type: inner + est: (rows=35419) + HashJoin + predicate: =(SS_ITEM_SK, I_ITEM_SK) + type: inner + est: (rows=35419) + HashJoin + predicate: =(SS_SOLD_DATE_SK, D_DATE_SK) + type: inner + est: (rows=35419) + HashJoin + predicate: =(SS_CDEMO_SK, CD_DEMO_SK) + type: inner + est: (rows=106362) + Exchange + distribution: single + est: (rows=2880404) + TableScan + table: PUBLIC.STORE_SALES + fieldNames: [SS_SOLD_DATE_SK, SS_ITEM_SK, SS_CDEMO_SK, SS_STORE_SK, SS_QUANTITY, SS_LIST_PRICE, SS_SALES_PRICE, SS_COUPON_AMT] + est: (rows=2880404) + Exchange + distribution: single + est: (rows=70928) + TableScan + table: PUBLIC.CUSTOMER_DEMOGRAPHICS + predicate: AND(=(CD_GENDER, _UTF-8'F'), =(CD_MARITAL_STATUS, _UTF-8'U'), =(CD_EDUCATION_STATUS, _UTF-8'2 yr Degree')) + fieldNames: [CD_DEMO_SK, CD_GENDER, CD_MARITAL_STATUS, CD_EDUCATION_STATUS] + est: (rows=70928) + Exchange + distribution: single + est: (rows=24325) + TableScan + table: PUBLIC.DATE_DIM + predicate: =(D_YEAR, 2000) + fieldNames: [D_DATE_SK, D_YEAR] + est: (rows=24325) + Exchange + distribution: single + est: (rows=18000) + TableScan + table: PUBLIC.ITEM + fieldNames: [I_ITEM_SK, I_ITEM_ID] + est: (rows=18000) + Exchange + distribution: single + est: (rows=12) + TableScan + table: PUBLIC.STORE + predicate: SEARCH(S_STATE, Sarg[_UTF-8'AL':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'FL':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'IN':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'NY':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'OH':VARCHAR(2) CHARACTER SET "UTF-8", _UTF-8'SC':VARCHAR(2) CHARACTER SET "UTF-8"]:VARCHAR(2) CHARACTER SET "UTF-8") + fieldNames: [S_STORE_SK, S_STATE] + est: (rows=12)