From 37e8e2d7ccc5b211594f9710b0cdf0452b803496 Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 28 Oct 2026 10:20:07 +0900 Subject: [PATCH] fix: return UnsupportedStatement for CREATE forms that no rule models CREATE falls back to UnsupportedStatement only when no earlier alternative claims the statement. CREATE TABLE and CREATE INDEX accept any leading words as modifiers, so PostgreSQL statements such as CREATE LANGUAGE, CREATE OPERATOR CLASS, CREATE DEFAULT CONVERSION or CREATE TRANSFORM entered one of them and failed before the fallback, and parseStatements failed for the whole script. Take the fallback first when the statement contains none of TABLE, INDEX, VIEW and POLICY, which every remaining alternative needs, so statements that parse today keep their result. The fallback also keeps OR REPLACE, which Create() reads before choosing an alternative. Signed-off-by: minleejae --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 31 ++++++++++++-- .../statement/UnsupportedStatementTest.java | 41 +++++++++++++++++++ .../create/CreateFunctionalStatementTest.java | 7 ++-- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8420622293..0dd3c98258 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2838,6 +2838,30 @@ public class CCJSqlParser extends AbstractJSqlParser { "SUBJECT").contains(token.image.toUpperCase(Locale.ROOT)); } + /** An unsupported CREATE statement that keeps OR REPLACE, which Create() has already read. */ + private static UnsupportedStatement unsupportedCreate(boolean orReplace, List rest) { + List declarations = new ArrayList(); + if (orReplace) { + declarations.add("OR"); + declarations.add("REPLACE"); + } + declarations.addAll(rest); + return new UnsupportedStatement("CREATE", declarations); + } + + /** + * Whether a CREATE statement names none of TABLE, INDEX, VIEW and POLICY before it ends. Every + * remaining CREATE form needs one of them, and CREATE TABLE and CREATE INDEX accept any leading + * words as modifiers, so such a statement would fail there instead of reaching the fallback. + */ + private boolean isUnknownCreateAhead() { + for (int i = 1; ; i++) { + int kind = getToken(i).kind; + if (kind == EOF || kind == ST_SEMICOLON) { return true; } + if (kind == K_TABLE || kind == K_INDEX || kind == K_VIEW || kind == K_POLICY) { return false; } + } + } + /** * Whether ALTER USER uses MySQL account syntax rather than the PostgreSQL alias of ALTER ROLE, * which names one unquoted role followed by an action: IF EXISTS, USER(), a quoted name, a @@ -23579,6 +23603,9 @@ Statement Create(): | statement = CreateSynonym(isUsingOrReplace) | + LOOKAHEAD({ isUnknownCreateAhead() }) captureRest = captureRest() + { statement = unsupportedCreate(isUsingOrReplace, captureRest); } + | LOOKAHEAD(3) statement = CreateTable(isUsingOrReplace) | LOOKAHEAD(2) statement = CreateView(isUsingOrReplace) @@ -23592,9 +23619,7 @@ Statement Create(): statement = CreateIndex() | captureRest = captureRest() - { - statement = new UnsupportedStatement("CREATE", captureRest); - } + { statement = unsupportedCreate(isUsingOrReplace, captureRest); } ) { return statement; diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index 540d46746b..095fb8c0a4 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -15,9 +15,15 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import java.util.function.Consumer; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.test.TestUtils; @@ -25,6 +31,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class UnsupportedStatementTest { @Test @@ -196,6 +204,39 @@ void testInformixSetStatementIssue1945() throws JSQLParserException { "set isolation to dirty read;", true, parser -> parser.withUnsupportedStatements()); } + @ParameterizedTest + @ValueSource(strings = { + "CREATE LANGUAGE plperl", + "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plsample HANDLER plsample_call_handler", + "CREATE OPERATOR CLASS gist__int_ops DEFAULT FOR TYPE _int4 USING gist AS OPERATOR 3 &&", + "CREATE OPERATOR FAMILY myfam USING btree", + "CREATE DEFAULT CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM latin1_to_utf8", + "CREATE TRANSFORM FOR hstore LANGUAGE plpython3u (FROM SQL WITH FUNCTION " + + "hstore_to_plpython(internal), TO SQL WITH FUNCTION plpython_to_hstore(internal))", + "CREATE OR REPLACE AGGREGATE myavg(float8) (SFUNC = float8_accum, STYPE = float8[])"}) + void unknownCreateFallsBackToUnsupportedStatement(String sql) throws JSQLParserException { + for (Consumer dialect : Arrays.>asList(parser -> { + }, parser -> parser.withDialect(Dialect.POSTGRESQL))) { + Statement statement = CCJSqlParserUtil.parse(sql, dialect); + + assertInstanceOf(UnsupportedStatement.class, statement); + // the captured tokens render with single spaces; OR REPLACE is kept + assertEquals(sql.replaceAll("\\s+", ""), statement.toString().replaceAll("\\s+", "")); + } + } + + @Test + void unknownCreateDoesNotStopAScript() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "CREATE TABLE a (x int); CREATE LANGUAGE plperl; CREATE INDEX i ON a (x);", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + + assertEquals(3, statements.size()); + assertInstanceOf(CreateTable.class, statements.get(0)); + assertInstanceOf(UnsupportedStatement.class, statements.get(1)); + assertInstanceOf(CreateIndex.class, statements.get(2)); + } + @Test void testRedshiftSetStatementIssue1708() throws JSQLParserException { Statement st = TestUtils.assertSqlCanBeParsedAndDeparsed( diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java index d2059b6ba5..a3e514364b 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java @@ -12,13 +12,13 @@ import static net.sf.jsqlparser.test.TestUtils.assertDeparse; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Arrays; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.UnsupportedStatement; import net.sf.jsqlparser.statement.MySqlDefiner; import net.sf.jsqlparser.statement.CreateFunctionalStatement; import net.sf.jsqlparser.statement.Statements; @@ -154,8 +154,9 @@ public void definerDispatchKeepsTriggersAndViews() throws JSQLParserException { assertThat(CCJSqlParserUtil.parse("CREATE FUNCTION f() RETURNS int AS 'SELECT 1'")) .isInstanceOf(CreateFunction.class) .extracting(statement -> ((CreateFunction) statement).getDefiner()).isNull(); - assertThatThrownBy(() -> CCJSqlParserUtil.parse("CREATE DEFINER = x EVENT e " - + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(JSQLParserException.class); + // DEFINER is not modelled for EVENT, so the statement falls back like other unknown forms + assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = x EVENT e " + + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(UnsupportedStatement.class); } @Test