Skip to content
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ out. `is()` answers "did the grammar prove it", `may()` answers "could it be rul
guard uses `may()` and a dispatcher uses `is()`. Function volatility is not a syntactic property,
so anything the caller has not declared pure stays unproven and is listed by name.

Legacy MySQL `GROUP BY ... ASC/DESC` is available with `Dialect.MYSQL` and the explicit
`withLegacyMySqlGroupBy(true)` option; modern/default parsing keeps it disabled.

## Piped SQL

Support is progressing for Piped SQL, which writes queries in the order they actually
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ public P withUnsupportedStatements(boolean allowUnsupportedStatements) {
return withFeature(Feature.allowUnsupportedStatements, allowUnsupportedStatements);
}

/** Enables GROUP BY ASC/DESC for MySQL versions before 8.0.13. Requires MYSQL dialect. */
public P withLegacyMySqlGroupBy(boolean enabled) {
return withFeature(Feature.allowLegacyMySqlGroupBy, enabled);
}

public P withTimeOut(long timeOutMillSeconds) {
return withFeature(Feature.timeOut, timeOutMillSeconds);
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public enum Feature {
* "GROUP BY"
*/
selectGroupBy,
/** Explicit ASC/DESC on GROUP BY items in legacy MySQL. */
selectGroupByOrdering,
/**
* "GROUPING SETS"
*/
Expand Down Expand Up @@ -786,6 +788,9 @@ public enum Feature {
*/
allowPostgresSpecificSyntax(false),

/** Enables legacy GROUP BY ordering with the MYSQL dialect; disabled by default. */
allowLegacyMySqlGroupBy(false),

// PERFORMANCE

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,26 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Objects;
import java.util.function.Consumer;

import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;

public class GroupByElement implements Serializable {
public enum SortDirection {
ASC, DESC
}

private final List<SortDirection> groupBySortDirections = new ArrayList<>();
private ExpressionList<Expression> groupByExpressions = new ExpressionList<>();
private List<ExpressionList<Expression>> groupingSets = new ArrayList<>();
// postgres rollup is an ExpressionList
private boolean mysqlWithRollup = false;

public boolean isUsingBrackets() {
return groupByExpressions.isUsingBrackets();

Check warning on line 38 in src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isUsingBrackets() in ExpressionList has been deprecated

Check warning on line 38 in src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isUsingBrackets() in ExpressionList has been deprecated

Check warning on line 38 in src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isUsingBrackets() in ExpressionList has been deprecated
}

public <T, S> T accept(GroupByVisitor<T> groupByVisitor, S context) {
Expand All @@ -45,6 +52,9 @@
}

public void setGroupByExpressions(ExpressionList<Expression> groupByExpressions) {
if (this.groupByExpressions != groupByExpressions) {
groupBySortDirections.clear();
}
this.groupByExpressions = groupByExpressions;
}

Expand All @@ -68,35 +78,83 @@
this.groupingSets.add(list);
}

/** Returns the explicit direction at a grouping-list position, or null if omitted. */
public SortDirection getGroupBySortDirection(int index) {
Objects.checkIndex(index, groupByExpressions.size());
return index < groupBySortDirections.size() ? groupBySortDirections.get(index) : null;
}

/** Directions belong to list positions. Replacing the expression list clears them. */
public void setGroupBySortDirection(int index, SortDirection direction) {
Objects.checkIndex(index, groupByExpressions.size());
while (groupBySortDirections.size() <= index) {
groupBySortDirections.add(null);
}
groupBySortDirections.set(index, direction);
}

public boolean hasGroupBySortDirections() {
if (groupByExpressions != null && !groupBySortDirections.isEmpty()) {
for (int i = 0; i < groupByExpressions.size(); i++) {
if (getGroupBySortDirection(i) != null) {
return true;
}
}
}
return false;
}

@Override
@SuppressWarnings({"PMD.CyclomaticComplexity"})
public String toString() {
StringBuilder b = new StringBuilder();
b.append("GROUP BY ");
StringBuilder builder = new StringBuilder();
appendTo(builder, builder::append, builder::append);
return builder.toString();
}

/** Shares clause layout while letting a deparser visit each expression. */
public void appendTo(StringBuilder builder, Consumer<ExpressionList<?>> listRenderer,
Consumer<Expression> expressionRenderer) {
builder.append("GROUP BY ");
if (groupByExpressions != null) {
b.append(groupByExpressions);
if (hasGroupBySortDirections()) {
appendOrderedExpressions(builder, expressionRenderer);
} else {
listRenderer.accept(groupByExpressions);
}
}

int i = 0;
if (!groupingSets.isEmpty()) {
if (b.charAt(b.length() - 1) != ' ') {
b.append(' ');
if (builder.charAt(builder.length() - 1) != ' ') {
builder.append(' ');
}
b.append("GROUPING SETS (");
for (ExpressionList<?> expressionList : groupingSets) {
b.append(i++ > 0 ? ", " : "").append(Select.getStringList(
expressionList,
true, expressionList instanceof ParenthesedExpressionList));
builder.append("GROUPING SETS (");
for (int i = 0; i < groupingSets.size(); i++) {
builder.append(i > 0 ? ", " : "");
listRenderer.accept(groupingSets.get(i));
}
b.append(")");
builder.append(")");
}

if (isMysqlWithRollup()) {
b.append(" WITH ROLLUP");
builder.append(" WITH ROLLUP");
}
}

return b.toString();
private void appendOrderedExpressions(StringBuilder builder,
Consumer<Expression> expressionRenderer) {
boolean brackets = groupByExpressions instanceof ParenthesedExpressionList<?>;
if (brackets) {
builder.append('(');
}
for (int i = 0; i < groupByExpressions.size(); i++) {
builder.append(i > 0 ? ", " : "");
expressionRenderer.accept(groupByExpressions.get(i));
SortDirection direction = getGroupBySortDirection(i);
if (direction != null) {
builder.append(' ').append(direction);
}
}
if (brackets) {
builder.append(')');
}
}

public GroupByElement withGroupByExpressions(ExpressionList<Expression> groupByExpressions) {
Expand All @@ -115,9 +173,9 @@

public GroupByElement addGroupByExpressions(
Collection<? extends Expression> groupByExpressions) {
ExpressionList collection =
Optional.ofNullable(getGroupByExpressions()).orElseGet(ExpressionList::new);
Collections.addAll(collection, groupByExpressions);
ExpressionList<Expression> collection =
Optional.ofNullable(getGroupByExpressionList()).orElseGet(ExpressionList::new);
collection.addAll(groupByExpressions);
return this.withGroupByExpressions(collection);
}

Expand Down
25 changes: 4 additions & 21 deletions src/main/java/net/sf/jsqlparser/util/deparser/GroupByDeParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,24 @@
package net.sf.jsqlparser.util.deparser;

import net.sf.jsqlparser.expression.ExpressionVisitor;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.statement.select.GroupByElement;

public class GroupByDeParser extends AbstractDeParser<GroupByElement> {

private final ExpressionListDeParser<?> expressionListDeParser;
private final ExpressionVisitor<StringBuilder> expressionVisitor;

public GroupByDeParser(ExpressionVisitor<StringBuilder> expressionVisitor,
StringBuilder buffer) {
super(buffer);
this.expressionVisitor = expressionVisitor;
this.expressionListDeParser = new ExpressionListDeParser<>(expressionVisitor, buffer);
this.builder = buffer;
}

@Override
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
public void deParse(GroupByElement groupBy) {
builder.append("GROUP BY ");
expressionListDeParser.deParse(groupBy.getGroupByExpressionList());

int i = 0;
if (!groupBy.getGroupingSets().isEmpty()) {
if (builder.charAt(builder.length() - 1) != ' ') {
builder.append(' ');
}
builder.append("GROUPING SETS (");
for (ExpressionList<?> expressionList : groupBy.getGroupingSets()) {
builder.append(i++ > 0 ? ", " : "");
expressionListDeParser.deParse(expressionList);
}
builder.append(")");
}

if (groupBy.isMysqlWithRollup()) {
builder.append(" WITH ROLLUP");
}
groupBy.appendTo(builder, expressionListDeParser::deParse,
expression -> expression.accept(expressionVisitor, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public void validate(GroupByElement groupBy) {
public <S> Void visit(GroupByElement groupBy, S context) {
for (ValidationCapability c : getCapabilities()) {
validateFeature(c, Feature.selectGroupBy);
if (groupBy.hasGroupBySortDirections()) {
validateFeature(c, Feature.selectGroupByOrdering);
}
if (isNotEmpty(groupBy.getGroupingSets())) {
validateFeature(c, Feature.selectGroupByGroupingSets);
}
Expand Down
29 changes: 28 additions & 1 deletion src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -7727,7 +7727,13 @@ GroupByElement GroupByColumnReferences():
)
|
(
list = ExpressionList() { groupBy.setGroupByExpressions(list); }
(
LOOKAHEAD({ getAsBoolean(Feature.allowLegacyMySqlGroupBy)
&& "MYSQL".equals(getAsString(Feature.dialect)) })
LegacyMySqlGroupByExpressions(groupBy)
|
list = ExpressionList() { groupBy.setGroupByExpressions(list); }
)
(
LOOKAHEAD(2) <K_GROUPING> <K_SETS>
"("
Expand All @@ -7743,6 +7749,27 @@ GroupByElement GroupByColumnReferences():
}
}

void LegacyMySqlGroupByExpressions(GroupByElement groupBy):
{}
{
LegacyMySqlGroupByExpression(groupBy)
( LOOKAHEAD(2) "," LegacyMySqlGroupByExpression(groupBy) )*
}

void LegacyMySqlGroupByExpression(GroupByElement groupBy):
{
Expression expression;
Token direction;
}
{
expression = Expression() { groupBy.getGroupByExpressionList().add(expression); }
[ LOOKAHEAD(2) (direction=<K_ASC> | direction=<K_DESC>) {
groupBy.setGroupBySortDirection(groupBy.getGroupByExpressionList().size() - 1,
direction.kind == K_ASC ? GroupByElement.SortDirection.ASC
: GroupByElement.SortDirection.DESC);
} ]
}

ExpressionList<Expression> GroupingSet():
{
ExpressionList<Expression> list;
Expand Down
18 changes: 18 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -919,3 +919,21 @@ handler bodies. Procedure side effects remain unknown; table discovery reports u
procedure calls, and feature analysis remains conservative. This covers anonymous blocks
with variable declarations, SQL statements, assignments, calls, nesting and handlers, not
all PL/SQL declarations, loops, packages or procedure definitions.

Legacy MySQL GROUP BY ordering
==============================

MySQL before 8.0.13 accepted ``ASC`` and ``DESC`` on individual ``GROUP BY`` items.
Select the existing ``MYSQL`` dialect and explicitly enable this legacy syntax:

.. code-block:: java

Statement statement = CCJSqlParserUtil.parse(
"SELECT a FROM t GROUP BY a DESC",
parser -> parser.withDialect(Dialect.MYSQL).withLegacyMySqlGroupBy(true));

The option is disabled by default and does not enable this syntax in other dialects.
``GroupByElement`` keeps its existing expression list; ``getGroupBySortDirection(index)``
returns each explicit direction, or null when omitted. Directions follow list positions;
replacing the expression list clears them. Validators report the separate
``selectGroupByOrdering`` feature, which is not enabled in the MySQL 8.0 capability.
Loading
Loading