Skip to content

Commit d473eeb

Browse files
committed
Expose structured ORDER BY fragment parsing
1 parent 0ecf096 commit d473eeb

3 files changed

Lines changed: 143 additions & 19 deletions

File tree

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

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.io.InputStream;
1414
import java.io.Reader;
1515
import java.util.Stack;
16+
import java.util.ArrayList;
17+
import java.util.List;
1618
import java.util.concurrent.Callable;
1719
import java.util.concurrent.ExecutorService;
1820
import java.util.concurrent.Executors;
@@ -31,6 +33,7 @@
3133
import net.sf.jsqlparser.statement.Statement;
3234
import net.sf.jsqlparser.statement.Statements;
3335
import net.sf.jsqlparser.statement.create.table.ColDataType;
36+
import net.sf.jsqlparser.statement.select.OrderByElement;
3437

3538
/**
3639
* Toolfunctions to start and use JSqlParser.
@@ -242,34 +245,68 @@ public static ColDataType parseColDataType(String columnDataType) throws JSQLPar
242245
*/
243246
public static ColDataType parseColDataType(String columnDataType,
244247
Consumer<CCJSqlParser> consumer) throws JSQLParserException {
245-
if (columnDataType == null || columnDataType.isEmpty()) {
248+
return parseFragment(columnDataType, "column data type", consumer,
249+
CCJSqlParser::ColDataType);
250+
}
251+
252+
/**
253+
* Parses comma-separated ORDER BY elements without the ORDER BY keywords. Trailing tokens are
254+
* rejected. Returns an empty list for null or empty input.
255+
*/
256+
public static List<OrderByElement> parseOrderByElements(String elements)
257+
throws JSQLParserException {
258+
return parseOrderByElements(elements, null);
259+
}
260+
261+
/**
262+
* Parses ORDER BY elements with optional parser configuration. The input contains only the
263+
* elements (for example, {@code name DESC, id ASC}), and must be consumed completely.
264+
*
265+
* @return the parsed elements, or an empty list for null or empty input
266+
* @throws JSQLParserException when the input cannot be parsed completely
267+
*/
268+
public static List<OrderByElement> parseOrderByElements(String elements,
269+
Consumer<CCJSqlParser> consumer) throws JSQLParserException {
270+
return elements == null || elements.isEmpty() ? new ArrayList<>()
271+
: parseFragment(elements, "ORDER BY elements", consumer,
272+
CCJSqlParser::OrderByElementList);
273+
}
274+
275+
@FunctionalInterface
276+
private interface FragmentParser<T> {
277+
T parse(CCJSqlParser parser) throws ParseException;
278+
}
279+
280+
private static <T> T parseFragment(String input, String description,
281+
Consumer<CCJSqlParser> consumer, FragmentParser<T> fragment)
282+
throws JSQLParserException {
283+
if (input == null || input.isEmpty()) {
246284
return null;
247285
}
248-
249286
try {
250-
return parseColDataType(columnDataType, false, consumer);
287+
return parseFragment(input, description, false, consumer, fragment);
251288
} catch (JSQLParserException ex) {
252-
return parseColDataType(columnDataType, true, consumer);
289+
return parseFragment(input, description, true, consumer, fragment);
253290
}
254291
}
255292

256-
private static ColDataType parseColDataType(String columnDataType, boolean allowComplexParsing,
257-
Consumer<CCJSqlParser> consumer) throws JSQLParserException {
258-
CCJSqlParser parser = newParser(columnDataType)
259-
.withAllowComplexParsing(allowComplexParsing);
293+
private static <T> T parseFragment(String input, String description,
294+
boolean allowComplexParsing,
295+
Consumer<CCJSqlParser> consumer, FragmentParser<T> fragment)
296+
throws JSQLParserException {
297+
CCJSqlParser parser = newParser(input).withAllowComplexParsing(allowComplexParsing);
260298
if (consumer != null) {
261299
consumer.accept(parser);
262300
}
263-
264301
try {
265-
ColDataType result = parser.ColDataType();
302+
T result = fragment.parse(parser);
266303
if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) {
267304
throw new JSQLParserException(
268-
"could only parse partial column data type " + result);
305+
"could only parse partial " + description + " " + result);
269306
}
270307
return result;
271308
} catch (ParseException ex) {
272-
throw new JSQLParserException(columnDataType, ex);
309+
throw new JSQLParserException(input, ex);
273310
}
274311
}
275312

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8122,15 +8122,23 @@ Expression Qualify():
81228122

