Skip to content

Commit 4c442d1

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 c458ecb commit 4c442d1

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
@@ -435,6 +435,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
435435
}
436436
}
437437

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

21488+
/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */
2148221489
CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2148321490
{
2148421491
CreateFunctionalStatement type = null;
2148521492
List<String> tokens = new LinkedList<String>();
2148621493
String statementType = null;
2148721494
MySqlDefiner definer = null;
21495+
List<RoutineCharacteristic> characteristics = new ArrayList<RoutineCharacteristic>();
2148821496
}
2148921497
{
2149021498
[ definer=MySqlRoutineDefiner() ]
@@ -21493,14 +21501,15 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2149321501
|
2149421502
<K_PROCEDURE> { statementType = "PROCEDURE"; }
2149521503
)
21496-
tokens=captureFunctionBody()
21504+
tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null)
2149721505
{
2149821506
if(statementType.equals("FUNCTION")) {
2149921507
type = new CreateFunction(isUsingOrReplace, tokens);
2150021508
}
2150121509
if(statementType.equals("PROCEDURE")) {
2150221510
type = new CreateProcedure(isUsingOrReplace, tokens);
2150321511
}
21512+
type.setFunctionDeclarationParts(tokens, characteristics);
2150421513
type.setDefiner(definer);
2150521514
return type;
2150621515
}
@@ -21546,7 +21555,7 @@ AlterRoutine AlterRoutine():
2154621555
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
2154721556
(
2154821557
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
21549-
characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
21558+
characteristic=MySqlRoutineCharacteristic(false) { result.getCharacteristics().add(characteristic); }
2155021559
)+
2155121560
{
2155221561
requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE
@@ -21624,7 +21633,8 @@ RoutineCharacteristic PostgreSqlRoutineCharacteristic():
2162421633
}
2162521634
}
2162621635

21627-
RoutineCharacteristic MySqlRoutineCharacteristic():
21636+
/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */
21637+
RoutineCharacteristic MySqlRoutineCharacteristic(boolean create):
2162821638
{
2162921639
RoutineCharacteristic.Kind kind;
2163021640
RoutineCharacteristic.SecurityClause clause = null;
@@ -21644,6 +21654,12 @@ RoutineCharacteristic MySqlRoutineCharacteristic():
2164421654
|
2164521655
<K_NO> ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
2164621656
|
21657+
LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC")
21658+
{ kind = RoutineCharacteristic.Kind.DETERMINISTIC; }
21659+
|
21660+
LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") })
21661+
<K_NOT> ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; }
21662+
|
2164721663
LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") <K_DATA>
2164821664
{ kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
2164921665
|
@@ -22444,16 +22460,29 @@ List<String> captureRest() {
2244422460
* PostgreSQL AS string bodies can use ordinary, escape or dollar quotes.
2244522461
* The boundary tracker distinguishes simple statements, declaration sections,
2244622462
* compound bodies, and expression CASE from procedural END qualifiers.
22463+
* With a list, MySQL characteristics between the header and the body are also
22464+
* parsed into it; their tokens are captured as well.
2244722465
*/
2244822466

2244922467
JAVACODE
22450-
List<String> captureFunctionBody() {
22468+
List<String> captureFunctionBody(List<RoutineCharacteristic> characteristics) {
2245122469
List<String> tokens = new LinkedList<String>();
2245222470
Token tok;
2245322471
RoutineBodyBoundary boundary = new RoutineBodyBoundary();
2245422472
while(true) {
2245522473
tok = getToken(1);
2245622474
int l = tokens.size();
22475+
if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) {
22476+
characteristics.add(MySqlRoutineCharacteristic(true));
22477+
for (Token consumed = tok; ; consumed = consumed.next) {
22478+
tokens.add(consumed.image);
22479+
boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next);
22480+
if (consumed == token) {
22481+
break;
22482+
}
22483+
}
22484+
continue;
22485+
}
2245722486
if (boundary.endsAt(tok)) {
2245822487
if (tok.kind == ST_SEMICOLON) {
2245922488
tokens.add(tok.image);

‎src/site/sphinx/usage.rst‎

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

19601960
``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of
19611961
``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and
1962-
triggers use. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
1962+
triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``,
1963+
``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the
1964+
routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or
1965+
``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC``
1966+
and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds
1967+
every token and renders the statement unchanged, so the list is read-only. Replacing the
1968+
tokens with ``setFunctionDeclarationParts(parts)`` clears it, while
1969+
``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens
1970+
keeps it. Without a MySQL dialect the list stays empty. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
19631971
and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL
19641972
statements remains a ``Block``; a compound body with procedural statements such as
19651973
``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with

0 commit comments

Comments
 (0)