From 4bf367b8a508b0a678c37a00474d02613ee2fedb Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 22 Aug 2026 22:51:02 +0800 Subject: [PATCH] [CALCITE-6537] Add syntax to allow non-aggregated rows to be used in GROUPING SETS --- core/src/main/codegen/templates/Parser.jj | 67 ++++++++++- .../calcite/sql/validate/SqlConformance.java | 18 +++ .../sql/validate/SqlConformanceEnum.java | 10 ++ .../validate/SqlDelegatingConformance.java | 4 + .../sql/validate/SqlValidatorImpl.java | 74 ++++++++++++ .../calcite/test/GroupingSetsStarTest.java | 109 ++++++++++++++++++ .../apache/calcite/test/SqlValidatorTest.java | 26 +++++ core/src/test/resources/sql/agg.iq | 90 +++++++++++++++ 8 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/org/apache/calcite/test/GroupingSetsStarTest.java diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 16bf6b5e9509..ac9069450a0e 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -187,6 +187,26 @@ public class ${parser.class} extends SqlAbstractParserImpl return rowValueStarCount > 0; } + /** + * Whether {@code *} is currently allowed as a bare grouping element in + * GROUPING SETS / ROLLUP / CUBE. Set while parsing a grouping element + * when the {@code isGroupingSetsStarAllowed} conformance is on. + */ + private int groupingStarCount; + + private void pushGroupingStar() { + groupingStarCount++; + } + + private void popGroupingStar() { + assert groupingStarCount > 0; + groupingStarCount--; + } + + private boolean allowGroupingStar() { + return groupingStarCount > 0; + } + /** * {@link SqlParserImplFactory} implementation for creating parser. */ @@ -2895,11 +2915,23 @@ SqlNodeList GroupBy() : List GroupingElementList() : { final List list = new ArrayList(); + final boolean starAllowed = + this.conformance.isGroupingSetsStarAllowed(); } { + { + if (starAllowed) { + pushGroupingStar(); + } + } AddGroupingElement(list) ( LOOKAHEAD(2) AddGroupingElement(list) )* - { return list; } + { + if (starAllowed) { + popGroupingStar(); + } + return list; + } } void AddGroupingElement(List list) : @@ -2907,6 +2939,8 @@ void AddGroupingElement(List list) : final List subList; final SqlNodeList nodes; final Span s; + final boolean starAllowed = + this.conformance.isGroupingSetsStarAllowed(); } { LOOKAHEAD(2) @@ -2916,14 +2950,30 @@ void AddGroupingElement(List list) : SqlStdOperatorTable.GROUPING_SETS.createCall(s.end(this), subList)); } | { s = span(); } - nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY) + { + if (starAllowed) { + pushGroupingStar(); + } + } + nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY) { + if (starAllowed) { + popGroupingStar(); + } list.add( SqlStdOperatorTable.ROLLUP.createCall(s.end(this), nodes.getList())); } | { s = span(); } - nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY) + { + if (starAllowed) { + pushGroupingStar(); + } + } + nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY) { + if (starAllowed) { + popGroupingStar(); + } list.add( SqlStdOperatorTable.CUBE.createCall(s.end(this), nodes.getList())); } @@ -2931,6 +2981,10 @@ void AddGroupingElement(List list) : { s = span(); } { list.add(new SqlNodeList(s.end(this))); } +| LOOKAHEAD({ allowGroupingStar() && getToken(1).kind == STAR }) + { + list.add(SqlIdentifier.star(getPos())); + } | AddExpression(list, ExprContext.ACCEPT_SUB_QUERY) } @@ -4818,6 +4872,13 @@ SqlNode AtomicRowExpression() : | { return starId; } ) + | + // Parses a bare "*" grouping element appearing inside a tuple or + // argument list of GROUPING SETS / ROLLUP / CUBE. + LOOKAHEAD({ allowGroupingStar() && getToken(1).kind == STAR }) + { + return SqlIdentifier.star(getPos()); + } | e = NewSpecification() | diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java index 703dfd4a1447..1b4da250e710 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java @@ -747,4 +747,22 @@ default boolean isColonFieldAccessAllowed() { default boolean isCorrelatedAggregateAllowed() { return false; } + + /** + * Whether {@code *} is allowed as a grouping element in + * {@code GROUPING SETS}, {@code ROLLUP} and {@code CUBE} sub-clauses of + * {@code GROUP BY}, to produce non-aggregated (detail) rows. + * + *

By analogy with {@code COUNT(*)}, a grouping set containing {@code *} + * groups by every input column, so no rows are merged (except identical + * duplicate rows) and each input row appears in the output. + * + *

