From 4f2f65333de5b82f380228960db63cc1e7558177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Sat, 15 Aug 2026 17:31:37 +0800 Subject: [PATCH] feat: support CREATE DATABASE statement (#2070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse CREATE DATABASE [IF NOT EXISTS] into a typed CreateDatabase statement instead of the generic UnsupportedStatement fallback. Vendor-specific options after the name (e.g. MySQL's DEFAULT CHARACTER SET / COLLATE) are captured verbatim and round-trip unchanged. Signed-off-by: 付典 --- .../sf/jsqlparser/parser/feature/Feature.java | 7 ++ .../statement/StatementVisitor.java | 7 ++ .../statement/StatementVisitorAdapter.java | 6 + .../create/database/CreateDatabase.java | 113 ++++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 12 ++ .../util/deparser/StatementDeParser.java | 7 ++ .../validator/StatementValidator.java | 8 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 19 +++ .../create/database/CreateDatabaseTest.java | 76 ++++++++++++ 9 files changed, 255 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java 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 d786f5170..4c1b6dd51 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -30,6 +30,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; 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.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; @@ -475,6 +476,12 @@ public enum Feature { */ drop, dropTable, dropIndex, dropView, dropSchema, dropSequence, dropTableIfExists, dropIndexIfExists, dropViewIfExists, dropSchemaIfExists, dropSequenceIfExists, + /** + * SQL "CREATE DATABASE" statement is allowed + * + * @see CreateDatabase + */ + createDatabase, /** * SQL "CREATE SCHEMA" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 9ebab53a8..24a5ed6ab 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -16,6 +16,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; 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.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -118,6 +119,12 @@ default void visit(CreateSchema createSchema) { this.visit(createSchema, null); } + T visit(CreateDatabase createDatabase, S context); + + default void visit(CreateDatabase createDatabase) { + this.visit(createDatabase, null); + } + T visit(CreateTable createTable, S context); default void visit(CreateTable createTable) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 3b12c01c0..38a32ea1f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -20,6 +20,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; 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.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -261,6 +262,11 @@ public T visit(CreateSchema createSchema, S context) { return null; } + @Override + public T visit(CreateDatabase createDatabase, S context) { + return null; + } + @Override public T visit(CreateTable createTable, S context) { return createTable.getTable().accept(fromItemVisitor, context); 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 new file mode 100644 index 000000000..9444d6bad --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java @@ -0,0 +1,113 @@ +/*- + * #%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.create.database; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +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. + */ +public class CreateDatabase implements Statement { + + private String databaseName; + private boolean hasIfNotExists = false; + private List databaseOptions = null; + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + /** + * The name of the database. + * + * @return the database name + */ + public String getDatabaseName() { + return databaseName; + } + + public CreateDatabase setDatabaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + public boolean hasIfNotExists() { + return hasIfNotExists; + } + + public CreateDatabase setIfNotExists(boolean hasIfNotExists) { + this.hasIfNotExists = hasIfNotExists; + return this; + } + + /** + * The vendor-specific options of the database, captured as raw tokens. + * + * @return the list of option tokens, {@code null} when no options were given + */ + public List getDatabaseOptions() { + return databaseOptions; + } + + public CreateDatabase setDatabaseOptions(List databaseOptions) { + this.databaseOptions = databaseOptions; + return this; + } + + public CreateDatabase withDatabaseName(String databaseName) { + return this.setDatabaseName(databaseName); + } + + public CreateDatabase withIfNotExists(boolean hasIfNotExists) { + return this.setIfNotExists(hasIfNotExists); + } + + public CreateDatabase withDatabaseOptions(List databaseOptions) { + return this.setDatabaseOptions(databaseOptions); + } + + public CreateDatabase addDatabaseOptions(String... databaseOptions) { + List collection = + Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, databaseOptions); + return this.withDatabaseOptions(collection); + } + + public CreateDatabase addDatabaseOptions(Collection databaseOptions) { + List collection = + Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); + collection.addAll(databaseOptions); + return this.withDatabaseOptions(collection); + } + + @Override + public String toString() { + String sql = "CREATE DATABASE"; + if (hasIfNotExists) { + sql += " IF NOT EXISTS"; + } + if (databaseName != null) { + sql += " " + databaseName; + } + if (databaseOptions != null && !databaseOptions.isEmpty()) { + sql += " " + String.join(" ", databaseOptions); + } + return sql; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 99ee4eacc..720e16254 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -94,6 +94,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; 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.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -1166,6 +1167,17 @@ public void visit(CreateSchema createSchema) { StatementVisitor.super.visit(createSchema); } + @Override + public Void visit(CreateDatabase createDatabase, S context) { + throwUnsupported(createDatabase); + return null; + } + + @Override + public void visit(CreateDatabase createDatabase) { + StatementVisitor.super.visit(createDatabase); + } + @Override public Void visit(CreateTable create, S context) { visit(create.getTable(), 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 751c4bf64..51d06c7f1 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -41,6 +41,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; 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.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -431,6 +432,12 @@ public StringBuilder visit(CreateSchema aThis, S context) { return builder; } + @Override + public StringBuilder visit(CreateDatabase aThis, S context) { + builder.append(aThis.toString()); + return builder; + } + @Override public StringBuilder visit(CreateSequence createSequence, S context) { new CreateSequenceDeParser(builder).deParse(createSequence); 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 9e073a227..a3debcba3 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 @@ -37,6 +37,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; 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.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; @@ -299,6 +300,13 @@ public Void visit(CreateSchema aThis, S context) { return null; } + @Override + public Void visit(CreateDatabase aThis, S context) { + validateFeatureAndName(Feature.createDatabase, NamedObject.database, + aThis.getDatabaseName()); + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { getValidator(CreateSequenceValidator.class).validate(createSequence); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5f8e91d11..9da42b5d6 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -47,6 +47,7 @@ import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; import net.sf.jsqlparser.statement.alter.sequence.*; import net.sf.jsqlparser.statement.comment.*; +import net.sf.jsqlparser.statement.create.database.*; import net.sf.jsqlparser.statement.create.function.*; import net.sf.jsqlparser.statement.create.index.*; import net.sf.jsqlparser.statement.create.policy.*; @@ -10820,6 +10821,22 @@ List PathSpecification(): } } +CreateDatabase CreateDatabase(): +{ + CreateDatabase createDatabase = new CreateDatabase(); + String databaseName = null; + List databaseOptions = null; +} +{ + + [ LOOKAHEAD(2) { createDatabase.setIfNotExists(true); } ] + databaseName = RelObjectName() { createDatabase.setDatabaseName(databaseName); } + [ databaseOptions = captureRest() { createDatabase.setDatabaseOptions(databaseOptions); } ] + { + return createDatabase; + } +} + /** * Parses a single table-level constraint inside CREATE TABLE (...). * Handles INDEX, PRIMARY KEY, UNIQUE, KEY, FOREIGN KEY, CHECK, EXCLUDE. @@ -13267,6 +13284,8 @@ Statement Create(): | statement = CreateSchema() | + statement = CreateDatabase() + | statement = CreateSequence() | statement = CreateSynonym(isUsingOrReplace) 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 new file mode 100644 index 000000000..724b5ac05 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java @@ -0,0 +1,76 @@ +/*- + * #%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.create.database; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +public class CreateDatabaseTest { + + @Test + void testCreateDatabaseIssue2070() throws JSQLParserException { + String statement = "CREATE DATABASE USERS"; + assertSqlCanBeParsedAndDeparsed(statement); + assertDeparse(new CreateDatabase().withDatabaseName("USERS"), statement); + } + + @Test + void testCreateDatabaseIfNotExists() throws JSQLParserException { + String statement = "CREATE DATABASE IF NOT EXISTS mydb"; + assertSqlCanBeParsedAndDeparsed(statement); + assertDeparse(new CreateDatabase().withDatabaseName("mydb").withIfNotExists(true), + statement); + } + + @Test + void testCreateDatabaseQuotedName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE `my db`"); + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE \"my db\""); + } + + @Test + void testCreateDatabaseWithOptions() throws JSQLParserException { + String statement = + "CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"; + assertSqlCanBeParsedAndDeparsed(statement); + + Statement parsed = CCJSqlParserUtil.parse(statement); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, parsed); + assertEquals("mydb", createDatabase.getDatabaseName()); + assertTrue(createDatabase.hasIfNotExists()); + assertEquals( + Arrays.asList("DEFAULT", "CHARACTER", "SET", "utf8mb4", "COLLATE", + "utf8mb4_unicode_ci"), + createDatabase.getDatabaseOptions()); + } + + @Test + void testCreateDatabaseAST() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("CREATE DATABASE USERS"); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertEquals("USERS", createDatabase.getDatabaseName()); + assertFalse(createDatabase.hasIfNotExists()); + + statement = CCJSqlParserUtil.parse("CREATE DATABASE IF NOT EXISTS mydb"); + createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertEquals("mydb", createDatabase.getDatabaseName()); + assertTrue(createDatabase.hasIfNotExists()); + } +}