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..65ee61461 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
*
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..868c7f6e8
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/ConfigurationParameterChange.java
@@ -0,0 +1,174 @@
+/*-
+ * #%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, such as {@code ALTER ROLE} and {@code ALTER DATABASE}:
+ *
+ *
+ * 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/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/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/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
index 2490baa3e..c06ff88b5 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
@@ -838,7 +838,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
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..b9bfba466 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,10 @@ 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/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..270fbbf35 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,10 @@ 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/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..4e91f10c8 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,10 @@ 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/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..654331c98 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
@@ -85,6 +85,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 +441,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;
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 2210dbe9d..bcd2ee199 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -269,9 +269,54 @@ 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);
}
private static class ObjectNames {
@@ -15888,6 +15933,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 +15958,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 +16109,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 +16121,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;
@@ -19591,10 +19764,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 +20588,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 +20601,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 +20617,36 @@ AlterRole AlterRole():
}
}
+/** PostgreSQL SET/RESET of a configuration parameter, shared by ALTER ROLE and ALTER DATABASE. */
+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; } }
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index 901608d62..d887ffe86 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -1868,6 +1868,60 @@ 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 `_.
+
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..304d6da92 100644
--- a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java
@@ -39,6 +39,8 @@ 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 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/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/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");
+ }
}