Among the built-in conformance levels, true in + * {@link SqlConformanceEnum#BABEL}, + * {@link SqlConformanceEnum#LENIENT}; + * false otherwise. + */ + default boolean isGroupingSetsStarAllowed() { + return false; + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java index 681835724fb1..072cdc0f6828 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java @@ -572,4 +572,14 @@ public enum SqlConformanceEnum implements SqlConformance { return false; } } + + @Override public boolean isGroupingSetsStarAllowed() { + switch (this) { + case BABEL: + case LENIENT: + return true; + default: + return false; + } + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java index 7bed86a41072..94ed901bca36 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java @@ -193,4 +193,8 @@ protected SqlDelegatingConformance(SqlConformance delegate) { @Override public boolean isCorrelatedAggregateAllowed() { return delegate.isCorrelatedAggregateAllowed(); } + + @Override public boolean isGroupingSetsStarAllowed() { + return delegate.isGroupingSetsStarAllowed(); + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 6642a1522b76..a4681e693fb7 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5500,6 +5500,7 @@ private static SqlNode measureToValue(SqlNode e) { */ protected void validateGroupClause(SqlSelect select) { rewriteGroupByAll(select); + rewriteGroupingStar(select); SqlNodeList groupList = select.getGroup(); if (groupList == null) { return; @@ -5601,6 +5602,79 @@ private void rewriteGroupByAll(SqlSelect select) { select.setGroupBy(new SqlNodeList(keys, groupList.getParserPosition())); } + /** If the conformance allows {@code *} as a grouping element, rewrites every + * bare {@code *} in GROUPING SETS / ROLLUP / CUBE (and at the top level of + * GROUP BY) into a {@code ROW} of every input column. A grouping set + * containing all input columns does not merge any rows (except identical + * duplicates), yielding the non-aggregated "detail" rows. + * + * @see SqlConformance#isGroupingSetsStarAllowed() */ + private void rewriteGroupingStar(SqlSelect select) { + if (!config.conformance().isGroupingSetsStarAllowed()) { + return; + } + final SqlNodeList groupList = select.getGroup(); + if (groupList == null) { + return; + } + boolean changed = false; + final List newItems = new ArrayList<>(); + for (SqlNode groupItem : groupList) { + final SqlNode newItem = rewriteGroupingStarNode(groupItem, select); + if (newItem != groupItem) { + changed = true; + } + newItems.add(newItem); + } + if (changed) { + select.setGroupBy(new SqlNodeList(newItems, groupList.getParserPosition())); + } + } + + /** Recursively rewrites bare {@code *} grouping elements into a ROW of all + * input columns, descending into the operands of GROUPING SETS, ROLLUP, CUBE + * and ROW (tuple) calls. */ + private SqlNode rewriteGroupingStarNode(SqlNode node, SqlSelect select) { + if (node instanceof SqlIdentifier) { + final SqlIdentifier id = (SqlIdentifier) node; + if (id.isStar() && id.names.size() == 1) { + return starToRow(select, id.getParserPosition()); + } + return node; + } + if (node instanceof SqlCall) { + final SqlCall call = (SqlCall) node; + final List operands = call.getOperandList(); + List newOperands = null; + for (int i = 0; i < operands.size(); i++) { + final SqlNode operand = operands.get(i); + final SqlNode newOperand = rewriteGroupingStarNode(operand, select); + if (newOperand != operand) { + if (newOperands == null) { + newOperands = new ArrayList<>(operands); + } + newOperands.set(i, newOperand); + } + } + if (newOperands != null) { + return call.getOperator().createCall( + call.getParserPosition(), newOperands); + } + } + return node; + } + + /** Builds a {@code ROW} of every input column of the FROM clause, expanding + * a bare grouping {@code *}. */ + private SqlNode starToRow(SqlSelect select, SqlParserPos pos) { + final SqlIdentifier star = SqlIdentifier.star(pos); + final List columns = expandStarForAllRewrite(select, star); + if (columns.isEmpty()) { + throw newValidationError(star, RESOURCE.selectStarRequiresFrom()); + } + return SqlStdOperatorTable.ROW.createCall(pos, columns); + } + private void validateGroupItem(SqlValidatorScope groupScope, @Nullable AggregatingSelectScope aggregatingScope, SqlNode groupItem) { diff --git a/core/src/test/java/org/apache/calcite/test/GroupingSetsStarTest.java b/core/src/test/java/org/apache/calcite/test/GroupingSetsStarTest.java new file mode 100644 index 000000000000..630f238ad7b6 --- /dev/null +++ b/core/src/test/java/org/apache/calcite/test/GroupingSetsStarTest.java @@ -0,0 +1,109 @@ +/* + * 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; + +import org.apache.calcite.sql.validate.SqlConformanceEnum; +import org.apache.calcite.test.CalciteAssert.Config; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@code *} as a grouping element in GROUPING SETS, ROLLUP and CUBE, + * yielding non-aggregated (detail) rows. + * + * @see org.apache.calcite.sql.validate.SqlConformance#isGroupingSetsStarAllowed() + */ +class GroupingSetsStarTest { + /** {@code GROUPING SETS ((deptno), (*))} returns department totals (ename + * NULL) plus one detail row per employee. */ + @Test void testGroupingSetsStar() { + CalciteAssert.that() + .with(Config.SCOTT) + .with(SqlConformanceEnum.LENIENT) + .query("select deptno, ename, sum(sal) as sumsal\n" + + "from emp\n" + + "group by grouping sets ((deptno), (*))\n" + + "order by deptno, ename nulls first") + .returnsUnordered( + "DEPTNO=10; ENAME=null; SUMSAL=8750.00", + "DEPTNO=10; ENAME=CLARK; SUMSAL=2450.00", + "DEPTNO=10; ENAME=KING; SUMSAL=5000.00", + "DEPTNO=10; ENAME=MILLER; SUMSAL=1300.00", + "DEPTNO=20; ENAME=null; SUMSAL=10875.00", + "DEPTNO=20; ENAME=ADAMS; SUMSAL=1100.00", + "DEPTNO=20; ENAME=FORD; SUMSAL=3000.00", + "DEPTNO=20; ENAME=JONES; SUMSAL=2975.00", + "DEPTNO=20; ENAME=SCOTT; SUMSAL=3000.00", + "DEPTNO=20; ENAME=SMITH; SUMSAL=800.00", + "DEPTNO=30; ENAME=null; SUMSAL=9400.00", + "DEPTNO=30; ENAME=ALLEN; SUMSAL=1600.00", + "DEPTNO=30; ENAME=BLAKE; SUMSAL=2850.00", + "DEPTNO=30; ENAME=JAMES; SUMSAL=950.00", + "DEPTNO=30; ENAME=MARTIN; SUMSAL=1250.00", + "DEPTNO=30; ENAME=TURNER; SUMSAL=1500.00", + "DEPTNO=30; ENAME=WARD; SUMSAL=1250.00"); + } + + /** {@code ROLLUP (deptno, *)} expands (standard semantics) to detail rows, + * department totals and a grand total. */ + @Test void testRollupStar() { + CalciteAssert.that() + .with(Config.SCOTT) + .with(SqlConformanceEnum.LENIENT) + .query("select deptno, ename, sum(sal) as sumsal\n" + + "from emp\n" + + "group by rollup (deptno, *)\n" + + "order by deptno nulls last, ename nulls first") + .returnsUnordered( + "DEPTNO=10; ENAME=null; SUMSAL=8750.00", + "DEPTNO=10; ENAME=CLARK; SUMSAL=2450.00", + "DEPTNO=10; ENAME=KING; SUMSAL=5000.00", + "DEPTNO=10; ENAME=MILLER; SUMSAL=1300.00", + "DEPTNO=20; ENAME=null; SUMSAL=10875.00", + "DEPTNO=20; ENAME=ADAMS; SUMSAL=1100.00", + "DEPTNO=20; ENAME=FORD; SUMSAL=3000.00", + "DEPTNO=20; ENAME=JONES; SUMSAL=2975.00", + "DEPTNO=20; ENAME=SCOTT; SUMSAL=3000.00", + "DEPTNO=20; ENAME=SMITH; SUMSAL=800.00", + "DEPTNO=30; ENAME=null; SUMSAL=9400.00", + "DEPTNO=30; ENAME=ALLEN; SUMSAL=1600.00", + "DEPTNO=30; ENAME=BLAKE; SUMSAL=2850.00", + "DEPTNO=30; ENAME=JAMES; SUMSAL=950.00", + "DEPTNO=30; ENAME=MARTIN; SUMSAL=1250.00", + "DEPTNO=30; ENAME=TURNER; SUMSAL=1500.00", + "DEPTNO=30; ENAME=WARD; SUMSAL=1250.00", + "DEPTNO=null; ENAME=null; SUMSAL=29025.00"); + } + + /** {@code GROUPING} distinguishes the detail set (ename grouped, g=0) from + * the department set (ename not grouped, g=1). */ + @Test void testGroupingSetsStarWithGrouping() { + CalciteAssert.that() + .with(Config.SCOTT) + .with(SqlConformanceEnum.LENIENT) + .query("select g, count(*) as cnt\n" + + "from (\n" + + " select grouping(ename) as g\n" + + " from emp\n" + + " group by grouping sets ((deptno), (*)))\n" + + "group by g\n" + + "order by g") + .returnsUnordered( + "G=0; CNT=14", // detail rows: ename grouped + "G=1; CNT=3"); // department totals: ename not grouped + } +} diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 854a5ce8fff2..1e5dad32269c 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -8181,6 +8181,32 @@ public boolean isBangEqualAllowed() { + " ()").ok(); } + /** Test case for + * [CALCITE-6537] + * Add syntax to allow non-aggregated rows to be used in GROUPING SETS. */ + @Test void testGroupingSetsStarConformance() { + // Under DEFAULT conformance, "*" is not a valid grouping element. + sql("select deptno, ename, sum(sal)\n" + + "from emp\n" + + "group by grouping sets ((deptno), (^*^))") + .fails("(?s).*Encountered.*\\*.*"); + + // Under LENIENT conformance, "*" expands to a ROW of every input column, + // so the non-aggregated SELECT columns are grouped in the detail set. + sql("select deptno, ename, sum(sal)\n" + + "from emp\n" + + "group by grouping sets ((deptno), (*))") + .withConformance(SqlConformanceEnum.LENIENT).ok(); + + // ROLLUP and CUBE accept "*" too. + sql("select deptno, ename, sum(sal) from emp\n" + + "group by rollup(deptno, *)") + .withConformance(SqlConformanceEnum.LENIENT).ok(); + sql("select deptno, ename, sum(sal) from emp\n" + + "group by cube(deptno, *)") + .withConformance(SqlConformanceEnum.LENIENT).ok(); + } + @Test void testRollup() { // DEPTNO is not null in database, but rollup introduces nulls sql("select deptno, count(*) as c, sum(sal) as s\n" diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 41f2436a25b7..e1bdc8970a8e 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4750,3 +4750,93 @@ order by y; !ok # End agg.iq + +# [CALCITE-6537] Add syntax to allow non-aggregated rows to be used in GROUPING SETS +# non-aggregated (detail) rows. Requires LENIENT conformance. + +!use scott-lenient + +# GROUPING SETS ((deptno), (*)): department totals (ename NULL) plus one +# detail row per employee. +select deptno, ename, sum(sal) as sumsal +from emp +group by grouping sets ((deptno), (*)) +order by deptno, ename nulls first; ++--------+--------+----------+ +| DEPTNO | ENAME | SUMSAL | ++--------+--------+----------+ +| 10 | | 8750.00 | +| 10 | CLARK | 2450.00 | +| 10 | KING | 5000.00 | +| 10 | MILLER | 1300.00 | +| 20 | | 10875.00 | +| 20 | ADAMS | 1100.00 | +| 20 | FORD | 3000.00 | +| 20 | JONES | 2975.00 | +| 20 | SCOTT | 3000.00 | +| 20 | SMITH | 800.00 | +| 30 | | 9400.00 | +| 30 | ALLEN | 1600.00 | +| 30 | BLAKE | 2850.00 | +| 30 | JAMES | 950.00 | +| 30 | MARTIN | 1250.00 | +| 30 | TURNER | 1500.00 | +| 30 | WARD | 1250.00 | ++--------+--------+----------+ +(17 rows) + +!ok + +# ROLLUP (deptno, *): detail rows, department totals and a grand total. +select deptno, ename, sum(sal) as sumsal +from emp +group by rollup (deptno, *) +order by deptno nulls last, ename nulls first; ++--------+--------+----------+ +| DEPTNO | ENAME | SUMSAL | ++--------+--------+----------+ +| 10 | | 8750.00 | +| 10 | CLARK | 2450.00 | +| 10 | KING | 5000.00 | +| 10 | MILLER | 1300.00 | +| 20 | | 10875.00 | +| 20 | ADAMS | 1100.00 | +| 20 | FORD | 3000.00 | +| 20 | JONES | 2975.00 | +| 20 | SCOTT | 3000.00 | +| 20 | SMITH | 800.00 | +| 30 | | 9400.00 | +| 30 | ALLEN | 1600.00 | +| 30 | BLAKE | 2850.00 | +| 30 | JAMES | 950.00 | +| 30 | MARTIN | 1250.00 | +| 30 | TURNER | 1500.00 | +| 30 | WARD | 1250.00 | +| | | 29025.00 | ++--------+--------+----------+ +(18 rows) + +!ok + +# GROUPING distinguishes the detail set (ename grouped, g=0) from the +# department set (ename not grouped, g=1). +select g, count(*) as cnt +from ( + select grouping(ename) as g + from emp + group by grouping sets ((deptno), (*))) +group by g +order by g; ++---+-----+ +| G | CNT | ++---+-----+ +| 0 | 14 | +| 1 | 3 | ++---+-----+ +(2 rows) + +!ok + +# Under DEFAULT conformance, * in GROUPING SETS is a parse error; +# see SqlValidatorTest.testGroupingSetsStarConformance. +!use post