From daaf7d47d2fa3f17d2eb7366ba4acc8f04e0d92d Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 01:36:48 +0900 Subject: [PATCH] feat: share MySQL database options with ALTER DATABASE Signed-off-by: minleejae --- .../statement/StatementVisitor.java | 9 +++ .../alter/database/AlterDatabase.java | 67 ++++++++++++++++++ .../create/database/DatabaseOption.java | 7 +- .../sf/jsqlparser/util/TablesNamesFinder.java | 6 ++ .../util/deparser/StatementDeParser.java | 6 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 34 +++++++++ .../alter/database/AlterDatabaseTest.java | 69 +++++++++++++++++++ 7 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 3b477e274..e1a0d055e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; @@ -671,4 +672,12 @@ default void visit(OracleNullStatement statement) { visit(statement, null); } + default T visit(AlterDatabase statement, S context) { + return null; + } + + default void visit(AlterDatabase statement) { + visit(statement, null); + } + } 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 new file mode 100644 index 000000000..d0c7a79ea --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/database/AlterDatabase.java @@ -0,0 +1,67 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter.database; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +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. */ +public class AlterDatabase implements Statement { + private String databaseName; + private boolean useSchemaKeyword; + private final List options = new ArrayList<>(); + + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + public boolean isUseSchemaKeyword() { + return useSchemaKeyword; + } + + public void setUseSchemaKeyword(boolean useSchemaKeyword) { + this.useSchemaKeyword = useSchemaKeyword; + } + + public List getOptions() { + return options; + } + + public Optional getOption(DatabaseOption.Kind kind) { + return options.stream().filter(option -> option.getKind() == kind).findFirst(); + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(useSchemaKeyword ? "ALTER SCHEMA" : "ALTER DATABASE"); + if (databaseName != null) { + builder.append(' ').append(databaseName); + } + options.forEach(option -> builder.append(' ').append(option)); + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} 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 75a9cabae..497aa91d7 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 @@ -14,11 +14,14 @@ import java.util.Collections; import java.util.List; -/** A structured option shared by MySQL {@code CREATE DATABASE} and {@code CREATE SCHEMA}. */ +/** + * A structured option shared by MySQL {@code CREATE/ALTER DATABASE} and + * {@code CREATE/ALTER SCHEMA}. + */ public class DatabaseOption implements Serializable { public enum Kind { - CHARACTER_SET, COLLATE, ENCRYPTION, OTHER + CHARACTER_SET, COLLATE, ENCRYPTION, READ_ONLY, OTHER } private Kind kind = Kind.OTHER; diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 798234f9a..7d49f55c2 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.select.MatchRecognize; import net.sf.jsqlparser.expression.RowPatternFunction; @@ -2912,4 +2913,9 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + @Override + public Void visit(AlterDatabase statement, S context) { + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 4e82f77da..83a40d6fd 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; @@ -809,4 +810,9 @@ public StringBuilder visit(OracleNullStatement statement, S context) { return builder.append("NULL"); } + @Override + public StringBuilder visit(AlterDatabase statement, S context) { + return statement.appendTo(builder); + } + } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e6..efde4cb30 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -50,6 +50,7 @@ import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; import net.sf.jsqlparser.statement.comment.*; import net.sf.jsqlparser.statement.create.database.*; @@ -14652,6 +14653,35 @@ DatabaseOption MySqlDatabaseOption(): { return new DatabaseOption(kind, name, value, useDefault, useEquals); } } +/** Reuses CREATE's database options; READ ONLY is specific to ALTER. */ +AlterDatabase AlterDatabase(): +{ + AlterDatabase result = new AlterDatabase(); + String name; + DatabaseOption option; + Token value; + boolean equals; +} +{ + ( | { result.setUseSchemaKeyword(true); } ) + [ LOOKAHEAD({ !isMySqlDatabaseOptionAhead() && getToken(1).kind != K_READ }) + name=RelObjectName() { result.setDatabaseName(name); } ] + ( + ( + LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) option=MySqlDatabaseOption() + | + { 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); } + )+ + { return result; } +} + List PathSpecification(): { Token tk; @@ -17951,6 +17981,10 @@ Statement Alter(): | statement = AlterDefaultPrivileges() | + LOOKAHEAD({ getToken(1).kind == K_DATABASE || (getToken(1).kind == K_SCHEMA + && Dialect.MYSQL.name().equals(getAsString(Feature.dialect))) }) + statement = AlterDatabase() + | LOOKAHEAD() statement = AlterSchema() | LOOKAHEAD() statement = AlterType() 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 new file mode 100644 index 000000000..56b46adde --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/database/AlterDatabaseTest.java @@ -0,0 +1,69 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter.database; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterDatabaseTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER DATABASE d CHARACTER SET utf8mb4 COLLATE utf8mb4_bin", + "ALTER SCHEMA `d` DEFAULT CHARACTER SET = utf8mb4 DEFAULT COLLATE = utf8mb4_bin", + "ALTER DATABASE d DEFAULT ENCRYPTION = 'N'", + "ALTER DATABASE d READ ONLY = 0", "ALTER DATABASE d READ ONLY 1", + "ALTER DATABASE READ ONLY = DEFAULT", "ALTER SCHEMA DEFAULT CHARACTER SET utf8mb4"}) + void roundTripAndStatementBoundary(String sql) throws JSQLParserException { + AlterDatabase statement = parse(sql); + assertEquals(sql, statement.toString()); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(sql, buffer.toString()); + assertEquals(sql, parse(buffer.toString()).toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.MYSQL)).size()); + } + + @Test + void optionMutationAndVisitorDispatch() throws JSQLParserException { + AlterDatabase statement = parse("ALTER DATABASE old_db READ ONLY = 0"); + statement.setDatabaseName("new_db"); + statement.getOption(DatabaseOption.Kind.READ_ONLY).orElseThrow().setValue("1"); + assertEquals("ALTER DATABASE new_db READ ONLY = 1", statement.toString()); + assertEquals("context:new_db", statement.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterDatabase database, S context) { + return context + ":" + database.getDatabaseName(); + } + }, "context")); + } + + @Test + void rejectIncompleteAndInvalidReadOnly() { + for (String sql : new String[] {"ALTER DATABASE d", "ALTER DATABASE d READ ONLY = 2", + "ALTER DATABASE d DEFAULT READ ONLY = 0"}) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + private static AlterDatabase parse(String sql) throws JSQLParserException { + return (AlterDatabase) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } +}