diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index e999caecd..4cf71e719 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -60,6 +60,13 @@ public static Statement parse(Reader statementReader) throws JSQLParserException return statement; } + /** + * Parses a single SQL statement. + * + * @param sql the SQL statement to parse + * @return the parsed statement + * @throws JSQLParserException if the input is null, empty, or cannot be parsed + */ public static Statement parse(String sql) throws JSQLParserException { return parse(sql, null); } @@ -73,17 +80,15 @@ public static Statement parse(String sql) throws JSQLParserException { * CCJSqlParserUtil.parse("select * from [mytable]", parser -> parser.withSquareBracketQuotation(true)); * } * - * @param sql - * @param consumer - * @return - * @throws JSQLParserException + * @param sql the SQL statement to parse + * @param consumer parser configuration callback, or {@code null} + * @return the parsed statement + * @throws JSQLParserException if the input is null, empty, or cannot be parsed */ public static Statement parse(String sql, Consumer consumer) throws JSQLParserException { - if (sql == null || sql.isEmpty()) { - return null; - } + requireStatementInput(sql); ExecutorService executorService = Executors.newSingleThreadExecutor(); Statement statement; @@ -97,12 +102,19 @@ public static Statement parse(String sql, Consumer consumer) return statement; } + /** + * Parses a single SQL statement using the caller's executor, which is left open. + * + * @param sql the SQL statement to parse + * @param executorService executor to use for parsing + * @param consumer parser configuration callback, or {@code null} + * @return the parsed statement + * @throws JSQLParserException if the input is null, empty, or cannot be parsed + */ public static Statement parse(String sql, ExecutorService executorService, Consumer consumer) throws JSQLParserException { - if (sql == null || sql.isEmpty()) { - return null; - } + requireStatementInput(sql); Statement statement; // first, try to parse fast and simple @@ -134,6 +146,12 @@ public static Statement parse(String sql, ExecutorService executorService, return statement; } + private static void requireStatementInput(String sql) throws JSQLParserException { + if (sql == null || sql.isEmpty()) { + throw new JSQLParserException("SQL statement must not be null or empty."); + } + } + public static CCJSqlParser newParser(String sql) { if (sql == null || sql.isEmpty()) { return null; @@ -412,20 +430,21 @@ public Statement call() throws ParseException { /** * Parse a statement list. * - * @return the statements parsed + * @return the statements parsed, or a new empty list for null or empty input */ public static Statements parseStatements(String sqls) throws JSQLParserException { - if (sqls == null || sqls.isEmpty()) { - return null; - } - return parseStatements(sqls, null); } + /** + * Parses a statement list with optional parser configuration. + * + * @return the statements parsed, or a new empty list for null or empty input + */ public static Statements parseStatements(String sqls, Consumer consumer) throws JSQLParserException { if (sqls == null || sqls.isEmpty()) { - return null; + return new Statements(); } ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -437,15 +456,15 @@ public static Statements parseStatements(String sqls, Consumer con } /** - * Parse a statement list. + * Parses a statement list using the caller's executor, which is left open. * - * @return the statements parsed + * @return the statements parsed, or a new empty list for null or empty input */ public static Statements parseStatements(String sqls, ExecutorService executorService, Consumer consumer) throws JSQLParserException { if (sqls == null || sqls.isEmpty()) { - return null; + return new Statements(); } CCJSqlParser parser = newParser(sqls); diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6d3995c0c..d169b53d3 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -642,6 +642,14 @@ The object model works in both directions. Build the tree from Java and print it Handle Parse Errors ============================== +``CCJSqlParserUtil.parse(String, ...)`` requires a statement: null and empty string +inputs throw ``JSQLParserException``, matching the default behavior for whitespace-only +and comment-only input. ``CCJSqlParserUtil.parseStatements(String, ...)`` returns a new, +mutable empty ``Statements`` list for null or empty input, as it already does for +whitespace-only and comment-only input. This applies to the overloads with parser +configuration callbacks and caller-provided executors; caller-provided executors remain +open. These empty-input results replace the previous null returns of these methods. + By default a syntax error aborts the whole parse. Two features let a script survive one bad statement: - ``parser.withErrorRecovery(true)`` skips to the next statement separator and returns an empty statement. diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 5aa92e1c7..6de42eb6a 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -520,9 +520,9 @@ void testUnbalancedPosition() { } @Test - void testParseEmpty() throws JSQLParserException { - assertNull(CCJSqlParserUtil.parse("")); - assertNull(CCJSqlParserUtil.parse((String) null)); + void testParseEmpty() { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse((String) null)); } @Test diff --git a/src/test/java/net/sf/jsqlparser/parser/EmptyStatementInputTest.java b/src/test/java/net/sf/jsqlparser/parser/EmptyStatementInputTest.java new file mode 100644 index 000000000..9d7f87eb7 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/EmptyStatementInputTest.java @@ -0,0 +1,89 @@ +/*- + * #%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 java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.Statements; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class EmptyStatementInputTest { + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"}) + void rejectsInputWithoutASingleStatement(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (boolean allowComplex : new boolean[] {false, true}) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withAllowComplexParsing(allowComplex))); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"}) + void returnsAnEmptyStatementList(String sql) throws Exception { + assertTrue(CCJSqlParserUtil.parseStatements(sql).isEmpty()); + for (boolean allowComplex : new boolean[] {false, true}) { + assertTrue(CCJSqlParserUtil.parseStatements(sql, + parser -> parser.withAllowComplexParsing(allowComplex)).isEmpty()); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"}) + void preservesTheContractWithACallerExecutor(String sql) throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + for (boolean allowComplex : new boolean[] {false, true}) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, executor, + parser -> parser.withAllowComplexParsing(allowComplex))); + assertTrue(CCJSqlParserUtil.parseStatements(sql, executor, + parser -> parser.withAllowComplexParsing(allowComplex)).isEmpty()); + } + assertFalse(executor.isShutdown()); + assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1", executor, null).toString()); + assertEquals(2, + CCJSqlParserUtil.parseStatements("SELECT 1; SELECT 2", executor, null).size()); + } finally { + executor.shutdownNow(); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", " \t\r\n", "/* nothing */", "-- nothing\n"}) + void agreesWithReaderAndStreamParsing(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(new StringReader(sql))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)))); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8")); + } + + @Test + void emptyResultsRemainIndependentAndMutable() throws Exception { + Statements first = CCJSqlParserUtil.parseStatements(""); + first.add(CCJSqlParserUtil.parse("SELECT 1")); + assertTrue(CCJSqlParserUtil.parseStatements("").isEmpty()); + assertTrue(CCJSqlParserUtil.parseStatements((String) null).isEmpty()); + assertEquals(1, first.size()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java b/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java index ee45377af..376d2ad03 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java @@ -69,9 +69,9 @@ void reportsTimeoutWhenComplexParsingIsDisabled() { } @Test - void preservesEmptyInputAndUnsupportedStatementContracts() throws Exception { - assertNull(CCJSqlParserUtil.parseStatements((String) null)); - assertNull(CCJSqlParserUtil.parseStatements("")); + void returnsEmptyListsAndPreservesUnsupportedStatements() throws Exception { + assertTrue(CCJSqlParserUtil.parseStatements((String) null).isEmpty()); + assertTrue(CCJSqlParserUtil.parseStatements("").isEmpty()); assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parseStatements("SELECT 1; WHATEVER !", parser -> parser.withAllowComplexParsing(false)