81238123
List<OrderByElement> OrderByElements():
81248124
{
8125-
List<OrderByElement> orderByList = new ArrayList<OrderByElement>();
8126-
OrderByElement orderByElement = null;
8125+
List<OrderByElement> elements;
81278126
}
81288127
{
8129-
<K_ORDER> [ <K_SIBLINGS> ] <K_BY> orderByElement=OrderByElement() { orderByList.add(orderByElement); }
8130-
( LOOKAHEAD(2) "," orderByElement=OrderByElement() { orderByList.add(orderByElement); } )*
8131-
{
8132-
return orderByList;
8133-
}
8128+
<K_ORDER> [ <K_SIBLINGS> ] <K_BY> elements=OrderByElementList()
8129+
{ return elements; }
8130+
}
8131+
8132+
/** Shared element list for statement clauses and the fragment parsing API. */
8133+
List<OrderByElement> OrderByElementList():
8134+
{
8135+
List<OrderByElement> elements = new ArrayList<OrderByElement>();
8136+
OrderByElement element;
8137+
}
8138+
{
8139+
element=OrderByElement() { elements.add(element); }
8140+
( LOOKAHEAD(2) "," element=OrderByElement() { elements.add(element); } )*
8141+
{ return elements; }
81348142
}
81358143

81368144
OrderByElement OrderByElement():
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
14+
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
import java.util.List;
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.LongValue;
18+
import net.sf.jsqlparser.expression.JdbcParameter;
19+
import net.sf.jsqlparser.statement.select.OrderByElement;
20+
import net.sf.jsqlparser.statement.select.PlainSelect;
21+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
22+
import net.sf.jsqlparser.util.deparser.OrderByDeParser;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
class OrderByFragmentTest {
28+
@ParameterizedTest
29+
@ValueSource(strings = {"a DESC NULLS LAST, b ASC", "COALESCE(a, 0) DESC, b + ? ASC",
30+
"(1 + a) / (1 + b) DESC", "a COLLATE \"C\" ASC", "a WITH FILL FROM 1 TO 5"})
31+
void usesTheSameStructuredElementsAsAnOrderByClause(String fragment) throws Exception {
32+
List<OrderByElement> elements = CCJSqlParserUtil.parseOrderByElements(fragment);
33+
PlainSelect select =
34+
(PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM t ORDER BY " + fragment);
35+
assertEquals(select.getOrderByElements().toString(), elements.toString());
36+
StringBuilder output = new StringBuilder();
37+
new OrderByDeParser(new ExpressionDeParser(null, output), output).deParse(elements);
38+
assertEquals(elements.toString(),
39+
CCJSqlParserUtil
40+
.parseOrderByElements(output.toString().substring(" ORDER BY ".length()))
41+
.toString());
42+
}
43+
44+
@Test
45+
void exposesSortFlagsAndSupportsCustomExpressionRendering() throws Exception {
46+
List<OrderByElement> elements =
47+
CCJSqlParserUtil.parseOrderByElements("1 DESC NULLS LAST, 2 ASC");
48+
assertEquals(OrderByElement.NullOrdering.NULLS_LAST, elements.get(0).getNullOrdering());
49+
assertTrue(elements.get(1).isAscDescPresent());
50+
StringBuilder output = new StringBuilder();
51+
ExpressionDeParser expressions = new ExpressionDeParser(null, output) {
52+
@Override
53+
public <S> StringBuilder visit(LongValue value, S context) {
54+
return getBuilder().append(value.getValue() + 100);
55+
}
56+
};
57+
new OrderByDeParser(expressions, output).deParse(elements);
58+
assertEquals(" ORDER BY 101 DESC NULLS LAST, 102 ASC", output.toString());
59+
}
60+
61+
@Test
62+
void appliesConfigurationAndStartsEachFragmentWithFreshParameterState() throws Exception {
63+
assertEquals("[my column] DESC", CCJSqlParserUtil.parseOrderByElements("[my column] DESC",
64+
parser -> parser.withSquareBracketQuotation(true)).get(0).toString());
65+
for (int i = 0; i < 2; i++) {
66+
OrderByElement element = CCJSqlParserUtil.parseOrderByElements("? DESC").get(0);
67+
assertEquals(1, ((JdbcParameter) element.getExpression()).getIndex());
68+
}
69+
assertTrue(CCJSqlParserUtil.parseOrderByElements(null).isEmpty());
70+
assertTrue(CCJSqlParserUtil.parseOrderByElements("").isEmpty());
71+
}
72+
73+
@ParameterizedTest
74+
@ValueSource(strings = {"a DESC LIMIT 1", "a DESC,", "a DESC; SELECT 1", "ORDER BY a", "a +"})
75+
void rejectsTrailingInputAndIncompleteElements(String fragment) {
76+
assertThrows(JSQLParserException.class,
77+
() -> CCJSqlParserUtil.parseOrderByElements(fragment));
78+
}
79+
}

0 commit comments

Comments
 (0)