Skip to content
Draft
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
61 changes: 49 additions & 12 deletions src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import java.io.InputStream;
import java.io.Reader;
import java.util.Stack;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand All @@ -31,6 +33,7 @@
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.create.table.ColDataType;
import net.sf.jsqlparser.statement.select.OrderByElement;

/**
* Toolfunctions to start and use JSqlParser.
Expand Down Expand Up @@ -242,34 +245,68 @@ public static ColDataType parseColDataType(String columnDataType) throws JSQLPar
*/
public static ColDataType parseColDataType(String columnDataType,
Consumer<CCJSqlParser> consumer) throws JSQLParserException {
if (columnDataType == null || columnDataType.isEmpty()) {
return parseFragment(columnDataType, "column data type", consumer,
CCJSqlParser::ColDataType);
}

/**
* Parses comma-separated ORDER BY elements without the ORDER BY keywords. Trailing tokens are
* rejected. Returns an empty list for null or empty input.
*/
public static List<OrderByElement> parseOrderByElements(String elements)
throws JSQLParserException {
return parseOrderByElements(elements, null);
}

/**
* Parses ORDER BY elements with optional parser configuration. The input contains only the
* elements (for example, {@code name DESC, id ASC}), and must be consumed completely.
*
* @return the parsed elements, or an empty list for null or empty input
* @throws JSQLParserException when the input cannot be parsed completely
*/
public static List<OrderByElement> parseOrderByElements(String elements,
Consumer<CCJSqlParser> consumer) throws JSQLParserException {
return elements == null || elements.isEmpty() ? new ArrayList<>()
: parseFragment(elements, "ORDER BY elements", consumer,
CCJSqlParser::OrderByElementList);
}

@FunctionalInterface
private interface FragmentParser<T> {
T parse(CCJSqlParser parser) throws ParseException;
}

private static <T> T parseFragment(String input, String description,
Consumer<CCJSqlParser> consumer, FragmentParser<T> fragment)
throws JSQLParserException {
if (input == null || input.isEmpty()) {
return null;
}

try {
return parseColDataType(columnDataType, false, consumer);
return parseFragment(input, description, false, consumer, fragment);
} catch (JSQLParserException ex) {
return parseColDataType(columnDataType, true, consumer);
return parseFragment(input, description, true, consumer, fragment);
}
}

private static ColDataType parseColDataType(String columnDataType, boolean allowComplexParsing,
Consumer<CCJSqlParser> consumer) throws JSQLParserException {
CCJSqlParser parser = newParser(columnDataType)
.withAllowComplexParsing(allowComplexParsing);
private static <T> T parseFragment(String input, String description,
boolean allowComplexParsing,
Consumer<CCJSqlParser> consumer, FragmentParser<T> fragment)
throws JSQLParserException {
CCJSqlParser parser = newParser(input).withAllowComplexParsing(allowComplexParsing);
if (consumer != null) {
consumer.accept(parser);
}

try {
ColDataType result = parser.ColDataType();
T result = fragment.parse(parser);
if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) {
throw new JSQLParserException(
"could only parse partial column data type " + result);
"could only parse partial " + description + " " + result);
}
return result;
} catch (ParseException ex) {
throw new JSQLParserException(columnDataType, ex);
throw new JSQLParserException(input, ex);
}
}

Expand Down
22 changes: 15 additions & 7 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -8122,15 +8122,23 @@ Expression Qualify():

List<OrderByElement> OrderByElements():
{
List<OrderByElement> orderByList = new ArrayList<OrderByElement>();
OrderByElement orderByElement = null;
List<OrderByElement> elements;
}
{
<K_ORDER> [ <K_SIBLINGS> ] <K_BY> orderByElement=OrderByElement() { orderByList.add(orderByElement); }
( LOOKAHEAD(2) "," orderByElement=OrderByElement() { orderByList.add(orderByElement); } )*
{
return orderByList;
}
<K_ORDER> [ <K_SIBLINGS> ] <K_BY> elements=OrderByElementList()
{ return elements; }
}

/** Shared element list for statement clauses and the fragment parsing API. */
List<OrderByElement> OrderByElementList():
{
List<OrderByElement> elements = new ArrayList<OrderByElement>();
OrderByElement element;
}
{
element=OrderByElement() { elements.add(element); }
( LOOKAHEAD(2) "," element=OrderByElement() { elements.add(element); } )*
{ return elements; }
}

OrderByElement OrderByElement():
Expand Down
79 changes: 79 additions & 0 deletions src/test/java/net/sf/jsqlparser/parser/OrderByFragmentTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.parser;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.JdbcParameter;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.OrderByDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class OrderByFragmentTest {
@ParameterizedTest
@ValueSource(strings = {"a DESC NULLS LAST, b ASC", "COALESCE(a, 0) DESC, b + ? ASC",
"(1 + a) / (1 + b) DESC", "a COLLATE \"C\" ASC", "a WITH FILL FROM 1 TO 5"})
void usesTheSameStructuredElementsAsAnOrderByClause(String fragment) throws Exception {
List<OrderByElement> elements = CCJSqlParserUtil.parseOrderByElements(fragment);
PlainSelect select =
(PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM t ORDER BY " + fragment);
assertEquals(select.getOrderByElements().toString(), elements.toString());
StringBuilder output = new StringBuilder();
new OrderByDeParser(new ExpressionDeParser(null, output), output).deParse(elements);
assertEquals(elements.toString(),
CCJSqlParserUtil
.parseOrderByElements(output.toString().substring(" ORDER BY ".length()))
.toString());
}

@Test
void exposesSortFlagsAndSupportsCustomExpressionRendering() throws Exception {
List<OrderByElement> elements =
CCJSqlParserUtil.parseOrderByElements("1 DESC NULLS LAST, 2 ASC");
assertEquals(OrderByElement.NullOrdering.NULLS_LAST, elements.get(0).getNullOrdering());
assertTrue(elements.get(1).isAscDescPresent());
StringBuilder output = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser(null, output) {
@Override
public <S> StringBuilder visit(LongValue value, S context) {
return getBuilder().append(value.getValue() + 100);
}
};
new OrderByDeParser(expressions, output).deParse(elements);
assertEquals(" ORDER BY 101 DESC NULLS LAST, 102 ASC", output.toString());
}

@Test
void appliesConfigurationAndStartsEachFragmentWithFreshParameterState() throws Exception {
assertEquals("[my column] DESC", CCJSqlParserUtil.parseOrderByElements("[my column] DESC",
parser -> parser.withSquareBracketQuotation(true)).get(0).toString());
for (int i = 0; i < 2; i++) {
OrderByElement element = CCJSqlParserUtil.parseOrderByElements("? DESC").get(0);
assertEquals(1, ((JdbcParameter) element.getExpression()).getIndex());
}
assertTrue(CCJSqlParserUtil.parseOrderByElements(null).isEmpty());
assertTrue(CCJSqlParserUtil.parseOrderByElements("").isEmpty());
}

@ParameterizedTest
@ValueSource(strings = {"a DESC LIMIT 1", "a DESC,", "a DESC; SELECT 1", "ORDER BY a", "a +"})
void rejectsTrailingInputAndIncompleteElements(String fragment) {
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parseOrderByElements(fragment));
}
}
Loading