Skip to content

Commit 1bef513

Browse files
authored
fix(parser): align empty statement input handling (#2594)
Reject null and empty single-statement input with JSQLParserException and return independent empty statement lists from all String overloads. Add regression coverage and document the behavior change. Fixes #2576.
1 parent 17f033d commit 1bef513

5 files changed

Lines changed: 141 additions & 25 deletions

File tree

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/site/sphinx/usage.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,14 @@ The object model works in both directions. Build the tree from Java and print it
642642
Handle Parse Errors
643643
==============================
644644

645+
``CCJSqlParserUtil.parse(String, ...)`` requires a statement: null and empty string
646+
inputs throw ``JSQLParserException``, matching the default behavior for whitespace-only
647+
and comment-only input. ``CCJSqlParserUtil.parseStatements(String, ...)`` returns a new,
648+
mutable empty ``Statements`` list for null or empty input, as it already does for
649+
whitespace-only and comment-only input. This applies to the overloads with parser
650+
configuration callbacks and caller-provided executors; caller-provided executors remain
651+
open. These empty-input results replace the previous null returns of these methods.
652+
645653
By default a syntax error aborts the whole parse. Two features let a script survive one bad statement:
646654

647655
- ``parser.withErrorRecovery(true)`` skips to the next statement separator and returns an empty statement.

src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,9 @@ void testUnbalancedPosition() {
520520
}
521521

522522
@Test
523-
void testParseEmpty() throws JSQLParserException {
524-
assertNull(CCJSqlParserUtil.parse(""));
525-
assertNull(CCJSqlParserUtil.parse((String) null));
523+
void testParseEmpty() {
524+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(""));
525+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse((String) null));
526526
}
527527

528528
@Test
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 java.io.ByteArrayInputStream;
13+
import java.io.StringReader;
14+
import java.nio.charset.StandardCharsets;
15+
import java.util.concurrent.ExecutorService;
16+
import java.util.concurrent.Executors;
17+
import net.sf.jsqlparser.JSQLParserException;
18+
import net.sf.jsqlparser.statement.Statements;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.NullAndEmptySource;
22+
import org.junit.jupiter.params.provider.ValueSource;
23+
24+
import static org.junit.jupiter.api.Assertions.*;
25+
26+
class EmptyStatementInputTest {
27+
@ParameterizedTest
28+
@NullAndEmptySource
29+
@ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"})
30+
void rejectsInputWithoutASingleStatement(String sql) {
31+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
32+
for (boolean allowComplex : new boolean[] {false, true}) {
33+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql,
34+
parser -> parser.withAllowComplexParsing(allowComplex)));
35+
}
36+
}
37+
38+
@ParameterizedTest
39+
@NullAndEmptySource
40+
@ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"})
41+
void returnsAnEmptyStatementList(String sql) throws Exception {
42+
assertTrue(CCJSqlParserUtil.parseStatements(sql).isEmpty());
43+
for (boolean allowComplex : new boolean[] {false, true}) {
44+
assertTrue(CCJSqlParserUtil.parseStatements(sql,
45+
parser -> parser.withAllowComplexParsing(allowComplex)).isEmpty());
46+
}
47+
}
48+
49+
@ParameterizedTest
50+
@NullAndEmptySource
51+
@ValueSource(strings = {" \t\r\n", "/* nothing */", "-- nothing\n"})
52+
void preservesTheContractWithACallerExecutor(String sql) throws Exception {
53+
ExecutorService executor = Executors.newSingleThreadExecutor();
54+
try {
55+
for (boolean allowComplex : new boolean[] {false, true}) {
56+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, executor,
57+
parser -> parser.withAllowComplexParsing(allowComplex)));
58+
assertTrue(CCJSqlParserUtil.parseStatements(sql, executor,
59+
parser -> parser.withAllowComplexParsing(allowComplex)).isEmpty());
60+
}
61+
assertFalse(executor.isShutdown());
62+
assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1", executor, null).toString());
63+
assertEquals(2,
64+
CCJSqlParserUtil.parseStatements("SELECT 1; SELECT 2", executor, null).size());
65+
} finally {
66+
executor.shutdownNow();
67+
}
68+
}
69+
70+
@ParameterizedTest
71+
@ValueSource(strings = {"", " \t\r\n", "/* nothing */", "-- nothing\n"})
72+
void agreesWithReaderAndStreamParsing(String sql) {
73+
assertThrows(JSQLParserException.class,
74+
() -> CCJSqlParserUtil.parse(new StringReader(sql)));
75+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
76+
new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8))));
77+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
78+
new ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8"));
79+
}
80+
81+
@Test
82+
void emptyResultsRemainIndependentAndMutable() throws Exception {
83+
Statements first = CCJSqlParserUtil.parseStatements("");
84+
first.add(CCJSqlParserUtil.parse("SELECT 1"));
85+
assertTrue(CCJSqlParserUtil.parseStatements("").isEmpty());
86+
assertTrue(CCJSqlParserUtil.parseStatements((String) null).isEmpty());
87+
assertEquals(1, first.size());
88+
}
89+
}

src/test/java/net/sf/jsqlparser/parser/ParseStatementsFailureTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ void reportsTimeoutWhenComplexParsingIsDisabled() {
6969
}
7070

7171
@Test
72-
void preservesEmptyInputAndUnsupportedStatementContracts() throws Exception {
73-
assertNull(CCJSqlParserUtil.parseStatements((String) null));
74-
assertNull(CCJSqlParserUtil.parseStatements(""));
72+
void returnsEmptyListsAndPreservesUnsupportedStatements() throws Exception {
73+
assertTrue(CCJSqlParserUtil.parseStatements((String) null).isEmpty());
74+
assertTrue(CCJSqlParserUtil.parseStatements("").isEmpty());
7575
assertInstanceOf(UnsupportedStatement.class,
7676
CCJSqlParserUtil.parseStatements("SELECT 1; WHATEVER !",
7777
parser -> parser.withAllowComplexParsing(false)

0 commit comments

Comments
 (0)