Skip to content

Commit 893751a

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 d7b398c commit 893751a

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) {
@@ -21683,12 +21689,14 @@ Statement Create():
2168321689
}
2168421690
}
2168521691

21692+
/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */
2168621693
CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2168721694
{
2168821695
CreateFunctionalStatement type = null;
2168921696
List<String> tokens = new LinkedList<String>();
2169021697
String statementType = null;
2169121698
MySqlDefiner definer = null;
21699+
List<RoutineCharacteristic> characteristics = new ArrayList<RoutineCharacteristic>();
2169221700
}
2169321701
{
2169421702
[ definer=MySqlRoutineDefiner() ]
@@ -21697,14 +21705,15 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
2169721705
|
2169821706
<K_PROCEDURE> { statementType = "PROCEDURE"; }
2169921707
)
21700-
tokens=captureFunctionBody()
21708+
tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null)
2170121709
{
2170221710
if(statementType.equals("FUNCTION")) {
2170321711
type = new CreateFunction(isUsingOrReplace, tokens);
2170421712
}
2170521713
if(statementType.equals("PROCEDURE")) {
2170621714
type = new CreateProcedure(isUsingOrReplace, tokens);
2170721715
}
21716+
type.setFunctionDeclarationParts(tokens, characteristics);
2170821717
type.setDefiner(definer);
2170921718
return type;
2171021719
}
@@ -21750,7 +21759,7 @@ AlterRoutine AlterRoutine():
2175021759
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
2175121760
(
2175221761
LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
21753-
characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
21762+
characteristic=MySqlRoutineCharacteristic(false) { result.getCharacteristics().add(characteristic); }
2175421763
)+
2175521764
{
2175621765
requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE
@@ -21828,7 +21837,8 @@ RoutineCharacteristic PostgreSqlRoutineCharacteristic():
2182821837
}
2182921838
}
2183021839

21831-
RoutineCharacteristic MySqlRoutineCharacteristic():
21840+
/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */
21841+
RoutineCharacteristic MySqlRoutineCharacteristic(boolean create):
2183221842
{
2183321843
RoutineCharacteristic.Kind kind;
2183421844
RoutineCharacteristic.SecurityClause clause = null;
@@ -21848,6 +21858,12 @@ RoutineCharacteristic MySqlRoutineCharacteristic():
2184821858
|
2184921859
<K_NO> ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
2185021860
|
21861+
LOOKAHEAD({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC")
21862+
{ kind = RoutineCharacteristic.Kind.DETERMINISTIC; }
21863+
|
21864+
LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") })
21865+
<K_NOT> ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; }
21866+
|
2185121867
LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL") <K_DATA>
2185221868
{ kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
2185321869
|
@@ -22648,16 +22664,29 @@ List<String> captureRest() {
2264822664
* PostgreSQL AS string bodies can use ordinary, escape or dollar quotes.
2264922665
* The boundary tracker distinguishes simple statements, declaration sections,
2265022666
* compound bodies, and expression CASE from procedural END qualifiers.
22667+
* With a list, MySQL characteristics between the header and the body are also
22668+
* parsed into it; their tokens are captured as well.
2265122669
*/
2265222670

2265322671
JAVACODE
22654-
List<String> captureFunctionBody() {
22672+
List<String> captureFunctionBody(List<RoutineCharacteristic> characteristics) {
2265522673
List<String> tokens = new LinkedList<String>();
2265622674
Token tok;
2265722675
RoutineBodyBoundary boundary = new RoutineBodyBoundary();
2265822676
while(true) {
2265922677
tok = getToken(1);
2266022678
int l = tokens.size();
22679+
if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) {
22680+
characteristics.add(MySqlRoutineCharacteristic(true));
22681+
for (Token consumed = tok; ; consumed = consumed.next) {
22682+
tokens.add(consumed.image);
22683+
boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next);
22684+
if (consumed == token) {
22685+
break;
22686+
}
22687+
}
22688+
continue;
22689+
}
2266122690
if (boundary.endsAt(tok)) {
2266222691
if (tok.kind == ST_SEMICOLON) {
2266322692
tokens.add(tok.image);

‎src/site/sphinx/usage.rst‎

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

21352135
``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of
21362136
``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and
2137-
triggers use. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
2137+
triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``,
2138+
``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the
2139+
routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or
2140+
``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC``
2141+
and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds
2142+
every token and renders the statement unchanged, so the list is read-only. Replacing the
2143+
tokens with ``setFunctionDeclarationParts(parts)`` clears it, while
2144+
``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens
2145+
keeps it. Without a MySQL dialect the list stays empty. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
21382146
and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL
21392147
statements remains a ``Block``; a compound body with procedural statements such as
21402148
``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with

0 commit comments

Comments
 (0)