Skip to content

Commit c458ecb

Browse files
committed
feat: parse ALTER FUNCTION, PROCEDURE and TRIGGER actions and MySQL routine definers
Model PostgreSQL ALTER FUNCTION/PROCEDURE/ROUTINE and MySQL ALTER FUNCTION/PROCEDURE as AlterRoutine with typed RoutineCharacteristic items shared by both dialects; SET/RESET reuse ConfigurationParameterChange and the MySQL COMMENT keeps its literal as a StringValue. Add PostgreSQL ALTER TRIGGER as AlterTrigger. Unmodelled forms remain unsupported statements. Keep the MySQL DEFINER of CREATE FUNCTION/PROCEDURE, accept CREATE TRIGGER IF NOT EXISTS, keep MariaDB's OR REPLACE, and capture procedural trigger bodies with the routine body boundary so the next statement is split correctly. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 74ce2df commit c458ecb

26 files changed

Lines changed: 2010 additions & 11 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
145145
| **DDL** | `CREATE …` · `ALTER …` · `DROP …` |
146146
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` |
147147
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |
148+
| **Routine changes** | `ALTER FUNCTION` · `ALTER PROCEDURE` · `ALTER ROUTINE` · PostgreSQL `ALTER TRIGGER` |
148149
| **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` |
149150
| **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` |
150151

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ private enum Block {
3939
private boolean handlerAction;
4040
private boolean statementStart = true;
4141

42+
/**
43+
* Tracks a compound statement without a routine header, such as a MySQL trigger body. Its
44+
* tokens belong to the body from the start, as after a routine's parameter list.
45+
*/
46+
static RoutineBodyBoundary forCompoundStatement() {
47+
RoutineBodyBoundary boundary = new RoutineBodyBoundary();
48+
boundary.hasParameters = true;
49+
return boundary;
50+
}
51+
4252
boolean endsAt(Token token) throws ParseException {
4353
if (token.kind == EOF) {
4454
if (!blocks.isEmpty()) {

‎src/main/java/net/sf/jsqlparser/parser/feature/Feature.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,18 @@ public enum Feature {
717717
functionalStatement,
718718

719719
alterFunction, alterProcedure, createOrAlterRoutine,
720+
/**
721+
* PostgreSQL "ALTER ROUTINE" statement is allowed
722+
*
723+
* @see net.sf.jsqlparser.statement.alter.AlterRoutine
724+
*/
725+
alterRoutine,
726+
/**
727+
* PostgreSQL "ALTER TRIGGER" statement is allowed
728+
*
729+
* @see net.sf.jsqlparser.statement.alter.AlterTrigger
730+
*/
731+
alterTrigger,
720732
/**
721733
* SQL block starting with "BEGIN" and ends with "END" statement is allowed
722734
*

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
/**
1818
* A PostgreSQL {@code SET} or {@code RESET} of a configuration parameter, shared by statements that
19-
* store parameter defaults, such as {@code ALTER ROLE} and {@code ALTER DATABASE}:
19+
* store parameter defaults: {@code ALTER ROLE}, {@code ALTER DATABASE} and the routine
20+
* characteristics of {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} and {@code ALTER ROUTINE}:
2021
*
2122
* <pre>
2223
* SET parameter { TO | = } { value [, ...] | DEFAULT }

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public enum Operation {
3131
}
3232

3333
private Operation operation = Operation.CREATE;
34+
private MySqlDefiner definer;
3435
private FunctionReturnType returnType;
3536
private List<String> routineBodyParts;
3637

@@ -42,6 +43,19 @@ public void setOperation(Operation operation) {
4243
this.operation = operation;
4344
}
4445

46+
/**
47+
* The MySQL {@code DEFINER} account, rendered after {@code CREATE [OR REPLACE]}.
48+
*
49+
* @return the definer, or {@code null}
50+
*/
51+
public MySqlDefiner getDefiner() {
52+
return definer;
53+
}
54+
55+
public void setDefiner(MySqlDefiner definer) {
56+
this.definer = definer;
57+
}
58+
4559
public FunctionReturnType getReturnType() {
4660
return returnType;
4761
}
@@ -139,6 +153,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<TableElement> prin
139153
if (orReplace && operation == Operation.CREATE) {
140154
builder.append("OR REPLACE ");
141155
}
156+
if (definer != null) {
157+
builder.append("DEFINER = ").append(definer).append(' ');
158+
}
142159
builder.append(kind).append(' ');
143160
return appendDeclarationTo(builder, printer);
144161
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement;
11+
12+
import java.io.Serializable;
13+
import java.util.function.Consumer;
14+
import net.sf.jsqlparser.expression.Expression;
15+
import net.sf.jsqlparser.expression.StringValue;
16+
17+
/**
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:
20+
*
21+
* <ul>
22+
* <li>PostgreSQL: the volatility, the null input behavior, {@code [NOT] LEAKPROOF},
23+
* {@code [EXTERNAL] SECURITY}, {@code PARALLEL}, {@code COST}, {@code ROWS}, {@code SUPPORT} and
24+
* 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>
27+
* </ul>
28+
*
29+
* The security kinds are shared; {@link #getSecurityClause()} keeps the spelling. Values keep their
30+
* SQL text, such as {@code 100}. The {@code COMMENT} literal is a {@link StringValue}.
31+
*/
32+
public class RoutineCharacteristic implements Serializable {
33+
34+
public enum Kind {
35+
/** PostgreSQL {@code IMMUTABLE}. */
36+
IMMUTABLE("IMMUTABLE"),
37+
/** PostgreSQL {@code STABLE}. */
38+
STABLE("STABLE"),
39+
/** PostgreSQL {@code VOLATILE}. */
40+
VOLATILE("VOLATILE"),
41+
/** PostgreSQL {@code CALLED ON NULL INPUT}. */
42+
CALLED_ON_NULL_INPUT("CALLED ON NULL INPUT"),
43+
/** PostgreSQL {@code RETURNS NULL ON NULL INPUT}. */
44+
RETURNS_NULL_ON_NULL_INPUT("RETURNS NULL ON NULL INPUT"),
45+
/** PostgreSQL {@code STRICT}. */
46+
STRICT("STRICT"),
47+
/** PostgreSQL {@code LEAKPROOF}. */
48+
LEAKPROOF("LEAKPROOF"),
49+
/** PostgreSQL {@code NOT LEAKPROOF}. */
50+
NOT_LEAKPROOF("NOT LEAKPROOF"),
51+
/** {@code DEFINER}, preceded by the {@link SecurityClause}. */
52+
SECURITY_DEFINER("DEFINER"),
53+
/** {@code INVOKER}, preceded by the {@link SecurityClause}. */
54+
SECURITY_INVOKER("INVOKER"),
55+
/** PostgreSQL {@code PARALLEL UNSAFE}. */
56+
PARALLEL_UNSAFE("PARALLEL UNSAFE"),
57+
/** PostgreSQL {@code PARALLEL RESTRICTED}. */
58+
PARALLEL_RESTRICTED("PARALLEL RESTRICTED"),
59+
/** PostgreSQL {@code PARALLEL SAFE}. */
60+
PARALLEL_SAFE("PARALLEL SAFE"),
61+
/** PostgreSQL {@code COST} followed by the numeric {@link #getValue()}. */
62+
COST("COST"),
63+
/** PostgreSQL {@code ROWS} followed by the numeric {@link #getValue()}. */
64+
ROWS("ROWS"),
65+
/** PostgreSQL {@code SUPPORT} followed by the function name in {@link #getValue()}. */
66+
SUPPORT("SUPPORT"),
67+
/** PostgreSQL {@code SET} or {@code RESET}, see {@link #getParameterChange()}. */
68+
CONFIGURATION_PARAMETER(null),
69+
/** MySQL {@code COMMENT} followed by the string literal of {@link #getComment()}. */
70+
COMMENT("COMMENT"),
71+
/** MySQL {@code LANGUAGE SQL}. */
72+
LANGUAGE_SQL("LANGUAGE SQL"),
73+
/** MySQL {@code CONTAINS SQL}. */
74+
CONTAINS_SQL("CONTAINS SQL"),
75+
/** MySQL {@code NO SQL}. */
76+
NO_SQL("NO SQL"),
77+
/** MySQL {@code READS SQL DATA}. */
78+
READS_SQL_DATA("READS SQL DATA"),
79+
/** MySQL {@code MODIFIES SQL DATA}. */
80+
MODIFIES_SQL_DATA("MODIFIES SQL DATA");
81+
82+
private final String keywords;
83+
84+
Kind(String keywords) {
85+
this.keywords = keywords;
86+
}
87+
88+
private boolean hasValue() {
89+
return this == COST || this == ROWS || this == SUPPORT || this == COMMENT;
90+
}
91+
}
92+
93+
/** The words before {@code DEFINER} or {@code INVOKER}. */
94+
public enum SecurityClause {
95+
/** PostgreSQL {@code SECURITY}. */
96+
SECURITY("SECURITY"),
97+
/** PostgreSQL {@code EXTERNAL SECURITY}; {@code EXTERNAL} has no effect. */
98+
EXTERNAL_SECURITY("EXTERNAL SECURITY"),
99+
/** MySQL {@code SQL SECURITY}. */
100+
SQL_SECURITY("SQL SECURITY");
101+
102+
private final String keywords;
103+
104+
SecurityClause(String keywords) {
105+
this.keywords = keywords;
106+
}
107+
}
108+
109+
private Kind kind;
110+
private String value;
111+
private StringValue comment;
112+
private SecurityClause securityClause;
113+
private ConfigurationParameterChange parameterChange;
114+
115+
public RoutineCharacteristic(Kind kind) {
116+
this.kind = kind;
117+
}
118+
119+
public RoutineCharacteristic(Kind kind, String value) {
120+
this.kind = kind;
121+
setValue(value);
122+
}
123+
124+
public RoutineCharacteristic(ConfigurationParameterChange parameterChange) {
125+
this.kind = Kind.CONFIGURATION_PARAMETER;
126+
this.parameterChange = parameterChange;
127+
}
128+
129+
/**
130+
* Creates a MySQL {@code COMMENT} characteristic.
131+
*
132+
* @param comment the string literal
133+
* @return the characteristic
134+
*/
135+
public static RoutineCharacteristic comment(StringValue comment) {
136+
return new RoutineCharacteristic(Kind.COMMENT).withComment(comment);
137+
}
138+
139+
public Kind getKind() {
140+
return kind;
141+
}
142+
143+
public void setKind(Kind kind) {
144+
this.kind = kind;
145+
}
146+
147+
/**
148+
* The SQL text after the keyword of {@link Kind#COST}, {@link Kind#ROWS}, {@link Kind#SUPPORT}
149+
* and {@link Kind#COMMENT}. For {@code COMMENT} it is derived from {@link #getComment()}, such
150+
* as {@code 'it\'s'}.
151+
*
152+
* @return the value as written, or {@code null} for other kinds
153+
*/
154+
public String getValue() {
155+
if (kind == Kind.COMMENT) {
156+
return comment == null ? null : comment.toString();
157+
}
158+
return value;
159+
}
160+
161+
/**
162+
* Sets the SQL text of the value. For {@link Kind#COMMENT} it replaces {@link #getComment()}
163+
* with a literal read without dialect escaping; use {@link #setComment(StringValue)} to keep
164+
* MySQL backslash escapes.
165+
*
166+
* @param value the SQL text
167+
*/
168+
public void setValue(String value) {
169+
if (kind == Kind.COMMENT) {
170+
comment = value == null ? null : new StringValue(value);
171+
} else {
172+
this.value = value;
173+
}
174+
}
175+
176+
/**
177+
* The string literal of {@link Kind#COMMENT}. {@link StringValue#getNotExcapedValue()} returns
178+
* the text that MySQL stores.
179+
*
180+
* @return the literal, or {@code null} for other kinds
181+
*/
182+
public StringValue getComment() {
183+
return comment;
184+
}
185+
186+
public void setComment(StringValue comment) {
187+
this.comment = comment;
188+
}
189+
190+
public RoutineCharacteristic withComment(StringValue comment) {
191+
setComment(comment);
192+
return this;
193+
}
194+
195+
/**
196+
* The spelling of {@link Kind#SECURITY_DEFINER} and {@link Kind#SECURITY_INVOKER}; {@code null}
197+
* renders {@link SecurityClause#SECURITY}.
198+
*
199+
* @return the clause, or {@code null}
200+
*/
201+
public SecurityClause getSecurityClause() {
202+
return securityClause;
203+
}
204+
205+
public void setSecurityClause(SecurityClause securityClause) {
206+
this.securityClause = securityClause;
207+
}
208+
209+
/**
210+
* The change of {@link Kind#CONFIGURATION_PARAMETER}.
211+
*
212+
* @return the change, or {@code null} for other kinds
213+
*/
214+
public ConfigurationParameterChange getParameterChange() {
215+
return parameterChange;
216+
}
217+
218+
public void setParameterChange(ConfigurationParameterChange parameterChange) {
219+
this.parameterChange = parameterChange;
220+
}
221+
222+
public RoutineCharacteristic withValue(String value) {
223+
setValue(value);
224+
return this;
225+
}
226+
227+
public RoutineCharacteristic withSecurityClause(SecurityClause securityClause) {
228+
setSecurityClause(securityClause);
229+
return this;
230+
}
231+
232+
/**
233+
* Appends this characteristic to a builder.
234+
*
235+
* @param builder the target
236+
* @param valueRenderer appends each configuration parameter value
237+
* @return the builder
238+
*/
239+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> valueRenderer) {
240+
switch (kind) {
241+
case CONFIGURATION_PARAMETER:
242+
if (parameterChange == null) {
243+
throw new IllegalStateException("Expected a configuration parameter change");
244+
}
245+
return parameterChange.appendTo(builder, valueRenderer);
246+
case SECURITY_DEFINER:
247+
case SECURITY_INVOKER:
248+
return builder
249+
.append((securityClause == null ? SecurityClause.SECURITY
250+
: securityClause).keywords)
251+
.append(' ').append(kind.keywords);
252+
default:
253+
builder.append(kind.keywords);
254+
return kind.hasValue() ? builder.append(' ').append(getValue()) : builder;
255+
}
256+
}
257+
258+
@Override
259+
public String toString() {
260+
StringBuilder builder = new StringBuilder();
261+
return appendTo(builder, builder::append).toString();
262+
}
263+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
2727
import net.sf.jsqlparser.statement.alter.Alter;
2828
import net.sf.jsqlparser.statement.alter.AlterCollation;
29+
import net.sf.jsqlparser.statement.alter.AlterRoutine;
30+
import net.sf.jsqlparser.statement.alter.AlterTrigger;
2931
import net.sf.jsqlparser.statement.alter.AlterDomain;
3032
import net.sf.jsqlparser.statement.alter.AlterExtension;
3133
import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper;
@@ -1181,6 +1183,16 @@ public <S> Void visit(AlterCollation statement, S context) {
11811183
return schemaOnly();
11821184
}
11831185

1186+
@Override
1187+
public <S> Void visit(AlterRoutine statement, S context) {
1188+
return schemaOnly();
1189+
}
1190+
1191+
@Override
1192+
public <S> Void visit(AlterTrigger statement, S context) {
1193+
return schemaOnly();
1194+
}
1195+
11841196
@Override
11851197
public <S> Void visit(AlterDatabase statement, S context) {
11861198
return schemaOnly();

0 commit comments

Comments
 (0)