Skip to content

Commit e2482b8

Browse files
mjmj
authored andcommitted
Merge master and preserve concurrent documentation additions
2 parents 9f2ae73 + 7cc8638 commit e2482b8

20 files changed

Lines changed: 859 additions & 104 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ out. `is()` answers "did the grammar prove it", `may()` answers "could it be rul
189189
guard uses `may()` and a dispatcher uses `is()`. Function volatility is not a syntactic property,
190190
so anything the caller has not declared pure stays unproven and is listed by name.
191191

192+
Legacy MySQL `GROUP BY ... ASC/DESC` is available with `Dialect.MYSQL` and the explicit
193+
`withLegacyMySqlGroupBy(true)` option; modern/default parsing keeps it disabled.
194+
192195
## Piped SQL
193196

194197
Support is progressing for Piped SQL, which writes queries in the order they actually
@@ -208,6 +211,9 @@ Background reading: the [Google research paper](https://storage.googleapis.com/g
208211
[BigQuery pipe syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax)
209212
and [DuckDB FROM-first syntax](https://duckdb.org/docs/sql/query_syntax/from.html#from-first-syntax).
210213

214+
ODBC `{fn TIMESTAMPADD(...)}` and `{fn TIMESTAMPDIFF(...)}` expose standard
215+
`SQL_TSI_*` interval arguments as time-unit expressions, preserving column traversal.
216+
211217
## Java version
212218

213219
| JSqlParser | Runtime | Notes |

src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,19 @@ public String toString() {
4242
}
4343

4444
public enum DateUnit {
45-
CENTURY, DECADE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND;
45+
CENTURY, DECADE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND, SQL_TSI_FRAC_SECOND, SQL_TSI_SECOND, SQL_TSI_MINUTE, SQL_TSI_HOUR, SQL_TSI_DAY, SQL_TSI_WEEK, SQL_TSI_MONTH, SQL_TSI_QUARTER, SQL_TSI_YEAR;
46+
47+
/** Returns an ODBC interval keyword, or null when the text is not one. */
48+
public static DateUnit fromOdbcInterval(String text) {
49+
if (text == null || !text.toUpperCase(Locale.ROOT).startsWith("SQL_TSI_")) {
50+
return null;
51+
}
52+
try {
53+
return from(text);
54+
} catch (IllegalArgumentException exception) {
55+
return null;
56+
}
57+
}
4658

4759
public static DateUnit from(String UnitStr) {
4860
return Enum.valueOf(DateUnit.class, UnitStr.toUpperCase(Locale.ROOT));

src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public enum Dialect {
4646
AdjacentStringLiterals.WHITESPACE,
4747
Feature.allowDoubleQuotedStrings,
4848
Feature.allowBackslashEscapeCharacter), SNOWFLAKE(
49-
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER;
49+
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS;
5050

5151
private final Set<Feature> lexerFeatures;
5252
private final AdjacentStringLiterals adjacentStringLiterals;
@@ -102,6 +102,11 @@ public P withUnsupportedStatements(boolean allowUnsupportedStatements) {
102102
return withFeature(Feature.allowUnsupportedStatements, allowUnsupportedStatements);
103103
}
104104

105+
/** Enables GROUP BY ASC/DESC for MySQL versions before 8.0.13. Requires MYSQL dialect. */
106+
public P withLegacyMySqlGroupBy(boolean enabled) {
107+
return withFeature(Feature.allowLegacyMySqlGroupBy, enabled);
108+
}
109+
105110
public P withTimeOut(long timeOutMillSeconds) {
106111
return withFeature(Feature.timeOut, timeOutMillSeconds);
107112
}

src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ public static Statement parse(Reader statementReader) throws JSQLParserException
6060
return statement;
6161
}
6262

63+
/**
64+
* Parses a single SQL statement.
65+
*
66+
* @param sql the SQL statement to parse
67+
* @return the parsed statement
68+
* @throws JSQLParserException if the input is null, empty, or cannot be parsed
69+
*/
6370
public static Statement parse(String sql) throws JSQLParserException {
6471
return parse(sql, null);
6572
}
@@ -73,17 +80,15 @@ public static Statement parse(String sql) throws JSQLParserException {
7380
* CCJSqlParserUtil.parse("select * from [mytable]", parser -> parser.withSquareBracketQuotation(true));
7481
* }
7582
*
76-
* @param sql
77-
* @param consumer
78-
* @return
79-
* @throws JSQLParserException
83+
* @param sql the SQL statement to parse
84+
* @param consumer parser configuration callback, or {@code null}
85+
* @return the parsed statement
86+
* @throws JSQLParserException if the input is null, empty, or cannot be parsed
8087
*/
8188
public static Statement parse(String sql, Consumer<CCJSqlParser> consumer)
8289
throws JSQLParserException {
8390

84-
if (sql == null || sql.isEmpty()) {
85-
return null;
86-
}
91+
requireStatementInput(sql);
8792

8893
ExecutorService executorService = Executors.newSingleThreadExecutor();
8994
Statement statement;
@@ -97,12 +102,19 @@ public static Statement parse(String sql, Consumer<CCJSqlParser> consumer)
97102
return statement;
98103
}
99104

105+
/**
106+
* Parses a single SQL statement using the caller's executor, which is left open.
107+
*
108+
* @param sql the SQL statement to parse
109+
* @param executorService executor to use for parsing
110+
* @param consumer parser configuration callback, or {@code null}
111+
* @return the parsed statement
112+
* @throws JSQLParserException if the input is null, empty, or cannot be parsed
113+
*/
100114
public static Statement parse(String sql, ExecutorService executorService,
101115
Consumer<CCJSqlParser> consumer)
102116
throws JSQLParserException {
103-
if (sql == null || sql.isEmpty()) {
104-
return null;
105-
}
117+
requireStatementInput(sql);
106118

107119
Statement statement;
108120
// first, try to parse fast and simple
@@ -134,6 +146,12 @@ public static Statement parse(String sql, ExecutorService executorService,
134146
return statement;
135147
}
136148

149+
private static void requireStatementInput(String sql) throws JSQLParserException {
150+
if (sql == null || sql.isEmpty()) {
151+
throw new JSQLParserException("SQL statement must not be null or empty.");
152+
}
153+
}
154+
137155
public static CCJSqlParser newParser(String sql) {
138156
if (sql == null || sql.isEmpty()) {
139157
return null;
@@ -412,20 +430,21 @@ public Statement call() throws ParseException {
412430
/**
413431
* Parse a statement list.
414432
*
415-
* @return the statements parsed
433+
* @return the statements parsed, or a new empty list for null or empty input
416434
*/
417435
public static Statements parseStatements(String sqls) throws JSQLParserException {
418-
if (sqls == null || sqls.isEmpty()) {
419-
return null;
420-
}
421-
422436
return parseStatements(sqls, null);
423437
}
424438

439+
/**
440+
* Parses a statement list with optional parser configuration.
441+
*
442+
* @return the statements parsed, or a new empty list for null or empty input
443+
*/
425444
public static Statements parseStatements(String sqls, Consumer<CCJSqlParser> consumer)
426445
throws JSQLParserException {
427446
if (sqls == null || sqls.isEmpty()) {
428-
return null;
447+
return new Statements();
429448
}
430449

431450
ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -437,15 +456,15 @@ public static Statements parseStatements(String sqls, Consumer<CCJSqlParser> con
437456
}
438457

439458
/**
440-
* Parse a statement list.
459+
* Parses a statement list using the caller's executor, which is left open.
441460
*
442-
* @return the statements parsed
461+
* @return the statements parsed, or a new empty list for null or empty input
443462
*/
444463
public static Statements parseStatements(String sqls, ExecutorService executorService,
445464
Consumer<CCJSqlParser> consumer)
446465
throws JSQLParserException {
447466
if (sqls == null || sqls.isEmpty()) {
448-
return null;
467+
return new Statements();
449468
}
450469

451470
CCJSqlParser parser = newParser(sqls);

src/main/java/net/sf/jsqlparser/parser/feature/Feature.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public enum Feature {
7575
* "GROUP BY"
7676
*/
7777
selectGroupBy,
78+
/** Explicit ASC/DESC on GROUP BY items in legacy MySQL. */
79+
selectGroupByOrdering,
7880
/**
7981
* "GROUPING SETS"
8082
*/
@@ -786,6 +788,9 @@ public enum Feature {
786788
*/
787789
allowPostgresSpecificSyntax(false),
788790

791+
/** Enables legacy GROUP BY ordering with the MYSQL dialect; disabled by default. */
792+
allowLegacyMySqlGroupBy(false),
793+
789794
// PERFORMANCE
790795

791796
/**

src/main/java/net/sf/jsqlparser/statement/select/GroupByElement.java

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@
1616
import java.util.Collections;
1717
import java.util.List;
1818
import java.util.Optional;
19+
import java.util.Objects;
20+
import java.util.function.Consumer;
1921

2022
import net.sf.jsqlparser.expression.Expression;
2123
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
2224
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
2325

2426
public class GroupByElement implements Serializable {
27+
public enum SortDirection {
28+
ASC, DESC
29+
}
30+
31+
private final List<SortDirection> groupBySortDirections = new ArrayList<>();
2532
private ExpressionList<Expression> groupByExpressions = new ExpressionList<>();
2633
private List<ExpressionList<Expression>> groupingSets = new ArrayList<>();
2734
// postgres rollup is an ExpressionList
@@ -45,6 +52,9 @@ public ExpressionList<Expression> getGroupByExpressions() {
4552
}
4653

4754
public void setGroupByExpressions(ExpressionList<Expression> groupByExpressions) {
55+
if (this.groupByExpressions != groupByExpressions) {
56+
groupBySortDirections.clear();
57+
}
4858
this.groupByExpressions = groupByExpressions;
4959
}
5060

@@ -68,35 +78,83 @@ public void addGroupingSet(ExpressionList<Expression> list) {
6878
this.groupingSets.add(list);
6979
}
7080

81+
/** Returns the explicit direction at a grouping-list position, or null if omitted. */
82+
public SortDirection getGroupBySortDirection(int index) {
83+
Objects.checkIndex(index, groupByExpressions.size());
84+
return index < groupBySortDirections.size() ? groupBySortDirections.get(index) : null;
85+
}
86+
87+
/** Directions belong to list positions. Replacing the expression list clears them. */
88+
public void setGroupBySortDirection(int index, SortDirection direction) {
89+
Objects.checkIndex(index, groupByExpressions.size());
90+
while (groupBySortDirections.size() <= index) {
91+
groupBySortDirections.add(null);
92+
}
93+
groupBySortDirections.set(index, direction);
94+
}
95+
96+
public boolean hasGroupBySortDirections() {
97+
if (groupByExpressions != null && !groupBySortDirections.isEmpty()) {
98+
for (int i = 0; i < groupByExpressions.size(); i++) {
99+
if (getGroupBySortDirection(i) != null) {
100+
return true;
101+
}
102+
}
103+
}
104+
return false;
105+
}
106+
71107
@Override
72-
@SuppressWarnings({"PMD.CyclomaticComplexity"})
73108
public String toString() {
74-
StringBuilder b = new StringBuilder();
75-
b.append("GROUP BY ");
109+
StringBuilder builder = new StringBuilder();
110+
appendTo(builder, builder::append, builder::append);
111+
return builder.toString();
112+
}
76113

114+
/** Shares clause layout while letting a deparser visit each expression. */
115+
public void appendTo(StringBuilder builder, Consumer<ExpressionList<?>> listRenderer,
116+
Consumer<Expression> expressionRenderer) {
117+
builder.append("GROUP BY ");
77118
if (groupByExpressions != null) {
78-
b.append(groupByExpressions);
119+
if (hasGroupBySortDirections()) {
120+
appendOrderedExpressions(builder, expressionRenderer);
121+
} else {
122+
listRenderer.accept(groupByExpressions);
123+
}
79124
}
80-
81-
int i = 0;
82125
if (!groupingSets.isEmpty()) {
83-
if (b.charAt(b.length() - 1) != ' ') {
84-
b.append(' ');
126+
if (builder.charAt(builder.length() - 1) != ' ') {
127+
builder.append(' ');
85128
}
86-
b.append("GROUPING SETS (");
87-
for (ExpressionList<?> expressionList : groupingSets) {
88-
b.append(i++ > 0 ? ", " : "").append(Select.getStringList(
89-
expressionList,
90-
true, expressionList instanceof ParenthesedExpressionList));
129+
builder.append("GROUPING SETS (");
130+
for (int i = 0; i < groupingSets.size(); i++) {
131+
builder.append(i > 0 ? ", " : "");
132+
listRenderer.accept(groupingSets.get(i));
91133
}
92-
b.append(")");
134+
builder.append(")");
93135
}
94-
95136
if (isMysqlWithRollup()) {
96-
b.append(" WITH ROLLUP");
137+
builder.append(" WITH ROLLUP");
97138
}
139+
}
98140

99-
return b.toString();
141+
private void appendOrderedExpressions(StringBuilder builder,
142+
Consumer<Expression> expressionRenderer) {
143+
boolean brackets = groupByExpressions instanceof ParenthesedExpressionList<?>;
144+
if (brackets) {
145+
builder.append('(');
146+
}
147+
for (int i = 0; i < groupByExpressions.size(); i++) {
148+
builder.append(i > 0 ? ", " : "");
149+
expressionRenderer.accept(groupByExpressions.get(i));
150+
SortDirection direction = getGroupBySortDirection(i);
151+
if (direction != null) {
152+
builder.append(' ').append(direction);
153+
}
154+
}
155+
if (brackets) {
156+
builder.append(')');
157+
}
100158
}
101159

102160
public GroupByElement withGroupByExpressions(ExpressionList<Expression> groupByExpressions) {
@@ -115,9 +173,9 @@ public GroupByElement addGroupByExpressions(Expression... groupByExpressions) {
115173

116174
public GroupByElement addGroupByExpressions(
117175
Collection<? extends Expression> groupByExpressions) {
118-
ExpressionList collection =
119-
Optional.ofNullable(getGroupByExpressions()).orElseGet(ExpressionList::new);
120-
Collections.addAll(collection, groupByExpressions);
176+
ExpressionList<Expression> collection =
177+
Optional.ofNullable(getGroupByExpressionList()).orElseGet(ExpressionList::new);
178+
collection.addAll(groupByExpressions);
121179
return this.withGroupByExpressions(collection);
122180
}
123181

src/main/java/net/sf/jsqlparser/statement/select/Join.java

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,27 @@ public Join setJoinHint(JoinHint joinHint) {
464464
return this;
465465
}
466466

467+
/** Appends the join keyword, hint and FETCH modifier, followed by a space. */
468+
public StringBuilder appendJoinKeywordTo(StringBuilder builder) {
469+
if (isStraight()) {
470+
builder.append("STRAIGHT_JOIN ");
471+
} else if (isApply()) {
472+
builder.append("APPLY ");
473+
} else {
474+
if (joinHint != null && joinHint.getPosition() == JoinHint.Position.BEFORE_JOIN) {
475+
builder.append(joinHint).append(' ');
476+
}
477+
builder.append("JOIN ");
478+
if (joinHint != null && joinHint.getPosition() == JoinHint.Position.AFTER_JOIN) {
479+
builder.append(joinHint).append(' ');
480+
}
481+
if (fetch) {
482+
builder.append("FETCH ");
483+
}
484+
}
485+
return builder;
486+
}
487+
467488
@Override
468489
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
469490
public String toString() {
@@ -510,19 +531,7 @@ public String toString() {
510531
builder.append("ARRAY ");
511532
}
512533

513-
if (isStraight()) {
514-
builder.append("STRAIGHT_JOIN ");
515-
} else if (isApply()) {
516-
builder.append("APPLY ");
517-
} else {
518-
if (joinHint != null) {
519-
builder.append(joinHint).append(" ");
520-
}
521-
builder.append("JOIN ");
522-
if (fetch) {
523-
builder.append("FETCH ");
524-
}
525-
}
534+
appendJoinKeywordTo(builder);
526535

527536
builder.append(fromItem).append((joinWindow != null) ? " WITHIN " + joinWindow : "");
528537
}

0 commit comments

Comments
 (0)