Skip to content

Commit cc4a97a

Browse files
committed
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 <mmj9808@gmail.com>
1 parent 6c5fc6c commit cc4a97a

6 files changed

Lines changed: 269 additions & 12 deletions

File tree

‎src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ boolean endsAt(Token token) throws ParseException {
6060
&& !declarations && (hasParameters || quotedBody || completed);
6161
}
6262

63+
/**
64+
* Whether the routine header, including a return type, is complete and the body has not
65+
* started, which is where MySQL routine characteristics appear.
66+
*/
67+
boolean isBeforeBody() {
68+
return hasParameters && parentheses == 0 && !bodyStarted && !returnTypeName
69+
&& !declarations && !quotedBody && !completed;
70+
}
71+
6372
boolean startsQuotedBody(Token token, Token next) {
6473
return parentheses == 0 && !compoundBody && !quotedBody && token.kind == K_AS
6574
&& next.kind == S_CHAR_LITERAL
@@ -228,7 +237,10 @@ private void acceptStatementToken(Token token, Token next) {
228237
}
229238
if (!bodyStarted) {
230239
// SET may also be a PostgreSQL header option; AS remains recognizable above.
231-
if (!isSimpleStatementStart(kind)) {
240+
// CHARACTER SET and CHAR SET belong to a MySQL return type.
241+
boolean characterSet =
242+
kind == K_SET && (previousKind == K_CHARACTER || previousKind == K_CHAR);
243+
if (!isSimpleStatementStart(kind) || characterSet) {
232244
return;
233245
}
234246
bodyStarted = true;

‎src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public enum Operation {
3434
private MySqlDefiner definer;
3535
private FunctionReturnType returnType;
3636
private List<String> routineBodyParts;
37+
private List<RoutineCharacteristic> characteristics = Collections.emptyList();
3738

3839
public Operation getOperation() {
3940
return operation;
@@ -99,8 +100,38 @@ public List<String> getFunctionDeclarationParts() {
99100
return functionDeclarationParts;
100101
}
101102

103+
/**
104+
* Replaces the declaration parts. The {@link #getCharacteristics() characteristics} described
105+
* the replaced parts, so they are cleared.
106+
*
107+
* @param functionDeclarationParts the new parts
108+
*/
102109
public void setFunctionDeclarationParts(List<String> functionDeclarationParts) {
110+
setFunctionDeclarationParts(functionDeclarationParts, Collections.emptyList());
111+
}
112+
113+
/**
114+
* Replaces the declaration parts together with the characteristics written in them.
115+
*
116+
* @param functionDeclarationParts the new parts, which keep rendering the statement
117+
* @param characteristics the characteristics among the parts, in source order
118+
*/
119+
public void setFunctionDeclarationParts(List<String> functionDeclarationParts,
120+
List<RoutineCharacteristic> characteristics) {
103121
this.functionDeclarationParts = functionDeclarationParts;
122+
this.characteristics = List.copyOf(characteristics);
123+
}
124+
125+
/**
126+
* The MySQL characteristics of {@code CREATE FUNCTION|PROCEDURE}, such as {@code COMMENT},
127+
* {@code DETERMINISTIC} or {@code SQL SECURITY}, parsed with a MySQL or MariaDB dialect. They
128+
* describe tokens of {@link #getFunctionDeclarationParts()}, which still render the statement,
129+
* so the list is read-only.
130+
*
131+
* @return the characteristics in source order, empty if none were parsed
132+
*/
133+
public List<RoutineCharacteristic> getCharacteristics() {
134+
return characteristics;
104135
}
105136

106137
/**
@@ -177,19 +208,23 @@ public CreateFunctionalStatement withFunctionDeclarationParts(
177208
return this;
178209
}
179210

211+
/** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */
180212
public CreateFunctionalStatement addFunctionDeclarationParts(
181213
String... functionDeclarationParts) {
182214
List<String> collection =
183215
Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new);
184216
Collections.addAll(collection, functionDeclarationParts);
185-
return this.withFunctionDeclarationParts(collection);
217+
setFunctionDeclarationParts(collection, characteristics);
218+
return this;
186219
}
187220

221+
/** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */
188222
public CreateFunctionalStatement addFunctionDeclarationParts(
189223
Collection<String> functionDeclarationParts) {
190224
List<String> collection =
191225
Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new);
192226
collection.addAll(functionDeclarationParts);
193-
return this.withFunctionDeclarationParts(collection);
227+
setFunctionDeclarationParts(collection, characteristics);
228+
return this;
194229
}
195230
}

‎src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
import net.sf.jsqlparser.expression.StringValue;
1616

1717
/**
18-
* A characteristic set by {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE}.
19-
* PostgreSQL and MySQL share the model, and each {@link Kind} renders its own SQL:
18+
* A characteristic set by {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE},
19+
* or declared by MySQL's {@code CREATE FUNCTION|PROCEDURE}. PostgreSQL and MySQL share the model,
20+
* and each {@link Kind} renders its own SQL:
2021
*
2122
* <ul>
2223
* <li>PostgreSQL: the volatility, the null input behavior, {@code [NOT] LEAKPROOF},
2324
* {@code [EXTERNAL] SECURITY}, {@code PARALLEL}, {@code COST}, {@code ROWS}, {@code SUPPORT} and
2425
* the {@code SET} or {@code RESET} of a configuration parameter;</li>
25-
* <li>MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, the SQL data access and
26-
* {@code SQL SECURITY}.</li>
26+
* <li>MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, {@code [NOT] DETERMINISTIC} (CREATE only), the
27+
* SQL data access and {@code SQL SECURITY}.</li>
2728
* </ul>
2829
*
2930
* The security kinds are shared; {@link #getSecurityClause()} keeps the spelling. Values keep their
@@ -70,6 +71,10 @@ public enum Kind {
7071
COMMENT("COMMENT"),
7172
/** MySQL {@code LANGUAGE SQL}. */
7273
LANGUAGE_SQL("LANGUAGE SQL"),
74+
/** MySQL {@code DETERMINISTIC}, only in {@code CREATE FUNCTION|PROCEDURE}. */
75+
DETERMINISTIC("DETERMINISTIC"),
76+
/** MySQL {@code NOT DETERMINISTIC}, only in {@code CREATE FUNCTION|PROCEDURE}. */
77+
NOT_DETERMINISTIC("NOT DETERMINISTIC"),
7378
/** MySQL {@code CONTAINS SQL}. */
7479
CONTAINS_SQL("CONTAINS SQL"),
7580
/** MySQL {@code NO SQL}. */

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
436436
}
437437
}
438438

