Skip to content
Merged
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
57 changes: 38 additions & 19 deletions src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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<CCJSqlParser> consumer)
throws JSQLParserException {

if (sql == null || sql.isEmpty()) {
return null;
}
requireStatementInput(sql);

ExecutorService executorService = Executors.newSingleThreadExecutor();
Statement statement;
Expand All @@ -97,12 +102,19 @@ public static Statement parse(String sql, Consumer<CCJSqlParser> 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<CCJSqlParser> consumer)
throws JSQLParserException {
if (sql == null || sql.isEmpty()) {
return null;
}
requireStatementInput(sql);

Statement statement;
// first, try to parse fast and simple
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<CCJSqlParser> consumer)
throws JSQLParserException {
if (sqls == null || sqls.isEmpty()) {
return null;
return new Statements();
}

ExecutorService executorService = Executors.newSingleThreadExecutor();
Expand All @@ -437,15 +456,15 @@ public static Statements parseStatements(String sqls, Consumer<CCJSqlParser> 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<CCJSqlParser> consumer)
throws JSQLParserException {
if (sqls == null || sqls.isEmpty()) {
return null;
return new Statements();
}

CCJSqlParser parser = newParser(sqls);
Expand Down
8 changes: 8 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading