Skip to content

Commit c4137c2

Browse files
authored
feat: model MySQL CREATE FUNCTION and PROCEDURE characteristics (#2823)
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 5148362 commit c4137c2

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
@@ -437,6 +437,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
437437
}
438438
}
439439

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

22844+
/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */
2283822845
CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2283922846
{
2284022847
CreateFunctionalStatement type = null;
2284122848
List<String> tokens = new LinkedList<String>();
2284222849
String statementType = null;
2284322850
MySqlDefiner definer = null;
22851+
List<RoutineCharacteristic> characteristics = new ArrayList<RoutineCharacteristic>();
2284422852
}
2284522853
{
2284622854
[ definer=MySqlRoutineDefiner() ]
@@ -22849,14 +22857,15 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2284922857
|
2285022858
<K_PROCEDURE> { statementType = "PROCEDURE"; }
2285122859
)
22852-
tokens=captureFunctionBody()
22860+
tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null)
2285322861
{
2285422862
if(statementType.equals("FUNCTION")) {
2285522863
type = new CreateFunction(isUsingOrReplace, tokens);
2285622864
}
2285722865
if(statementType.equals("PROCEDURE")) {
2285822866
type = new CreateProcedure(isUsingOrReplace, tokens);
2285922867
}
22868+
type.setFunctionDeclarationParts(tokens, characteristics);
2286022869
type.setDefiner(definer);
2286122870
return type;
2286222871
}
@@ -22902,7 +22911,7 @@ AlterRoutine AlterRoutine():
2290222911
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
2290322912
(
2290422913
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
22905-
characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
22914+
characteristic=MySqlRoutineCharacteristic(false) { result.getCharacteristics().add(characteristic); }
2290622915
)+
2290722916
{
2290822917
requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE
@@ -22980,7 +22989,8 @@ RoutineCharacteristic PostgreSqlRoutineCharacteristic():
2298022989
}
2298122990
}
2298222991

22983-
RoutineCharacteristic MySqlRoutineCharacteristic():
22992+
/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */
22993+
RoutineCharacteristic MySqlRoutineCharacteristic(boolean create):
2298422994
{
2298522995
RoutineCharacteristic.Kind kind;
2298622996
RoutineCharacteristic.SecurityClause clause = null;
@@ -23000,6 +23010,12 @@ RoutineCharacteristic MySqlRoutineCharacteristic():
2300023010
|
2300123011
<K_NO> ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
2300223012
|
23013+
LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC")
23014+
{ kind = RoutineCharacteristic.Kind.DETERMINISTIC; }
23015+
|
23016+
LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") })
23017+
<K_NOT> ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; }
23018+
|
2300323019
LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") <K_DATA>
2300423020
{ kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
2300523021
|
@@ -23836,16 +23852,29 @@ List<String> captureRest() {
2383623852
* PostgreSQL AS string bodies can use ordinary, escape or dollar quotes.
2383723853
* The boundary tracker distinguishes simple statements, declaration sections,
2383823854
* compound bodies, and expression CASE from procedural END qualifiers.
23855+
* With a list, MySQL characteristics between the header and the body are also
23856+
* parsed into it; their tokens are captured as well.
2383923857
*/
2384023858

2384123859
JAVACODE
23842-
List<String> captureFunctionBody() {
23860+
List<String> captureFunctionBody(List<RoutineCharacteristic> characteristics) {
2384323861
List<String> tokens = new LinkedList<String>();
2384423862
Token tok;
2384523863
RoutineBodyBoundary boundary = new RoutineBodyBoundary();
2384623864
while(true) {
2384723865
tok = getToken(1);
2384823866
int l = tokens.size();
23867+
if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) {
23868+
characteristics.add(MySqlRoutineCharacteristic(true));
23869+
for (Token consumed = tok; ; consumed = consumed.next) {
23870+
tokens.add(consumed.image);
23871+
boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next);
23872+
if (consumed == token) {
23873+
break;
23874+
}
23875+
}
23876+
continue;
23877+
}
2384923878
if (boundary.endsAt(tok)) {
2385023879
if (tok.kind == ST_SEMICOLON) {
2385123880
tokens.add(tok.image);

‎src/site/sphinx/usage.rst‎

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

23692369
``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of
23702370
``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and
2371-
triggers use. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
2371+
triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``,
2372+
``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the
2373+
routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or
2374+
``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC``
2375+
and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds
2376+
every token and renders the statement unchanged, so the list is read-only. Replacing the
2377+
tokens with ``setFunctionDeclarationParts(parts)`` clears it, while
2378+
``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens
2379+
keeps it. Without a MySQL dialect the list stays empty. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
23722380
and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL
23732381
statements remains a ``Block``; a compound body with procedural statements such as
23742382
``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with

0 commit comments

Comments
 (0)