439+
/** Whether a MySQL characteristic of CREATE FUNCTION or PROCEDURE starts here. */
440+
private boolean isMySqlCreateRoutineCharacteristicAhead() {
441+
return isMySqlRoutineCharacteristicAhead(1) || isWordSequenceAhead(1, "DETERMINISTIC")
442+
|| isWordSequenceAhead(1, "NOT", "DETERMINISTIC");
443+
}
444+
439445
/** Whether a MySQL characteristic of ALTER FUNCTION or PROCEDURE starts at the offset. */
440446
private boolean isMySqlRoutineCharacteristicAhead(int offset) {
441447
if (getToken(offset).kind == K_COMMENT) {
@@ -22574,12 +22580,14 @@ Statement Create():
2257422580
}
2257522581
}
2257622582

22583+
/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */
2257722584
CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2257822585
{
2257922586
CreateFunctionalStatement type = null;
2258022587
List<String> tokens = new LinkedList<String>();
2258122588
String statementType = null;
2258222589
MySqlDefiner definer = null;
22590+
List<RoutineCharacteristic> characteristics = new ArrayList<RoutineCharacteristic>();
2258322591
}
2258422592
{
2258522593
[ definer=MySqlRoutineDefiner() ]
@@ -22588,14 +22596,15 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2258822596
|
2258922597
<K_PROCEDURE> { statementType = "PROCEDURE"; }
2259022598
)
22591-
tokens=captureFunctionBody()
22599+
tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null)
2259222600
{
2259322601
if(statementType.equals("FUNCTION")) {
2259422602
type = new CreateFunction(isUsingOrReplace, tokens);
2259522603
}
2259622604
if(statementType.equals("PROCEDURE")) {
2259722605
type = new CreateProcedure(isUsingOrReplace, tokens);
2259822606
}
22607+
type.setFunctionDeclarationParts(tokens, characteristics);
2259922608
type.setDefiner(definer);
2260022609
return type;
2260122610
}
@@ -22641,7 +22650,7 @@ AlterRoutine AlterRoutine():
2264122650
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
2264222651
(
2264322652
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
22644-
characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
22653+
characteristic=MySqlRoutineCharacteristic(false) { result.getCharacteristics().add(characteristic); }
2264522654
)+
2264622655
{
2264722656
requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE
@@ -22719,7 +22728,8 @@ RoutineCharacteristic PostgreSqlRoutineCharacteristic():
2271922728
}
2272022729
}
2272122730

