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
14 changes: 13 additions & 1 deletion src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public enum Operation {
private MySqlDefiner definer;
private FunctionReturnType returnType;
private List<String> routineBodyParts;
private List<RoutineCharacteristic> characteristics = Collections.emptyList();

public Operation getOperation() {
return operation;
Expand Down Expand Up @@ -99,8 +100,38 @@ public List<String> 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<String> 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<String> functionDeclarationParts,
List<RoutineCharacteristic> 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<RoutineCharacteristic> getCharacteristics() {
return characteristics;
}

/**
Expand Down Expand Up @@ -177,19 +208,23 @@ public CreateFunctionalStatement withFunctionDeclarationParts(
return this;
}

/** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */
public CreateFunctionalStatement addFunctionDeclarationParts(
String... functionDeclarationParts) {
List<String> 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<String> functionDeclarationParts) {
List<String> collection =
Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new);
collection.addAll(functionDeclarationParts);
return this.withFunctionDeclarationParts(collection);
setFunctionDeclarationParts(collection, characteristics);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
* <ul>
* <li>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;</li>
* <li>MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, the SQL data access and
* {@code SQL SECURITY}.</li>
* <li>MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, {@code [NOT] DETERMINISTIC} (CREATE only), the
* SQL data access and {@code SQL SECURITY}.</li>
* </ul>
*
* The security kinds are shared; {@link #getSecurityClause()} keeps the spelling. Values keep their
Expand Down Expand Up @@ -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}. */
Expand Down
37 changes: 33 additions & 4 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}
}

/** 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) {
Expand Down Expand Up @@ -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<String> tokens = new LinkedList<String>();
String statementType = null;
MySqlDefiner definer = null;
List<RoutineCharacteristic> characteristics = new ArrayList<RoutineCharacteristic>();
}
{
[ definer=MySqlRoutineDefiner() ]
Expand All @@ -22588,14 +22596,15 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
|
<K_PROCEDURE> { statementType = "PROCEDURE"; }
)
tokens=captureFunctionBody()
tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null)
{
if(statementType.equals("FUNCTION")) {
type = new CreateFunction(isUsingOrReplace, tokens);
}
if(statementType.equals("PROCEDURE")) {
type = new CreateProcedure(isUsingOrReplace, tokens);
}
type.setFunctionDeclarationParts(tokens, characteristics);
type.setDefiner(definer);
return type;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -22739,6 +22749,12 @@ RoutineCharacteristic MySqlRoutineCharacteristic():
|
<K_NO> ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
|
LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC")
{ kind = RoutineCharacteristic.Kind.DETERMINISTIC; }
|
LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") })
<K_NOT> ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; }
|
LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") <K_DATA>
{ kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
|
Expand Down Expand Up @@ -23541,16 +23557,29 @@ List<String> 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<String> captureFunctionBody() {
List<String> captureFunctionBody(List<RoutineCharacteristic> characteristics) {
List<String> tokens = new LinkedList<String>();
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);
Expand Down
10 changes: 9 additions & 1 deletion src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading