diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 842062229..0dd3c9825 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 540d46746..095fb8c0a 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 d2059b6ba..a3e514364 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