22722-
RoutineCharacteristic MySqlRoutineCharacteristic():
22731+
/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */
22732+
RoutineCharacteristic MySqlRoutineCharacteristic(boolean create):
2272322733
{
2272422734
RoutineCharacteristic.Kind kind;
2272522735
RoutineCharacteristic.SecurityClause clause = null;
@@ -22739,6 +22749,12 @@ RoutineCharacteristic MySqlRoutineCharacteristic():
2273922749
|
2274022750
<K_NO> ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
2274122751
|
22752+
LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC")
22753+
{ kind = RoutineCharacteristic.Kind.DETERMINISTIC; }
22754+
|
22755+
LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") })
22756+
<K_NOT> ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; }
22757+
|
2274222758
LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") <K_DATA>
2274322759
{ kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
2274422760
|
@@ -23541,16 +23557,29 @@ List<String> captureRest() {
2354123557
* PostgreSQL AS string bodies can use ordinary, escape or dollar quotes.
2354223558
* The boundary tracker distinguishes simple statements, declaration sections,
2354323559
* compound bodies, and expression CASE from procedural END qualifiers.
23560+
* With a list, MySQL characteristics between the header and the body are also
23561+
* parsed into it; their tokens are captured as well.
2354423562
*/
2354523563

2354623564
JAVACODE
23547-
List<String> captureFunctionBody() {
23565+
List<String> captureFunctionBody(List<RoutineCharacteristic> characteristics) {
2354823566
List<String> tokens = new LinkedList<String>();
2354923567
Token tok;
2355023568
RoutineBodyBoundary boundary = new RoutineBodyBoundary();
2355123569
while(true) {
2355223570
tok = getToken(1);
2355323571
int l = tokens.size();
23572+
if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) {
23573+
characteristics.add(MySqlRoutineCharacteristic(true));
23574+
for (Token consumed = tok; ; consumed = consumed.next) {
23575+
tokens.add(consumed.image);
23576+
boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next);
23577+
if (consumed == token) {
23578+
break;
23579+
}
23580+
}
23581+
continue;
23582+
}
2355423583
if (boundary.endsAt(tok)) {
2355523584
if (tok.kind == ST_SEMICOLON) {
2355623585
tokens.add(tok.image);

‎src/site/sphinx/usage.rst‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2260,7 +2260,15 @@ Validators report ``alterFunction``, ``alterProcedure``, ``alterRoutine`` and
22602260

22612261
``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of
22622262
``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and
2263-
triggers use. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
2263+
triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``,
2264+
``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the
2265+
routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or
2266+
``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC``
2267+
and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds
2268+
every token and renders the statement unchanged, so the list is read-only. Replacing the
2269+
tokens with ``setFunctionDeclarationParts(parts)`` clears it, while
2270+
``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens
2271+
keeps it. Without a MySQL dialect the list stays empty. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
22642272
and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL
22652273
statements remains a ``Block``; a compound body with procedural statements such as
22662274
``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with

0 commit comments

Comments
 (0)