diff --git a/README.md b/README.md index 58fd6618f0..1d153a16f1 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **PostgreSQL ownership** | `DROP OWNED` · `REASSIGN OWNED` | | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | +| **Routine changes** | `ALTER FUNCTION` · `ALTER PROCEDURE` · `ALTER ROUTINE` · PostgreSQL `ALTER TRIGGER` | | **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | diff --git a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java index 4a5935fd4b..a8d3d88259 100644 --- a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java +++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java @@ -39,6 +39,16 @@ private enum Block { private boolean handlerAction; private boolean statementStart = true; + /** + * Tracks a compound statement without a routine header, such as a MySQL trigger body. Its + * tokens belong to the body from the start, as after a routine's parameter list. + */ + static RoutineBodyBoundary forCompoundStatement() { + RoutineBodyBoundary boundary = new RoutineBodyBoundary(); + boundary.hasParameters = true; + return boundary; + } + boolean endsAt(Token token) throws ParseException { if (token.kind == EOF) { if (!blocks.isEmpty()) { diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 41d7092e32..60f8521f93 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -724,6 +724,18 @@ public enum Feature { functionalStatement, alterFunction, alterProcedure, createOrAlterRoutine, + /** + * PostgreSQL "ALTER ROUTINE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterRoutine + */ + alterRoutine, + /** + * PostgreSQL "ALTER TRIGGER" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterTrigger + */ + alterTrigger, /** * SQL block starting with "BEGIN" and ends with "END" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java b/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java index 868c7f6e8c..913cace5e6 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java +++ b/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java @@ -16,7 +16,8 @@ /** * A PostgreSQL {@code SET} or {@code RESET} of a configuration parameter, shared by statements that - * store parameter defaults, such as {@code ALTER ROLE} and {@code ALTER DATABASE}: + * store parameter defaults: {@code ALTER ROLE}, {@code ALTER DATABASE} and the routine + * characteristics of {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} and {@code ALTER ROUTINE}: * *
* SET parameter { TO | = } { value [, ...] | DEFAULT }
diff --git a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
index 0845742e9b..c34037ada1 100644
--- a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
@@ -31,6 +31,7 @@ public enum Operation {
}
private Operation operation = Operation.CREATE;
+ private MySqlDefiner definer;
private FunctionReturnType returnType;
private List routineBodyParts;
@@ -42,6 +43,19 @@ public void setOperation(Operation operation) {
this.operation = operation;
}
+ /**
+ * The MySQL {@code DEFINER} account, rendered after {@code CREATE [OR REPLACE]}.
+ *
+ * @return the definer, or {@code null}
+ */
+ public MySqlDefiner getDefiner() {
+ return definer;
+ }
+
+ public void setDefiner(MySqlDefiner definer) {
+ this.definer = definer;
+ }
+
public FunctionReturnType getReturnType() {
return returnType;
}
@@ -139,6 +153,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer prin
if (orReplace && operation == Operation.CREATE) {
builder.append("OR REPLACE ");
}
+ if (definer != null) {
+ builder.append("DEFINER = ").append(definer).append(' ');
+ }
builder.append(kind).append(' ');
return appendDeclarationTo(builder, printer);
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java
new file mode 100644
index 0000000000..93df5a7aaf
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java
@@ -0,0 +1,263 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement;
+
+import java.io.Serializable;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+
+/**
+ * A characteristic set by {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE}.
+ * PostgreSQL and MySQL share the model, and each {@link Kind} renders its own SQL:
+ *
+ *
+ * - PostgreSQL: the volatility, the null input behavior, {@code [NOT] LEAKPROOF},
+ * {@code [EXTERNAL] SECURITY}, {@code PARALLEL}, {@code COST}, {@code ROWS}, {@code SUPPORT} and
+ * the {@code SET} or {@code RESET} of a configuration parameter;
+ * - MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, the SQL data access and
+ * {@code SQL SECURITY}.
+ *
+ *
+ * The security kinds are shared; {@link #getSecurityClause()} keeps the spelling. Values keep their
+ * SQL text, such as {@code 100}. The {@code COMMENT} literal is a {@link StringValue}.
+ */
+public class RoutineCharacteristic implements Serializable {
+
+ public enum Kind {
+ /** PostgreSQL {@code IMMUTABLE}. */
+ IMMUTABLE("IMMUTABLE"),
+ /** PostgreSQL {@code STABLE}. */
+ STABLE("STABLE"),
+ /** PostgreSQL {@code VOLATILE}. */
+ VOLATILE("VOLATILE"),
+ /** PostgreSQL {@code CALLED ON NULL INPUT}. */
+ CALLED_ON_NULL_INPUT("CALLED ON NULL INPUT"),
+ /** PostgreSQL {@code RETURNS NULL ON NULL INPUT}. */
+ RETURNS_NULL_ON_NULL_INPUT("RETURNS NULL ON NULL INPUT"),
+ /** PostgreSQL {@code STRICT}. */
+ STRICT("STRICT"),
+ /** PostgreSQL {@code LEAKPROOF}. */
+ LEAKPROOF("LEAKPROOF"),
+ /** PostgreSQL {@code NOT LEAKPROOF}. */
+ NOT_LEAKPROOF("NOT LEAKPROOF"),
+ /** {@code DEFINER}, preceded by the {@link SecurityClause}. */
+ SECURITY_DEFINER("DEFINER"),
+ /** {@code INVOKER}, preceded by the {@link SecurityClause}. */
+ SECURITY_INVOKER("INVOKER"),
+ /** PostgreSQL {@code PARALLEL UNSAFE}. */
+ PARALLEL_UNSAFE("PARALLEL UNSAFE"),
+ /** PostgreSQL {@code PARALLEL RESTRICTED}. */
+ PARALLEL_RESTRICTED("PARALLEL RESTRICTED"),
+ /** PostgreSQL {@code PARALLEL SAFE}. */
+ PARALLEL_SAFE("PARALLEL SAFE"),
+ /** PostgreSQL {@code COST} followed by the numeric {@link #getValue()}. */
+ COST("COST"),
+ /** PostgreSQL {@code ROWS} followed by the numeric {@link #getValue()}. */
+ ROWS("ROWS"),
+ /** PostgreSQL {@code SUPPORT} followed by the function name in {@link #getValue()}. */
+ SUPPORT("SUPPORT"),
+ /** PostgreSQL {@code SET} or {@code RESET}, see {@link #getParameterChange()}. */
+ CONFIGURATION_PARAMETER(null),
+ /** MySQL {@code COMMENT} followed by the string literal of {@link #getComment()}. */
+ COMMENT("COMMENT"),
+ /** MySQL {@code LANGUAGE SQL}. */
+ LANGUAGE_SQL("LANGUAGE SQL"),
+ /** MySQL {@code CONTAINS SQL}. */
+ CONTAINS_SQL("CONTAINS SQL"),
+ /** MySQL {@code NO SQL}. */
+ NO_SQL("NO SQL"),
+ /** MySQL {@code READS SQL DATA}. */
+ READS_SQL_DATA("READS SQL DATA"),
+ /** MySQL {@code MODIFIES SQL DATA}. */
+ MODIFIES_SQL_DATA("MODIFIES SQL DATA");
+
+ private final String keywords;
+
+ Kind(String keywords) {
+ this.keywords = keywords;
+ }
+
+ private boolean hasValue() {
+ return this == COST || this == ROWS || this == SUPPORT || this == COMMENT;
+ }
+ }
+
+ /** The words before {@code DEFINER} or {@code INVOKER}. */
+ public enum SecurityClause {
+ /** PostgreSQL {@code SECURITY}. */
+ SECURITY("SECURITY"),
+ /** PostgreSQL {@code EXTERNAL SECURITY}; {@code EXTERNAL} has no effect. */
+ EXTERNAL_SECURITY("EXTERNAL SECURITY"),
+ /** MySQL {@code SQL SECURITY}. */
+ SQL_SECURITY("SQL SECURITY");
+
+ private final String keywords;
+
+ SecurityClause(String keywords) {
+ this.keywords = keywords;
+ }
+ }
+
+ private Kind kind;
+ private String value;
+ private StringValue comment;
+ private SecurityClause securityClause;
+ private ConfigurationParameterChange parameterChange;
+
+ public RoutineCharacteristic(Kind kind) {
+ this.kind = kind;
+ }
+
+ public RoutineCharacteristic(Kind kind, String value) {
+ this.kind = kind;
+ setValue(value);
+ }
+
+ public RoutineCharacteristic(ConfigurationParameterChange parameterChange) {
+ this.kind = Kind.CONFIGURATION_PARAMETER;
+ this.parameterChange = parameterChange;
+ }
+
+ /**
+ * Creates a MySQL {@code COMMENT} characteristic.
+ *
+ * @param comment the string literal
+ * @return the characteristic
+ */
+ public static RoutineCharacteristic comment(StringValue comment) {
+ return new RoutineCharacteristic(Kind.COMMENT).withComment(comment);
+ }
+
+ public Kind getKind() {
+ return kind;
+ }
+
+ public void setKind(Kind kind) {
+ this.kind = kind;
+ }
+
+ /**
+ * The SQL text after the keyword of {@link Kind#COST}, {@link Kind#ROWS}, {@link Kind#SUPPORT}
+ * and {@link Kind#COMMENT}. For {@code COMMENT} it is derived from {@link #getComment()}, such
+ * as {@code 'it\'s'}.
+ *
+ * @return the value as written, or {@code null} for other kinds
+ */
+ public String getValue() {
+ if (kind == Kind.COMMENT) {
+ return comment == null ? null : comment.toString();
+ }
+ return value;
+ }
+
+ /**
+ * Sets the SQL text of the value. For {@link Kind#COMMENT} it replaces {@link #getComment()}
+ * with a literal read without dialect escaping; use {@link #setComment(StringValue)} to keep
+ * MySQL backslash escapes.
+ *
+ * @param value the SQL text
+ */
+ public void setValue(String value) {
+ if (kind == Kind.COMMENT) {
+ comment = value == null ? null : new StringValue(value);
+ } else {
+ this.value = value;
+ }
+ }
+
+ /**
+ * The string literal of {@link Kind#COMMENT}. {@link StringValue#getNotExcapedValue()} returns
+ * the text that MySQL stores.
+ *
+ * @return the literal, or {@code null} for other kinds
+ */
+ public StringValue getComment() {
+ return comment;
+ }
+
+ public void setComment(StringValue comment) {
+ this.comment = comment;
+ }
+
+ public RoutineCharacteristic withComment(StringValue comment) {
+ setComment(comment);
+ return this;
+ }
+
+ /**
+ * The spelling of {@link Kind#SECURITY_DEFINER} and {@link Kind#SECURITY_INVOKER}; {@code null}
+ * renders {@link SecurityClause#SECURITY}.
+ *
+ * @return the clause, or {@code null}
+ */
+ public SecurityClause getSecurityClause() {
+ return securityClause;
+ }
+
+ public void setSecurityClause(SecurityClause securityClause) {
+ this.securityClause = securityClause;
+ }
+
+ /**
+ * The change of {@link Kind#CONFIGURATION_PARAMETER}.
+ *
+ * @return the change, or {@code null} for other kinds
+ */
+ public ConfigurationParameterChange getParameterChange() {
+ return parameterChange;
+ }
+
+ public void setParameterChange(ConfigurationParameterChange parameterChange) {
+ this.parameterChange = parameterChange;
+ }
+
+ public RoutineCharacteristic withValue(String value) {
+ setValue(value);
+ return this;
+ }
+
+ public RoutineCharacteristic withSecurityClause(SecurityClause securityClause) {
+ setSecurityClause(securityClause);
+ return this;
+ }
+
+ /**
+ * Appends this characteristic to a builder.
+ *
+ * @param builder the target
+ * @param valueRenderer appends each configuration parameter value
+ * @return the builder
+ */
+ public StringBuilder appendTo(StringBuilder builder, Consumer valueRenderer) {
+ switch (kind) {
+ case CONFIGURATION_PARAMETER:
+ if (parameterChange == null) {
+ throw new IllegalStateException("Expected a configuration parameter change");
+ }
+ return parameterChange.appendTo(builder, valueRenderer);
+ case SECURITY_DEFINER:
+ case SECURITY_INVOKER:
+ return builder
+ .append((securityClause == null ? SecurityClause.SECURITY
+ : securityClause).keywords)
+ .append(' ').append(kind.keywords);
+ default:
+ builder.append(kind.keywords);
+ return kind.hasValue() ? builder.append(' ').append(getValue()) : builder;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ return appendTo(builder, builder::append).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
index e0a53b9e6a..c2f5a0479c 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
@@ -26,6 +26,8 @@
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterCollation;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.alter.AlterDomain;
import net.sf.jsqlparser.statement.alter.AlterExtension;
import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper;
@@ -1187,6 +1189,16 @@ public Void visit(AlterCollation statement, S context) {
return schemaOnly();
}
+ @Override
+ public Void visit(AlterRoutine statement, S context) {
+ return schemaOnly();
+ }
+
+ @Override
+ public Void visit(AlterTrigger statement, S context) {
+ return schemaOnly();
+ }
+
@Override
public Void visit(AlterDatabase statement, S context) {
return schemaOnly();
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
index 6da8f9bea6..5699579431 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
@@ -23,6 +23,8 @@
import net.sf.jsqlparser.statement.notify.NotifyStatement;
import net.sf.jsqlparser.statement.create.collation.CreateCollation;
import net.sf.jsqlparser.statement.alter.AlterCollation;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.alter.AlterPolicy;
import net.sf.jsqlparser.statement.drop.DropPolicy;
import net.sf.jsqlparser.statement.create.statistics.CreateStatistics;
@@ -884,4 +886,20 @@ default T visit(AlterCollation statement, S context) {
default void visit(AlterCollation statement) {
visit(statement, null);
}
+
+ default T visit(AlterRoutine statement, S context) {
+ return null;
+ }
+
+ default void visit(AlterRoutine statement) {
+ visit(statement, null);
+ }
+
+ default T visit(AlterTrigger statement, S context) {
+ return null;
+ }
+
+ default void visit(AlterTrigger statement) {
+ visit(statement, null);
+ }
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
index a5284bd670..c6a0092f1b 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
@@ -11,6 +11,8 @@
import net.sf.jsqlparser.statement.alter.AlterRelation;
import net.sf.jsqlparser.statement.alter.AlterPolicy;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.drop.DropPolicy;
import net.sf.jsqlparser.statement.create.statistics.CreateStatistics;
import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper;
@@ -1021,4 +1023,16 @@ public T visit(CreateCollation statement, S context) {
return null;
}
+ @Override
+ public T visit(AlterRoutine statement, S context) {
+ statement.visitExpressions(expression -> expression.accept(expressionVisitor, context));
+ return null;
+ }
+
+ @Override
+ public T visit(AlterTrigger statement, S context) {
+ fromItemVisitor.visitFromItem(statement.getTable(), context);
+ return null;
+ }
+
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterRoutine.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterRoutine.java
new file mode 100644
index 0000000000..66f1717cc3
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterRoutine.java
@@ -0,0 +1,244 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.alter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.statement.RoutineCharacteristic;
+import net.sf.jsqlparser.statement.RoutineReference;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+
+/**
+ * PostgreSQL {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE}, or MySQL
+ * {@code ALTER FUNCTION} or {@code ALTER PROCEDURE}. Both change characteristics through the shared
+ * {@link RoutineCharacteristic} model. PostgreSQL can instead rename the routine, change its owner
+ * or schema, or mark it as dependent on an extension. Names retain SQL identifier quoting. SQL
+ * Server's routine redefinitions remain
+ * {@link net.sf.jsqlparser.statement.CreateFunctionalStatement} nodes.
+ */
+public class AlterRoutine implements Statement {
+
+ public enum RoutineType {
+ FUNCTION, PROCEDURE, ROUTINE
+ }
+
+ public enum Action {
+ /** Characteristics, see {@link #getCharacteristics()} and {@link #isRestrict()}. */
+ CHARACTERISTICS,
+ /** {@code RENAME TO}, see {@link #getNewName()}. */
+ RENAME,
+ /** {@code OWNER TO}, see {@link #getOwner()}. */
+ OWNER,
+ /** {@code SET SCHEMA}, see {@link #getSchemaName()}. */
+ SET_SCHEMA,
+ /**
+ * {@code [NO] DEPENDS ON EXTENSION}, see {@link #getExtensionName()} and
+ * {@link #isNoDependency()}.
+ */
+ DEPENDS_ON_EXTENSION
+ }
+
+ private RoutineType routineType = RoutineType.FUNCTION;
+ private RoutineReference routine;
+ private Action action = Action.CHARACTERISTICS;
+ private final List characteristics = new ArrayList<>();
+ private boolean restrict;
+ private String newName;
+ private String owner;
+ private String schemaName;
+ private String extensionName;
+ private boolean noDependency;
+
+ public RoutineType getRoutineType() {
+ return routineType;
+ }
+
+ public void setRoutineType(RoutineType routineType) {
+ this.routineType = routineType;
+ }
+
+ /**
+ * The routine name and, for PostgreSQL, its optional argument types.
+ *
+ * @return the routine
+ */
+ public RoutineReference getRoutine() {
+ return routine;
+ }
+
+ public void setRoutine(RoutineReference routine) {
+ this.routine = routine;
+ }
+
+ /**
+ * The rendered alteration. Payloads of other actions are retained for a later switch back.
+ *
+ * @return the action, {@link Action#CHARACTERISTICS} unless changed
+ */
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = action;
+ }
+
+ /**
+ * The characteristics of {@link Action#CHARACTERISTICS}, as a mutable list.
+ *
+ * @return the characteristics in source order
+ */
+ public List getCharacteristics() {
+ return characteristics;
+ }
+
+ public Optional getCharacteristic(RoutineCharacteristic.Kind kind) {
+ return characteristics.stream().filter(characteristic -> characteristic.getKind() == kind)
+ .findFirst();
+ }
+
+ /**
+ * Whether PostgreSQL's {@code RESTRICT}, which has no effect, follows the characteristics.
+ *
+ * @return {@code true} if {@code RESTRICT} is rendered
+ */
+ public boolean isRestrict() {
+ return restrict;
+ }
+
+ public void setRestrict(boolean restrict) {
+ this.restrict = restrict;
+ }
+
+ public String getNewName() {
+ return newName;
+ }
+
+ public void setNewName(String newName) {
+ this.newName = newName;
+ }
+
+ /**
+ * The new owner, a role name or {@code CURRENT_ROLE}, {@code CURRENT_USER} or
+ * {@code SESSION_USER}.
+ *
+ * @return the owner
+ */
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getSchemaName() {
+ return schemaName;
+ }
+
+ public void setSchemaName(String schemaName) {
+ this.schemaName = schemaName;
+ }
+
+ public String getExtensionName() {
+ return extensionName;
+ }
+
+ public void setExtensionName(String extensionName) {
+ this.extensionName = extensionName;
+ }
+
+ /**
+ * Whether {@code NO DEPENDS ON EXTENSION} removes the dependency.
+ *
+ * @return {@code true} for {@code NO DEPENDS}
+ */
+ public boolean isNoDependency() {
+ return noDependency;
+ }
+
+ public void setNoDependency(boolean noDependency) {
+ this.noDependency = noDependency;
+ }
+
+ public AlterRoutine withRoutineType(RoutineType routineType) {
+ setRoutineType(routineType);
+ return this;
+ }
+
+ public AlterRoutine withRoutine(RoutineReference routine) {
+ setRoutine(routine);
+ return this;
+ }
+
+ public AlterRoutine withAction(Action action) {
+ setAction(action);
+ return this;
+ }
+
+ /**
+ * Visits the configuration parameter values of the characteristics.
+ *
+ * @param visitor receives each value
+ */
+ public void visitExpressions(Consumer visitor) {
+ for (RoutineCharacteristic characteristic : characteristics) {
+ if (characteristic.getParameterChange() != null
+ && characteristic.getParameterChange().getValues() != null) {
+ characteristic.getParameterChange().getValues().forEach(visitor);
+ }
+ }
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ return appendTo(builder, builder::append);
+ }
+
+ /**
+ * Appends this statement to a builder.
+ *
+ * @param builder the target
+ * @param valueRenderer appends each configuration parameter value
+ * @return the builder
+ */
+ public StringBuilder appendTo(StringBuilder builder, Consumer valueRenderer) {
+ builder.append("ALTER ").append(routineType).append(' ').append(routine);
+ switch (action) {
+ case RENAME:
+ return builder.append(" RENAME TO ").append(newName);
+ case OWNER:
+ return builder.append(" OWNER TO ").append(owner);
+ case SET_SCHEMA:
+ return builder.append(" SET SCHEMA ").append(schemaName);
+ case DEPENDS_ON_EXTENSION:
+ return builder.append(noDependency ? " NO DEPENDS" : " DEPENDS")
+ .append(" ON EXTENSION ").append(extensionName);
+ default:
+ for (RoutineCharacteristic characteristic : characteristics) {
+ characteristic.appendTo(builder.append(' '), valueRenderer);
+ }
+ return restrict ? builder.append(" RESTRICT") : builder;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterTrigger.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTrigger.java
new file mode 100644
index 0000000000..de3be507e7
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTrigger.java
@@ -0,0 +1,135 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.alter;
+
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+
+/**
+ * PostgreSQL {@code ALTER TRIGGER name ON table}, which renames the trigger or marks it as
+ * dependent on an extension. Names retain SQL identifier quoting.
+ */
+public class AlterTrigger implements Statement {
+
+ public enum Action {
+ /** {@code RENAME TO}, see {@link #getNewName()}. */
+ RENAME,
+ /**
+ * {@code [NO] DEPENDS ON EXTENSION}, see {@link #getExtensionName()} and
+ * {@link #isNoDependency()}.
+ */
+ DEPENDS_ON_EXTENSION
+ }
+
+ private String name;
+ private Table table;
+ private Action action = Action.RENAME;
+ private String newName;
+ private String extensionName;
+ private boolean noDependency;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * The table, view or foreign table that owns the trigger.
+ *
+ * @return the table
+ */
+ public Table getTable() {
+ return table;
+ }
+
+ public void setTable(Table table) {
+ this.table = table;
+ }
+
+ /**
+ * The rendered alteration. Payloads of the other action are retained for a later switch back.
+ *
+ * @return the action, {@link Action#RENAME} unless changed
+ */
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = action;
+ }
+
+ public String getNewName() {
+ return newName;
+ }
+
+ public void setNewName(String newName) {
+ this.newName = newName;
+ }
+
+ public String getExtensionName() {
+ return extensionName;
+ }
+
+ public void setExtensionName(String extensionName) {
+ this.extensionName = extensionName;
+ }
+
+ /**
+ * Whether {@code NO DEPENDS ON EXTENSION} removes the dependency.
+ *
+ * @return {@code true} for {@code NO DEPENDS}
+ */
+ public boolean isNoDependency() {
+ return noDependency;
+ }
+
+ public void setNoDependency(boolean noDependency) {
+ this.noDependency = noDependency;
+ }
+
+ public AlterTrigger withName(String name) {
+ setName(name);
+ return this;
+ }
+
+ public AlterTrigger withTable(Table table) {
+ setTable(table);
+ return this;
+ }
+
+ public AlterTrigger withAction(Action action) {
+ setAction(action);
+ return this;
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append("ALTER TRIGGER ").append(name).append(" ON ").append(table);
+ if (action == Action.DEPENDS_ON_EXTENSION) {
+ return builder.append(noDependency ? " NO DEPENDS" : " DEPENDS")
+ .append(" ON EXTENSION ").append(extensionName);
+ }
+ return builder.append(" RENAME TO ").append(newName);
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java
index 4c3b8f3f80..ce48fdf0d1 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java
@@ -46,6 +46,7 @@ public enum ExecuteKeyword {
private Table trigger;
private Timing timing;
private boolean orReplace;
+ private boolean ifNotExists;
private boolean constraint;
private Table referencedTable;
private Orientation orientation;
@@ -76,6 +77,19 @@ public void setOrReplace(boolean orReplace) {
this.orReplace = orReplace;
}
+ /**
+ * Whether MySQL's {@code IF NOT EXISTS} follows {@code TRIGGER}.
+ *
+ * @return {@code true} if {@code IF NOT EXISTS} is rendered
+ */
+ public boolean isIfNotExists() {
+ return ifNotExists;
+ }
+
+ public void setIfNotExists(boolean ifNotExists) {
+ this.ifNotExists = ifNotExists;
+ }
+
public boolean isConstraint() {
return constraint;
}
@@ -207,6 +221,14 @@ public void setOtherTrigger(Table otherTrigger) {
this.otherTrigger = otherTrigger;
}
+ /**
+ * The MySQL trigger statement: a single statement, a {@link net.sf.jsqlparser.statement.Block}
+ * of statements, or an {@link net.sf.jsqlparser.statement.UnsupportedStatement} holding the
+ * tokens of a compound statement with procedural statements, such as {@code IF} or
+ * {@code DECLARE}.
+ *
+ * @return the body
+ */
public Statement getBody() {
return body;
}
@@ -307,10 +329,17 @@ public String toString() {
private String mysqlSql() {
StringBuilder builder = new StringBuilder("CREATE ");
+ if (orReplace) {
+ builder.append("OR REPLACE ");
+ }
if (definer != null) {
builder.append("DEFINER = ").append(definer).append(" ");
}
- builder.append("TRIGGER ").append(trigger).append(" ").append(timing).append(" ")
+ builder.append("TRIGGER ");
+ if (ifNotExists) {
+ builder.append("IF NOT EXISTS ");
+ }
+ builder.append(trigger).append(" ").append(timing).append(" ")
.append(getEvent()).append(" ON ").append(table).append(" FOR EACH ROW ");
if (order != null) {
builder.append(order).append(" ").append(otherTrigger).append(" ");
diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
index 97e9c83aa9..229ed07a5d 100644
--- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
+++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
@@ -112,6 +112,8 @@
import net.sf.jsqlparser.statement.UseStatement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterCollation;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.alter.AlterDomain;
import net.sf.jsqlparser.statement.alter.AlterExtension;
import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper;
@@ -3174,4 +3176,16 @@ public Void visit(CreateCollation statement, S context) {
public Void visit(AlterCollation statement, S context) {
return null;
}
+
+ @Override
+ public Void visit(AlterRoutine statement, S context) {
+ statement.visitExpressions(expression -> expression.accept(this, context));
+ return null;
+ }
+
+ @Override
+ public Void visit(AlterTrigger statement, S context) {
+ visit(statement.getTable(), context);
+ return null;
+ }
}
diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
index cb6db673ea..0d7c6a356f 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
@@ -23,6 +23,8 @@
import net.sf.jsqlparser.statement.notify.NotifyStatement;
import net.sf.jsqlparser.statement.create.collation.CreateCollation;
import net.sf.jsqlparser.statement.alter.AlterCollation;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.alter.AlterPolicy;
import net.sf.jsqlparser.statement.drop.DropPolicy;
import net.sf.jsqlparser.statement.create.statistics.CreateStatistics;
@@ -950,4 +952,15 @@ public StringBuilder visit(CreateCollation statement, S context) {
public StringBuilder visit(AlterCollation statement, S context) {
return statement.appendTo(builder);
}
+
+ @Override
+ public StringBuilder visit(AlterRoutine statement, S context) {
+ return statement.appendTo(builder,
+ expression -> expression.accept(expressionDeParser, context));
+ }
+
+ @Override
+ public StringBuilder visit(AlterTrigger statement, S context) {
+ return statement.appendTo(builder);
+ }
}
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java
index b9bfba466b..f8b709f2be 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java
@@ -120,6 +120,10 @@ public enum MariaDbVersion implements Version {
// https://mariadb.com/kb/en/alter-database/
Feature.createDatabase, Feature.alterDatabase,
+ // https://mariadb.com/kb/en/alter-function/
+ // https://mariadb.com/kb/en/alter-procedure/
+ Feature.alterFunction, Feature.alterProcedure,
+
// https://mariadb.com/kb/en/describe/
Feature.describe, // https://mariadb.com/kb/en/explain/
Feature.explain, // https://mariadb.com/kb/en/show/
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java
index 270fbbf35c..a8bf66abb1 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java
@@ -97,6 +97,10 @@ public enum MySqlVersion implements Version {
// https://dev.mysql.com/doc/refman/8.0/en/alter-database.html
Feature.createDatabase, Feature.alterDatabase,
+ // https://dev.mysql.com/doc/refman/8.0/en/alter-function.html
+ // https://dev.mysql.com/doc/refman/8.0/en/alter-procedure.html
+ Feature.alterFunction, Feature.alterProcedure,
+
// https://dev.mysql.com/doc/refman/8.0/en/create-database.html
Feature.createSchema, // https://dev.mysql.com/doc/refman/8.0/en/create-view.html
Feature.createView,
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java
index bab59a7691..ba07fd180a 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java
@@ -124,6 +124,13 @@ public enum PostgresqlVersion implements Version {
// https://www.postgresql.org/docs/current/sql-alterdatabase.html
Feature.createDatabase, Feature.alterDatabase,
+ // https://www.postgresql.org/docs/current/sql-alterfunction.html
+ // https://www.postgresql.org/docs/current/sql-alterprocedure.html
+ // https://www.postgresql.org/docs/current/sql-alterroutine.html
+ // https://www.postgresql.org/docs/current/sql-altertrigger.html
+ Feature.alterFunction, Feature.alterProcedure, Feature.alterRoutine,
+ Feature.alterTrigger,
+
// https://www.postgresql.org/docs/16/sql-refreshmaterializedview.html
Feature.refreshMaterializedView,
Feature.refreshMaterializedWithNoDataView,
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java
index 3b6b7f0b73..986a295c7a 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java
@@ -28,6 +28,8 @@
import net.sf.jsqlparser.statement.notify.NotifyStatement;
import net.sf.jsqlparser.statement.create.collation.CreateCollation;
import net.sf.jsqlparser.statement.alter.AlterCollation;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
import net.sf.jsqlparser.statement.oracle.OracleBlock;
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
@@ -1208,6 +1210,29 @@ public Void visit(AlterCollation statement, S context) {
return null;
}
+ @Override
+ public Void visit(AlterRoutine statement, S context) {
+ switch (statement.getRoutineType()) {
+ case PROCEDURE:
+ validateFeature(Feature.alterProcedure);
+ break;
+ case ROUTINE:
+ validateFeature(Feature.alterRoutine);
+ break;
+ default:
+ validateFeature(Feature.alterFunction);
+ }
+ statement.visitExpressions(this::validateOptionalExpression);
+ return null;
+ }
+
+ @Override
+ public Void visit(AlterTrigger statement, S context) {
+ validateFeature(Feature.alterTrigger);
+ validateOptionalFromItem(statement.getTable());
+ return null;
+ }
+
@Override
public Void visit(AlterRelation statement, S context) {
boolean index = statement.getObjectType() == AlterRelation.ObjectType.INDEX;
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index f49b3ddbae..ff7a619622 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -320,6 +320,193 @@ public class CCJSqlParser extends AbstractJSqlParser {
"ALLOW_CONNECTIONS", "IS_TEMPLATE", "OID").contains(name);
}
+ /** Whether the words, matched without case, start at the offset. */
+ private boolean isWordSequenceAhead(int offset, String... words) {
+ for (int i = 0; i < words.length; i++) {
+ Token token = getToken(offset + i);
+ if (token.kind == EOF || !words[i].equalsIgnoreCase(token.image)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isSignedNumberAhead(int offset) {
+ Token token = getToken(offset);
+ if ("+".equals(token.image) || "-".equals(token.image)) {
+ token = getToken(offset + 1);
+ }
+ return token.kind == S_LONG || token.kind == S_DOUBLE;
+ }
+
+ /**
+ * Whether ALTER FUNCTION, PROCEDURE or ROUTINE has a PostgreSQL or MySQL alteration after the
+ * routine name and optional signature. Other forms, such as SQL Server routine definitions,
+ * remain unsupported statements.
+ */
+ private boolean isAlterRoutineAhead() {
+ Token token = getToken(1);
+ if (token.kind != K_FUNCTION && token.kind != K_PROCEDURE && !isKeywordAhead("ROUTINE")) {
+ return false;
+ }
+ int offset = 3;
+ while (".".equals(getToken(offset).image)) {
+ offset += 2;
+ }
+ if (getToken(offset).kind == OPENING_BRACKET) {
+ for (int depth = 0; ; offset++) {
+ int kind = getToken(offset).kind;
+ if (kind == EOF || kind == ST_SEMICOLON) {
+ return false;
+ }
+ depth += kind == OPENING_BRACKET ? 1 : kind == CLOSING_BRACKET ? -1 : 0;
+ if (depth == 0) {
+ offset++;
+ break;
+ }
+ }
+ }
+ Token next = getToken(offset + 1);
+ return getToken(offset).kind == K_RENAME && next.kind == K_TO
+ || isWordSequenceAhead(offset, "OWNER", "TO")
+ || getToken(offset).kind == K_SET && next.kind == K_SCHEMA
+ || isExtensionDependencyAhead(offset)
+ || isPostgreSqlRoutineCharacteristicAhead(offset)
+ || isMySqlRoutineCharacteristicAhead(offset);
+ }
+
+ /** Whether PostgreSQL's [NO] DEPENDS ON EXTENSION starts at the offset. */
+ private boolean isExtensionDependencyAhead(int offset) {
+ return isWordSequenceAhead(getToken(offset).kind == K_NO ? offset + 1 : offset,
+ "DEPENDS", "ON", "EXTENSION");
+ }
+
+ /**
+ * Whether a PostgreSQL characteristic of ALTER FUNCTION, PROCEDURE or ROUTINE starts at the
+ * offset. SET without TO, = or FROM after the parameter name is SET SCHEMA instead.
+ */
+ private boolean isPostgreSqlRoutineCharacteristicAhead(int offset) {
+ Token token = getToken(offset);
+ switch (token.kind) {
+ case K_SET:
+ int nameEnd = offset + 2;
+ while (".".equals(getToken(nameEnd).image)) {
+ nameEnd += 2;
+ }
+ Token assignment = getToken(nameEnd);
+ return assignment.kind == K_TO || "=".equals(assignment.image)
+ || assignment.kind == K_FROM;
+ case K_RESET:
+ case K_VOLATILE:
+ case K_STRICT:
+ return true;
+ case K_ROWS:
+ return isSignedNumberAhead(offset + 1);
+ case K_NOT:
+ return isWordSequenceAhead(offset + 1, "LEAKPROOF");
+ case K_RETURNS:
+ return isWordSequenceAhead(offset + 1, "NULL", "ON", "NULL", "INPUT");
+ case K_PARALLEL:
+ return isWordSequenceAhead(offset + 1, "UNSAFE")
+ || isWordSequenceAhead(offset + 1, "RESTRICTED")
+ || isWordSequenceAhead(offset + 1, "SAFE");
+ case K_SECURITY:
+ return isRoutineSecurityAhead(offset + 1);
+ case S_IDENTIFIER:
+ break;
+ default:
+ return false;
+ }
+ switch (token.image.toUpperCase(Locale.ROOT)) {
+ case "IMMUTABLE":
+ case "STABLE":
+ case "LEAKPROOF":
+ return true;
+ case "CALLED":
+ return isWordSequenceAhead(offset + 1, "ON", "NULL", "INPUT");
+ case "EXTERNAL":
+ return getToken(offset + 1).kind == K_SECURITY && isRoutineSecurityAhead(offset + 2);
+ case "COST":
+ return isSignedNumberAhead(offset + 1);
+ case "SUPPORT":
+ int kind = getToken(offset + 1).kind;
+ return kind != EOF && kind != ST_SEMICOLON;
+ default:
+ return false;
+ }
+ }
+
+ /** Whether a MySQL characteristic of ALTER FUNCTION or PROCEDURE starts at the offset. */
+ private boolean isMySqlRoutineCharacteristicAhead(int offset) {
+ if (getToken(offset).kind == K_COMMENT) {
+ return getToken(offset + 1).kind == S_CHAR_LITERAL;
+ }
+ return isWordSequenceAhead(offset, "LANGUAGE", "SQL")
+ || isWordSequenceAhead(offset, "CONTAINS", "SQL")
+ || isWordSequenceAhead(offset, "NO", "SQL")
+ || isWordSequenceAhead(offset, "READS", "SQL", "DATA")
+ || isWordSequenceAhead(offset, "MODIFIES", "SQL", "DATA")
+ || isWordSequenceAhead(offset, "SQL", "SECURITY") && isRoutineSecurityAhead(offset + 2);
+ }
+
+ private boolean isRoutineSecurityAhead(int offset) {
+ return isWordSequenceAhead(offset, "DEFINER") || isWordSequenceAhead(offset, "INVOKER");
+ }
+
+ /**
+ * Whether a MySQL compound statement starts here: BEGIN ... END or a flow control statement,
+ * where BEGIN, LOOP, REPEAT and WHILE may have a label.
+ */
+ private boolean isMySqlCompoundStatementAhead() {
+ boolean labeled = getToken(1).kind == S_IDENTIFIER && ":".equals(getToken(2).image);
+ Token token = getToken(labeled ? 3 : 1);
+ if (token.kind == K_BEGIN || token.kind == K_LOOP
+ || isWordSequenceAhead(labeled ? 3 : 1, "REPEAT")
+ || isWordSequenceAhead(labeled ? 3 : 1, "WHILE")) {
+ return true;
+ }
+ return !labeled && (token.kind == K_IF || token.kind == K_CASE);
+ }
+
+ /**
+ * Whether BEGIN ... END holds only SQL statements, each ending with a semicolon, and ends the
+ * enclosing statement. CASE ... END inside a statement is an expression.
+ */
+ private boolean isStatementBlockAhead() {
+ if (getToken(1).kind != K_BEGIN) {
+ return false;
+ }
+ boolean statementStart = true;
+ boolean statements = false;
+ int caseDepth = 0;
+ for (int i = 2; ; i++) {
+ Token token = getToken(i);
+ if (token.kind == EOF) {
+ return false;
+ } else if (token.kind == ST_SEMICOLON) {
+ statementStart = true;
+ } else if (statementStart) {
+ if (token.kind == K_END) {
+ int next = getToken(i + 1).kind;
+ return statements && (next == ST_SEMICOLON || next == EOF);
+ }
+ switch (token.kind) {
+ case K_SELECT: case K_INSERT: case K_UPDATE: case K_DELETE: case K_REPLACE:
+ case K_SET: case K_CALL: case K_WITH:
+ break;
+ default:
+ return false;
+ }
+ statementStart = false;
+ statements = true;
+ } else if (token.kind == K_CASE) {
+ caseDepth++;
+ } else if (token.kind == K_END && caseDepth-- == 0) {
+ return false;
+ }
+ }
+ }
+
private static class ObjectNames {
private final List names;
@@ -4192,6 +4379,18 @@ DoStatement DoStatement():
}
Block Block() #Block : {
+ Block block;
+}
+{
+ block = BeginEndBlock()
+ [LOOKAHEAD(2) { block.setSemicolonAfterEnd(true); } ]
+ {
+ return block;
+ }
+}
+
+/** BEGIN, statements and END; unlike Block(), it leaves a following semicolon to the caller. */
+Block BeginEndBlock(): {
Statements stmts = new Statements();
List list = new ArrayList();
Statement stm;
@@ -4223,7 +4422,7 @@ Block Block() #Block : {
block.setStatements(stmts);
}
- [LOOKAHEAD(2) { block.setSemicolonAfterEnd(true); } ]
+
{
return block;
}
@@ -15897,6 +16096,10 @@ TriggerDefiner MySqlTriggerDefiner():
{ TriggerDefiner definer = new TriggerDefiner(); }
{ MySqlDefiner(definer) { return definer; } }
+MySqlDefiner MySqlRoutineDefiner():
+{ MySqlDefiner definer = new MySqlDefiner(); }
+{ MySqlDefiner(definer) { return definer; } }
+
/** MySQL fixes the prefix order: ALGORITHM, DEFINER, SQL SECURITY. */
MySqlViewOptions MySqlViewPrefix():
{
@@ -15952,7 +16155,58 @@ CreateTrigger.Order MySqlTriggerOrder():
{ return order; }
}
-CreateTrigger CreateTrigger():
+/**
+ * MySQL trigger statement. BEGIN ... END with SQL statements only remains a Block. Other compound
+ * statements, such as those with IF or DECLARE, keep their tokens like routine bodies and use the
+ * same boundary tracking. The semicolon after END is left to separate statements.
+ */
+Statement MySqlTriggerBody():
+{
+ Statement body;
+ List tokens;
+}
+{
+ (
+ LOOKAHEAD({ isStatementBlockAhead() }) body=BeginEndBlock()
+ |
+ LOOKAHEAD({ isMySqlCompoundStatementAhead() }) tokens=captureCompoundStatement()
+ { body = new UnsupportedStatement(tokens); }
+ |
+ body=SingleStatement()
+ )
+ { return body; }
+}
+
+/**
+ * Captures a compound statement through its END and optional end label. Qualified names are
+ * joined, and a semicolon or label colon is attached to the preceding token.
+ */
+JAVACODE
+List captureCompoundStatement() {
+ List tokens = new LinkedList();
+ RoutineBodyBoundary boundary = RoutineBodyBoundary.forCompoundStatement();
+ boolean afterDot = false;
+ Token tok;
+ while (true) {
+ tok = getToken(1);
+ if (boundary.endsAt(tok)) {
+ break;
+ }
+ int last = tokens.size() - 1;
+ if (last >= 0 && (afterDot || ".".equals(tok.image) || ":".equals(tok.image)
+ || tok.kind == ST_SEMICOLON)) {
+ tokens.set(last, tokens.get(last) + tok.image);
+ } else {
+ tokens.add(tok.image);
+ }
+ afterDot = ".".equals(tok.image);
+ boundary.accept(tok, getToken(2));
+ getNextToken();
+ }
+ return tokens;
+}
+
+CreateTrigger CreateTrigger(boolean orReplace):
{
CreateTrigger createTrigger = new CreateTrigger();
TriggerDefiner definer = null;
@@ -15967,7 +16221,14 @@ CreateTrigger CreateTrigger():
{
[ LOOKAHEAD({ "DEFINER".equalsIgnoreCase(getToken(1).image) })
definer=MySqlTriggerDefiner() { createTrigger.setDefiner(definer); } ]
- trigger=Table() { createTrigger.setTrigger(trigger); }
+
+ [ LOOKAHEAD(2) { createTrigger.setIfNotExists(true); } ]
+ {
+ requireDdlSyntax(!orReplace || !createTrigger.isIfNotExists(),
+ "OR REPLACE and IF NOT EXISTS cannot be combined");
+ createTrigger.setOrReplace(orReplace);
+ }
+ trigger=Table() { createTrigger.setTrigger(trigger); }
timing=MySqlTriggerTiming() { createTrigger.setTiming(timing); }
( event= { createTrigger.setEvent(CreateTrigger.Event.INSERT); }
| event= { createTrigger.setEvent(CreateTrigger.Event.UPDATE); }
@@ -15979,7 +16240,7 @@ CreateTrigger CreateTrigger():
createTrigger.setOrder(order);
createTrigger.setOtherTrigger(otherTrigger);
} ]
- ( body=SingleStatement() | body=Block() ) { createTrigger.setBody(body); }
+ body=MySqlTriggerBody() { createTrigger.setBody(body); }
{ return createTrigger; }
}
@@ -19961,6 +20222,11 @@ Statement Alter():
|
LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics()
|
+ // Other routine forms, such as SQL Server definitions, remain unsupported.
+ LOOKAHEAD({ isAlterRoutineAhead() }) statement = AlterRoutine()
+ |
+ LOOKAHEAD(AlterTrigger() ( | )) statement = AlterTrigger()
+ |
LOOKAHEAD() statement = AlterPolicy()
|
LOOKAHEAD({ isKeywordAhead("ROLE") || isKeywordAhead("USER") || isKeywordAhead("GROUP") })
@@ -20821,7 +21087,10 @@ AlterRole AlterRole():
}
}
-/** PostgreSQL SET/RESET of a configuration parameter, shared by ALTER ROLE and ALTER DATABASE. */
+/**
+ * PostgreSQL SET/RESET of a configuration parameter, shared by ALTER ROLE, ALTER DATABASE and
+ * ALTER FUNCTION/PROCEDURE/ROUTINE.
+ */
ConfigurationParameterChange ConfigurationParameterChange():
{
ConfigurationParameterChange change = new ConfigurationParameterChange();
@@ -21036,6 +21305,27 @@ TransitionRelation PostgreSqlTransitionRelation():
[ { result.setUseAs(true); } ] name=RelObjectName() { result.setName(name); return result; }
}
+/** PostgreSQL ALTER TRIGGER name ON table RENAME TO or [NO] DEPENDS ON EXTENSION. */
+AlterTrigger AlterTrigger():
+{
+ AlterTrigger result = new AlterTrigger();
+ String name;
+ Table table;
+}
+{
+ name=RelObjectName() { result.setName(name); }
+ table=Table() { result.setTable(table); }
+ (
+ name=RelObjectName() { result.setNewName(name); }
+ |
+ LOOKAHEAD({ isExtensionDependencyAhead(1) })
+ [ { result.setNoDependency(true); } ]
+ ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName()
+ { result.setAction(AlterTrigger.Action.DEPENDS_ON_EXTENSION); result.setExtensionName(name); }
+ )
+ { return result; }
+}
+
CreateTrigger PostgreSqlTrigger(boolean replace):
{
CreateTrigger result = new CreateTrigger(); String name; Table table; CreateTrigger.Timing timing;
@@ -21351,14 +21641,15 @@ Statement Create():
|
LOOKAHEAD(MySqlViewPrefix() ) statement = CreateView(isUsingOrReplace)
|
- LOOKAHEAD({ getToken(1).kind == K_TRIGGER
- || "DEFINER".equalsIgnoreCase(getToken(1).image) })
- statement = CreateTrigger()
+ LOOKAHEAD([ MySqlTriggerDefiner() ] ,
+ { getToken(1).kind == K_TRIGGER || isKeywordAhead("DEFINER") })
+ statement = CreateTrigger(isUsingOrReplace)
|
LOOKAHEAD({ isCreateMacroAhead() }) statement = CreateMacro(isUsingOrReplace)
|
LOOKAHEAD({ isCreateFunctionMacroAhead() }) statement = CreateMacro(isUsingOrReplace)
|
+ LOOKAHEAD([ MySqlRoutineDefiner() ] ( | ))
statement = CreateFunctionStatement(isUsingOrReplace)
|
statement = CreateSchema()
@@ -21397,8 +21688,10 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
CreateFunctionalStatement type = null;
List tokens = new LinkedList();
String statementType = null;
+ MySqlDefiner definer = null;
}
{
+ [ definer=MySqlRoutineDefiner() ]
(
{ statementType = "FUNCTION"; }
|
@@ -21412,11 +21705,186 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
if(statementType.equals("PROCEDURE")) {
type = new CreateProcedure(isUsingOrReplace, tokens);
}
-
+ type.setDefiner(definer);
return type;
}
}
+/**
+ * PostgreSQL ALTER FUNCTION, PROCEDURE or ROUTINE, or MySQL ALTER FUNCTION or PROCEDURE. One
+ * statement's characteristics belong to one dialect; MySQL's take no argument list.
+ */
+AlterRoutine AlterRoutine():
+{
+ AlterRoutine result = new AlterRoutine();
+ RoutineReference routine;
+ RoutineCharacteristic characteristic;
+ String name;
+}
+{
+ (
+ { result.setRoutineType(AlterRoutine.RoutineType.FUNCTION); }
+ |
+ { result.setRoutineType(AlterRoutine.RoutineType.PROCEDURE); }
+ |
+ ContextualKeyword("ROUTINE") { result.setRoutineType(AlterRoutine.RoutineType.ROUTINE); }
+ )
+ routine=RoutineReference(false) { result.setRoutine(routine); }
+ (
+ name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.RENAME); result.setNewName(name); }
+ |
+ LOOKAHEAD({ isWordSequenceAhead(1, "OWNER", "TO") }) ContextualKeyword("OWNER") name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.OWNER); result.setOwner(name); }
+ |
+ LOOKAHEAD({ getToken(1).kind == K_SET && getToken(2).kind == K_SCHEMA
+ && !isPostgreSqlRoutineCharacteristicAhead(1) })
+ name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.SET_SCHEMA); result.setSchemaName(name); }
+ |
+ LOOKAHEAD({ isExtensionDependencyAhead(1) })
+ [ { result.setNoDependency(true); } ]
+ ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.DEPENDS_ON_EXTENSION); result.setExtensionName(name); }
+ |
+ LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
+ (
+ LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
+ characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
+ )+
+ {
+ requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE
+ && routine.getArguments() == null,
+ "MySQL characteristics require ALTER FUNCTION or PROCEDURE without arguments");
+ }
+ |
+ (
+ LOOKAHEAD({ isPostgreSqlRoutineCharacteristicAhead(1) })
+ characteristic=PostgreSqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
+ )+
+ [ { result.setRestrict(true); } ]
+ )
+ { return result; }
+}
+
+RoutineCharacteristic PostgreSqlRoutineCharacteristic():
+{
+ RoutineCharacteristic.Kind kind = RoutineCharacteristic.Kind.CONFIGURATION_PARAMETER;
+ RoutineCharacteristic.SecurityClause clause = null;
+ ConfigurationParameterChange change = null;
+ String value = null;
+}
+{
+ (
+ change=ConfigurationParameterChange()
+ |
+ LOOKAHEAD({ isKeywordAhead("IMMUTABLE") }) ContextualKeyword("IMMUTABLE")
+ { kind = RoutineCharacteristic.Kind.IMMUTABLE; }
+ |
+ LOOKAHEAD({ isKeywordAhead("STABLE") }) ContextualKeyword("STABLE")
+ { kind = RoutineCharacteristic.Kind.STABLE; }
+ |
+ { kind = RoutineCharacteristic.Kind.VOLATILE; }
+ |
+ LOOKAHEAD({ isKeywordAhead("CALLED") }) ContextualKeyword("CALLED") ContextualKeyword("INPUT")
+ { kind = RoutineCharacteristic.Kind.CALLED_ON_NULL_INPUT; }
+ |
+ ContextualKeyword("INPUT")
+ { kind = RoutineCharacteristic.Kind.RETURNS_NULL_ON_NULL_INPUT; }
+ |
+ { kind = RoutineCharacteristic.Kind.STRICT; }
+ |
+ LOOKAHEAD({ isKeywordAhead("LEAKPROOF") }) ContextualKeyword("LEAKPROOF")
+ { kind = RoutineCharacteristic.Kind.LEAKPROOF; }
+ |
+ ContextualKeyword("LEAKPROOF") { kind = RoutineCharacteristic.Kind.NOT_LEAKPROOF; }
+ |
+ LOOKAHEAD({ isKeywordAhead("EXTERNAL") }) ContextualKeyword("EXTERNAL")
+ kind=RoutineSecurity() { clause = RoutineCharacteristic.SecurityClause.EXTERNAL_SECURITY; }
+ |
+ kind=RoutineSecurity() { clause = RoutineCharacteristic.SecurityClause.SECURITY; }
+ |
+
+ (
+ LOOKAHEAD({ isKeywordAhead("UNSAFE") }) ContextualKeyword("UNSAFE")
+ { kind = RoutineCharacteristic.Kind.PARALLEL_UNSAFE; }
+ |
+ { kind = RoutineCharacteristic.Kind.PARALLEL_RESTRICTED; }
+ |
+ ContextualKeyword("SAFE") { kind = RoutineCharacteristic.Kind.PARALLEL_SAFE; }
+ )
+ |
+ LOOKAHEAD({ isKeywordAhead("COST") }) ContextualKeyword("COST") value=PostgreSqlNumericValue()
+ { kind = RoutineCharacteristic.Kind.COST; }
+ |
+ value=PostgreSqlNumericValue() { kind = RoutineCharacteristic.Kind.ROWS; }
+ |
+ LOOKAHEAD({ isKeywordAhead("SUPPORT") }) ContextualKeyword("SUPPORT") value=AccessQualifiedName()
+ { kind = RoutineCharacteristic.Kind.SUPPORT; }
+ )
+ {
+ return change != null ? new RoutineCharacteristic(change)
+ : new RoutineCharacteristic(kind, value).withSecurityClause(clause);
+ }
+}
+
+RoutineCharacteristic MySqlRoutineCharacteristic():
+{
+ RoutineCharacteristic.Kind kind;
+ RoutineCharacteristic.SecurityClause clause = null;
+ Token token;
+ StringValue comment = null;
+}
+{
+ (
+ token=
+ { kind = RoutineCharacteristic.Kind.COMMENT; comment = stringValue(token); }
+ |
+ LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) ContextualKeyword("LANGUAGE") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.LANGUAGE_SQL; }
+ |
+ LOOKAHEAD({ isKeywordAhead("CONTAINS") }) ContextualKeyword("CONTAINS") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.CONTAINS_SQL; }
+ |
+ ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
+ |
+ LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
+ |
+ LOOKAHEAD({ isKeywordAhead("MODIFIES") }) ContextualKeyword("MODIFIES") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.MODIFIES_SQL_DATA; }
+ |
+ ContextualKeyword("SQL") kind=RoutineSecurity()
+ { clause = RoutineCharacteristic.SecurityClause.SQL_SECURITY; }
+ )
+ {
+ return new RoutineCharacteristic(kind).withComment(comment).withSecurityClause(clause);
+ }
+}
+
+RoutineCharacteristic.Kind RoutineSecurity():
+{ RoutineCharacteristic.Kind kind; }
+{
+ (
+ LOOKAHEAD({ isKeywordAhead("DEFINER") }) ContextualKeyword("DEFINER")
+ { kind = RoutineCharacteristic.Kind.SECURITY_DEFINER; }
+ |
+ ContextualKeyword("INVOKER") { kind = RoutineCharacteristic.Kind.SECURITY_INVOKER; }
+ )
+ { return kind; }
+}
+
+/** PostgreSQL NumericOnly: an optionally signed integer or decimal, as written. */
+String PostgreSqlNumericValue():
+{
+ Token sign = null;
+ Token number;
+}
+{
+ [ sign="+" | sign="-" ] ( number= | number= )
+ { return (sign == null ? "" : sign.image) + number.image; }
+}
+
CreateFunctionalStatement SqlServerRoutine():
{
CreateFunctionalStatement result;
diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst
index 50c52ed5e3..f673ba50cb 100644
--- a/src/site/sphinx/index.rst
+++ b/src/site/sphinx/index.rst
@@ -162,6 +162,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad
- ``ANALYZE`` · ``CHECK`` · ``OPTIMIZE`` · ``REPAIR TABLE``
* - **PostgreSQL RLS**
- ``CREATE POLICY`` · ``ALTER TABLE … ENABLE``/``DISABLE``/``FORCE``/``NO FORCE ROW LEVEL SECURITY``
+ * - **Routine changes**
+ - ``ALTER FUNCTION`` · ``ALTER PROCEDURE`` · ``ALTER ROUTINE`` · PostgreSQL ``ALTER TRIGGER``
* - **Salesforce SOQL**
- ``INCLUDES`` · ``EXCLUDES``
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index fac9650c5a..d6e03dca28 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -2097,6 +2097,58 @@ References: MySQL `CREATE DATABASE `_,
`ALTER DATABASE `_.
+Routine and trigger alterations
+-------------------------------
+
+PostgreSQL ``ALTER FUNCTION``, ``ALTER PROCEDURE`` and ``ALTER ROUTINE`` and MySQL
+``ALTER FUNCTION`` and ``ALTER PROCEDURE`` parse to ``AlterRoutine``. ``getRoutineType()``
+names the keyword and ``getRoutine()`` is a ``RoutineReference`` with the optional
+PostgreSQL argument types. ``getAction()`` is ``CHARACTERISTICS``, ``RENAME``, ``OWNER``,
+``SET_SCHEMA`` or ``DEPENDS_ON_EXTENSION`` (``isNoDependency()`` for ``NO DEPENDS``);
+payloads of the other actions are kept. ``getCharacteristics()`` is a mutable list of
+``RoutineCharacteristic`` in source order, shared by both dialects. Each ``Kind`` renders
+its own keywords, such as ``IMMUTABLE``, ``CALLED_ON_NULL_INPUT``, ``PARALLEL_SAFE``,
+``COST`` or MySQL's ``READS_SQL_DATA``. ``SECURITY_DEFINER`` and ``SECURITY_INVOKER`` keep
+the ``[EXTERNAL] SECURITY`` or ``SQL SECURITY`` spelling in ``getSecurityClause()``.
+``COST``, ``ROWS``, ``SUPPORT`` and ``COMMENT`` keep their value as SQL text in ``getValue()``.
+``getComment()`` returns the ``COMMENT`` literal as a ``StringValue`` read with the dialect's
+escaping, so ``getNotExcapedValue()`` is the text MySQL stores. ``SET`` and ``RESET`` reuse
+``ConfigurationParameterChange``:
+
+.. code-block:: java
+
+ AlterRoutine alter = (AlterRoutine) CCJSqlParserUtil.parse(
+ "ALTER FUNCTION f(int) IMMUTABLE SET search_path = public",
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ alter.getRoutine().getArguments(); // [int]
+ alter.getCharacteristics().get(0).getKind(); // IMMUTABLE
+ alter.getCharacteristics().get(1).getParameterChange().getParameter(); // search_path
+
+The characteristics of one statement belong to one dialect, and MySQL's take no argument
+list. Other forms after ``ALTER FUNCTION`` or ``ALTER PROCEDURE``, such as Oracle's
+``COMPILE`` or SQL Server routine definitions without the SQL Server dialect, remain an
+``UnsupportedStatement``. PostgreSQL's ``ALTER TRIGGER name ON table RENAME TO`` and
+``[NO] DEPENDS ON EXTENSION`` parse to ``AlterTrigger``; table visitors report its table.
+Validators report ``alterFunction``, ``alterProcedure``, ``alterRoutine`` and
+``alterTrigger``; PostgreSQL enables all of them, MySQL and MariaDB the first two.
+
+``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of
+``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and
+triggers use. MySQL ``CREATE TRIGGER`` accepts ``IF NOT EXISTS`` (``isIfNotExists()``)
+and keeps MariaDB's ``OR REPLACE``. A ``BEGIN ... END`` trigger body with only SQL
+statements remains a ``Block``; a compound body with procedural statements such as
+``IF``, ``DECLARE`` or loops is an ``UnsupportedStatement`` with its tokens, captured with
+the boundary of routine bodies. In both cases the semicolon after ``END`` separates the
+next statement.
+
+References: PostgreSQL `ALTER FUNCTION `_,
+`ALTER PROCEDURE `_,
+`ALTER ROUTINE `_,
+`ALTER TRIGGER `_;
+MySQL `ALTER FUNCTION `_,
+`ALTER PROCEDURE `_,
+`CREATE TRIGGER `_.
+
MySQL table maintenance statements
----------------------------------
diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java
index 304d6da92c..0e4f938e7d 100644
--- a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java
@@ -17,9 +17,11 @@
import java.util.stream.Stream;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.alter.AlterCollation;
+import net.sf.jsqlparser.statement.alter.AlterRoutine;
import net.sf.jsqlparser.statement.alter.AlterStatistics;
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration;
+import net.sf.jsqlparser.statement.alter.AlterTrigger;
import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration;
@@ -41,6 +43,10 @@ static Stream definitions() {
AlterDatabase.class),
Arguments.of("ALTER DATABASE d SET work_mem = '64MB'", AlterDatabase.class),
Arguments.of("ALTER DATABASE d OWNER TO CURRENT_USER", AlterDatabase.class),
+ Arguments.of("ALTER FUNCTION f(int) OWNER TO u1", AlterRoutine.class),
+ Arguments.of("ALTER PROCEDURE p SET search_path = public", AlterRoutine.class),
+ Arguments.of("ALTER FUNCTION f READS SQL DATA", AlterRoutine.class),
+ Arguments.of("ALTER TRIGGER t ON tbl RENAME TO t2", AlterTrigger.class),
Arguments.of("ALTER SCHEMA s RENAME TO s2", AlterSchema.class),
Arguments.of("ALTER SCHEMA s OWNER TO CURRENT_USER", AlterSchema.class),
Arguments.of("ALTER STATISTICS s RENAME TO s2", AlterStatistics.class),
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterRoutineTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRoutineTest.java
new file mode 100644
index 0000000000..e0992bae18
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRoutineTest.java
@@ -0,0 +1,334 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.alter;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.feature.Feature;
+import net.sf.jsqlparser.statement.ConfigurationParameterChange;
+import net.sf.jsqlparser.statement.RoutineCharacteristic;
+import net.sf.jsqlparser.statement.RoutineCharacteristic.Kind;
+import net.sf.jsqlparser.statement.RoutineCharacteristic.SecurityClause;
+import net.sf.jsqlparser.statement.RoutineReference;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitorAdapter;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.create.function.CreateFunction;
+import net.sf.jsqlparser.statement.create.procedure.CreateProcedure;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import net.sf.jsqlparser.util.validation.Validation;
+import net.sf.jsqlparser.util.validation.feature.DatabaseType;
+import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class AlterRoutineTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER FUNCTION f2(int) OWNER TO u1",
+ "ALTER FUNCTION public.f2(integer, text) OWNER TO CURRENT_ROLE",
+ "ALTER FUNCTION f2 OWNER TO SESSION_USER", "ALTER FUNCTION f2(int) RENAME TO g2",
+ "ALTER PROCEDURE p1() SET SCHEMA archive",
+ "ALTER ROUTINE \"My Func\"(IN a int, VARIADIC b text[]) RENAME TO \"Other\"",
+ "ALTER FUNCTION f2(int) DEPENDS ON EXTENSION ext",
+ "ALTER ROUTINE f2(int) NO DEPENDS ON EXTENSION ext",
+ "ALTER FUNCTION f2(int) SET search_path = public",
+ "ALTER FUNCTION f2(int) SET search_path TO app, public SET work_mem = '64MB'",
+ "ALTER FUNCTION f2(int) SET work_mem TO DEFAULT RESET search_path RESET ALL",
+ "ALTER PROCEDURE p1(int) SET search_path FROM CURRENT",
+ "ALTER PROCEDURE p1(int) SECURITY DEFINER",
+ "ALTER PROCEDURE p1 EXTERNAL SECURITY INVOKER",
+ "ALTER ROUTINE f2(int) IMMUTABLE", "ALTER FUNCTION f2(int) STABLE NOT LEAKPROOF",
+ "ALTER FUNCTION f2(int) VOLATILE LEAKPROOF STRICT RESTRICT",
+ "ALTER FUNCTION f2(int) CALLED ON NULL INPUT",
+ "ALTER FUNCTION f2(int) RETURNS NULL ON NULL INPUT",
+ "ALTER FUNCTION f2(int) PARALLEL SAFE PARALLEL RESTRICTED PARALLEL UNSAFE",
+ "ALTER FUNCTION f2(int) COST 100 ROWS 2.5 SUPPORT pg_catalog.my_support",
+ "ALTER FUNCTION f2(int) COST -1 ROWS +10"})
+ void testPostgreSqlForms(String sql) throws JSQLParserException {
+ assertRoundTrip(sql, Dialect.POSTGRESQL);
+ assertRoundTrip(sql, null);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER PROCEDURE p1 COMMENT 'x' SQL SECURITY INVOKER",
+ "ALTER FUNCTION f1 READS SQL DATA", "ALTER FUNCTION db1.f1 LANGUAGE SQL CONTAINS SQL",
+ "ALTER PROCEDURE `p 1` NO SQL MODIFIES SQL DATA SQL SECURITY DEFINER",
+ "ALTER FUNCTION f1 COMMENT 'it''s'"})
+ void testMySqlForms(String sql) throws JSQLParserException {
+ assertRoundTrip(sql, Dialect.MYSQL);
+ assertRoundTrip(sql, Dialect.MARIADB);
+ assertRoundTrip(sql, null);
+ }
+
+ @Test
+ void testPostgreSqlActions() throws JSQLParserException {
+ AlterRoutine owner = parsePostgreSql("ALTER FUNCTION s.f2(IN a int, text) OWNER TO u1");
+ assertEquals(AlterRoutine.RoutineType.FUNCTION, owner.getRoutineType());
+ assertEquals(AlterRoutine.Action.OWNER, owner.getAction());
+ assertEquals("u1", owner.getOwner());
+ RoutineReference routine = owner.getRoutine();
+ assertEquals("s.f2", routine.getName());
+ assertEquals(2, routine.getArguments().size());
+ assertEquals(RoutineReference.Argument.Mode.IN, routine.getArguments().get(0).getMode());
+ assertEquals("a", routine.getArguments().get(0).getName());
+ assertEquals("text", routine.getArguments().get(1).getDataType().toString());
+ assertTrue(owner.getCharacteristics().isEmpty());
+
+ AlterRoutine rename = parsePostgreSql("ALTER ROUTINE f2 RENAME TO g2");
+ assertEquals(AlterRoutine.RoutineType.ROUTINE, rename.getRoutineType());
+ assertEquals(AlterRoutine.Action.RENAME, rename.getAction());
+ assertEquals("g2", rename.getNewName());
+ assertNull(rename.getRoutine().getArguments());
+
+ AlterRoutine schema = parsePostgreSql("ALTER PROCEDURE p1() SET SCHEMA archive");
+ assertEquals(AlterRoutine.RoutineType.PROCEDURE, schema.getRoutineType());
+ assertEquals(AlterRoutine.Action.SET_SCHEMA, schema.getAction());
+ assertEquals("archive", schema.getSchemaName());
+ assertTrue(schema.getRoutine().getArguments().isEmpty());
+
+ AlterRoutine dependency =
+ parsePostgreSql("ALTER FUNCTION f2(int) NO DEPENDS ON EXTENSION ext");
+ assertEquals(AlterRoutine.Action.DEPENDS_ON_EXTENSION, dependency.getAction());
+ assertEquals("ext", dependency.getExtensionName());
+ assertTrue(dependency.isNoDependency());
+
+ // A configuration parameter named "schema" is a SET, not SET SCHEMA.
+ AlterRoutine parameter = parsePostgreSql("ALTER FUNCTION f2(int) SET schema = 'x'");
+ assertEquals(AlterRoutine.Action.CHARACTERISTICS, parameter.getAction());
+ assertEquals("schema", parameter.getCharacteristics().get(0).getParameterChange()
+ .getParameter());
+ }
+
+ @Test
+ void testPostgreSqlCharacteristics() throws JSQLParserException {
+ AlterRoutine statement = parsePostgreSql("ALTER FUNCTION f2(int) IMMUTABLE STRICT "
+ + "EXTERNAL SECURITY DEFINER PARALLEL SAFE COST 10 ROWS 5 SUPPORT s.sup "
+ + "SET search_path TO app, public RESET ALL RESTRICT");
+ assertEquals(AlterRoutine.Action.CHARACTERISTICS, statement.getAction());
+ assertTrue(statement.isRestrict());
+ List characteristics = statement.getCharacteristics();
+ assertEquals(List.of(Kind.IMMUTABLE, Kind.STRICT, Kind.SECURITY_DEFINER,
+ Kind.PARALLEL_SAFE, Kind.COST, Kind.ROWS, Kind.SUPPORT,
+ Kind.CONFIGURATION_PARAMETER, Kind.CONFIGURATION_PARAMETER),
+ characteristics.stream().map(RoutineCharacteristic::getKind)
+ .collect(java.util.stream.Collectors.toList()));
+ assertNull(characteristics.get(0).getSecurityClause());
+ assertEquals(SecurityClause.EXTERNAL_SECURITY,
+ characteristics.get(2).getSecurityClause());
+ assertEquals("10", statement.getCharacteristic(Kind.COST).orElseThrow().getValue());
+ assertEquals("5", statement.getCharacteristic(Kind.ROWS).orElseThrow().getValue());
+ assertEquals("s.sup", statement.getCharacteristic(Kind.SUPPORT).orElseThrow().getValue());
+
+ ConfigurationParameterChange set = characteristics.get(7).getParameterChange();
+ assertEquals(ConfigurationParameterChange.Action.SET, set.getAction());
+ assertEquals("search_path", set.getParameter());
+ assertEquals(2, set.getValues().size());
+ ConfigurationParameterChange reset = characteristics.get(8).getParameterChange();
+ assertEquals(ConfigurationParameterChange.Action.RESET, reset.getAction());
+ assertEquals("ALL", reset.getParameter());
+
+ AlterRoutine plainSecurity = parsePostgreSql("ALTER PROCEDURE p1(int) SECURITY INVOKER");
+ RoutineCharacteristic security = plainSecurity.getCharacteristics().get(0);
+ assertEquals(Kind.SECURITY_INVOKER, security.getKind());
+ assertEquals(SecurityClause.SECURITY, security.getSecurityClause());
+ assertFalse(plainSecurity.isRestrict());
+ }
+
+ @Test
+ void testMySqlCharacteristics() throws JSQLParserException {
+ AlterRoutine statement = (AlterRoutine) CCJSqlParserUtil.parse(
+ "ALTER PROCEDURE db1.p1 COMMENT 'x' LANGUAGE SQL READS SQL DATA SQL SECURITY INVOKER",
+ parser -> parser.withDialect(Dialect.MYSQL));
+ assertEquals(AlterRoutine.RoutineType.PROCEDURE, statement.getRoutineType());
+ assertEquals("db1.p1", statement.getRoutine().getName());
+ assertNull(statement.getRoutine().getArguments());
+ List characteristics = statement.getCharacteristics();
+ assertEquals(4, characteristics.size());
+ assertEquals(Kind.COMMENT, characteristics.get(0).getKind());
+ assertEquals("'x'", characteristics.get(0).getValue());
+ assertEquals(Kind.LANGUAGE_SQL, characteristics.get(1).getKind());
+ assertEquals(Kind.READS_SQL_DATA, characteristics.get(2).getKind());
+ assertEquals(Kind.SECURITY_INVOKER, characteristics.get(3).getKind());
+ assertEquals(SecurityClause.SQL_SECURITY, characteristics.get(3).getSecurityClause());
+ assertFalse(statement.isRestrict());
+ }
+
+ @ParameterizedTest
+ @CsvSource(delimiter = '|', quoteCharacter = '`', value = {
+ "'it\\'s'|it's", "'it''s'|it's", "\"dq\"|dq", "'a\\\\b'|a\\b",
+ "'line1\\nline2'|`line1\nline2`", "'tab\\there'|`tab\there`",
+ "'percent\\% underscore\\_'|percent\\% underscore\\_",
+ "'한글 설명 😀'|한글 설명 😀"})
+ void testMySqlCommentKeepsLiteralAndStoredText(String literal, String stored)
+ throws JSQLParserException {
+ String sql = "ALTER PROCEDURE p1 COMMENT " + literal + " SQL SECURITY INVOKER";
+ AlterRoutine statement = (AlterRoutine) assertSqlCanBeParsedAndDeparsed(sql, false,
+ parser -> parser.withDialect(Dialect.MYSQL));
+ RoutineCharacteristic comment = statement.getCharacteristic(Kind.COMMENT).orElseThrow();
+ assertEquals(literal, comment.getValue());
+ assertEquals(literal, comment.getComment().toString());
+ assertEquals(stored, comment.getComment().getNotExcapedValue());
+ }
+
+ @Test
+ void testCommentMutation() throws JSQLParserException {
+ AlterRoutine statement = (AlterRoutine) CCJSqlParserUtil.parse(
+ "ALTER FUNCTION f1 COMMENT 'it\\'s' NO SQL",
+ parser -> parser.withDialect(Dialect.MYSQL));
+ RoutineCharacteristic comment = statement.getCharacteristics().get(0);
+ assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, comment.getComment().getEscaping());
+
+ comment.getComment().setValue("changed");
+ assertEquals("ALTER FUNCTION f1 COMMENT 'changed' NO SQL", statement.toString());
+ comment.setValue("'x''y'");
+ assertEquals("x'y", comment.getComment().getNotExcapedValue());
+ comment.setComment(new StringValue("plain"));
+ assertEquals("'plain'", comment.getValue());
+
+ RoutineCharacteristic created = RoutineCharacteristic.comment(new StringValue("note"));
+ assertEquals(Kind.COMMENT, created.getKind());
+ assertEquals("COMMENT 'note'", created.toString());
+ assertEquals("COMMENT 'v'", new RoutineCharacteristic(Kind.COMMENT, "'v'").toString());
+ assertNull(new RoutineCharacteristic(Kind.COST, "5").getComment());
+ }
+
+ @Test
+ void testMutationAndActionSwitch() throws JSQLParserException {
+ AlterRoutine statement = parsePostgreSql("ALTER FUNCTION f2(int) RENAME TO g2");
+ statement.setAction(AlterRoutine.Action.OWNER);
+ statement.setOwner("CURRENT_USER");
+ assertEquals("ALTER FUNCTION f2(int) OWNER TO CURRENT_USER", statement.toString());
+ statement.setAction(AlterRoutine.Action.RENAME);
+ assertEquals("ALTER FUNCTION f2(int) RENAME TO g2", statement.toString());
+
+ statement.setAction(AlterRoutine.Action.CHARACTERISTICS);
+ statement.setRoutineType(AlterRoutine.RoutineType.ROUTINE);
+ statement.getCharacteristics().add(new RoutineCharacteristic(Kind.SECURITY_DEFINER));
+ statement.getCharacteristics().add(new RoutineCharacteristic(Kind.COST, "5"));
+ statement.getCharacteristics().add(new RoutineCharacteristic(
+ new ConfigurationParameterChange(ConfigurationParameterChange.Action.SET, "a")
+ .withValues(new ExpressionList<>(new StringValue("b")))));
+ statement.setRestrict(true);
+ assertEquals("ALTER ROUTINE f2(int) SECURITY DEFINER COST 5 SET a TO 'b' RESTRICT",
+ statement.toString());
+
+ RoutineCharacteristic security = statement.getCharacteristics().get(0);
+ security.setSecurityClause(SecurityClause.SQL_SECURITY);
+ security.setKind(Kind.SECURITY_INVOKER);
+ assertEquals("SQL SECURITY INVOKER", security.toString());
+
+ RoutineCharacteristic missing = new RoutineCharacteristic(Kind.CONFIGURATION_PARAMETER);
+ assertThrows(IllegalStateException.class, missing::toString);
+ }
+
+ @Test
+ void testVisitorsAndDeparserUseExpressions() throws JSQLParserException {
+ AlterRoutine statement =
+ parsePostgreSql("ALTER FUNCTION f2(int) SET search_path TO app, public");
+ assertEquals("context:f2", statement.accept(new StatementVisitorAdapter() {
+ @Override
+ public String visit(AlterRoutine routine, S context) {
+ return context + ":" + routine.getRoutine().getName();
+ }
+ }, "context"));
+ assertTrue(new TablesNamesFinder().getTables((Statement) statement).isEmpty());
+
+ StringBuilder deparsed = new StringBuilder();
+ new StatementDeParser(deparsed).visit(statement, null);
+ assertEquals("ALTER FUNCTION f2(int) SET search_path TO app, public",
+ deparsed.toString());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER FUNCTION f", "ALTER PROCEDURE p1",
+ "ALTER FUNCTION f COMPILE", "ALTER FUNCTION f(int) NOT FENCED",
+ "ALTER FUNCTION f(int) EXTERNAL NAME 'lib'", "ALTER FUNCTION f(int) SET SECURE",
+ "ALTER FUNCTION IF EXISTS f(int) RENAME TO g", "ALTER PROCEDURE dbo.p AS SELECT 1",
+ "ALTER FUNCTION dbo.f(@a int) RETURNS int AS BEGIN RETURN 1 END",
+ "ALTER FUNCTION f(int) PARALLEL FAST", "ALTER FUNCTION f(int) SECURITY OWNER"})
+ void testOtherFormsRemainUnsupported(String sql) throws JSQLParserException {
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql));
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL)));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER FUNCTION f(int) IMMUTABLE, STRICT",
+ "ALTER FUNCTION f(int) RENAME TO g RESTRICT",
+ "ALTER FUNCTION f(int) OWNER TO u IMMUTABLE",
+ "ALTER FUNCTION f COMMENT 'x' IMMUTABLE", "ALTER FUNCTION f IMMUTABLE COMMENT 'x'",
+ "ALTER FUNCTION f(int) COMMENT 'x'", "ALTER ROUTINE r COMMENT 'x'",
+ "ALTER FUNCTION f COMMENT 'x' RESTRICT"})
+ void testRejectInvalidCombinations(String sql) {
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.MYSQL)));
+ }
+
+ @Test
+ void testSqlServerRoutinesAreUnchanged() throws JSQLParserException {
+ Consumer sqlServer = parser -> parser.withDialect(Dialect.SQLSERVER);
+ assertInstanceOf(CreateFunction.class, CCJSqlParserUtil.parse(
+ "ALTER FUNCTION dbo.f(@a int) RETURNS int AS BEGIN RETURN @a END", sqlServer));
+ assertInstanceOf(CreateProcedure.class,
+ CCJSqlParserUtil.parse("ALTER PROCEDURE dbo.p AS SELECT 1", sqlServer));
+ }
+
+ @Test
+ void testValidation() {
+ validateNoErrors("ALTER FUNCTION f2(int) OWNER TO u1", 1, DatabaseType.POSTGRESQL);
+ validateNoErrors("ALTER PROCEDURE p1(int) SET search_path = public", 1,
+ DatabaseType.POSTGRESQL);
+ validateNoErrors("ALTER ROUTINE f2(int) IMMUTABLE", 1, DatabaseType.POSTGRESQL);
+ validateNoErrors("ALTER FUNCTION f1 READS SQL DATA", 1, DatabaseType.MYSQL,
+ DatabaseType.MARIADB);
+ validateNoErrors("ALTER PROCEDURE p1 COMMENT 'x'", 1, DatabaseType.MYSQL,
+ DatabaseType.MARIADB);
+ assertFalse(new Validation(List.of(DatabaseType.MYSQL), "ALTER ROUTINE f2(int) STABLE")
+ .validate().isEmpty());
+ assertFalse(new Validation(List.of(FeaturesAllowed.SELECT),
+ "ALTER FUNCTION f2(int) STABLE").validate().isEmpty());
+ assertTrue(new Validation(List.of(new FeaturesAllowed().add(Feature.alterProcedure)),
+ "ALTER PROCEDURE p1(int) RESET ALL").validate().isEmpty());
+ }
+
+ /** Parses, deparses and reparses with the dialect, or without one if it is null. */
+ private static void assertRoundTrip(String sql, Dialect dialect) throws JSQLParserException {
+ Consumer consumer = dialect == null ? parser -> {
+ } : parser -> parser.withDialect(dialect);
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer);
+ assertInstanceOf(AlterRoutine.class, statement);
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, deparsed.toString());
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ assertTrue(new TablesNamesFinder().getTables(statement).isEmpty());
+ assertEquals(2, CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer).size());
+ }
+
+ private static AlterRoutine parsePostgreSql(String sql) throws JSQLParserException {
+ return (AlterRoutine) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTriggerTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTriggerTest.java
new file mode 100644
index 0000000000..cf33ab0f3d
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTriggerTest.java
@@ -0,0 +1,109 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.alter;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitorAdapter;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import net.sf.jsqlparser.util.validation.Validation;
+import net.sf.jsqlparser.util.validation.feature.DatabaseType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class AlterTriggerTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER TRIGGER trg ON users RENAME TO trg_new",
+ "ALTER TRIGGER \"Trg\" ON app.\"Users\" RENAME TO \"Trg 2\"",
+ "ALTER TRIGGER trg ON users DEPENDS ON EXTENSION ext",
+ "ALTER TRIGGER trg ON users NO DEPENDS ON EXTENSION ext"})
+ void testRoundTrip(String sql) throws JSQLParserException {
+ for (Consumer consumer : List.>of(parser -> {
+ }, parser -> parser.withDialect(Dialect.POSTGRESQL))) {
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer);
+ assertInstanceOf(AlterTrigger.class, statement);
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, deparsed.toString());
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ assertEquals(2,
+ CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer).size());
+ }
+ }
+
+ @Test
+ void testActionsAndTable() throws JSQLParserException {
+ AlterTrigger rename = parse("ALTER TRIGGER trg ON app.users RENAME TO trg_new");
+ assertEquals("trg", rename.getName());
+ assertEquals("app.users", rename.getTable().getFullyQualifiedName());
+ assertEquals(AlterTrigger.Action.RENAME, rename.getAction());
+ assertEquals("trg_new", rename.getNewName());
+ assertEquals(List.of("app.users"),
+ List.copyOf(new TablesNamesFinder().getTables((Statement) rename)));
+
+ AlterTrigger dependency = parse("ALTER TRIGGER trg ON users NO DEPENDS ON EXTENSION ext");
+ assertEquals(AlterTrigger.Action.DEPENDS_ON_EXTENSION, dependency.getAction());
+ assertEquals("ext", dependency.getExtensionName());
+ assertTrue(dependency.isNoDependency());
+
+ dependency.setNoDependency(false);
+ dependency.setTable(new Table("orders"));
+ assertEquals("ALTER TRIGGER trg ON orders DEPENDS ON EXTENSION ext",
+ dependency.toString());
+ dependency.setAction(AlterTrigger.Action.RENAME);
+ dependency.setNewName("trg2");
+ assertEquals("ALTER TRIGGER trg ON orders RENAME TO trg2", dependency.toString());
+
+ assertEquals("context:trg", rename.accept(new StatementVisitorAdapter() {
+ @Override
+ public String visit(AlterTrigger trigger, S context) {
+ return context + ":" + trigger.getName();
+ }
+ }, "context"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER TRIGGER trg ENABLE", "ALTER TRIGGER trg COMPILE",
+ "ALTER TRIGGER trg ON t AFTER INSERT AS SELECT 1",
+ "ALTER TRIGGER trg ON t RENAME TO t2 CASCADE",
+ "ALTER TRIGGER trg ON t DEPENDS ON SCHEMA s"})
+ void testOtherFormsRemainUnsupported(String sql) throws JSQLParserException {
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql));
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL)));
+ }
+
+ @Test
+ void testValidation() {
+ validateNoErrors("ALTER TRIGGER trg ON users RENAME TO trg_new", 1,
+ DatabaseType.POSTGRESQL);
+ assertFalse(new Validation(List.of(DatabaseType.MYSQL),
+ "ALTER TRIGGER trg ON users RENAME TO trg_new").validate().isEmpty());
+ }
+
+ private static AlterTrigger parse(String sql) throws JSQLParserException {
+ return (AlterTrigger) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java
index 66880938c2..d2059b6ba5 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java
@@ -12,11 +12,21 @@
import static net.sf.jsqlparser.test.TestUtils.assertDeparse;
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Arrays;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.MySqlDefiner;
+import net.sf.jsqlparser.statement.CreateFunctionalStatement;
import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.create.view.CreateView;
+import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import net.sf.jsqlparser.statement.create.function.CreateFunction;
import net.sf.jsqlparser.statement.create.procedure.CreateProcedure;
import org.junit.jupiter.api.Test;
@@ -89,6 +99,65 @@ public void createOrReplaceFunctionMinimal() throws JSQLParserException {
assertDeparse(func, statement);
}
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE DEFINER = CURRENT_USER FUNCTION f2 (s CHAR(20)) RETURNS CHAR(50) "
+ + "CHARSET utf8mb4 NO SQL RETURN CONCAT('Hello, ', s, '!')",
+ "CREATE DEFINER = CURRENT_USER PROCEDURE p3 () SELECT 1",
+ "CREATE DEFINER = `root`@`%` FUNCTION f3 () RETURNS INT RETURN 1",
+ "CREATE DEFINER = 'admin'@'localhost' PROCEDURE p (IN a INT) BEGIN SELECT a; END",
+ "CREATE OR REPLACE DEFINER = CURRENT_USER() PROCEDURE p () SELECT 1"})
+ public void createRoutineWithMySqlDefiner(String sql) throws JSQLParserException {
+ for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB, null}) {
+ CreateFunctionalStatement statement =
+ (CreateFunctionalStatement) assertSqlCanBeParsedAndDeparsed(
+ sql, true, dialect == null ? parser -> {
+ } : parser -> parser.withDialect(dialect));
+ assertThat(statement.getDefiner()).isNotNull();
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertThat(deparsed.toString()).isEqualTo(statement.toString());
+ assertThat(CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1").getStatements())
+ .hasSize(2);
+ }
+ }
+
+ @Test
+ public void mySqlDefinerAccount() throws JSQLParserException {
+ CreateFunction function = (CreateFunction) CCJSqlParserUtil.parse(
+ "CREATE DEFINER='root'@'%' FUNCTION f3 () RETURNS INT RETURN 1");
+ MySqlDefiner definer = function.getDefiner();
+ assertThat(definer.getUser().getValue()).isEqualTo("root");
+ assertThat(definer.getHost().getValue()).isEqualTo("%");
+ assertThat(function.getFunctionDeclarationParts()).startsWith("f3", "(", ")");
+
+ CreateProcedure procedure = (CreateProcedure) CCJSqlParserUtil
+ .parse("CREATE DEFINER = CURRENT_USER PROCEDURE p3 () SELECT 1");
+ assertThat(procedure.getDefiner().getUser().getValue()).isEqualTo("CURRENT_USER");
+ assertThat(procedure.getDefiner().getHost()).isNull();
+
+ procedure.setDefiner(new MySqlDefiner().withUser(new StringValue("app"))
+ .withHost(new StringValue("%")));
+ assertThat(procedure.toString())
+ .isEqualTo("CREATE DEFINER = 'app'@'%' PROCEDURE p3 ( ) SELECT 1");
+ procedure.setDefiner(null);
+ assertThat(procedure.toString()).isEqualTo("CREATE PROCEDURE p3 ( ) SELECT 1");
+ }
+
+ @Test
+ public void definerDispatchKeepsTriggersAndViews() throws JSQLParserException {
+ assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = CURRENT_USER TRIGGER t "
+ + "BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1"))
+ .isInstanceOf(CreateTrigger.class);
+ assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = CURRENT_USER VIEW v AS SELECT 1"))
+ .isInstanceOf(CreateView.class);
+ assertThat(CCJSqlParserUtil.parse("CREATE FUNCTION f() RETURNS int AS 'SELECT 1'"))
+ .isInstanceOf(CreateFunction.class)
+ .extracting(statement -> ((CreateFunction) statement).getDefiner()).isNull();
+ assertThatThrownBy(() -> CCJSqlParserUtil.parse("CREATE DEFINER = x EVENT e "
+ + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(JSQLParserException.class);
+ }
+
@Test
public void createFunctionWithPositionalParametersAcrossStatementsIssue2322()
throws JSQLParserException {
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java b/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java
index f199bc293b..ce5e896761 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java
@@ -11,13 +11,32 @@
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Block;
import net.sf.jsqlparser.statement.SetStatement;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
class CreateTriggerTest {
@@ -71,4 +90,121 @@ void parsesTriggerOrdering() throws JSQLParserException {
assertEquals(CreateTrigger.Order.FOLLOWS, trigger.getOrder());
assertEquals("normalize_name", trigger.getOtherTrigger().getFullyQualifiedName());
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE DEFINER = `root`@`localhost` TRIGGER IF NOT EXISTS trg2 AFTER UPDATE ON t1 "
+ + "FOR EACH ROW FOLLOWS trg INSERT INTO audit (id) VALUES (NEW.id)",
+ "CREATE TRIGGER IF NOT EXISTS trg2 AFTER UPDATE ON t1 FOR EACH ROW "
+ + "INSERT INTO audit (id) VALUES (NEW.id)",
+ "CREATE OR REPLACE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1",
+ "CREATE OR REPLACE DEFINER = CURRENT_USER TRIGGER trg BEFORE INSERT ON t1 "
+ + "FOR EACH ROW SET NEW.a = 1",
+ "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW "
+ + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW lbl: BEGIN "
+ + "DECLARE x INT DEFAULT 0; WHILE x < 3 DO SET x = x + 1; END WHILE; "
+ + "LEAVE lbl; END lbl",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW "
+ + "IF NEW.a IS NULL THEN SET NEW.a = 'a.b'; END IF",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW CASE WHEN NEW.a > 0 THEN "
+ + "SET NEW.b = 1; ELSE SET NEW.b = CASE NEW.c WHEN 1 THEN 2 END; END CASE",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN END"})
+ void parsesMySqlTriggerForms(String sql) throws JSQLParserException {
+ for (Consumer consumer : List.>of(
+ parser -> parser.withDialect(Dialect.MYSQL),
+ parser -> parser.withDialect(Dialect.MARIADB), parser -> {
+ })) {
+ CreateTrigger trigger = assertInstanceOf(CreateTrigger.class,
+ assertSqlCanBeParsedAndDeparsed(sql, false, consumer));
+ StringBuilder deparsed = new StringBuilder();
+ trigger.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, deparsed.toString());
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer);
+ assertEquals(2, script.size());
+ assertEquals(sql, script.get(0).toString());
+ }
+ }
+
+ @Test
+ void parsesIfNotExistsAndOrReplace() throws JSQLParserException {
+ CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(
+ "CREATE DEFINER = CURRENT_USER TRIGGER IF NOT EXISTS trg AFTER UPDATE ON t1 "
+ + "FOR EACH ROW INSERT INTO audit (id) VALUES (NEW.id)");
+ assertTrue(trigger.isIfNotExists());
+ assertFalse(trigger.isOrReplace());
+ assertEquals("trg", trigger.getTrigger().getFullyQualifiedName());
+ assertEquals("CURRENT_USER", trigger.getDefiner().getUser().getValue());
+
+ trigger.setIfNotExists(false);
+ trigger.setOrReplace(true);
+ assertEquals("CREATE OR REPLACE DEFINER = CURRENT_USER TRIGGER trg AFTER UPDATE ON t1 "
+ + "FOR EACH ROW INSERT INTO audit (id) VALUES (NEW.id)", trigger.toString());
+
+ CreateTrigger replaced = (CreateTrigger) CCJSqlParserUtil.parse(
+ "CREATE OR REPLACE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1",
+ parser -> parser.withDialect(Dialect.MARIADB));
+ assertTrue(replaced.isOrReplace());
+ assertFalse(replaced.isIfNotExists());
+
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
+ "CREATE OR REPLACE TRIGGER IF NOT EXISTS trg BEFORE INSERT ON t1 "
+ + "FOR EACH ROW SET NEW.a = 1"));
+ }
+
+ @Test
+ void keepsStatementBlocksStructured() throws JSQLParserException {
+ String sql = "CREATE TRIGGER trg AFTER INSERT ON t1 FOR EACH ROW BEGIN "
+ + "INSERT INTO audit (id) VALUES (NEW.id); "
+ + "UPDATE counters SET n = CASE WHEN NEW.a > 0 THEN n + 1 ELSE n END; "
+ + "DELETE FROM pending WHERE id = NEW.id; END;";
+ CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.MYSQL));
+ Block body = assertInstanceOf(Block.class, trigger.getBody());
+ assertEquals(3, body.getStatements().size());
+ assertInstanceOf(Insert.class, body.getStatements().get(0));
+ // The semicolon after END ends the CREATE TRIGGER statement.
+ assertFalse(body.hasSemicolonAfterEnd());
+ assertEquals(Set.of("t1", "audit", "counters", "pending"),
+ new HashSet<>(new TablesNamesFinder().getTables((Statement) trigger)));
+ }
+
+ @Test
+ void keepsProceduralBodiesAsTokens() throws JSQLParserException {
+ CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(
+ "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW "
+ + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END",
+ parser -> parser.withDialect(Dialect.MYSQL));
+ UnsupportedStatement body = assertInstanceOf(UnsupportedStatement.class, trigger.getBody());
+ assertEquals("BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END", body.toString());
+ assertEquals(Set.of("t1"),
+ new HashSet<>(new TablesNamesFinder().getTables((Statement) trigger)));
+ }
+
+ @Test
+ void splitsScriptsAfterCompoundBodies() throws JSQLParserException {
+ String script = "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW "
+ + "BEGIN SET NEW.a = 1; SET NEW.b = 2; END;\n"
+ + "CREATE TABLE b (y INT);\n"
+ + "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW "
+ + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END;\n"
+ + "CREATE TABLE c (z INT);";
+ Statements statements = CCJSqlParserUtil.parseStatements(script,
+ parser -> parser.withDialect(Dialect.MYSQL));
+ assertEquals(4, statements.size());
+ assertInstanceOf(Block.class, ((CreateTrigger) statements.get(0)).getBody());
+ assertInstanceOf(CreateTable.class, statements.get(1));
+ assertInstanceOf(UnsupportedStatement.class, ((CreateTrigger) statements.get(2)).getBody());
+ assertInstanceOf(CreateTable.class, statements.get(3));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN IF NEW.a < 0 THEN SET NEW.a = 0;",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN SET NEW.a = 1"})
+ void rejectsUnterminatedCompoundBodies(String sql) {
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)));
+ }
}