From d473eeb637e6e55f8edced5a5567b891b3f00c46 Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 12 Sep 2026 20:00:47 +0900 Subject: [PATCH] Expose structured ORDER BY fragment parsing --- .../jsqlparser/parser/CCJSqlParserUtil.java | 61 +++++++++++--- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 22 ++++-- .../parser/OrderByFragmentTest.java | 79 +++++++++++++++++++ 3 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/parser/OrderByFragmentTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index 4cf71e719..eed84b0eb 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -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; @@ -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. @@ -242,34 +245,68 @@ public static ColDataType parseColDataType(String columnDataType) throws JSQLPar */ public static ColDataType parseColDataType(String columnDataType, Consumer 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 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 parseOrderByElements(String elements, + Consumer consumer) throws JSQLParserException { + return elements == null || elements.isEmpty() ? new ArrayList<>() + : parseFragment(elements, "ORDER BY elements", consumer, + CCJSqlParser::OrderByElementList); + } + + @FunctionalInterface + private interface FragmentParser { + T parse(CCJSqlParser parser) throws ParseException; + } + + private static T parseFragment(String input, String description, + Consumer consumer, FragmentParser 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 consumer) throws JSQLParserException { - CCJSqlParser parser = newParser(columnDataType) - .withAllowComplexParsing(allowComplexParsing); + private static T parseFragment(String input, String description, + boolean allowComplexParsing, + Consumer consumer, FragmentParser 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); } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 92f99568d..20f62ce72 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -8122,15 +8122,23 @@ Expression Qualify(): List OrderByElements(): { - List orderByList = new ArrayList(); - OrderByElement orderByElement = null; + List elements; } { - [ ] orderByElement=OrderByElement() { orderByList.add(orderByElement); } - ( LOOKAHEAD(2) "," orderByElement=OrderByElement() { orderByList.add(orderByElement); } )* - { - return orderByList; - } + [ ] elements=OrderByElementList() + { return elements; } +} + +/** Shared element list for statement clauses and the fragment parsing API. */ +List OrderByElementList(): +{ + List elements = new ArrayList(); + OrderByElement element; +} +{ + element=OrderByElement() { elements.add(element); } + ( LOOKAHEAD(2) "," element=OrderByElement() { elements.add(element); } )* + { return elements; } } OrderByElement OrderByElement(): diff --git a/src/test/java/net/sf/jsqlparser/parser/OrderByFragmentTest.java b/src/test/java/net/sf/jsqlparser/parser/OrderByFragmentTest.java new file mode 100644 index 000000000..ae0bef7f2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/OrderByFragmentTest.java @@ -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 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 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 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)); + } +}