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..a8d3d8825 100644
--- a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java
+++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java
@@ -39,6 +39,16 @@ private enum Block {
private boolean handlerAction;
private boolean statementStart = true;
+ /**
+ * Tracks a compound statement without a routine header, such as a MySQL trigger body. Its
+ * tokens belong to the body from the start, as after a routine's parameter list.
+ */
+ static RoutineBodyBoundary forCompoundStatement() {
+ RoutineBodyBoundary boundary = new RoutineBodyBoundary();
+ boundary.hasParameters = true;
+ return boundary;
+ }
+
boolean endsAt(Token token) throws ParseException {
if (token.kind == EOF) {
if (!blocks.isEmpty()) {
diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
index 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..c34037ada 100644
--- a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java
@@ -31,6 +31,7 @@ public enum Operation {
}
private Operation operation = Operation.CREATE;
+ private MySqlDefiner definer;
private FunctionReturnType returnType;
private List routineBodyParts;
@@ -42,6 +43,19 @@ public void setOperation(Operation operation) {
this.operation = operation;
}
+ /**
+ * The MySQL {@code DEFINER} account, rendered after {@code CREATE [OR REPLACE]}.
+ *
+ * @return the definer, or {@code null}
+ */
+ public MySqlDefiner getDefiner() {
+ return definer;
+ }
+
+ public void setDefiner(MySqlDefiner definer) {
+ this.definer = definer;
+ }
+
public FunctionReturnType getReturnType() {
return returnType;
}
@@ -139,6 +153,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer prin
if (orReplace && operation == Operation.CREATE) {
builder.append("OR REPLACE ");
}
+ if (definer != null) {
+ builder.append("DEFINER = ").append(definer).append(' ');
+ }
builder.append(kind).append(' ');
return appendDeclarationTo(builder, printer);
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java
new file mode 100644
index 000000000..93df5a7aa
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/RoutineCharacteristic.java
@@ -0,0 +1,263 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement;
+
+import java.io.Serializable;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+
+/**
+ * A characteristic set by {@code ALTER FUNCTION}, {@code ALTER PROCEDURE} or {@code ALTER ROUTINE}.
+ * PostgreSQL and MySQL share the model, and each {@link Kind} renders its own SQL:
+ *
+ *
+ * - PostgreSQL: the volatility, the null input behavior, {@code [NOT] LEAKPROOF},
+ * {@code [EXTERNAL] SECURITY}, {@code PARALLEL}, {@code COST}, {@code ROWS}, {@code SUPPORT} and
+ * the {@code SET} or {@code RESET} of a configuration parameter;
+ * - MySQL: {@code COMMENT}, {@code LANGUAGE SQL}, the SQL data access and
+ * {@code SQL SECURITY}.
+ *
+ *
+ * The security kinds are shared; {@link #getSecurityClause()} keeps the spelling. Values keep their
+ * SQL text, such as {@code 100}. The {@code COMMENT} literal is a {@link StringValue}.
+ */
+public class RoutineCharacteristic implements Serializable {
+
+ public enum Kind {
+ /** PostgreSQL {@code IMMUTABLE}. */
+ IMMUTABLE("IMMUTABLE"),
+ /** PostgreSQL {@code STABLE}. */
+ STABLE("STABLE"),
+ /** PostgreSQL {@code VOLATILE}. */
+ VOLATILE("VOLATILE"),
+ /** PostgreSQL {@code CALLED ON NULL INPUT}. */
+ CALLED_ON_NULL_INPUT("CALLED ON NULL INPUT"),
+ /** PostgreSQL {@code RETURNS NULL ON NULL INPUT}. */
+ RETURNS_NULL_ON_NULL_INPUT("RETURNS NULL ON NULL INPUT"),
+ /** PostgreSQL {@code STRICT}. */
+ STRICT("STRICT"),
+ /** PostgreSQL {@code LEAKPROOF}. */
+ LEAKPROOF("LEAKPROOF"),
+ /** PostgreSQL {@code NOT LEAKPROOF}. */
+ NOT_LEAKPROOF("NOT LEAKPROOF"),
+ /** {@code DEFINER}, preceded by the {@link SecurityClause}. */
+ SECURITY_DEFINER("DEFINER"),
+ /** {@code INVOKER}, preceded by the {@link SecurityClause}. */
+ SECURITY_INVOKER("INVOKER"),
+ /** PostgreSQL {@code PARALLEL UNSAFE}. */
+ PARALLEL_UNSAFE("PARALLEL UNSAFE"),
+ /** PostgreSQL {@code PARALLEL RESTRICTED}. */
+ PARALLEL_RESTRICTED("PARALLEL RESTRICTED"),
+ /** PostgreSQL {@code PARALLEL SAFE}. */
+ PARALLEL_SAFE("PARALLEL SAFE"),
+ /** PostgreSQL {@code COST} followed by the numeric {@link #getValue()}. */
+ COST("COST"),
+ /** PostgreSQL {@code ROWS} followed by the numeric {@link #getValue()}. */
+ ROWS("ROWS"),
+ /** PostgreSQL {@code SUPPORT} followed by the function name in {@link #getValue()}. */
+ SUPPORT("SUPPORT"),
+ /** PostgreSQL {@code SET} or {@code RESET}, see {@link #getParameterChange()}. */
+ CONFIGURATION_PARAMETER(null),
+ /** MySQL {@code COMMENT} followed by the string literal of {@link #getComment()}. */
+ COMMENT("COMMENT"),
+ /** MySQL {@code LANGUAGE SQL}. */
+ LANGUAGE_SQL("LANGUAGE SQL"),
+ /** MySQL {@code CONTAINS SQL}. */
+ CONTAINS_SQL("CONTAINS SQL"),
+ /** MySQL {@code NO SQL}. */
+ NO_SQL("NO SQL"),
+ /** MySQL {@code READS SQL DATA}. */
+ READS_SQL_DATA("READS SQL DATA"),
+ /** MySQL {@code MODIFIES SQL DATA}. */
+ MODIFIES_SQL_DATA("MODIFIES SQL DATA");
+
+ private final String keywords;
+
+ Kind(String keywords) {
+ this.keywords = keywords;
+ }
+
+ private boolean hasValue() {
+ return this == COST || this == ROWS || this == SUPPORT || this == COMMENT;
+ }
+ }
+
+ /** The words before {@code DEFINER} or {@code INVOKER}. */
+ public enum SecurityClause {
+ /** PostgreSQL {@code SECURITY}. */
+ SECURITY("SECURITY"),
+ /** PostgreSQL {@code EXTERNAL SECURITY}; {@code EXTERNAL} has no effect. */
+ EXTERNAL_SECURITY("EXTERNAL SECURITY"),
+ /** MySQL {@code SQL SECURITY}. */
+ SQL_SECURITY("SQL SECURITY");
+
+ private final String keywords;
+
+ SecurityClause(String keywords) {
+ this.keywords = keywords;
+ }
+ }
+
+ private Kind kind;
+ private String value;
+ private StringValue comment;
+ private SecurityClause securityClause;
+ private ConfigurationParameterChange parameterChange;
+
+ public RoutineCharacteristic(Kind kind) {
+ this.kind = kind;
+ }
+
+ public RoutineCharacteristic(Kind kind, String value) {
+ this.kind = kind;
+ setValue(value);
+ }
+
+ public RoutineCharacteristic(ConfigurationParameterChange parameterChange) {
+ this.kind = Kind.CONFIGURATION_PARAMETER;
+ this.parameterChange = parameterChange;
+ }
+
+ /**
+ * Creates a MySQL {@code COMMENT} characteristic.
+ *
+ * @param comment the string literal
+ * @return the characteristic
+ */
+ public static RoutineCharacteristic comment(StringValue comment) {
+ return new RoutineCharacteristic(Kind.COMMENT).withComment(comment);
+ }
+
+ public Kind getKind() {
+ return kind;
+ }
+
+ public void setKind(Kind kind) {
+ this.kind = kind;
+ }
+
+ /**
+ * The SQL text after the keyword of {@link Kind#COST}, {@link Kind#ROWS}, {@link Kind#SUPPORT}
+ * and {@link Kind#COMMENT}. For {@code COMMENT} it is derived from {@link #getComment()}, such
+ * as {@code 'it\'s'}.
+ *
+ * @return the value as written, or {@code null} for other kinds
+ */
+ public String getValue() {
+ if (kind == Kind.COMMENT) {
+ return comment == null ? null : comment.toString();
+ }
+ return value;
+ }
+
+ /**
+ * Sets the SQL text of the value. For {@link Kind#COMMENT} it replaces {@link #getComment()}
+ * with a literal read without dialect escaping; use {@link #setComment(StringValue)} to keep
+ * MySQL backslash escapes.
+ *
+ * @param value the SQL text
+ */
+ public void setValue(String value) {
+ if (kind == Kind.COMMENT) {
+ comment = value == null ? null : new StringValue(value);
+ } else {
+ this.value = value;
+ }
+ }
+
+ /**
+ * The string literal of {@link Kind#COMMENT}. {@link StringValue#getNotExcapedValue()} returns
+ * the text that MySQL stores.
+ *
+ * @return the literal, or {@code null} for other kinds
+ */
+ public StringValue getComment() {
+ return comment;
+ }
+
+ public void setComment(StringValue comment) {
+ this.comment = comment;
+ }
+
+ public RoutineCharacteristic withComment(StringValue comment) {
+ setComment(comment);
+ return this;
+ }
+
+ /**
+ * The spelling of {@link Kind#SECURITY_DEFINER} and {@link Kind#SECURITY_INVOKER}; {@code null}
+ * renders {@link SecurityClause#SECURITY}.
+ *
+ * @return the clause, or {@code null}
+ */
+ public SecurityClause getSecurityClause() {
+ return securityClause;
+ }
+
+ public void setSecurityClause(SecurityClause securityClause) {
+ this.securityClause = securityClause;
+ }
+
+ /**
+ * The change of {@link Kind#CONFIGURATION_PARAMETER}.
+ *
+ * @return the change, or {@code null} for other kinds
+ */
+ public ConfigurationParameterChange getParameterChange() {
+ return parameterChange;
+ }
+
+ public void setParameterChange(ConfigurationParameterChange parameterChange) {
+ this.parameterChange = parameterChange;
+ }
+
+ public RoutineCharacteristic withValue(String value) {
+ setValue(value);
+ return this;
+ }
+
+ public RoutineCharacteristic withSecurityClause(SecurityClause securityClause) {
+ setSecurityClause(securityClause);
+ return this;
+ }
+
+ /**
+ * Appends this characteristic to a builder.
+ *
+ * @param builder the target
+ * @param valueRenderer appends each configuration parameter value
+ * @return the builder
+ */
+ public StringBuilder appendTo(StringBuilder builder, Consumer valueRenderer) {
+ switch (kind) {
+ case CONFIGURATION_PARAMETER:
+ if (parameterChange == null) {
+ throw new IllegalStateException("Expected a configuration parameter change");
+ }
+ return parameterChange.appendTo(builder, valueRenderer);
+ case SECURITY_DEFINER:
+ case SECURITY_INVOKER:
+ return builder
+ .append((securityClause == null ? SecurityClause.SECURITY
+ : securityClause).keywords)
+ .append(' ').append(kind.keywords);
+ default:
+ builder.append(kind.keywords);
+ return kind.hasValue() ? builder.append(' ').append(getValue()) : builder;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ return appendTo(builder, builder::append).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
index 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..c7a6aecdf 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -269,9 +269,241 @@ 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 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 +4348,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 +4391,7 @@ Block Block() #Block : {
block.setStatements(stmts);
}
- [LOOKAHEAD(2) { block.setSemicolonAfterEnd(true); } ]
+
{
return block;
}
@@ -15730,6 +15974,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 +16033,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 +16099,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 +16118,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 +16194,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 +16219,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 +16370,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 +16382,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 +20018,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 +20030,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 +20854,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 +20867,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 +20883,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 +21101,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 +21437,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()
@@ -21002,8 +21484,10 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
CreateFunctionalStatement type = null;
List tokens = new LinkedList();
String statementType = null;
+ MySqlDefiner definer = null;
}
{
+ [ definer=MySqlRoutineDefiner() ]
(
{ statementType = "FUNCTION"; }
|
@@ -21017,11 +21501,186 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace):
if(statementType.equals("PROCEDURE")) {
type = new CreateProcedure(isUsingOrReplace, tokens);
}
-
+ type.setDefiner(definer);
return type;
}
}
+/**
+ * PostgreSQL ALTER FUNCTION, PROCEDURE or ROUTINE, or MySQL ALTER FUNCTION or PROCEDURE. One
+ * statement's characteristics belong to one dialect; MySQL's take no argument list.
+ */
+AlterRoutine AlterRoutine():
+{
+ AlterRoutine result = new AlterRoutine();
+ RoutineReference routine;
+ RoutineCharacteristic characteristic;
+ String name;
+}
+{
+ (
+ { result.setRoutineType(AlterRoutine.RoutineType.FUNCTION); }
+ |
+ { result.setRoutineType(AlterRoutine.RoutineType.PROCEDURE); }
+ |
+ ContextualKeyword("ROUTINE") { result.setRoutineType(AlterRoutine.RoutineType.ROUTINE); }
+ )
+ routine=RoutineReference(false) { result.setRoutine(routine); }
+ (
+ name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.RENAME); result.setNewName(name); }
+ |
+ LOOKAHEAD({ isWordSequenceAhead(1, "OWNER", "TO") }) ContextualKeyword("OWNER") name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.OWNER); result.setOwner(name); }
+ |
+ LOOKAHEAD({ getToken(1).kind == K_SET && getToken(2).kind == K_SCHEMA
+ && !isPostgreSqlRoutineCharacteristicAhead(1) })
+ name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.SET_SCHEMA); result.setSchemaName(name); }
+ |
+ LOOKAHEAD({ isExtensionDependencyAhead(1) })
+ [ { result.setNoDependency(true); } ]
+ ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName()
+ { result.setAction(AlterRoutine.Action.DEPENDS_ON_EXTENSION); result.setExtensionName(name); }
+ |
+ LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
+ (
+ LOOKAHEAD({ isMySqlRoutineCharacteristicAhead(1) })
+ characteristic=MySqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
+ )+
+ {
+ requireDdlSyntax(result.getRoutineType() != AlterRoutine.RoutineType.ROUTINE
+ && routine.getArguments() == null,
+ "MySQL characteristics require ALTER FUNCTION or PROCEDURE without arguments");
+ }
+ |
+ (
+ LOOKAHEAD({ isPostgreSqlRoutineCharacteristicAhead(1) })
+ characteristic=PostgreSqlRoutineCharacteristic() { result.getCharacteristics().add(characteristic); }
+ )+
+ [ { result.setRestrict(true); } ]
+ )
+ { return result; }
+}
+
+RoutineCharacteristic PostgreSqlRoutineCharacteristic():
+{
+ RoutineCharacteristic.Kind kind = RoutineCharacteristic.Kind.CONFIGURATION_PARAMETER;
+ RoutineCharacteristic.SecurityClause clause = null;
+ ConfigurationParameterChange change = null;
+ String value = null;
+}
+{
+ (
+ change=ConfigurationParameterChange()
+ |
+ LOOKAHEAD({ isKeywordAhead("IMMUTABLE") }) ContextualKeyword("IMMUTABLE")
+ { kind = RoutineCharacteristic.Kind.IMMUTABLE; }
+ |
+ LOOKAHEAD({ isKeywordAhead("STABLE") }) ContextualKeyword("STABLE")
+ { kind = RoutineCharacteristic.Kind.STABLE; }
+ |
+ { kind = RoutineCharacteristic.Kind.VOLATILE; }
+ |
+ LOOKAHEAD({ isKeywordAhead("CALLED") }) ContextualKeyword("CALLED") ContextualKeyword("INPUT")
+ { kind = RoutineCharacteristic.Kind.CALLED_ON_NULL_INPUT; }
+ |
+ ContextualKeyword("INPUT")
+ { kind = RoutineCharacteristic.Kind.RETURNS_NULL_ON_NULL_INPUT; }
+ |
+ { kind = RoutineCharacteristic.Kind.STRICT; }
+ |
+ LOOKAHEAD({ isKeywordAhead("LEAKPROOF") }) ContextualKeyword("LEAKPROOF")
+ { kind = RoutineCharacteristic.Kind.LEAKPROOF; }
+ |
+ ContextualKeyword("LEAKPROOF") { kind = RoutineCharacteristic.Kind.NOT_LEAKPROOF; }
+ |
+ LOOKAHEAD({ isKeywordAhead("EXTERNAL") }) ContextualKeyword("EXTERNAL")
+ kind=RoutineSecurity() { clause = RoutineCharacteristic.SecurityClause.EXTERNAL_SECURITY; }
+ |
+ kind=RoutineSecurity() { clause = RoutineCharacteristic.SecurityClause.SECURITY; }
+ |
+
+ (
+ LOOKAHEAD({ isKeywordAhead("UNSAFE") }) ContextualKeyword("UNSAFE")
+ { kind = RoutineCharacteristic.Kind.PARALLEL_UNSAFE; }
+ |
+ { kind = RoutineCharacteristic.Kind.PARALLEL_RESTRICTED; }
+ |
+ ContextualKeyword("SAFE") { kind = RoutineCharacteristic.Kind.PARALLEL_SAFE; }
+ )
+ |
+ LOOKAHEAD({ isKeywordAhead("COST") }) ContextualKeyword("COST") value=PostgreSqlNumericValue()
+ { kind = RoutineCharacteristic.Kind.COST; }
+ |
+ value=PostgreSqlNumericValue() { kind = RoutineCharacteristic.Kind.ROWS; }
+ |
+ LOOKAHEAD({ isKeywordAhead("SUPPORT") }) ContextualKeyword("SUPPORT") value=AccessQualifiedName()
+ { kind = RoutineCharacteristic.Kind.SUPPORT; }
+ )
+ {
+ return change != null ? new RoutineCharacteristic(change)
+ : new RoutineCharacteristic(kind, value).withSecurityClause(clause);
+ }
+}
+
+RoutineCharacteristic MySqlRoutineCharacteristic():
+{
+ RoutineCharacteristic.Kind kind;
+ RoutineCharacteristic.SecurityClause clause = null;
+ Token token;
+ StringValue comment = null;
+}
+{
+ (
+ token=
+ { kind = RoutineCharacteristic.Kind.COMMENT; comment = stringValue(token); }
+ |
+ LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) ContextualKeyword("LANGUAGE") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.LANGUAGE_SQL; }
+ |
+ LOOKAHEAD({ isKeywordAhead("CONTAINS") }) ContextualKeyword("CONTAINS") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.CONTAINS_SQL; }
+ |
+ ContextualKeyword("SQL") { kind = RoutineCharacteristic.Kind.NO_SQL; }
+ |
+ LOOKAHEAD({ isKeywordAhead("READS") }) ContextualKeyword("READS") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.READS_SQL_DATA; }
+ |
+ LOOKAHEAD({ isKeywordAhead("MODIFIES") }) ContextualKeyword("MODIFIES") ContextualKeyword("SQL")
+ { kind = RoutineCharacteristic.Kind.MODIFIES_SQL_DATA; }
+ |
+ ContextualKeyword("SQL") kind=RoutineSecurity()
+ { clause = RoutineCharacteristic.SecurityClause.SQL_SECURITY; }
+ )
+ {
+ return new RoutineCharacteristic(kind).withComment(comment).withSecurityClause(clause);
+ }
+}
+
+RoutineCharacteristic.Kind RoutineSecurity():
+{ RoutineCharacteristic.Kind kind; }
+{
+ (
+ LOOKAHEAD({ isKeywordAhead("DEFINER") }) ContextualKeyword("DEFINER")
+ { kind = RoutineCharacteristic.Kind.SECURITY_DEFINER; }
+ |
+ ContextualKeyword("INVOKER") { kind = RoutineCharacteristic.Kind.SECURITY_INVOKER; }
+ )
+ { return kind; }
+}
+
+/** PostgreSQL NumericOnly: an optionally signed integer or decimal, as written. */
+String PostgreSqlNumericValue():
+{
+ Token sign = null;
+ Token number;
+}
+{
+ [ sign="+" | sign="-" ] ( number= | number= )
+ { return (sign == null ? "" : sign.image) + number.image; }
+}
+
CreateFunctionalStatement SqlServerRoutine():
{
CreateFunctionalStatement result;
diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst
index 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..54184d0c9 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -1868,6 +1868,112 @@ 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. 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.>of(parser -> {
+ }, parser -> parser.withDialect(Dialect.POSTGRESQL))) {
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer);
+ assertInstanceOf(AlterTrigger.class, statement);
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, deparsed.toString());
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ assertEquals(2,
+ CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer).size());
+ }
+ }
+
+ @Test
+ void testActionsAndTable() throws JSQLParserException {
+ AlterTrigger rename = parse("ALTER TRIGGER trg ON app.users RENAME TO trg_new");
+ assertEquals("trg", rename.getName());
+ assertEquals("app.users", rename.getTable().getFullyQualifiedName());
+ assertEquals(AlterTrigger.Action.RENAME, rename.getAction());
+ assertEquals("trg_new", rename.getNewName());
+ assertEquals(List.of("app.users"),
+ List.copyOf(new TablesNamesFinder().getTables((Statement) rename)));
+
+ AlterTrigger dependency = parse("ALTER TRIGGER trg ON users NO DEPENDS ON EXTENSION ext");
+ assertEquals(AlterTrigger.Action.DEPENDS_ON_EXTENSION, dependency.getAction());
+ assertEquals("ext", dependency.getExtensionName());
+ assertTrue(dependency.isNoDependency());
+
+ dependency.setNoDependency(false);
+ dependency.setTable(new Table("orders"));
+ assertEquals("ALTER TRIGGER trg ON orders DEPENDS ON EXTENSION ext",
+ dependency.toString());
+ dependency.setAction(AlterTrigger.Action.RENAME);
+ dependency.setNewName("trg2");
+ assertEquals("ALTER TRIGGER trg ON orders RENAME TO trg2", dependency.toString());
+
+ assertEquals("context:trg", rename.accept(new StatementVisitorAdapter() {
+ @Override
+ public String visit(AlterTrigger trigger, S context) {
+ return context + ":" + trigger.getName();
+ }
+ }, "context"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER TRIGGER trg ENABLE", "ALTER TRIGGER trg COMPILE",
+ "ALTER TRIGGER trg ON t AFTER INSERT AS SELECT 1",
+ "ALTER TRIGGER trg ON t RENAME TO t2 CASCADE",
+ "ALTER TRIGGER trg ON t DEPENDS ON SCHEMA s"})
+ void testOtherFormsRemainUnsupported(String sql) throws JSQLParserException {
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql));
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL)));
+ }
+
+ @Test
+ void testValidation() {
+ validateNoErrors("ALTER TRIGGER trg ON users RENAME TO trg_new", 1,
+ DatabaseType.POSTGRESQL);
+ assertFalse(new Validation(List.of(DatabaseType.MYSQL),
+ "ALTER TRIGGER trg ON users RENAME TO trg_new").validate().isEmpty());
+ }
+
+ private static AlterTrigger parse(String sql) throws JSQLParserException {
+ return (AlterTrigger) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java
index 56b46adde..48a10da95 100644
--- a/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java
@@ -9,14 +9,27 @@
*/
package net.sf.jsqlparser.statement.alter.database;
+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.parser.feature.Feature;
+import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitorAdapter;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
import net.sf.jsqlparser.statement.create.database.DatabaseOption;
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.ValueSource;
@@ -63,6 +76,180 @@ void rejectIncompleteAndInvalidReadOnly() {
}
}
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER DATABASE db1 DEFAULT CHARSET = utf8mb4",
+ "ALTER SCHEMA db1 DEFAULT ENCRYPTION = 'Y'", "ALTER SCHEMA db1 CHAR SET utf8mb4",
+ "ALTER DATABASE CHARSET utf8mb4 COLLATE utf8mb4_bin",
+ "ALTER DATABASE charset CHARSET = utf8mb4",
+ "ALTER DATABASE READ ONLY = DEFAULT", "ALTER SCHEMA DEFAULT CHARACTER SET utf8mb4"})
+ void testMySqlOptionsWithAndWithoutDialect(String sql) throws JSQLParserException {
+ assertRoundTrip(sql, Dialect.MYSQL);
+ assertRoundTrip(sql, null);
+ }
+
+ @Test
+ void testCharsetSpellingAndOmittedName() throws JSQLParserException {
+ AlterDatabase statement =
+ (AlterDatabase) CCJSqlParserUtil.parse("ALTER SCHEMA DEFAULT CHARSET utf8mb4");
+ assertTrue(statement.isUseSchemaKeyword());
+ assertNull(statement.getDatabaseName());
+ DatabaseOption charset =
+ statement.getOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow();
+ assertEquals("CHARSET", charset.getName());
+ assertEquals("utf8mb4", charset.getValue());
+ assertTrue(charset.isUseDefault());
+
+ // CHARSET is also a valid database name.
+ statement =
+ (AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE charset CHARSET utf8mb4");
+ assertEquals("charset", statement.getDatabaseName());
+ statement = (AlterDatabase) CCJSqlParserUtil.parse("ALTER DATABASE charset RENAME TO c2",
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ assertEquals("charset", statement.getDatabaseName());
+ assertEquals("c2", statement.getNewName());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER DATABASE mydb RENAME TO mydb2",
+ "ALTER DATABASE \"My Db\" RENAME TO \"Other Db\"", "ALTER DATABASE mydb OWNER TO u1",
+ "ALTER DATABASE mydb OWNER TO CURRENT_USER",
+ "ALTER DATABASE mydb OWNER TO CURRENT_ROLE",
+ "ALTER DATABASE mydb OWNER TO SESSION_USER", "ALTER DATABASE mydb SET TABLESPACE fast",
+ "ALTER DATABASE mydb REFRESH COLLATION VERSION",
+ "ALTER DATABASE mydb SET search_path TO app, public",
+ "ALTER DATABASE mydb SET search_path = \"$user\", public",
+ "ALTER DATABASE mydb SET work_mem = '64MB'",
+ "ALTER DATABASE mydb SET work_mem TO DEFAULT",
+ "ALTER DATABASE mydb SET work_mem FROM CURRENT", "ALTER DATABASE mydb RESET work_mem",
+ "ALTER DATABASE mydb RESET ALL", "ALTER DATABASE mydb WITH ALLOW_CONNECTIONS false",
+ "ALTER DATABASE mydb CONNECTION LIMIT = -1 IS_TEMPLATE true",
+ "ALTER DATABASE mydb WITH IS_TEMPLATE = true ALLOW_CONNECTIONS = false CONNECTION LIMIT 5"})
+ void testPostgreSqlForms(String sql) throws JSQLParserException {
+ assertRoundTrip(sql, Dialect.POSTGRESQL);
+ assertRoundTrip(sql, null);
+ }
+
+ @Test
+ void testPostgreSqlActions() throws JSQLParserException {
+ AlterDatabase rename = parsePostgreSql("ALTER DATABASE mydb RENAME TO mydb2");
+ assertEquals(AlterDatabase.Action.RENAME, rename.getAction());
+ assertEquals("mydb", rename.getDatabaseName());
+ assertEquals("mydb2", rename.getNewName());
+ assertTrue(rename.getOptions().isEmpty());
+
+ AlterDatabase owner = parsePostgreSql("ALTER DATABASE mydb OWNER TO CURRENT_USER");
+ assertEquals(AlterDatabase.Action.OWNER, owner.getAction());
+ assertEquals("CURRENT_USER", owner.getOwner());
+
+ AlterDatabase tablespace = parsePostgreSql("ALTER DATABASE mydb SET TABLESPACE fast");
+ assertEquals(AlterDatabase.Action.SET_TABLESPACE, tablespace.getAction());
+ assertEquals("fast", tablespace.getTablespace());
+ assertNull(tablespace.getParameterChange());
+
+ assertEquals(AlterDatabase.Action.REFRESH_COLLATION_VERSION,
+ parsePostgreSql("ALTER DATABASE mydb REFRESH COLLATION VERSION").getAction());
+
+ // A configuration parameter named "tablespace" is a SET, not SET TABLESPACE.
+ AlterDatabase parameter = parsePostgreSql("ALTER DATABASE mydb SET tablespace = fast");
+ assertEquals(AlterDatabase.Action.CONFIGURATION_PARAMETER, parameter.getAction());
+ assertEquals("tablespace", parameter.getParameterChange().getParameter());
+ }
+
+ @Test
+ void testPostgreSqlOptions() throws JSQLParserException {
+ AlterDatabase statement = parsePostgreSql(
+ "ALTER DATABASE mydb WITH ALLOW_CONNECTIONS false CONNECTION LIMIT = -1 IS_TEMPLATE true");
+ assertEquals(AlterDatabase.Action.OPTIONS, statement.getAction());
+ assertTrue(statement.isUseWith());
+ List options = statement.getOptions();
+ assertEquals(3, options.size());
+ assertOption(options.get(0), DatabaseOption.Kind.ALLOW_CONNECTIONS, "ALLOW_CONNECTIONS",
+ "false", false);
+ assertOption(options.get(1), DatabaseOption.Kind.CONNECTION_LIMIT, "CONNECTION LIMIT",
+ "-1", true);
+ assertOption(options.get(2), DatabaseOption.Kind.IS_TEMPLATE, "IS_TEMPLATE", "true",
+ false);
+
+ statement.getOption(DatabaseOption.Kind.CONNECTION_LIMIT).orElseThrow().setValue("10");
+ statement.setUseWith(false);
+ assertEquals("ALTER DATABASE mydb ALLOW_CONNECTIONS false CONNECTION LIMIT = 10 "
+ + "IS_TEMPLATE true", statement.toString());
+ }
+
+ @Test
+ void testSwitchingActionsKeepsPayloads() throws JSQLParserException {
+ AlterDatabase statement = parsePostgreSql("ALTER DATABASE mydb RENAME TO mydb2");
+ statement.setAction(AlterDatabase.Action.OWNER);
+ statement.setOwner("SESSION_USER");
+ assertEquals("ALTER DATABASE mydb OWNER TO SESSION_USER", statement.toString());
+ statement.setAction(AlterDatabase.Action.RENAME);
+ assertEquals("ALTER DATABASE mydb RENAME TO mydb2", statement.toString());
+
+ statement.setAction(AlterDatabase.Action.CONFIGURATION_PARAMETER);
+ assertThrows(IllegalStateException.class, statement::toString);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER DATABASE d", "ALTER DATABASE d SET TIME ZONE 'UTC'",
+ "ALTER DATABASE d SET RECOVERY SIMPLE", "ALTER DATABASE d MODIFY NAME = d2",
+ "ALTER DATABASE WITH ALLOW_CONNECTIONS true"})
+ void testUnmodelledFormsRemainUnsupportedOutsideMySql(String sql)
+ throws JSQLParserException {
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql));
+ assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL)));
+ assertThrows(JSQLParserException.class, () -> parse(sql));
+ }
+
+ @Test
+ void testPostgreSqlAlterSchemaIsUnchanged() throws JSQLParserException {
+ assertInstanceOf(AlterSchema.class, CCJSqlParserUtil.parse("ALTER SCHEMA s RENAME TO t"));
+ assertInstanceOf(AlterSchema.class,
+ CCJSqlParserUtil.parse("ALTER SCHEMA s OWNER TO CURRENT_USER"));
+ assertInstanceOf(AlterDatabase.class,
+ CCJSqlParserUtil.parse("ALTER SCHEMA s DEFAULT ENCRYPTION = 'Y'"));
+ }
+
+ @Test
+ void testValidation() {
+ validateNoErrors("ALTER DATABASE mydb SET work_mem = '64MB'", 1, DatabaseType.POSTGRESQL);
+ validateNoErrors("ALTER DATABASE mydb OWNER TO CURRENT_USER", 1, DatabaseType.POSTGRESQL);
+ validateNoErrors("ALTER DATABASE d CHARACTER SET utf8mb4", 1, DatabaseType.MYSQL,
+ DatabaseType.MARIADB);
+ assertFalse(new Validation(List.of(FeaturesAllowed.SELECT),
+ "ALTER DATABASE mydb RESET ALL").validate().isEmpty());
+ assertTrue(new Validation(List.of(new FeaturesAllowed().add(Feature.alterDatabase)),
+ "ALTER DATABASE mydb RESET ALL").validate().isEmpty());
+ }
+
+ private static void assertOption(DatabaseOption option, DatabaseOption.Kind kind, String name,
+ String value, boolean useEquals) {
+ assertEquals(kind, option.getKind());
+ assertEquals(name, option.getName());
+ assertEquals(value, option.getValue());
+ assertEquals(useEquals, option.isUseEquals());
+ assertFalse(option.isUseDefault());
+ }
+
+ /** 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(AlterDatabase.class, statement);
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ assertTrue(new TablesNamesFinder().getTables(statement).isEmpty());
+ assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", consumer).size());
+ }
+
+ private static AlterDatabase parsePostgreSql(String sql) throws JSQLParserException {
+ return (AlterDatabase) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ }
+
private static AlterDatabase parse(String sql) throws JSQLParserException {
return (AlterDatabase) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java
index 66880938c..d2059b6ba 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java
@@ -12,11 +12,21 @@
import static net.sf.jsqlparser.test.TestUtils.assertDeparse;
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Arrays;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.MySqlDefiner;
+import net.sf.jsqlparser.statement.CreateFunctionalStatement;
import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.create.view.CreateView;
+import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import net.sf.jsqlparser.statement.create.function.CreateFunction;
import net.sf.jsqlparser.statement.create.procedure.CreateProcedure;
import org.junit.jupiter.api.Test;
@@ -89,6 +99,65 @@ public void createOrReplaceFunctionMinimal() throws JSQLParserException {
assertDeparse(func, statement);
}
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE DEFINER = CURRENT_USER FUNCTION f2 (s CHAR(20)) RETURNS CHAR(50) "
+ + "CHARSET utf8mb4 NO SQL RETURN CONCAT('Hello, ', s, '!')",
+ "CREATE DEFINER = CURRENT_USER PROCEDURE p3 () SELECT 1",
+ "CREATE DEFINER = `root`@`%` FUNCTION f3 () RETURNS INT RETURN 1",
+ "CREATE DEFINER = 'admin'@'localhost' PROCEDURE p (IN a INT) BEGIN SELECT a; END",
+ "CREATE OR REPLACE DEFINER = CURRENT_USER() PROCEDURE p () SELECT 1"})
+ public void createRoutineWithMySqlDefiner(String sql) throws JSQLParserException {
+ for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB, null}) {
+ CreateFunctionalStatement statement =
+ (CreateFunctionalStatement) assertSqlCanBeParsedAndDeparsed(
+ sql, true, dialect == null ? parser -> {
+ } : parser -> parser.withDialect(dialect));
+ assertThat(statement.getDefiner()).isNotNull();
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertThat(deparsed.toString()).isEqualTo(statement.toString());
+ assertThat(CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1").getStatements())
+ .hasSize(2);
+ }
+ }
+
+ @Test
+ public void mySqlDefinerAccount() throws JSQLParserException {
+ CreateFunction function = (CreateFunction) CCJSqlParserUtil.parse(
+ "CREATE DEFINER='root'@'%' FUNCTION f3 () RETURNS INT RETURN 1");
+ MySqlDefiner definer = function.getDefiner();
+ assertThat(definer.getUser().getValue()).isEqualTo("root");
+ assertThat(definer.getHost().getValue()).isEqualTo("%");
+ assertThat(function.getFunctionDeclarationParts()).startsWith("f3", "(", ")");
+
+ CreateProcedure procedure = (CreateProcedure) CCJSqlParserUtil
+ .parse("CREATE DEFINER = CURRENT_USER PROCEDURE p3 () SELECT 1");
+ assertThat(procedure.getDefiner().getUser().getValue()).isEqualTo("CURRENT_USER");
+ assertThat(procedure.getDefiner().getHost()).isNull();
+
+ procedure.setDefiner(new MySqlDefiner().withUser(new StringValue("app"))
+ .withHost(new StringValue("%")));
+ assertThat(procedure.toString())
+ .isEqualTo("CREATE DEFINER = 'app'@'%' PROCEDURE p3 ( ) SELECT 1");
+ procedure.setDefiner(null);
+ assertThat(procedure.toString()).isEqualTo("CREATE PROCEDURE p3 ( ) SELECT 1");
+ }
+
+ @Test
+ public void definerDispatchKeepsTriggersAndViews() throws JSQLParserException {
+ assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = CURRENT_USER TRIGGER t "
+ + "BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1"))
+ .isInstanceOf(CreateTrigger.class);
+ assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = CURRENT_USER VIEW v AS SELECT 1"))
+ .isInstanceOf(CreateView.class);
+ assertThat(CCJSqlParserUtil.parse("CREATE FUNCTION f() RETURNS int AS 'SELECT 1'"))
+ .isInstanceOf(CreateFunction.class)
+ .extracting(statement -> ((CreateFunction) statement).getDefiner()).isNull();
+ assertThatThrownBy(() -> CCJSqlParserUtil.parse("CREATE DEFINER = x EVENT e "
+ + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(JSQLParserException.class);
+ }
+
@Test
public void createFunctionWithPositionalParametersAcrossStatementsIssue2322()
throws JSQLParserException {
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java
index 20f7db0ee..edeb7861c 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java
@@ -10,17 +10,29 @@
package net.sf.jsqlparser.statement.create.database;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import net.sf.jsqlparser.util.validation.feature.DatabaseType;
import static net.sf.jsqlparser.test.TestUtils.assertDeparse;
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
public class CreateDatabaseTest {
@@ -79,4 +91,176 @@ void testCreateDatabaseAST() throws JSQLParserException {
assertEquals("mydb", createDatabase.getDatabaseName());
assertTrue(createDatabase.hasIfNotExists());
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"CREATE DATABASE mydb2",
+ "CREATE DATABASE db1 DEFAULT CHARSET = utf8mb4",
+ "CREATE DATABASE db1 CHAR SET utf8mb4 DEFAULT ENCRYPTION = 'N'",
+ "CREATE DATABASE IF NOT EXISTS db1 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"})
+ void testMySqlForms(String sql) throws JSQLParserException {
+ assertRoundTrip(sql, Dialect.MYSQL);
+ assertRoundTrip(sql, null);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE DATABASE mydb WITH OWNER = app ENCODING = 'UTF8' LC_COLLATE = 'C' "
+ + "LC_CTYPE = 'C' TEMPLATE = template0 CONNECTION LIMIT = 10",
+ "CREATE DATABASE mydb OWNER app STRATEGY = file_copy LOCALE 'C' "
+ + "BUILTIN_LOCALE = 'C.UTF-8' LOCALE_PROVIDER = builtin "
+ + "COLLATION_VERSION = '1' TABLESPACE = fast ALLOW_CONNECTIONS = true "
+ + "CONNECTION LIMIT = -1 IS_TEMPLATE false OID = 16384",
+ "CREATE DATABASE mydb WITH TEMPLATE DEFAULT ENCODING UTF8 LOCALE_PROVIDER icu "
+ + "ICU_LOCALE 'de-DE' ICU_RULES = '&a < b'"})
+ void testPostgreSqlForms(String sql) throws JSQLParserException {
+ assertRoundTrip(sql, Dialect.POSTGRESQL);
+ assertRoundTrip(sql, null);
+ }
+
+ @Test
+ void testPostgreSqlOptionsAreTyped() throws JSQLParserException {
+ CreateDatabase createDatabase = parse("CREATE DATABASE mydb WITH OWNER = app "
+ + "ENCODING = 'UTF8' TEMPLATE template0 CONNECTION LIMIT = -1 IS_TEMPLATE false",
+ Dialect.POSTGRESQL);
+ assertTrue(createDatabase.isUseWith());
+ List options = createDatabase.getOptions();
+ assertEquals(Arrays.asList(DatabaseOption.Kind.OWNER, DatabaseOption.Kind.ENCODING,
+ DatabaseOption.Kind.TEMPLATE, DatabaseOption.Kind.CONNECTION_LIMIT,
+ DatabaseOption.Kind.IS_TEMPLATE),
+ options.stream().map(DatabaseOption::getKind).collect(
+ java.util.stream.Collectors.toList()));
+ assertEquals("app", options.get(0).getValue());
+ assertTrue(options.get(0).isUseEquals());
+ assertEquals("'UTF8'", options.get(1).getValue());
+ assertFalse(options.get(2).isUseEquals());
+ assertEquals("CONNECTION LIMIT", options.get(3).getName());
+ assertEquals("-1", options.get(3).getValue());
+ assertEquals("false", options.get(4).getValue());
+
+ createDatabase.getOption(DatabaseOption.Kind.OWNER).orElseThrow().setValue("CURRENT_USER");
+ options.remove(4);
+ assertEquals("CREATE DATABASE mydb WITH OWNER = CURRENT_USER ENCODING = 'UTF8' "
+ + "TEMPLATE template0 CONNECTION LIMIT = -1", createDatabase.toString());
+ }
+
+ @Test
+ void testCharsetSpellingIsTyped() throws JSQLParserException {
+ CreateDatabase createDatabase =
+ parse("CREATE DATABASE db1 DEFAULT CHARSET = utf8mb4", Dialect.MYSQL);
+ assertEquals(1, createDatabase.getOptions().size());
+ DatabaseOption charset = createDatabase.getOptions().get(0);
+ assertEquals(DatabaseOption.Kind.CHARACTER_SET, charset.getKind());
+ assertEquals("CHARSET", charset.getName());
+ assertEquals("utf8mb4", charset.getValue());
+ assertTrue(charset.isUseDefault());
+ assertTrue(charset.isUseEquals());
+ }
+
+ @Test
+ void testNoEmptyRawOption() throws JSQLParserException {
+ CreateDatabase createDatabase = parse("CREATE DATABASE mydb2", null);
+ assertTrue(createDatabase.getOptions().isEmpty());
+ assertNull(createDatabase.getDatabaseOptions());
+ assertEquals("CREATE DATABASE mydb2", createDatabase.toString());
+
+ createDatabase = parse("CREATE DATABASE db1 CHARACTER SET utf8mb4", Dialect.MYSQL);
+ assertEquals(1, createDatabase.getOptions().size());
+ assertEquals("CREATE DATABASE db1 CHARACTER SET utf8mb4", createDatabase.toString());
+ }
+
+ @Test
+ void testUnknownOptionTextStaysRaw() throws JSQLParserException {
+ CreateDatabase createDatabase =
+ assertInstanceOf(CreateDatabase.class, assertSqlCanBeParsedAndDeparsed(
+ "CREATE DATABASE mydb WITH OWNER app LOCATION 'x'"));
+ assertEquals(2, createDatabase.getOptions().size());
+ assertEquals(DatabaseOption.Kind.OWNER, createDatabase.getOptions().get(0).getKind());
+ DatabaseOption raw = createDatabase.getOptions().get(1);
+ assertEquals(DatabaseOption.Kind.OTHER, raw.getKind());
+ assertEquals(Arrays.asList("LOCATION", "'x'"), raw.getTokens());
+
+ // WITH is kept with the raw text when no known option follows it.
+ createDatabase = assertInstanceOf(CreateDatabase.class,
+ assertSqlCanBeParsedAndDeparsed("CREATE DATABASE mydb WITH LOCATION 'x'"));
+ assertFalse(createDatabase.isUseWith());
+ assertEquals(Arrays.asList("WITH", "LOCATION", "'x'"),
+ createDatabase.getOptions().get(0).getTokens());
+ }
+
+ @Test
+ void testLegacyDatabaseOptionTokens() throws JSQLParserException {
+ CreateDatabase createDatabase = parse(
+ "CREATE DATABASE mydb WITH OWNER = app CONNECTION LIMIT 5", Dialect.POSTGRESQL);
+ List tokens = createDatabase.getDatabaseOptions();
+ assertEquals(Arrays.asList("WITH", "OWNER", "=", "app", "CONNECTION", "LIMIT", "5"),
+ tokens);
+ assertThrows(UnsupportedOperationException.class, () -> tokens.add("x"));
+
+ createDatabase.getOptions().get(1).setValue("10");
+ assertEquals("10", createDatabase.getDatabaseOptions().get(6));
+
+ createDatabase.addDatabaseOptions("IS_TEMPLATE", "true");
+ assertEquals(DatabaseOption.Kind.OWNER, createDatabase.getOptions().get(0).getKind());
+ assertEquals(DatabaseOption.Kind.OTHER, createDatabase.getOptions().get(2).getKind());
+ assertEquals("CREATE DATABASE mydb WITH OWNER = app CONNECTION LIMIT 10 IS_TEMPLATE true",
+ createDatabase.toString());
+
+ List raw = new ArrayList<>(Arrays.asList("ENCODING", "'UTF8'"));
+ createDatabase.setDatabaseOptions(raw);
+ assertFalse(createDatabase.isUseWith());
+ assertEquals(DatabaseOption.Kind.OTHER, createDatabase.getOptions().get(0).getKind());
+ assertEquals("CREATE DATABASE mydb ENCODING 'UTF8'", createDatabase.toString());
+ createDatabase.setDatabaseOptions(null);
+ assertTrue(createDatabase.getOptions().isEmpty());
+ assertEquals("CREATE DATABASE mydb", createDatabase.toString());
+ }
+
+ @Test
+ void testOptionListOwnership() {
+ List options = new ArrayList<>();
+ options.add(new DatabaseOption(DatabaseOption.Kind.OWNER, "OWNER", "app", false, true));
+ CreateDatabase createDatabase =
+ new CreateDatabase().withDatabaseName("mydb").withUseWith(true)
+ .withOptions(options);
+ options.clear();
+ assertEquals("CREATE DATABASE mydb WITH OWNER = app", createDatabase.toString());
+ createDatabase.getOptions().add(
+ new DatabaseOption(DatabaseOption.Kind.IS_TEMPLATE, "IS_TEMPLATE", "true", false,
+ false));
+ assertEquals("CREATE DATABASE mydb WITH OWNER = app IS_TEMPLATE true",
+ createDatabase.toString());
+ createDatabase.setOptions(createDatabase.getOptions());
+ assertEquals(2, createDatabase.getOptions().size());
+ createDatabase.setOptions(null);
+ assertTrue(createDatabase.getOptions().isEmpty());
+ }
+
+ @Test
+ void testValidation() {
+ validateNoErrors("CREATE DATABASE mydb WITH OWNER = app", 1, DatabaseType.POSTGRESQL);
+ validateNoErrors("CREATE DATABASE db1 DEFAULT CHARSET = utf8mb4", 1, DatabaseType.MYSQL,
+ DatabaseType.MARIADB);
+ }
+
+ /** 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 = consumer(dialect);
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, consumer);
+ CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, statement);
+ assertTrue(createDatabase.getOptions().stream()
+ .noneMatch(option -> option.getKind() == DatabaseOption.Kind.OTHER));
+ StringBuilder deparsed = new StringBuilder();
+ statement.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", consumer).size());
+ }
+
+ private static CreateDatabase parse(String sql, Dialect dialect) throws JSQLParserException {
+ return (CreateDatabase) CCJSqlParserUtil.parse(sql, consumer(dialect));
+ }
+
+ private static Consumer consumer(Dialect dialect) {
+ return dialect == null ? parser -> {
+ } : parser -> parser.withDialect(dialect);
+ }
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java b/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java
index 5d6093f18..80b5db9d7 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/schema/CreateSchemaTest.java
@@ -15,6 +15,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.create.database.DatabaseOption;
import org.junit.jupiter.api.Test;
@@ -77,4 +78,20 @@ void testMySqlCreateSchemaOptions() throws JSQLParserException {
assertEquals(sql, CCJSqlParserUtil.parse(sql).toString());
}
+
+ @Test
+ void testMySqlCreateSchemaCharset() throws JSQLParserException {
+ String sql = "CREATE SCHEMA db1 DEFAULT CHARSET = utf8mb4 DEFAULT ENCRYPTION = 'N'";
+ for (Dialect dialect : new Dialect[] {Dialect.MYSQL, null}) {
+ CreateSchema schema = (CreateSchema) assertSqlCanBeParsedAndDeparsed(sql, false,
+ dialect == null ? parser -> {
+ } : parser -> parser.withDialect(dialect));
+ DatabaseOption charset =
+ schema.getDatabaseOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow();
+ assertEquals("CHARSET", charset.getName());
+ assertEquals("utf8mb4", charset.getValue());
+ assertTrue(charset.isUseDefault());
+ }
+ assertSqlCanBeParsedAndDeparsed("CREATE SCHEMA db1 CHAR SET utf8mb4");
+ }
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java b/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java
index f199bc293..ce5e89676 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/trigger/CreateTriggerTest.java
@@ -11,13 +11,32 @@
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Block;
import net.sf.jsqlparser.statement.SetStatement;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
class CreateTriggerTest {
@@ -71,4 +90,121 @@ void parsesTriggerOrdering() throws JSQLParserException {
assertEquals(CreateTrigger.Order.FOLLOWS, trigger.getOrder());
assertEquals("normalize_name", trigger.getOtherTrigger().getFullyQualifiedName());
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE DEFINER = `root`@`localhost` TRIGGER IF NOT EXISTS trg2 AFTER UPDATE ON t1 "
+ + "FOR EACH ROW FOLLOWS trg INSERT INTO audit (id) VALUES (NEW.id)",
+ "CREATE TRIGGER IF NOT EXISTS trg2 AFTER UPDATE ON t1 FOR EACH ROW "
+ + "INSERT INTO audit (id) VALUES (NEW.id)",
+ "CREATE OR REPLACE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1",
+ "CREATE OR REPLACE DEFINER = CURRENT_USER TRIGGER trg BEFORE INSERT ON t1 "
+ + "FOR EACH ROW SET NEW.a = 1",
+ "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW "
+ + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW lbl: BEGIN "
+ + "DECLARE x INT DEFAULT 0; WHILE x < 3 DO SET x = x + 1; END WHILE; "
+ + "LEAVE lbl; END lbl",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW "
+ + "IF NEW.a IS NULL THEN SET NEW.a = 'a.b'; END IF",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW CASE WHEN NEW.a > 0 THEN "
+ + "SET NEW.b = 1; ELSE SET NEW.b = CASE NEW.c WHEN 1 THEN 2 END; END CASE",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN END"})
+ void parsesMySqlTriggerForms(String sql) throws JSQLParserException {
+ for (Consumer consumer : List.>of(
+ parser -> parser.withDialect(Dialect.MYSQL),
+ parser -> parser.withDialect(Dialect.MARIADB), parser -> {
+ })) {
+ CreateTrigger trigger = assertInstanceOf(CreateTrigger.class,
+ assertSqlCanBeParsedAndDeparsed(sql, false, consumer));
+ StringBuilder deparsed = new StringBuilder();
+ trigger.accept(new StatementDeParser(deparsed));
+ assertEquals(sql, deparsed.toString());
+ assertEquals(sql, CCJSqlParserUtil.parse(deparsed.toString(), consumer).toString());
+ Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer);
+ assertEquals(2, script.size());
+ assertEquals(sql, script.get(0).toString());
+ }
+ }
+
+ @Test
+ void parsesIfNotExistsAndOrReplace() throws JSQLParserException {
+ CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(
+ "CREATE DEFINER = CURRENT_USER TRIGGER IF NOT EXISTS trg AFTER UPDATE ON t1 "
+ + "FOR EACH ROW INSERT INTO audit (id) VALUES (NEW.id)");
+ assertTrue(trigger.isIfNotExists());
+ assertFalse(trigger.isOrReplace());
+ assertEquals("trg", trigger.getTrigger().getFullyQualifiedName());
+ assertEquals("CURRENT_USER", trigger.getDefiner().getUser().getValue());
+
+ trigger.setIfNotExists(false);
+ trigger.setOrReplace(true);
+ assertEquals("CREATE OR REPLACE DEFINER = CURRENT_USER TRIGGER trg AFTER UPDATE ON t1 "
+ + "FOR EACH ROW INSERT INTO audit (id) VALUES (NEW.id)", trigger.toString());
+
+ CreateTrigger replaced = (CreateTrigger) CCJSqlParserUtil.parse(
+ "CREATE OR REPLACE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW SET NEW.a = 1",
+ parser -> parser.withDialect(Dialect.MARIADB));
+ assertTrue(replaced.isOrReplace());
+ assertFalse(replaced.isIfNotExists());
+
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
+ "CREATE OR REPLACE TRIGGER IF NOT EXISTS trg BEFORE INSERT ON t1 "
+ + "FOR EACH ROW SET NEW.a = 1"));
+ }
+
+ @Test
+ void keepsStatementBlocksStructured() throws JSQLParserException {
+ String sql = "CREATE TRIGGER trg AFTER INSERT ON t1 FOR EACH ROW BEGIN "
+ + "INSERT INTO audit (id) VALUES (NEW.id); "
+ + "UPDATE counters SET n = CASE WHEN NEW.a > 0 THEN n + 1 ELSE n END; "
+ + "DELETE FROM pending WHERE id = NEW.id; END;";
+ CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.MYSQL));
+ Block body = assertInstanceOf(Block.class, trigger.getBody());
+ assertEquals(3, body.getStatements().size());
+ assertInstanceOf(Insert.class, body.getStatements().get(0));
+ // The semicolon after END ends the CREATE TRIGGER statement.
+ assertFalse(body.hasSemicolonAfterEnd());
+ assertEquals(Set.of("t1", "audit", "counters", "pending"),
+ new HashSet<>(new TablesNamesFinder().getTables((Statement) trigger)));
+ }
+
+ @Test
+ void keepsProceduralBodiesAsTokens() throws JSQLParserException {
+ CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse(
+ "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW "
+ + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END",
+ parser -> parser.withDialect(Dialect.MYSQL));
+ UnsupportedStatement body = assertInstanceOf(UnsupportedStatement.class, trigger.getBody());
+ assertEquals("BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END", body.toString());
+ assertEquals(Set.of("t1"),
+ new HashSet<>(new TablesNamesFinder().getTables((Statement) trigger)));
+ }
+
+ @Test
+ void splitsScriptsAfterCompoundBodies() throws JSQLParserException {
+ String script = "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW "
+ + "BEGIN SET NEW.a = 1; SET NEW.b = 2; END;\n"
+ + "CREATE TABLE b (y INT);\n"
+ + "CREATE TRIGGER trg3 BEFORE UPDATE ON t1 FOR EACH ROW "
+ + "BEGIN IF NEW.a < 0 THEN SET NEW.a = 0; END IF; END;\n"
+ + "CREATE TABLE c (z INT);";
+ Statements statements = CCJSqlParserUtil.parseStatements(script,
+ parser -> parser.withDialect(Dialect.MYSQL));
+ assertEquals(4, statements.size());
+ assertInstanceOf(Block.class, ((CreateTrigger) statements.get(0)).getBody());
+ assertInstanceOf(CreateTable.class, statements.get(1));
+ assertInstanceOf(UnsupportedStatement.class, ((CreateTrigger) statements.get(2)).getBody());
+ assertInstanceOf(CreateTable.class, statements.get(3));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN IF NEW.a < 0 THEN SET NEW.a = 0;",
+ "CREATE TRIGGER trg BEFORE INSERT ON t1 FOR EACH ROW BEGIN SET NEW.a = 1"})
+ void rejectsUnterminatedCompoundBodies(String sql) {
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)));
+ }
}