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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions core/src/main/codegen/templates/Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -2895,18 +2915,32 @@ SqlNodeList GroupBy() :
List<SqlNode> GroupingElementList() :
{
final List<SqlNode> list = new ArrayList<SqlNode>();
final boolean starAllowed =
this.conformance.isGroupingSetsStarAllowed();
}
{
{
if (starAllowed) {
pushGroupingStar();
}
}
AddGroupingElement(list)
( LOOKAHEAD(2) <COMMA> AddGroupingElement(list) )*
{ return list; }
{
if (starAllowed) {
popGroupingStar();
}
return list;
}
}

void AddGroupingElement(List<SqlNode> list) :
{
final List<SqlNode> subList;
final SqlNodeList nodes;
final Span s;
final boolean starAllowed =
this.conformance.isGroupingSetsStarAllowed();
}
{
LOOKAHEAD(2)
Expand All @@ -2916,21 +2950,41 @@ void AddGroupingElement(List<SqlNode> list) :
SqlStdOperatorTable.GROUPING_SETS.createCall(s.end(this), subList));
}
| <ROLLUP> { s = span(); }
<LPAREN> nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY)
<LPAREN> {
if (starAllowed) {
pushGroupingStar();
}
}
nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY)
<RPAREN> {
if (starAllowed) {
popGroupingStar();
}
list.add(
SqlStdOperatorTable.ROLLUP.createCall(s.end(this), nodes.getList()));
}
| <CUBE> { s = span(); }
<LPAREN> nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY)
<LPAREN> {
if (starAllowed) {
pushGroupingStar();
}
}
nodes = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY)
<RPAREN> {
if (starAllowed) {
popGroupingStar();
}
list.add(
SqlStdOperatorTable.CUBE.createCall(s.end(this), nodes.getList()));
}
| LOOKAHEAD(3)
<LPAREN> { s = span(); } <RPAREN> {
list.add(new SqlNodeList(s.end(this)));
}
| LOOKAHEAD({ allowGroupingStar() && getToken(1).kind == STAR })
<STAR> {
list.add(SqlIdentifier.star(getPos()));
}
| AddExpression(list, ExprContext.ACCEPT_SUB_QUERY)
}

Expand Down Expand Up @@ -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 })
<STAR> {
return SqlIdentifier.star(getPos());
}
|
e = NewSpecification()
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.
*
* <p>Among the built-in conformance levels, true in
* {@link SqlConformanceEnum#BABEL},
* {@link SqlConformanceEnum#LENIENT};
* false otherwise.
*/
default boolean isGroupingSetsStarAllowed() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,8 @@ protected SqlDelegatingConformance(SqlConformance delegate) {
@Override public boolean isCorrelatedAggregateAllowed() {
return delegate.isCorrelatedAggregateAllowed();
}

@Override public boolean isGroupingSetsStarAllowed() {
return delegate.isGroupingSetsStarAllowed();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5500,6 +5500,7 @@
*/
protected void validateGroupClause(SqlSelect select) {
rewriteGroupByAll(select);
rewriteGroupingStar(select);
SqlNodeList groupList = select.getGroup();
if (groupList == null) {
return;
Expand Down Expand Up @@ -5601,6 +5602,79 @@
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<SqlNode> 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) {

Check failure on line 5637 in core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AaAsP53dvKLTf8kMsaVW&open=AaAsP53dvKLTf8kMsaVW&pullRequest=5208
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<SqlNode> operands = call.getOperandList();
List<SqlNode> 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<SqlNode> 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) {
Expand Down
109 changes: 109 additions & 0 deletions core/src/test/java/org/apache/calcite/test/GroupingSetsStarTest.java
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 26 additions & 0 deletions core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8181,6 +8181,32 @@ public boolean isBangEqualAllowed() {
+ " ()").ok();
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-6537">[CALCITE-6537]
* Add syntax to allow non-aggregated rows to be used in GROUPING SETS</a>. */
@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"
Expand Down
Loading
Loading