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"); + } }