From cc4a97a06ca3cffb37373834b9b7d2a1e9d8fbd4 Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 14 Oct 2026 18:23:39 +0900 Subject: [PATCH] feat: model MySQL CREATE FUNCTION and PROCEDURE characteristics With the MySQL or MariaDB dialect, parse the characteristics between the routine header and its body (COMMENT, LANGUAGE SQL, [NOT] DETERMINISTIC, the SQL data access and SQL SECURITY) into RoutineCharacteristic items with the production that ALTER FUNCTION/PROCEDURE uses. The declaration tokens are still captured and render the statement unchanged; getCharacteristics() is a read-only view of them, and the COMMENT literal is a StringValue. CHARACTER SET in a return type no longer starts the routine body. Signed-off-by: minleejae --- .../parser/RoutineBodyBoundary.java | 14 +- .../statement/CreateFunctionalStatement.java | 39 +++- .../statement/RoutineCharacteristic.java | 13 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 37 +++- src/site/sphinx/usage.rst | 10 +- .../MySqlRoutineCharacteristicsTest.java | 168 ++++++++++++++++++ 6 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java index a8d3d88259..c5ef37b5d0 100644 --- a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java +++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java @@ -60,6 +60,15 @@ boolean endsAt(Token token) throws ParseException { && !declarations && (hasParameters || quotedBody || completed); } + /** + * Whether the routine header, including a return type, is complete and the body has not + * started, which is where MySQL routine characteristics appear. + */ + boolean isBeforeBody() { + return hasParameters && parentheses == 0 && !bodyStarted && !returnTypeName + && !declarations && !quotedBody && !completed; + } + boolean startsQuotedBody(Token token, Token next) { return parentheses == 0 && !compoundBody && !quotedBody && token.kind == K_AS && next.kind == S_CHAR_LITERAL @@ -228,7 +237,10 @@ private void acceptStatementToken(Token token, Token next) { } if (!bodyStarted) { // SET may also be a PostgreSQL header option; AS remains recognizable above. - if (!isSimpleStatementStart(kind)) { + // CHARACTER SET and CHAR SET belong to a MySQL return type. + boolean characterSet = + kind == K_SET && (previousKind == K_CHARACTER || previousKind == K_CHAR); + if (!isSimpleStatementStart(kind) || characterSet) { return; } bodyStarted = true; diff --git a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java index c34037ada1..d3f75a1278 100644 --- a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java @@ -34,6 +34,7 @@ public enum Operation { private MySqlDefiner definer; private FunctionReturnType returnType; private List routineBodyParts; + private List characteristics = Collections.emptyList(); public Operation getOperation() { return operation; @@ -99,8 +100,38 @@ public List getFunctionDeclarationParts() { return functionDeclarationParts; } + /** + * Replaces the declaration parts. The {@link #getCharacteristics() characteristics} described + * the replaced parts, so they are cleared. + * + * @param functionDeclarationParts the new parts + */ public void setFunctionDeclarationParts(List functionDeclarationParts) { + setFunctionDeclarationParts(functionDeclarationParts, Collections.emptyList()); + } + + /** + * Replaces the declaration parts together with the characteristics written in them. + * + * @param functionDeclarationParts the new parts, which keep rendering the statement + * @param characteristics the characteristics among the parts, in source order + */ + public void setFunctionDeclarationParts(List functionDeclarationParts, + List characteristics) { this.functionDeclarationParts = functionDeclarationParts; + this.characteristics = List.copyOf(characteristics); + } + + /** + * The MySQL characteristics of {@code CREATE FUNCTION|PROCEDURE}, such as {@code COMMENT}, + * {@code DETERMINISTIC} or {@code SQL SECURITY}, parsed with a MySQL or MariaDB dialect. They + * describe tokens of {@link #getFunctionDeclarationParts()}, which still render the statement, + * so the list is read-only. + * + * @return the characteristics in source order, empty if none were parsed + */ + public List getCharacteristics() { + return characteristics; } /** @@ -177,19 +208,23 @@ public CreateFunctionalStatement withFunctionDeclarationParts( return this; } + /** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */ public CreateFunctionalStatement addFunctionDeclarationParts( String... functionDeclarationParts) { List collection = Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new); Collections.addAll(collection, functionDeclarationParts); - return this.withFunctionDeclarationParts(collection); + setFunctionDeclarationParts(collection, characteristics); + return this; } + /** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */ public CreateFunctionalStatement addFunctionDeclarationParts( Collection functionDeclarationParts) { List collection = Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new); collection.addAll(functionDeclarationParts); - return this.withFunctionDeclarationParts(collection); + setFunctionDeclarationParts(collection, characteristics); + return this; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java index 93df5a7aaf..90be0d3c62 100644 --- a/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java +++ b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java @@ -15,15 +15,16 @@ import net.sf.jsqlparser.expression.StringValue; /** - * A characteristic set by {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE}. - * PostgreSQL and MySQL share the model, and each {@link Kind} renders its own SQL: + * A characteristic set by {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE}, + * or declared by MySQL's {@code CREATE FUNCTION|PROCEDURE}. PostgreSQL and MySQL share the model, + * and each {@link Kind} renders its own SQL: * *
    *
  • PostgreSQL: the volatility, the null input behavior, {@code [NOT] LEAKPROOF}, * {@code [EXTERNAL] SECURITY}, {@code PARALLEL}, {@code COST}, {@code ROWS}, {@code SUPPORT} and * the {@code SET} or {@code RESET} of a configuration parameter;
  • - *
  • MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, the SQL data access and - * {@code SQL SECURITY}.
  • + *
  • MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, {@code [NOT] DETERMINISTIC} (CREATE only), the + * SQL data access and {@code SQL SECURITY}.
  • *
* * The security kinds are shared; {@link #getSecurityClause()} keeps the spelling. Values keep their @@ -70,6 +71,10 @@ public enum Kind { COMMENT("COMMENT"), /** MySQL {@code LANGUAGE SQL}. */ LANGUAGE_SQL("LANGUAGE SQL"), + /** MySQL {@code DETERMINISTIC}, only in {@code CREATE FUNCTION|PROCEDURE}. */ + DETERMINISTIC("DETERMINISTIC"), + /** MySQL {@code NOT DETERMINISTIC}, only in {@code CREATE FUNCTION|PROCEDURE}. */ + NOT_DETERMINISTIC("NOT DETERMINISTIC"), /** MySQL {@code CONTAINS SQL}. */ CONTAINS_SQL("CONTAINS SQL"), /** MySQL {@code NO SQL}. */ diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 97856f1072..a35db5f988 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -436,6 +436,12 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** Whether a MySQL characteristic of CREATE FUNCTION or PROCEDURE starts here. */ + private boolean isMySqlCreateRoutineCharacteristicAhead() { + return isMySqlRoutineCharacteristicAhead(1) || isWordSequenceAhead(1, "DETERMINISTIC") + || isWordSequenceAhead(1, "NOT", "DETERMINISTIC"); + } + /** Whether a MySQL characteristic of ALTER FUNCTION or PROCEDURE starts at the offset. */ private boolean isMySqlRoutineCharacteristicAhead(int offset) { if (getToken(offset).kind == K_COMMENT) { @@ -22574,12 +22580,14 @@ Statement Create(): } } +/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace): { CreateFunctionalStatement type = null; List tokens = new LinkedList(); String statementType = null; MySqlDefiner definer = null; + List characteristics = new ArrayList(); } { [ definer=MySqlRoutineDefiner() ] @@ -22588,7 +22596,7 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace): | { statementType = "PROCEDURE"; } ) - tokens=captureFunctionBody() + tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null) { if(statementType.equals("FUNCTION")) { type = new CreateFunction(isUsingOrReplace, tokens); @@ -22596,6 +22604,7 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace): if(statementType.equals("PROCEDURE")) { type = new CreateProcedure(isUsingOrReplace, tokens); } + type.setFunctionDeclarationParts(tokens, characteristics); type.setDefiner(definer); return type; } @@ -22641,7 +22650,7 @@ AlterRoutine AlterRoutine(): LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) }) ( LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) }) - characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); } + characteristic=MySqlRoutineCharacteristic(false) { result.getCharacteristics().add(characteristic); } )+ { requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE @@ -22719,7 +22728,8 @@ RoutineCharacteristic PostgreSqlRoutineCharacteristic(): } } -RoutineCharacteristic MySqlRoutineCharacteristic(): +/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */ +RoutineCharacteristic MySqlRoutineCharacteristic(boolean create): { RoutineCharacteristic.Kind kind; RoutineCharacteristic.SecurityClause clause = null; @@ -22739,6 +22749,12 @@ RoutineCharacteristic MySqlRoutineCharacteristic(): | ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; } | + LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC") + { kind = RoutineCharacteristic.Kind.DETERMINISTIC; } + | + LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") }) + ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; } + | LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.READS_SQL_DATA; } | @@ -23541,16 +23557,29 @@ List captureRest() { * PostgreSQL AS string bodies can use ordinary, escape or dollar quotes. * The boundary tracker distinguishes simple statements, declaration sections, * compound bodies, and expression CASE from procedural END qualifiers. + * With a list, MySQL characteristics between the header and the body are also + * parsed into it; their tokens are captured as well. */ JAVACODE -List captureFunctionBody() { +List captureFunctionBody(List characteristics) { List tokens = new LinkedList(); Token tok; RoutineBodyBoundary boundary = new RoutineBodyBoundary(); while(true) { tok = getToken(1); int l = tokens.size(); + if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) { + characteristics.add(MySqlRoutineCharacteristic(true)); + for (Token consumed = tok; ; consumed = consumed.next) { + tokens.add(consumed.image); + boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next); + if (consumed == token) { + break; + } + } + continue; + } if (boundary.endsAt(tok)) { if (tok.kind == ST_SEMICOLON) { tokens.add(tok.image); diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 0215f17542..7cd2a1a8ca 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -2260,7 +2260,15 @@ Validators report ``alterFunction``, ``alterProcedure``, ``alterRoutine`` and ``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of ``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and -triggers use. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``) +triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``, +``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the +routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or +``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC`` +and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds +every token and renders the statement unchanged, so the list is read-only. Replacing the +tokens with ``setFunctionDeclarationParts(parts)`` clears it, while +``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens +keeps it. Without a MySQL dialect the list stays empty. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``) and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL statements remains a ``Block``; a compound body with procedural statements such as ``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java new file mode 100644 index 0000000000..5593ec1835 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlRoutineCharacteristicsTest.java @@ -0,0 +1,168 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +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.CreateFunctionalStatement; +import net.sf.jsqlparser.statement.RoutineCharacteristic; +import net.sf.jsqlparser.statement.RoutineCharacteristic.Kind; +import net.sf.jsqlparser.statement.RoutineCharacteristic.SecurityClause; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlRoutineCharacteristicsTest { + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '`', value = { + "CREATE PROCEDURE cp1() COMMENT 'proc comment' SELECT 1|COMMENT", + "CREATE FUNCTION cf1() RETURNS INT DETERMINISTIC COMMENT 'fn' RETURN 1" + + "|DETERMINISTIC COMMENT", + "CREATE PROCEDURE gp13() COMMENT 'c' LANGUAGE SQL SQL SECURITY INVOKER SELECT 1" + + "|COMMENT LANGUAGE_SQL SECURITY_INVOKER", + "CREATE FUNCTION gf14() RETURNS INT DETERMINISTIC RETURN 1|DETERMINISTIC", + "CREATE FUNCTION f1 (a INT) RETURNS INT DETERMINISTIC RETURN a + 1|DETERMINISTIC", + "CREATE DEFINER=CURRENT_USER FUNCTION f2 (s CHAR(20)) RETURNS CHAR(50) " + + "CHARSET utf8mb4 NO SQL RETURN CONCAT('Hello, ', s, '!')|NO_SQL", + "CREATE FUNCTION f() RETURNS VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin " + + "READS SQL DATA RETURN 'a'|READS_SQL_DATA", + "CREATE FUNCTION f() RETURNS SET('a', 'b') CHAR SET latin1 MODIFIES SQL DATA " + + "RETURN 'a'|MODIFIES_SQL_DATA", + "CREATE PROCEDURE p (IN a INT) NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER " + + "BEGIN SELECT a; END|NOT_DETERMINISTIC CONTAINS_SQL SECURITY_DEFINER", + "CREATE FUNCTION f() RETURNS INT not deterministic comment 'x' return 1" + + "|NOT_DETERMINISTIC COMMENT", + "CREATE PROCEDURE p1 (IN a INT, OUT b INT) BEGIN SELECT a INTO b; END|``", + "CREATE DEFINER=CURRENT_USER PROCEDURE p3 () SELECT 1|``", + "CREATE FUNCTION f() RETURNS INT RETURN (SELECT deterministic FROM t)|``", + "CREATE PROCEDURE p() READS SQL DATA BEGIN DECLARE deterministic INT; END" + + "|READS_SQL_DATA"}) + void modelsCharacteristicsWithoutChangingTokens(String sql, String kinds) + throws JSQLParserException { + // Without a dialect, the declaration is only captured as tokens. + CreateFunctionalStatement plain = + (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql); + assertTrue(plain.getCharacteristics().isEmpty()); + + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB}) { + Consumer consumer = parser -> parser.withDialect(dialect); + CreateFunctionalStatement statement = + (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql, consumer); + assertEquals(kinds.isEmpty() ? List.of() : List.of(kinds.split(" ")), + statement.getCharacteristics().stream() + .map(characteristic -> characteristic.getKind().name()) + .collect(Collectors.toList())); + + assertEquals(plain.getFunctionDeclarationParts(), + statement.getFunctionDeclarationParts()); + assertEquals(plain.toString(), statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(plain.toString(), deparsed.toString()); + assertEquals(statement.toString(), + CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString()); + + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer); + assertEquals(2, script.size()); + assertEquals(statement.getCharacteristics().size(), + ((CreateFunctionalStatement) script.get(0)).getCharacteristics().size()); + } + } + + @Test + void exposesCharacteristicValues() throws JSQLParserException { + CreateFunctionalStatement procedure = parse("CREATE PROCEDURE gp13() COMMENT 'c' " + + "LANGUAGE SQL SQL SECURITY INVOKER SELECT 1"); + List characteristics = procedure.getCharacteristics(); + assertEquals("c", characteristics.get(0).getComment().getNotExcapedValue()); + assertEquals("'c'", characteristics.get(0).getValue()); + assertEquals(SecurityClause.SQL_SECURITY, characteristics.get(2).getSecurityClause()); + assertEquals("SQL SECURITY INVOKER", characteristics.get(2).toString()); + assertThrows(UnsupportedOperationException.class, + () -> characteristics.add(new RoutineCharacteristic(Kind.NO_SQL))); + + CreateFunctionalStatement function = + parse("CREATE FUNCTION f() RETURNS INT not deterministic comment 'x' return 1"); + // The tokens keep the spelling; the characteristics render canonical keywords. + assertEquals("CREATE FUNCTION f ( ) RETURNS INT not deterministic comment 'x' return 1", + function.toString()); + assertEquals("NOT DETERMINISTIC", function.getCharacteristics().get(0).toString()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '`', value = {"'it\\'s'|it's", "'it''s'|it's", + "\"dq\"|dq", "'a\\\\b'|a\\b", "'ν•œκΈ€ μ„€λͺ… πŸ˜€'|ν•œκΈ€ μ„€λͺ… πŸ˜€"}) + void decodesComments(String literal, String stored) throws JSQLParserException { + CreateFunctionalStatement statement = + parse("CREATE PROCEDURE p() COMMENT " + literal + " SELECT 1"); + RoutineCharacteristic comment = statement.getCharacteristics().get(0); + assertEquals(Kind.COMMENT, comment.getKind()); + assertEquals(literal, comment.getValue()); + assertEquals(stored, comment.getComment().getNotExcapedValue()); + assertEquals("CREATE PROCEDURE p ( ) COMMENT " + literal + " SELECT 1", + statement.toString()); + } + + @Test + void replacingTheTokensClearsTheCharacteristics() throws JSQLParserException { + CreateFunctionalStatement statement = + parse("CREATE FUNCTION gf14() RETURNS INT DETERMINISTIC RETURN 1"); + assertEquals(1, statement.getCharacteristics().size()); + + statement.addFunctionDeclarationParts("+", "1"); + assertEquals(1, statement.getCharacteristics().size()); + assertEquals("CREATE FUNCTION gf14 ( ) RETURNS INT DETERMINISTIC RETURN 1 + 1", + statement.toString()); + + statement.setFunctionDeclarationParts(List.of("g", "(", ")", "SELECT", "1")); + assertTrue(statement.getCharacteristics().isEmpty()); + assertEquals("CREATE FUNCTION g ( ) SELECT 1", statement.toString()); + + statement.setFunctionDeclarationParts(List.of("g", "(", ")", "NO", "SQL", "SELECT", "1"), + List.of(new RoutineCharacteristic(Kind.NO_SQL))); + assertEquals(Kind.NO_SQL, statement.getCharacteristics().get(0).getKind()); + assertEquals("CREATE FUNCTION g ( ) NO SQL SELECT 1", statement.toString()); + } + + @Test + void otherDialectsKeepOnlyTokens() throws JSQLParserException { + String postgres = "CREATE FUNCTION add(a integer, b integer) RETURNS integer " + + "LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT a + b $$"; + assertTrue(((CreateFunctionalStatement) CCJSqlParserUtil.parse(postgres, + parser -> parser.withDialect(Dialect.POSTGRESQL))).getCharacteristics().isEmpty()); + assertTrue(((CreateFunctionalStatement) CCJSqlParserUtil.parse(postgres)) + .getCharacteristics().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER FUNCTION f1 DETERMINISTIC", + "ALTER PROCEDURE p1 NOT DETERMINISTIC"}) + void alterKeepsRejectingDeterministic(String sql) throws JSQLParserException { + assertInstanceOf(UnsupportedStatement.class, + CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + private static CreateFunctionalStatement parse(String sql) throws JSQLParserException { + return (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL)); + } +}