diff --git a/README.md b/README.md
index d39e81ea6..d54e335db 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
| **DDL** | `CREATE …` · `ALTER …` · `DROP …` |
| **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 4a5935fd4..c5ef37b5d 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()) {
@@ -50,6 +60,15 @@ boolean endsAt(Token token) throws ParseException {
&& !declarations && (hasParameters || quotedBody || completed);
}
+ /**
+ * Whether the routine header, including a return type, is complete and the body has not
+ * started, which is where MySQL routine characteristics appear.
+ */
+ boolean isBeforeBody() {
+ return hasParameters && parentheses == 0 && !bodyStarted && !returnTypeName
+ && !declarations && !quotedBody && !completed;
+ }
+
boolean startsQuotedBody(Token token, Token next) {
return parentheses == 0 && !compoundBody && !quotedBody && token.kind == K_AS
&& next.kind == S_CHAR_LITERAL
@@ -218,7 +237,10 @@ private void acceptStatementToken(Token token, Token next) {
}
if (!bodyStarted) {
// SET may also be a PostgreSQL header option; AS remains recognizable above.
- if (!isSimpleStatementStart(kind)) {
+ // CHARACTER SET and CHAR SET belong to a MySQL return type.
+ boolean characterSet =
+ kind == K_SET && (previousKind == K_CHARACTER || previousKind == K_CHAR);
+ if (!isSimpleStatementStart(kind) || characterSet) {
return;
}
bodyStarted = true;
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 0d3d080fb..50ebf078e 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -509,6 +509,12 @@ public enum Feature {
* @see CreateDatabase
*/
createDatabase,
+ /**
+ * SQL "ALTER DATABASE" statement is allowed
+ *
+ * @see net.sf.jsqlparser.statement.alter.database.AlterDatabase
+ */
+ alterDatabase,
/**
* SQL "CREATE SCHEMA" statement is allowed
*
@@ -711,6 +717,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
new file mode 100644
index 000000000..913cace5e
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java
@@ -0,0 +1,175 @@
+/*-
+ * #%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.operators.relational.ExpressionList;
+
+/**
+ * A PostgreSQL {@code SET} or {@code RESET} of a configuration parameter, shared by statements that
+ * 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 }
+ * SET parameter FROM CURRENT
+ * RESET { parameter | ALL }
+ *
+ *
+ * Parameter names keep their SQL spelling, including qualifiers and identifier quotes.
+ * {@code RESET ALL} uses the parameter name {@code ALL}.
+ */
+public class ConfigurationParameterChange implements Serializable {
+
+ public enum Action {
+ SET, RESET
+ }
+
+ /** What a {@code SET} assigns. Only the selected kind is rendered. */
+ public enum ValueKind {
+ /** The explicit values of {@link #getValues()}. */
+ VALUES,
+ /** {@code TO DEFAULT} or {@code = DEFAULT}. */
+ DEFAULT,
+ /** {@code FROM CURRENT}. */
+ FROM_CURRENT
+ }
+
+ private Action action = Action.SET;
+ private String parameter;
+ private ValueKind valueKind = ValueKind.VALUES;
+ private boolean useEquals;
+ private ExpressionList values;
+
+ public ConfigurationParameterChange() {}
+
+ public ConfigurationParameterChange(Action action, String parameter) {
+ this.action = action;
+ this.parameter = parameter;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = action;
+ }
+
+ public String getParameter() {
+ return parameter;
+ }
+
+ public void setParameter(String parameter) {
+ this.parameter = parameter;
+ }
+
+ public ValueKind getValueKind() {
+ return valueKind;
+ }
+
+ /**
+ * Selects what a {@code SET} assigns. The values are retained when another kind is selected.
+ *
+ * @param valueKind the assigned value kind
+ */
+ public void setValueKind(ValueKind valueKind) {
+ this.valueKind = valueKind;
+ }
+
+ /**
+ * Whether {@code =} rather than {@code TO} precedes the value.
+ *
+ * @return {@code true} for {@code =}
+ */
+ public boolean isUseEquals() {
+ return useEquals;
+ }
+
+ public void setUseEquals(boolean useEquals) {
+ this.useEquals = useEquals;
+ }
+
+ /**
+ * The values assigned by {@link ValueKind#VALUES}, as a mutable list.
+ *
+ * @return the values, or {@code null} if none were set
+ */
+ public ExpressionList getValues() {
+ return values;
+ }
+
+ public void setValues(ExpressionList values) {
+ this.values = values;
+ }
+
+ public ConfigurationParameterChange withAction(Action action) {
+ setAction(action);
+ return this;
+ }
+
+ public ConfigurationParameterChange withParameter(String parameter) {
+ setParameter(parameter);
+ return this;
+ }
+
+ public ConfigurationParameterChange withValueKind(ValueKind valueKind) {
+ setValueKind(valueKind);
+ return this;
+ }
+
+ public ConfigurationParameterChange withUseEquals(boolean useEquals) {
+ setUseEquals(useEquals);
+ return this;
+ }
+
+ public ConfigurationParameterChange withValues(ExpressionList values) {
+ setValues(values);
+ return this;
+ }
+
+ /**
+ * Appends this clause to a builder.
+ *
+ * @param builder the target
+ * @param valueRenderer appends each value, for example through an expression deparser
+ * @return the builder
+ */
+ public StringBuilder appendTo(StringBuilder builder, Consumer valueRenderer) {
+ if (action == Action.RESET) {
+ return builder.append("RESET ").append(parameter);
+ }
+ builder.append("SET ").append(parameter);
+ if (valueKind == ValueKind.FROM_CURRENT) {
+ return builder.append(" FROM CURRENT");
+ }
+ builder.append(useEquals ? " = " : " TO ");
+ if (valueKind == ValueKind.DEFAULT) {
+ return builder.append("DEFAULT");
+ }
+ if (values != null) {
+ for (int i = 0; i < values.size(); i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+ valueRenderer.accept(values.get(i));
+ }
+ }
+ return 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/CreateFunctionalStatement.java b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
index 0845742e9..d3f75a127 100644
--- a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
@@ -31,8 +31,10 @@ public enum Operation {
}
private Operation operation = Operation.CREATE;
+ private MySqlDefiner definer;
private FunctionReturnType returnType;
private List routineBodyParts;
+ private List characteristics = Collections.emptyList();
public Operation getOperation() {
return operation;
@@ -42,6 +44,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;
}
@@ -85,8 +100,38 @@ public List getFunctionDeclarationParts() {
return functionDeclarationParts;
}
+ /**
+ * Replaces the declaration parts. The {@link #getCharacteristics() characteristics} described
+ * the replaced parts, so they are cleared.
+ *
+ * @param functionDeclarationParts the new parts
+ */
public void setFunctionDeclarationParts(List functionDeclarationParts) {
+ setFunctionDeclarationParts(functionDeclarationParts, Collections.emptyList());
+ }
+
+ /**
+ * Replaces the declaration parts together with the characteristics written in them.
+ *
+ * @param functionDeclarationParts the new parts, which keep rendering the statement
+ * @param characteristics the characteristics among the parts, in source order
+ */
+ public void setFunctionDeclarationParts(List functionDeclarationParts,
+ List characteristics) {
this.functionDeclarationParts = functionDeclarationParts;
+ this.characteristics = List.copyOf(characteristics);
+ }
+
+ /**
+ * The MySQL characteristics of {@code CREATE FUNCTION|PROCEDURE}, such as {@code COMMENT},
+ * {@code DETERMINISTIC} or {@code SQL SECURITY}, parsed with a MySQL or MariaDB dialect. They
+ * describe tokens of {@link #getFunctionDeclarationParts()}, which still render the statement,
+ * so the list is read-only.
+ *
+ * @return the characteristics in source order, empty if none were parsed
+ */
+ public List getCharacteristics() {
+ return characteristics;
}
/**
@@ -139,6 +184,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);
}
@@ -160,19 +208,23 @@ public CreateFunctionalStatement withFunctionDeclarationParts(
return this;
}
+ /** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */
public CreateFunctionalStatement addFunctionDeclarationParts(
String... functionDeclarationParts) {
List collection =
Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new);
Collections.addAll(collection, functionDeclarationParts);
- return this.withFunctionDeclarationParts(collection);
+ setFunctionDeclarationParts(collection, characteristics);
+ return this;
}
+ /** Appends declaration parts and keeps the {@link #getCharacteristics() characteristics}. */
public CreateFunctionalStatement addFunctionDeclarationParts(
Collection functionDeclarationParts) {
List collection =
Optional.ofNullable(getFunctionDeclarationParts()).orElseGet(ArrayList::new);
collection.addAll(functionDeclarationParts);
- return this.withFunctionDeclarationParts(collection);
+ setFunctionDeclarationParts(collection, characteristics);
+ return this;
}
}
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 000000000..90be0d3c6
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java
@@ -0,0 +1,268 @@
+/*-
+ * #%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},
+ * or declared by MySQL's {@code CREATE FUNCTION|PROCEDURE}. 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}, {@code [NOT] DETERMINISTIC} (CREATE only), 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 DETERMINISTIC}, only in {@code CREATE FUNCTION|PROCEDURE}. */
+ DETERMINISTIC("DETERMINISTIC"),
+ /** MySQL {@code NOT DETERMINISTIC}, only in {@code CREATE FUNCTION|PROCEDURE}. */
+ NOT_DETERMINISTIC("NOT DETERMINISTIC"),
+ /** 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 91b272124..8aa4919f7 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;
@@ -1181,6 +1183,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 bf0bf3e8b..3deb4a00e 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;
@@ -875,4 +877,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 8a80763ca..aef820df8 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;
@@ -1015,4 +1017,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 000000000..66f1717cc
--- /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 000000000..de3be507e
--- /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/alter/database/AlterDatabase.java b/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java
index d0c7a79ea..540d1fb1c 100644
--- a/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java
+++ b/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java
@@ -12,15 +12,44 @@
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.ConfigurationParameterChange;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitor;
import net.sf.jsqlparser.statement.create.database.DatabaseOption;
-/** MySQL ALTER DATABASE (or SCHEMA); a missing name selects the current database. */
+/**
+ * MySQL or PostgreSQL {@code ALTER DATABASE}. MySQL's {@code ALTER SCHEMA} synonym only changes
+ * options, and a missing name selects the current database. The options use the same
+ * {@link DatabaseOption} model as {@code CREATE DATABASE}. Names retain SQL identifier quoting.
+ */
public class AlterDatabase implements Statement {
+
+ public enum Action {
+ /** MySQL options or PostgreSQL {@code [WITH] option ...}, see {@link #getOptions()}. */
+ OPTIONS,
+ /** {@code RENAME TO}, see {@link #getNewName()}. */
+ RENAME,
+ /** {@code OWNER TO}, see {@link #getOwner()}. */
+ OWNER,
+ /** {@code SET TABLESPACE}, see {@link #getTablespace()}. */
+ SET_TABLESPACE,
+ /** {@code REFRESH COLLATION VERSION}. */
+ REFRESH_COLLATION_VERSION,
+ /** {@code SET} or {@code RESET}, see {@link #getParameterChange()}. */
+ CONFIGURATION_PARAMETER
+ }
+
private String databaseName;
private boolean useSchemaKeyword;
+ private Action action = Action.OPTIONS;
+ private boolean useWith;
private final List options = new ArrayList<>();
+ private String newName;
+ private String owner;
+ private String tablespace;
+ private ConfigurationParameterChange parameterChange;
public String getDatabaseName() {
return databaseName;
@@ -38,6 +67,37 @@ public void setUseSchemaKeyword(boolean useSchemaKeyword) {
this.useSchemaKeyword = useSchemaKeyword;
}
+ /**
+ * The rendered alteration. Payloads of other actions are retained for a later switch back.
+ *
+ * @return the action, {@link Action#OPTIONS} unless changed
+ */
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = action;
+ }
+
+ /**
+ * Whether PostgreSQL's optional {@code WITH} precedes the options.
+ *
+ * @return {@code true} if {@code WITH} is rendered
+ */
+ public boolean isUseWith() {
+ return useWith;
+ }
+
+ public void setUseWith(boolean useWith) {
+ this.useWith = useWith;
+ }
+
+ /**
+ * The options of {@link Action#OPTIONS}, as a mutable list.
+ *
+ * @return the options in source order
+ */
public List getOptions() {
return options;
}
@@ -46,13 +106,81 @@ public Optional getOption(DatabaseOption.Kind kind) {
return options.stream().filter(option -> option.getKind() == kind).findFirst();
}
+ public String getNewName() {
+ return newName;
+ }
+
+ public void setNewName(String newName) {
+ this.newName = newName;
+ }
+
+ /**
+ * The new owner, a role name or {@code CURRENT_USER}, {@code CURRENT_ROLE} or
+ * {@code SESSION_USER}.
+ *
+ * @return the owner
+ */
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getTablespace() {
+ return tablespace;
+ }
+
+ public void setTablespace(String tablespace) {
+ this.tablespace = tablespace;
+ }
+
+ public ConfigurationParameterChange getParameterChange() {
+ return parameterChange;
+ }
+
+ public void setParameterChange(ConfigurationParameterChange parameterChange) {
+ this.parameterChange = parameterChange;
+ }
+
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(useSchemaKeyword ? "ALTER SCHEMA" : "ALTER DATABASE");
if (databaseName != null) {
builder.append(' ').append(databaseName);
}
- options.forEach(option -> builder.append(' ').append(option));
- return builder;
+ switch (action) {
+ case RENAME:
+ return builder.append(" RENAME TO ").append(newName);
+ case OWNER:
+ return builder.append(" OWNER TO ").append(owner);
+ case SET_TABLESPACE:
+ return builder.append(" SET TABLESPACE ").append(tablespace);
+ case REFRESH_COLLATION_VERSION:
+ return builder.append(" REFRESH COLLATION VERSION");
+ case CONFIGURATION_PARAMETER:
+ if (parameterChange == null) {
+ throw new IllegalStateException("Expected a configuration parameter change");
+ }
+ return parameterChange.appendTo(builder.append(' '), valueRenderer);
+ default:
+ if (useWith) {
+ builder.append(" WITH");
+ }
+ options.forEach(option -> builder.append(' ').append(option));
+ return builder;
+ }
}
@Override
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java
index e1fb9ea05..76a447fa2 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java
@@ -10,25 +10,26 @@
package net.sf.jsqlparser.statement.create.database;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import java.util.stream.Collectors;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitor;
/**
- * A {@code CREATE DATABASE} statement. Modelled after MySQL, where the statement takes an optional
- * {@code IF NOT EXISTS} and vendor-specific options, which are captured verbatim.
+ * A {@code CREATE DATABASE} statement with an optional {@code IF NOT EXISTS} and options. MySQL and
+ * PostgreSQL options are typed {@link DatabaseOption}s; other option text is kept as a raw
+ * {@link DatabaseOption.Kind#OTHER} option.
*/
public class CreateDatabase implements Statement {
private String databaseName;
private boolean hasIfNotExists = false;
- private List databaseOptions = null;
- private List options;
+ private boolean useWith;
+ private final List options = new ArrayList<>();
@Override
public T accept(StatementVisitor statementVisitor, S context) {
@@ -59,35 +60,79 @@ public CreateDatabase setIfNotExists(boolean hasIfNotExists) {
}
/**
- * The vendor-specific options of the database, captured as raw tokens.
+ * Whether PostgreSQL's optional {@code WITH} precedes the options.
*
- * @return the list of option tokens, {@code null} when no options were given
+ * @return {@code true} if {@code WITH} is rendered
+ */
+ public boolean isUseWith() {
+ return useWith;
+ }
+
+ public CreateDatabase setUseWith(boolean useWith) {
+ this.useWith = useWith;
+ return this;
+ }
+
+ /**
+ * The options in their legacy flat-token form, including a leading {@code WITH}. This is an
+ * unmodifiable snapshot of {@link #getOptions()}; edit the options or use
+ * {@link #setDatabaseOptions(List)} instead.
+ *
+ * @return the option tokens, {@code null} when no options were given
*/
public List getDatabaseOptions() {
- return databaseOptions;
+ if (options.isEmpty() && !useWith) {
+ return null;
+ }
+ List tokens = new ArrayList<>();
+ if (useWith) {
+ tokens.add("WITH");
+ }
+ options.forEach(option -> tokens.addAll(option.getTokens()));
+ return Collections.unmodifiableList(tokens);
}
+ /**
+ * Replaces all options, including {@code WITH}, with one raw option of these tokens. The tokens
+ * are not parsed.
+ *
+ * @param databaseOptions the option tokens, or {@code null} to remove all options
+ * @return this statement
+ */
public CreateDatabase setDatabaseOptions(List databaseOptions) {
- this.databaseOptions = databaseOptions;
- this.options = null;
+ options.clear();
+ useWith = false;
+ if (databaseOptions != null && !databaseOptions.isEmpty()) {
+ options.add(DatabaseOption.raw(databaseOptions));
+ }
return this;
}
+ /**
+ * The options in source order, as a mutable list.
+ *
+ * @return the options, empty if there are none
+ */
public List getOptions() {
return options;
}
+ /**
+ * Replaces the options with a copy of the given list.
+ *
+ * @param options the options, or {@code null} to remove all options
+ * @return this statement
+ */
public CreateDatabase setOptions(List options) {
- this.options = options;
- this.databaseOptions = options == null ? null
- : options.stream().flatMap(option -> option.getTokens().stream())
- .collect(Collectors.toList());
+ List copy =
+ options == null ? Collections.emptyList() : new ArrayList<>(options);
+ this.options.clear();
+ this.options.addAll(copy);
return this;
}
public Optional getOption(DatabaseOption.Kind kind) {
- return Optional.ofNullable(options).orElseGet(Collections::emptyList).stream()
- .filter(option -> option.getKind() == kind).findFirst();
+ return options.stream().filter(option -> option.getKind() == kind).findFirst();
}
public CreateDatabase withDatabaseName(String databaseName) {
@@ -98,6 +143,10 @@ public CreateDatabase withIfNotExists(boolean hasIfNotExists) {
return this.setIfNotExists(hasIfNotExists);
}
+ public CreateDatabase withUseWith(boolean useWith) {
+ return this.setUseWith(useWith);
+ }
+
public CreateDatabase withDatabaseOptions(List databaseOptions) {
return this.setDatabaseOptions(databaseOptions);
}
@@ -107,40 +156,51 @@ public CreateDatabase withOptions(List options) {
}
public CreateDatabase addOptions(DatabaseOption... options) {
- List collection =
- Optional.ofNullable(getOptions()).orElseGet(ArrayList::new);
- Collections.addAll(collection, options);
- return withOptions(collection);
+ Collections.addAll(this.options, options);
+ return this;
}
+ /**
+ * Appends one raw option of these tokens, keeping the existing options.
+ *
+ * @param databaseOptions the option tokens
+ * @return this statement
+ */
public CreateDatabase addDatabaseOptions(String... databaseOptions) {
- List collection =
- Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new);
- Collections.addAll(collection, databaseOptions);
- return this.withDatabaseOptions(collection);
+ return addDatabaseOptions(Arrays.asList(databaseOptions));
}
+ /**
+ * Appends one raw option of these tokens, keeping the existing options.
+ *
+ * @param databaseOptions the option tokens
+ * @return this statement
+ */
public CreateDatabase addDatabaseOptions(Collection databaseOptions) {
- List collection =
- Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new);
- collection.addAll(databaseOptions);
- return this.withDatabaseOptions(collection);
+ if (!databaseOptions.isEmpty()) {
+ options.add(DatabaseOption.raw(new ArrayList<>(databaseOptions)));
+ }
+ return this;
}
@Override
public String toString() {
- String sql = "CREATE DATABASE";
+ StringBuilder sql = new StringBuilder("CREATE DATABASE");
if (hasIfNotExists) {
- sql += " IF NOT EXISTS";
+ sql.append(" IF NOT EXISTS");
}
if (databaseName != null) {
- sql += " " + databaseName;
+ sql.append(' ').append(databaseName);
+ }
+ if (useWith) {
+ sql.append(" WITH");
}
- if (options != null && !options.isEmpty()) {
- sql += " " + options.stream().map(Object::toString).collect(Collectors.joining(" "));
- } else if (databaseOptions != null && !databaseOptions.isEmpty()) {
- sql += " " + String.join(" ", databaseOptions);
+ for (DatabaseOption option : options) {
+ String text = option.toString();
+ if (!text.isEmpty()) {
+ sql.append(' ').append(text);
+ }
}
- return sql;
+ return sql.toString();
}
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java b/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java
index 497aa91d7..4a6c2b761 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java
@@ -16,12 +16,19 @@
/**
* A structured option shared by MySQL {@code CREATE/ALTER DATABASE} and
- * {@code CREATE/ALTER SCHEMA}.
+ * {@code CREATE/ALTER SCHEMA}, and by PostgreSQL {@code CREATE/ALTER DATABASE}. The name keeps the
+ * written spelling, for example {@code CHARSET} for {@link Kind#CHARACTER_SET}. Values keep their
+ * SQL text, including string quotes and signs. Unrecognized option text is an {@link Kind#OTHER}
+ * option with raw tokens.
*/
public class DatabaseOption implements Serializable {
+ /**
+ * The option. MySQL uses {@code CHARACTER_SET} to {@code READ_ONLY}, PostgreSQL uses
+ * {@code OWNER} to {@code OID}, and {@code OTHER} holds raw tokens.
+ */
public enum Kind {
- CHARACTER_SET, COLLATE, ENCRYPTION, READ_ONLY, OTHER
+ CHARACTER_SET, COLLATE, ENCRYPTION, READ_ONLY, OWNER, TEMPLATE, ENCODING, STRATEGY, LOCALE, LC_COLLATE, LC_CTYPE, BUILTIN_LOCALE, ICU_LOCALE, ICU_RULES, LOCALE_PROVIDER, COLLATION_VERSION, TABLESPACE, ALLOW_CONNECTIONS, CONNECTION_LIMIT, IS_TEMPLATE, OID, OTHER
}
private Kind kind = Kind.OTHER;
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 4c3b8f3f8..ce48fdf0d 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/statement/role/AlterRole.java b/src/main/java/net/sf/jsqlparser/statement/role/AlterRole.java
index e2e472b7a..27b452782 100644
--- a/src/main/java/net/sf/jsqlparser/statement/role/AlterRole.java
+++ b/src/main/java/net/sf/jsqlparser/statement/role/AlterRole.java
@@ -14,9 +14,16 @@
import java.util.function.Consumer;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.statement.ConfigurationParameterChange;
+import net.sf.jsqlparser.statement.ConfigurationParameterChange.ValueKind;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitor;
+/**
+ * PostgreSQL {@code ALTER ROLE}, {@code ALTER USER} or {@code ALTER GROUP}. The {@code SET} and
+ * {@code RESET} actions use the shared {@link ConfigurationParameterChange}; the parameter
+ * accessors of this class read and edit that change.
+ */
public class AlterRole implements Statement {
public enum Action {
OPTIONS, RENAME, SET, RESET, ADD_USER, DROP_USER
@@ -28,11 +35,7 @@ public enum Action {
private boolean useWith;
private String newName;
private String database;
- private String parameter;
- private boolean useEquals;
- private boolean fromCurrent;
- private boolean useDefault;
- private ExpressionList values;
+ private ConfigurationParameterChange parameterChange;
private List users;
private List options = new ArrayList<>();
@@ -52,12 +55,68 @@ public void setName(String name) {
this.name = name;
}
+ /**
+ * The role action. For {@code SET} and {@code RESET}, the action of the parameter change
+ * decides which of the two is returned.
+ *
+ * @return the action
+ */
public Action getAction() {
+ if (parameterChange != null && (action == Action.SET || action == Action.RESET)) {
+ return parameterChange.getAction() == ConfigurationParameterChange.Action.RESET
+ ? Action.RESET
+ : Action.SET;
+ }
return action;
}
+ /**
+ * Selects the role action. {@code SET} and {@code RESET} are also applied to the parameter
+ * change, which is created when missing. Other actions keep the parameter change for a later
+ * switch back.
+ *
+ * @param action the action
+ */
public void setAction(Action action) {
this.action = action;
+ if (action == Action.SET || action == Action.RESET) {
+ ensureParameterChange().setAction(action == Action.RESET
+ ? ConfigurationParameterChange.Action.RESET
+ : ConfigurationParameterChange.Action.SET);
+ }
+ }
+
+ /**
+ * The configuration parameter change of the {@code SET} and {@code RESET} actions.
+ *
+ * @return the change, or {@code null} if none was set
+ */
+ public ConfigurationParameterChange getParameterChange() {
+ return parameterChange;
+ }
+
+ /**
+ * Sets the configuration parameter change; a non-null change also selects its {@code SET} or
+ * {@code RESET} action.
+ *
+ * @param parameterChange the change
+ */
+ public void setParameterChange(ConfigurationParameterChange parameterChange) {
+ this.parameterChange = parameterChange;
+ if (parameterChange != null) {
+ action = parameterChange.getAction() == ConfigurationParameterChange.Action.RESET
+ ? Action.RESET
+ : Action.SET;
+ }
+ }
+
+ private ConfigurationParameterChange ensureParameterChange() {
+ if (parameterChange == null) {
+ parameterChange = new ConfigurationParameterChange(action == Action.RESET
+ ? ConfigurationParameterChange.Action.RESET
+ : ConfigurationParameterChange.Action.SET, null);
+ }
+ return parameterChange;
}
public boolean isUseWith() {
@@ -93,43 +152,75 @@ public void setDatabase(String database) {
}
public String getParameter() {
- return parameter;
+ return parameterChange == null ? null : parameterChange.getParameter();
}
public void setParameter(String parameter) {
- this.parameter = parameter;
+ ensureParameterChange().setParameter(parameter);
}
public boolean isUseEquals() {
- return useEquals;
+ return parameterChange != null && parameterChange.isUseEquals();
}
public void setUseEquals(boolean useEquals) {
- this.useEquals = useEquals;
+ ensureParameterChange().setUseEquals(useEquals);
}
+ /**
+ * Whether the parameter change assigns {@code FROM CURRENT}.
+ *
+ * @return {@code true} for {@link ValueKind#FROM_CURRENT}
+ */
public boolean isFromCurrent() {
- return fromCurrent;
+ return hasValueKind(ValueKind.FROM_CURRENT);
}
+ /**
+ * Selects {@link ValueKind#FROM_CURRENT}; {@code false} returns from it to the explicit values.
+ *
+ * @param fromCurrent whether to assign {@code FROM CURRENT}
+ */
public void setFromCurrent(boolean fromCurrent) {
- this.fromCurrent = fromCurrent;
+ setValueKind(ValueKind.FROM_CURRENT, fromCurrent);
}
+ /**
+ * Whether the parameter change assigns {@code DEFAULT}.
+ *
+ * @return {@code true} for {@link ValueKind#DEFAULT}
+ */
public boolean isUseDefault() {
- return useDefault;
+ return hasValueKind(ValueKind.DEFAULT);
}
+ /**
+ * Selects {@link ValueKind#DEFAULT}; {@code false} returns from it to the explicit values.
+ *
+ * @param useDefault whether to assign {@code DEFAULT}
+ */
public void setUseDefault(boolean useDefault) {
- this.useDefault = useDefault;
+ setValueKind(ValueKind.DEFAULT, useDefault);
+ }
+
+ private boolean hasValueKind(ValueKind kind) {
+ return parameterChange != null && parameterChange.getValueKind() == kind;
+ }
+
+ private void setValueKind(ValueKind kind, boolean selected) {
+ if (selected) {
+ ensureParameterChange().setValueKind(kind);
+ } else if (hasValueKind(kind)) {
+ parameterChange.setValueKind(ValueKind.VALUES);
+ }
}
public ExpressionList getValues() {
- return values;
+ return parameterChange == null ? null : parameterChange.getValues();
}
public void setValues(ExpressionList values) {
- this.values = values;
+ ensureParameterChange().setValues(values);
}
public List getUsers() {
@@ -145,7 +236,7 @@ public void appendTo(StringBuilder sql, Consumer visitor) {
if (database != null) {
sql.append(" IN DATABASE ").append(database);
}
- switch (action) {
+ switch (getAction()) {
case OPTIONS:
if (useWith) {
sql.append(" WITH");
@@ -159,25 +250,11 @@ public void appendTo(StringBuilder sql, Consumer visitor) {
sql.append(" RENAME TO ").append(newName);
break;
case RESET:
- sql.append(" RESET ").append(parameter);
- break;
case SET:
- sql.append(" SET ").append(parameter);
- if (fromCurrent) {
- sql.append(" FROM CURRENT");
- } else {
- sql.append(useEquals ? " = " : " TO ");
- if (useDefault) {
- sql.append("DEFAULT");
- } else {
- for (int i = 0; i < values.size(); i++) {
- if (i > 0) {
- sql.append(", ");
- }
- visitor.accept(values.get(i));
- }
- }
+ if (parameterChange == null) {
+ throw new IllegalStateException("Expected a configuration parameter change");
}
+ parameterChange.appendTo(sql.append(' '), visitor);
break;
case ADD_USER:
case DROP_USER:
diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
index aeecdb89d..1045eae02 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;
@@ -3133,4 +3135,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 2490baa3e..04f07e0e6 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;
@@ -838,7 +840,8 @@ public StringBuilder visit(AlterRelation statement, S context) {
@Override
public StringBuilder visit(AlterDatabase statement, S context) {
- return statement.appendTo(builder);
+ return statement.appendTo(builder,
+ expression -> expression.accept(expressionDeParser, context));
}
@Override
@@ -942,4 +945,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 de84d3f82..f8b709f2b 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
@@ -116,6 +116,14 @@ public enum MariaDbVersion implements Version {
Feature.createSchema, // https://mariadb.com/kb/en/create-trigger/
Feature.createTrigger,
+ // https://mariadb.com/kb/en/create-database/
+ // 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 6aabf1fe0..a8bf66abb 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
@@ -93,6 +93,14 @@ public enum MySqlVersion implements Version {
Feature.alterTable, // https://dev.mysql.com/doc/refman/8.0/en/alter-view.html
Feature.alterView,
+ // https://dev.mysql.com/doc/refman/8.0/en/create-database.html
+ // 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 023a8c0e1..991848350 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
@@ -120,6 +120,17 @@ public enum PostgresqlVersion implements Version {
Feature.createViewMaterialized, // https://www.postgresql.org/docs/current/sql-alterview.html
// Feature.alterView,
+ // https://www.postgresql.org/docs/current/sql-createdatabase.html
+ // 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 1de8380a7..0d98797d6 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;
@@ -85,6 +87,7 @@
import net.sf.jsqlparser.statement.analyze.Analyze;
import net.sf.jsqlparser.statement.comment.Comment;
import net.sf.jsqlparser.statement.create.database.CreateDatabase;
+import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
import net.sf.jsqlparser.statement.create.event.AlterEvent;
import net.sf.jsqlparser.statement.create.event.CreateEvent;
import net.sf.jsqlparser.statement.create.function.CreateFunction;
@@ -440,6 +443,16 @@ public Void visit(CreateDatabase aThis, S context) {
return null;
}
+ @Override
+ public Void visit(AlterDatabase statement, S context) {
+ validateFeature(Feature.alterDatabase);
+ if (statement.getParameterChange() != null
+ && statement.getParameterChange().getValues() != null) {
+ statement.getParameterChange().getValues().forEach(this::validateOptionalExpression);
+ }
+ return null;
+ }
+
@Override
public Void visit(CreateUser createUser, S context) {
return null;
@@ -1186,6 +1199,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 2210dbe9d..9314b88f3 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -269,9 +269,247 @@ public class CCJSqlParser extends AbstractJSqlParser {
}
private boolean isMySqlDatabaseOptionAhead() {
- int offset = getToken(1).kind == K_DEFAULT ? 2 : 1;
- int kind = getToken(offset).kind;
- return kind == K_CHARACTER || kind == K_COLLATE || kind == K_ENCRYPTION;
+ return isMySqlDatabaseOptionAhead(1);
+ }
+
+ private boolean isMySqlDatabaseOptionAhead(int offset) {
+ int start = getToken(offset).kind == K_DEFAULT ? offset + 1 : offset;
+ Token token = getToken(start);
+ return token.kind == K_CHARACTER || token.kind == K_COLLATE || token.kind == K_ENCRYPTION
+ || token.kind == K_CHAR && getToken(start + 1).kind == K_SET
+ || token.kind == S_IDENTIFIER && "CHARSET".equalsIgnoreCase(token.image);
+ }
+
+ /**
+ * Whether MySQL's ALTER DATABASE omits the database name before its options. CHARSET can also
+ * be a database name, so it starts an option only when "=" follows, or when its value ends the
+ * statement or precedes another option.
+ */
+ private boolean isAlterDatabaseNameOmitted() {
+ Token first = getToken(1);
+ if (first.kind == K_READ) {
+ return true;
+ }
+ if (!isMySqlDatabaseOptionAhead(1)) {
+ return false;
+ }
+ if (first.kind != S_IDENTIFIER || "=".equals(getToken(2).image)) {
+ return true;
+ }
+ Token afterValue = getToken(3);
+ return afterValue.kind == EOF || afterValue.kind == ST_SEMICOLON || afterValue.kind == K_READ
+ || isMySqlDatabaseOptionAhead(3);
+ }
+
+ /** Whether a PostgreSQL CREATE/ALTER DATABASE option name starts at the offset. */
+ private boolean isPostgreSqlDatabaseOptionAhead(int offset) {
+ Token token = getToken(offset);
+ if (token.kind == K_ENCODING || token.kind == K_TABLESPACE) {
+ return true;
+ }
+ if (token.kind != S_IDENTIFIER) {
+ return false;
+ }
+ String name = token.image.toUpperCase(Locale.ROOT);
+ if ("CONNECTION".equals(name)) {
+ return getToken(offset + 1).kind == K_LIMIT;
+ }
+ return Arrays.asList("OWNER", "TEMPLATE", "STRATEGY", "LOCALE", "LC_COLLATE", "LC_CTYPE",
+ "BUILTIN_LOCALE", "ICU_LOCALE", "ICU_RULES", "LOCALE_PROVIDER", "COLLATION_VERSION",
+ "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 CREATE FUNCTION or PROCEDURE starts here. */
+ private boolean isMySqlCreateRoutineCharacteristicAhead() {
+ return isMySqlRoutineCharacteristicAhead(1) || isWordSequenceAhead(1, "DETERMINISTIC")
+ || isWordSequenceAhead(1, "NOT", "DETERMINISTIC");
+ }
+
+ /** 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 {
@@ -4116,6 +4354,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;
@@ -4147,7 +4397,7 @@ Block Block() #Block : {
block.setStatements(stmts);
}
- [LOOKAHEAD(2) { block.setSemicolonAfterEnd(true); } ]
+
{
return block;
}
@@ -15730,6 +15980,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():
{
@@ -15785,7 +16039,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;
@@ -15800,7 +16105,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); }
@@ -15812,7 +16124,7 @@ CreateTrigger CreateTrigger():
createTrigger.setOrder(order);
createTrigger.setOtherTrigger(otherTrigger);
} ]
- ( body=SingleStatement() | body=Block() ) { createTrigger.setBody(body); }
+ body=MySqlTriggerBody() { createTrigger.setBody(body); }
{ return createTrigger; }
}
@@ -15888,6 +16200,16 @@ DatabaseOption MySqlDatabaseOption():
name = "CHARACTER SET";
}
|
+ {
+ kind = DatabaseOption.Kind.CHARACTER_SET;
+ name = "CHAR SET";
+ }
+ |
+ LOOKAHEAD({ isKeywordAhead("CHARSET") }) ContextualKeyword("CHARSET") {
+ kind = DatabaseOption.Kind.CHARACTER_SET;
+ name = "CHARSET";
+ }
+ |
{
kind = DatabaseOption.Kind.COLLATE;
name = "COLLATE";
@@ -15903,35 +16225,144 @@ DatabaseOption MySqlDatabaseOption():
{ return new DatabaseOption(kind, name, value, useDefault, useEquals); }
}
-/** Reuses CREATE's database options; READ ONLY is specific to ALTER. */
+/** PostgreSQL option of CREATE DATABASE; ALTER DATABASE accepts the same option list. */
+DatabaseOption PostgreSqlDatabaseOption():
+{
+ Token token;
+ DatabaseOption.Kind kind;
+ String name;
+ String value;
+ boolean useEquals = false;
+}
+{
+ (
+ LOOKAHEAD({ isKeywordAhead("CONNECTION") }) ContextualKeyword("CONNECTION")
+ { kind = DatabaseOption.Kind.CONNECTION_LIMIT; name = "CONNECTION LIMIT"; }
+ |
+ { kind = DatabaseOption.Kind.ENCODING; name = "ENCODING"; }
+ |
+ { kind = DatabaseOption.Kind.TABLESPACE; name = "TABLESPACE"; }
+ |
+ LOOKAHEAD({ isPostgreSqlDatabaseOptionAhead(1) }) token= {
+ name = token.image.toUpperCase(Locale.ROOT);
+ kind = DatabaseOption.Kind.valueOf(name);
+ }
+ )
+ [ "=" { useEquals = true; } ]
+ value=PostgreSqlDatabaseOptionValue()
+ { return new DatabaseOption(kind, name, value, false, useEquals); }
+}
+
+/** A name, string, signed number, boolean or DEFAULT, as written. */
+String PostgreSqlDatabaseOptionValue():
+{
+ Token token;
+ String value;
+ String sign = "";
+}
+{
+ (
+ LOOKAHEAD(1) token= { value = token.image; }
+ |
+ LOOKAHEAD(1) token= { value = token.image; }
+ |
+ ( token= | token= | token= ) { value = token.image; }
+ |
+ [ "+" { sign = "+"; } | "-" { sign = "-"; } ]
+ ( token= | token= ) { value = sign + token.image; }
+ |
+ value=RelObjectName()
+ )
+ { return value; }
+}
+
+/**
+ * MySQL ALTER DATABASE/SCHEMA options and the PostgreSQL ALTER DATABASE forms. Options reuse the
+ * CREATE DATABASE productions; READ ONLY is specific to MySQL's ALTER. Only MySQL options may
+ * follow an omitted name.
+ */
AlterDatabase AlterDatabase():
{
AlterDatabase result = new AlterDatabase();
String name;
DatabaseOption option;
- Token value;
- boolean equals;
+ ConfigurationParameterChange change;
}
{
- ( | { result.setUseSchemaKeyword(true); } )
- [ LOOKAHEAD({ !isMySqlDatabaseOptionAhead() && getToken(1).kind != K_READ })
- name=RelObjectName() { result.setDatabaseName(name); } ]
(
+ { result.setUseSchemaKeyword(true); }
(
- LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) option=MySqlDatabaseOption()
+ LOOKAHEAD({ isAlterDatabaseNameOmitted() }) MySqlAlterDatabaseOptions(result)
|
- { equals = false; }
- [ "=" { equals = true; } ]
- ( value= | value= ) {
- requireDdlSyntax(value.kind == K_DEFAULT || "0".equals(value.image) || "1".equals(value.image),
- "READ ONLY requires DEFAULT, 0, or 1");
- option = new DatabaseOption(DatabaseOption.Kind.READ_ONLY, "READ ONLY", value.image, false, equals);
- }
- ) { result.getOptions().add(option); }
- )+
+ name=RelObjectName() { result.setDatabaseName(name); } MySqlAlterDatabaseOptions(result)
+ )
+ |
+
+ (
+ LOOKAHEAD({ isAlterDatabaseNameOmitted() }) MySqlAlterDatabaseOptions(result)
+ |
+ name=RelObjectName() { result.setDatabaseName(name); }
+ (
+ name=RelObjectName()
+ { result.setAction(AlterDatabase.Action.RENAME); result.setNewName(name); }
+ |
+ LOOKAHEAD({ isKeywordAhead("OWNER") && getToken(2).kind == K_TO })
+ ContextualKeyword("OWNER") name=RelObjectName()
+ { result.setAction(AlterDatabase.Action.OWNER); result.setOwner(name); }
+ |
+ LOOKAHEAD({ getToken(1).kind == K_SET && getToken(2).kind == K_TABLESPACE
+ && getToken(3).kind != K_TO && getToken(3).kind != K_FROM && !"=".equals(getToken(3).image) })
+ name=RelObjectName()
+ { result.setAction(AlterDatabase.Action.SET_TABLESPACE); result.setTablespace(name); }
+ |
+ ContextualKeyword("COLLATION")
+ { result.setAction(AlterDatabase.Action.REFRESH_COLLATION_VERSION); }
+ |
+ change=ConfigurationParameterChange()
+ { result.setAction(AlterDatabase.Action.CONFIGURATION_PARAMETER); result.setParameterChange(change); }
+ |
+ [ { result.setUseWith(true); } ]
+ (
+ (
+ LOOKAHEAD({ isPostgreSqlDatabaseOptionAhead(1) }) option=PostgreSqlDatabaseOption()
+ |
+ option=MySqlAlterDatabaseOption()
+ ) { result.getOptions().add(option); }
+ )+
+ )
+ )
+ )
{ return result; }
}
+void MySqlAlterDatabaseOptions(AlterDatabase result):
+{ DatabaseOption option; }
+{
+ ( option=MySqlAlterDatabaseOption() { result.getOptions().add(option); } )+
+}
+
+/** MySQL ALTER DATABASE option: a CREATE DATABASE option or READ ONLY. */
+DatabaseOption MySqlAlterDatabaseOption():
+{
+ DatabaseOption option;
+ Token value;
+ boolean equals = false;
+}
+{
+ (
+ LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) option=MySqlDatabaseOption()
+ |
+
+ [ "=" { equals = true; } ]
+ ( value= | value= ) {
+ requireDdlSyntax(value.kind == K_DEFAULT || "0".equals(value.image) || "1".equals(value.image),
+ "READ ONLY requires DEFAULT, 0, or 1");
+ option = new DatabaseOption(DatabaseOption.Kind.READ_ONLY, "READ ONLY", value.image, false, equals);
+ }
+ )
+ { return option; }
+}
+
List PathSpecification():
{
Token tk;
@@ -15945,6 +16376,7 @@ List PathSpecification():
}
}
+/** Typed MySQL and PostgreSQL options; other option text is kept as one raw option. */
CreateDatabase CreateDatabase():
{
CreateDatabase createDatabase = new CreateDatabase();
@@ -15956,12 +16388,20 @@ CreateDatabase CreateDatabase():
[ LOOKAHEAD(2) { createDatabase.setIfNotExists(true); } ]
databaseName = RelObjectName() { createDatabase.setDatabaseName(databaseName); }
+ [ LOOKAHEAD({ getToken(1).kind == K_WITH && isPostgreSqlDatabaseOptionAhead(2) })
+ { createDatabase.setUseWith(true); } ]
(
- LOOKAHEAD({ isMySqlDatabaseOptionAhead() })
- databaseOption=MySqlDatabaseOption() { createDatabase.addOptions(databaseOption); }
+ LOOKAHEAD({ isMySqlDatabaseOptionAhead() || isPostgreSqlDatabaseOptionAhead(1) })
+ (
+ LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) databaseOption=MySqlDatabaseOption()
+ |
+ databaseOption=PostgreSqlDatabaseOption()
+ ) { createDatabase.addOptions(databaseOption); }
)*
[ databaseOptions = captureRest() {
- createDatabase.addOptions(DatabaseOption.raw(databaseOptions));
+ if (!databaseOptions.isEmpty()) {
+ createDatabase.addOptions(DatabaseOption.raw(databaseOptions));
+ }
} ]
{
return createDatabase;
@@ -19584,6 +20024,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") })
@@ -19591,10 +20036,13 @@ Statement Alter():
|
statement = AlterDefaultPrivileges()
|
- LOOKAHEAD({ getToken(1).kind == K_DATABASE || (getToken(1).kind == K_SCHEMA
- && Dialect.MYSQL.name().equals(getAsString(Feature.dialect))) })
+ LOOKAHEAD({ (getToken(1).kind == K_DATABASE || getToken(1).kind == K_SCHEMA)
+ && Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) })
statement = AlterDatabase()
|
+ // Other dialects keep unmodelled ALTER DATABASE forms as unsupported statements.
+ LOOKAHEAD(AlterDatabase()) statement = AlterDatabase()
+ |
LOOKAHEAD() statement = AlterSchema()
|
LOOKAHEAD() statement = AlterType()
@@ -20412,8 +20860,8 @@ AlterSchema AlterSchema():
AlterRole AlterRole():
{
- AlterRole role = new AlterRole(); String name; RoleOption option; Token token; Expression value;
- ExpressionList values = new ExpressionList(); List names;
+ AlterRole role = new AlterRole(); String name; RoleOption option; Token token;
+ ConfigurationParameterChange change; List names;
}
{
( token= | token= | token= ) { role.setCommand(accessEnum(CreateRole.Command.class, token.image)); }
@@ -20425,22 +20873,7 @@ AlterRole AlterRole():
( { role.setAction(AlterRole.Action.ADD_USER); } | { role.setAction(AlterRole.Action.DROP_USER); } )
names=AccessNames() { requireAccessSyntax(role.getCommand() == CreateRole.Command.GROUP, "ADD/DROP USER requires ALTER GROUP"); role.setUsers(names); }
|
- name=AccessQualifiedName() { role.setAction(AlterRole.Action.SET); role.setParameter(name); }
- (
- { role.setFromCurrent(true); }
- |
- ( | "=" { role.setUseEquals(true); } )
- (
- LOOKAHEAD(1) { role.setUseDefault(true); }
- |
- value=AccessSettingValue() { values.add(value); }
- ( "," value=AccessSettingValue() { values.add(value); } )*
- { role.setValues(values); }
- )
- )
- |
- ( LOOKAHEAD(1) { role.setParameter("ALL"); } | name=AccessQualifiedName() { role.setParameter(name); } )
- { role.setAction(AlterRole.Action.RESET); }
+ change=ConfigurationParameterChange() { role.setParameterChange(change); }
|
[ { role.setUseWith(true); } ]
option=RoleOption(false) { role.getOptions().add(option); role.setAction(AlterRole.Action.OPTIONS); }
@@ -20456,6 +20889,39 @@ AlterRole AlterRole():
}
}
+/**
+ * PostgreSQL SET/RESET of a configuration parameter, shared by ALTER ROLE, ALTER DATABASE and
+ * ALTER FUNCTION/PROCEDURE/ROUTINE.
+ */
+ConfigurationParameterChange ConfigurationParameterChange():
+{
+ ConfigurationParameterChange change = new ConfigurationParameterChange();
+ String name;
+ Expression value;
+ ExpressionList values = new ExpressionList();
+}
+{
+ (
+ name=AccessQualifiedName() { change.setParameter(name); }
+ (
+ { change.setValueKind(ConfigurationParameterChange.ValueKind.FROM_CURRENT); }
+ |
+ ( | "=" { change.setUseEquals(true); } )
+ (
+ LOOKAHEAD(1) { change.setValueKind(ConfigurationParameterChange.ValueKind.DEFAULT); }
+ |
+ value=AccessSettingValue() { values.add(value); }
+ ( "," value=AccessSettingValue() { values.add(value); } )*
+ { change.setValues(values); }
+ )
+ )
+ |
+ { change.setAction(ConfigurationParameterChange.Action.RESET); }
+ ( LOOKAHEAD(1) { change.setParameter("ALL"); } | name=AccessQualifiedName() { change.setParameter(name); } )
+ )
+ { return change; }
+}
+
Expression AccessSettingValue():
{ Expression value; Token token; }
{ ( LOOKAHEAD(1) token= { value = new Column(token.image); } | value=PrimaryExpression() ) { return value; } }
@@ -20641,6 +21107,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;
@@ -20956,14 +21443,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()
@@ -20997,19 +21485,23 @@ Statement Create():
}
}
+/** With a MySQL dialect, the characteristics are also parsed; the tokens still render the statement. */
CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
{
CreateFunctionalStatement type = null;
List tokens = new LinkedList();
String statementType = null;
+ MySqlDefiner definer = null;
+ List characteristics = new ArrayList();
}
{
+ [ definer=MySqlRoutineDefiner() ]
(
{ statementType = "FUNCTION"; }
|
{ statementType = "PROCEDURE"; }
)
- tokens=captureFunctionBody()
+ tokens=captureFunctionBody(isMySqlDialect() ? characteristics : null)
{
if(statementType.equals("FUNCTION")) {
type = new CreateFunction(isUsingOrReplace, tokens);
@@ -21017,11 +21509,194 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
if(statementType.equals("PROCEDURE")) {
type = new CreateProcedure(isUsingOrReplace, tokens);
}
-
+ type.setFunctionDeclarationParts(tokens, characteristics);
+ 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(false) { 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);
+ }
+}
+
+/** MySQL routine characteristic; only CREATE accepts [NOT] DETERMINISTIC. */
+RoutineCharacteristic MySqlRoutineCharacteristic(boolean create):
+{
+ 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({ create && isWordSequenceAhead(1, "DETERMINISTIC") }) ContextualKeyword("DETERMINISTIC")
+ { kind = RoutineCharacteristic.Kind.DETERMINISTIC; }
+ |
+ LOOKAHEAD({ create && isWordSequenceAhead(1, "NOT", "DETERMINISTIC") })
+ ContextualKeyword("DETERMINISTIC") { kind = RoutineCharacteristic.Kind.NOT_DETERMINISTIC; }
+ |
+ 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;
@@ -21785,16 +22460,29 @@ List captureRest() {
* PostgreSQL AS string bodies can use ordinary, escape or dollar quotes.
* The boundary tracker distinguishes simple statements, declaration sections,
* compound bodies, and expression CASE from procedural END qualifiers.
+ * With a list, MySQL characteristics between the header and the body are also
+ * parsed into it; their tokens are captured as well.
*/
JAVACODE
-List captureFunctionBody() {
+List captureFunctionBody(List characteristics) {
List tokens = new LinkedList();
Token tok;
RoutineBodyBoundary boundary = new RoutineBodyBoundary();
while(true) {
tok = getToken(1);
int l = tokens.size();
+ if (characteristics != null && boundary.isBeforeBody() && isMySqlCreateRoutineCharacteristicAhead()) {
+ characteristics.add(MySqlRoutineCharacteristic(true));
+ for (Token consumed = tok; ; consumed = consumed.next) {
+ tokens.add(consumed.image);
+ boundary.accept(consumed, consumed == token ? getToken(1) : consumed.next);
+ if (consumed == token) {
+ break;
+ }
+ }
+ continue;
+ }
if (boundary.endsAt(tok)) {
if (tok.kind == ST_SEMICOLON) {
tokens.add(tok.image);
diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst
index 5f3b0157b..36598eaa4 100644
--- a/src/site/sphinx/index.rst
+++ b/src/site/sphinx/index.rst
@@ -160,6 +160,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 901608d62..c02c8a3f5 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -1868,6 +1868,120 @@ visitors, both SQL renderers and metadata validation use this structured target.
Feature analysis reports ``MODIFIES_SESSION``; the directive itself inserts no rows.
The dedicated validation capability is ``setIdentityInsert``.
+Database options and configuration parameters
+---------------------------------------------
+
+``CreateDatabase``, MySQL's ``CREATE SCHEMA`` and ``AlterDatabase`` share the
+``DatabaseOption`` model. MySQL options are ``CHARACTER_SET`` (``getName()`` keeps
+the ``CHARACTER SET``, ``CHAR SET`` or ``CHARSET`` spelling), ``COLLATE``,
+``ENCRYPTION`` and, for ALTER only, ``READ_ONLY``. PostgreSQL options have one kind
+each, from ``OWNER`` and ``TEMPLATE`` to ``CONNECTION_LIMIT``, ``IS_TEMPLATE`` and
+``OID``; ``isUseWith()`` records the optional ``WITH``. Values keep their SQL text,
+such as ``'UTF8'`` or ``-1``. Other option text remains one raw ``OTHER`` option.
+
+``AlterDatabase.getAction()`` selects what is rendered: ``OPTIONS`` (the default),
+``RENAME``, ``OWNER``, ``SET_TABLESPACE``, ``REFRESH_COLLATION_VERSION`` or
+``CONFIGURATION_PARAMETER``. Payloads of the other actions are kept. ``SET`` and
+``RESET`` of a configuration parameter use ``ConfigurationParameterChange``, which
+``AlterRole`` shares:
+
+.. code-block:: java
+
+ AlterDatabase alter = (AlterDatabase) CCJSqlParserUtil.parse(
+ "ALTER DATABASE app SET search_path TO app, public",
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ ConfigurationParameterChange change = alter.getParameterChange();
+ change.getParameter(); // search_path
+ change.getValues(); // [app, public], a mutable ExpressionList
+ change.setValueKind(ConfigurationParameterChange.ValueKind.DEFAULT);
+ // ALTER DATABASE app SET search_path TO DEFAULT
+
+``getValueKind()`` is ``VALUES``, ``DEFAULT`` or ``FROM_CURRENT``; ``RESET ALL`` uses
+the parameter name ``ALL``. ``AlterRole`` keeps ``getParameter()``, ``isUseEquals()``,
+``isFromCurrent()``, ``isUseDefault()``, ``getValues()`` and their setters; they read
+and edit ``getParameterChange()``. ``FROM CURRENT`` and ``DEFAULT`` are now mutually
+exclusive: ``setFromCurrent(false)`` and ``setUseDefault(false)`` return to the values.
+
+Without the MySQL dialect, ``ALTER DATABASE`` forms that are neither MySQL nor
+PostgreSQL syntax, such as ``ALTER DATABASE d SET RECOVERY SIMPLE``, remain an
+``UnsupportedStatement``; ``Dialect.MYSQL`` reports them as errors. ``ALTER SCHEMA``
+followed by MySQL options is an ``AlterDatabase`` in every dialect, while PostgreSQL's
+``ALTER SCHEMA ... RENAME TO/OWNER TO`` remains an ``AlterSchema``. Validation
+reports the ``createDatabase`` and ``alterDatabase`` features, which the MySQL,
+MariaDB and PostgreSQL capabilities enable.
+
+``CreateDatabase.getOptions()`` is now a live list that is never null, and
+``setOptions`` copies its argument. ``getDatabaseOptions()`` is an unmodifiable
+token snapshot derived from the options, including ``WITH``, and is null without
+options. ``setDatabaseOptions`` replaces all options with one raw option, and
+``addDatabaseOptions`` appends one while keeping the typed options. Parsed
+statements no longer end with an empty raw option.
+
+References: MySQL `CREATE DATABASE `_,
+`ALTER DATABASE `_;
+PostgreSQL `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. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``,
+``CreateFunctionalStatement.getCharacteristics()`` lists the characteristics between the
+routine header and its body, such as ``COMMENT``, ``DETERMINISTIC``, ``READS SQL DATA`` or
+``SQL SECURITY INVOKER``, with the same ``RoutineCharacteristic`` kinds; ``DETERMINISTIC``
+and ``NOT_DETERMINISTIC`` occur only here. ``getFunctionDeclarationParts()`` still holds
+every token and renders the statement unchanged, so the list is read-only. Replacing the
+tokens with ``setFunctionDeclarationParts(parts)`` clears it, while
+``setFunctionDeclarationParts(parts, characteristics)`` sets both, and appending tokens
+keeps it. Without a MySQL dialect the list stays empty. 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/ConfigurationParameterChangeTest.java b/src/test/java/net/sf/jsqlparser/statement/ConfigurationParameterChangeTest.java
new file mode 100644
index 000000000..4f70e7746
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/ConfigurationParameterChangeTest.java
@@ -0,0 +1,166 @@
+/*-
+ * #%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 static org.assertj.core.api.Assertions.assertThat;
+
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.statement.ConfigurationParameterChange.Action;
+import net.sf.jsqlparser.statement.ConfigurationParameterChange.ValueKind;
+import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
+import net.sf.jsqlparser.statement.role.AlterRole;
+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 ConfigurationParameterChangeTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"SET search_path TO app, public", "SET work_mem = '64MB'",
+ "SET work_mem TO DEFAULT", "SET work_mem = DEFAULT", "SET work_mem FROM CURRENT",
+ "SET myext.setting = 1", "RESET work_mem", "RESET ALL"})
+ void testAlterRoleAndDatabaseShareTheClause(String clause) throws JSQLParserException {
+ AlterRole role = (AlterRole) CCJSqlParserUtil.parse("ALTER ROLE app " + clause);
+ AlterDatabase database =
+ (AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE app " + clause);
+
+ assertThat(role.getParameterChange().toString()).isEqualTo(clause);
+ assertThat(database.getParameterChange().toString()).isEqualTo(clause);
+ assertThat(database.getAction())
+ .isEqualTo(AlterDatabase.Action.CONFIGURATION_PARAMETER);
+ assertThat(role.getAction().name())
+ .isEqualTo(role.getParameterChange().getAction().name());
+ assertThat(deparse(role)).isEqualTo("ALTER ROLE app " + clause);
+ assertThat(deparse(database)).isEqualTo("ALTER DATABASE app " + clause);
+ }
+
+ @Test
+ void testParsedState() throws JSQLParserException {
+ ConfigurationParameterChange set = parseChange("SET search_path TO app, public");
+ assertThat(set.getAction()).isEqualTo(Action.SET);
+ assertThat(set.getParameter()).isEqualTo("search_path");
+ assertThat(set.getValueKind()).isEqualTo(ValueKind.VALUES);
+ assertThat(set.isUseEquals()).isFalse();
+ assertThat(set.getValues()).extracting(Object::toString).containsExactly("app", "public");
+
+ ConfigurationParameterChange setDefault = parseChange("SET work_mem = DEFAULT");
+ assertThat(setDefault.getValueKind()).isEqualTo(ValueKind.DEFAULT);
+ assertThat(setDefault.isUseEquals()).isTrue();
+ assertThat(setDefault.getValues()).isNull();
+
+ assertThat(parseChange("SET work_mem FROM CURRENT").getValueKind())
+ .isEqualTo(ValueKind.FROM_CURRENT);
+
+ ConfigurationParameterChange resetAll = parseChange("RESET ALL");
+ assertThat(resetAll.getAction()).isEqualTo(Action.RESET);
+ assertThat(resetAll.getParameter()).isEqualTo("ALL");
+ }
+
+ @Test
+ void testBuildAndEdit() {
+ ConfigurationParameterChange change =
+ new ConfigurationParameterChange(Action.SET, "statement_timeout")
+ .withUseEquals(true)
+ .withValues(new ExpressionList(new StringValue("5s")));
+ assertThat(change.toString()).isEqualTo("SET statement_timeout = '5s'");
+
+ change.setValueKind(ValueKind.DEFAULT);
+ assertThat(change.toString()).isEqualTo("SET statement_timeout = DEFAULT");
+ change.setValueKind(ValueKind.VALUES);
+ assertThat(change.toString()).isEqualTo("SET statement_timeout = '5s'");
+ change.setValueKind(ValueKind.FROM_CURRENT);
+ assertThat(change.toString()).isEqualTo("SET statement_timeout FROM CURRENT");
+
+ change.setAction(Action.RESET);
+ assertThat(change.toString()).isEqualTo("RESET statement_timeout");
+
+ AlterDatabase database = new AlterDatabase();
+ database.setDatabaseName("app");
+ database.setAction(AlterDatabase.Action.CONFIGURATION_PARAMETER);
+ database.setParameterChange(change);
+ assertThat(database.toString()).isEqualTo("ALTER DATABASE app RESET statement_timeout");
+ }
+
+ @Test
+ void testAlterRoleLegacyAccessorsReadTheChange() throws JSQLParserException {
+ AlterRole role = (AlterRole) CCJSqlParserUtil
+ .parse("ALTER ROLE ALL IN DATABASE appdb SET search_path = app, public");
+ assertThat(role.getAction()).isEqualTo(AlterRole.Action.SET);
+ assertThat(role.getParameter()).isEqualTo("search_path");
+ assertThat(role.isUseEquals()).isTrue();
+ assertThat(role.isUseDefault()).isFalse();
+ assertThat(role.isFromCurrent()).isFalse();
+ assertThat(role.getValues()).isSameAs(role.getParameterChange().getValues());
+
+ role.getValues().set(0, new Column("new_schema"));
+ role.setUseEquals(false);
+ assertThat(role.toString())
+ .isEqualTo(
+ "ALTER ROLE ALL IN DATABASE appdb SET search_path TO new_schema, public");
+
+ role.setUseDefault(true);
+ assertThat(role.getParameterChange().getValueKind()).isEqualTo(ValueKind.DEFAULT);
+ assertThat(role.toString()).endsWith("SET search_path TO DEFAULT");
+ role.setFromCurrent(true);
+ assertThat(role.isUseDefault()).isFalse();
+ assertThat(role.toString()).endsWith("SET search_path FROM CURRENT");
+ role.setFromCurrent(false);
+ assertThat(role.toString()).endsWith("SET search_path TO new_schema, public");
+
+ role.setAction(AlterRole.Action.RESET);
+ assertThat(role.getParameterChange().getAction()).isEqualTo(Action.RESET);
+ assertThat(role.toString()).isEqualTo("ALTER ROLE ALL IN DATABASE appdb RESET search_path");
+
+ role.getParameterChange().setAction(Action.SET);
+ assertThat(role.getAction()).isEqualTo(AlterRole.Action.SET);
+ }
+
+ @Test
+ void testAlterRoleLegacySetters() {
+ AlterRole role = new AlterRole();
+ role.setName("app");
+ assertThat(role.getParameterChange()).isNull();
+ assertThat(role.getParameter()).isNull();
+ assertThat(role.getValues()).isNull();
+ assertThat(role.isUseEquals()).isFalse();
+
+ role.setParameter("work_mem");
+ role.setAction(AlterRole.Action.SET);
+ role.setValues(new ExpressionList(new StringValue("64MB")));
+ assertThat(role.toString()).isEqualTo("ALTER ROLE app SET work_mem TO '64MB'");
+
+ role.setAction(AlterRole.Action.RENAME);
+ role.setNewName("app2");
+ assertThat(role.toString()).isEqualTo("ALTER ROLE app RENAME TO app2");
+ assertThat(role.getParameter()).isEqualTo("work_mem");
+
+ role.setParameterChange(new ConfigurationParameterChange(Action.RESET, "ALL"));
+ assertThat(role.getAction()).isEqualTo(AlterRole.Action.RESET);
+ assertThat(role.toString()).isEqualTo("ALTER ROLE app RESET ALL");
+ }
+
+ private static ConfigurationParameterChange parseChange(String clause)
+ throws JSQLParserException {
+ return ((AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE d " + clause))
+ .getParameterChange();
+ }
+
+ private static String deparse(Statement statement) {
+ StringBuilder builder = new StringBuilder();
+ statement.accept(new StatementDeParser(builder));
+ return builder.toString();
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java
index b15eb6580..0e4f938e7 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;
@@ -39,6 +41,12 @@ static Stream definitions() {
Arguments.of("ALTER DATABASE d READ ONLY = 0", AlterDatabase.class),
Arguments.of("ALTER DATABASE d CHARACTER SET utf8mb4 COLLATE utf8mb4_bin",
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 000000000..e0992bae1
--- /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 000000000..cf33ab0f3
--- /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.