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 9444d6bad3..e1fb9ea059 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 @@ -14,6 +14,7 @@ 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; @@ -27,6 +28,7 @@ public class CreateDatabase implements Statement { private String databaseName; private boolean hasIfNotExists = false; private List databaseOptions = null; + private List options; @Override public T accept(StatementVisitor statementVisitor, S context) { @@ -67,9 +69,27 @@ public List getDatabaseOptions() { public CreateDatabase setDatabaseOptions(List databaseOptions) { this.databaseOptions = databaseOptions; + this.options = null; return this; } + public List getOptions() { + return options; + } + + public CreateDatabase setOptions(List options) { + this.options = options; + this.databaseOptions = options == null ? null + : options.stream().flatMap(option -> option.getTokens().stream()) + .collect(Collectors.toList()); + return this; + } + + public Optional getOption(DatabaseOption.Kind kind) { + return Optional.ofNullable(options).orElseGet(Collections::emptyList).stream() + .filter(option -> option.getKind() == kind).findFirst(); + } + public CreateDatabase withDatabaseName(String databaseName) { return this.setDatabaseName(databaseName); } @@ -82,6 +102,17 @@ public CreateDatabase withDatabaseOptions(List databaseOptions) { return this.setDatabaseOptions(databaseOptions); } + public CreateDatabase withOptions(List options) { + return setOptions(options); + } + + public CreateDatabase addOptions(DatabaseOption... options) { + List collection = + Optional.ofNullable(getOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, options); + return withOptions(collection); + } + public CreateDatabase addDatabaseOptions(String... databaseOptions) { List collection = Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); @@ -105,7 +136,9 @@ public String toString() { if (databaseName != null) { sql += " " + databaseName; } - if (databaseOptions != null && !databaseOptions.isEmpty()) { + if (options != null && !options.isEmpty()) { + sql += " " + options.stream().map(Object::toString).collect(Collectors.joining(" ")); + } else if (databaseOptions != null && !databaseOptions.isEmpty()) { sql += " " + String.join(" ", databaseOptions); } return sql; 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 new file mode 100644 index 0000000000..75a9cabae7 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/database/DatabaseOption.java @@ -0,0 +1,142 @@ +/*- + * #%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.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** A structured option shared by MySQL {@code CREATE DATABASE} and {@code CREATE SCHEMA}. */ +public class DatabaseOption implements Serializable { + + public enum Kind { + CHARACTER_SET, COLLATE, ENCRYPTION, OTHER + } + + private Kind kind = Kind.OTHER; + private String name; + private String value; + private boolean useDefault; + private boolean useEquals; + private List rawTokens; + + public DatabaseOption() {} + + public DatabaseOption(Kind kind, String name, String value, boolean useDefault, + boolean useEquals) { + this.kind = kind; + this.name = name; + this.value = value; + this.useDefault = useDefault; + this.useEquals = useEquals; + } + + public static DatabaseOption raw(List tokens) { + DatabaseOption option = new DatabaseOption(); + option.setRawTokens(tokens); + return option; + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isUseDefault() { + return useDefault; + } + + public void setUseDefault(boolean useDefault) { + this.useDefault = useDefault; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public List getRawTokens() { + return rawTokens; + } + + public void setRawTokens(List rawTokens) { + this.rawTokens = rawTokens; + } + + /** Returns this option in the legacy flat-token representation. */ + public List getTokens() { + if (rawTokens != null) { + return Collections.unmodifiableList(rawTokens); + } + List tokens = new ArrayList<>(); + if (useDefault) { + tokens.add("DEFAULT"); + } + Collections.addAll(tokens, name.split(" ")); + if (useEquals) { + tokens.add("="); + } + tokens.add(value); + return tokens; + } + + @Override + public String toString() { + return String.join(" ", getTokens()); + } + + public DatabaseOption withKind(Kind kind) { + setKind(kind); + return this; + } + + public DatabaseOption withName(String name) { + setName(name); + return this; + } + + public DatabaseOption withValue(String value) { + setValue(value); + return this; + } + + public DatabaseOption withUseDefault(boolean useDefault) { + setUseDefault(useDefault); + return this; + } + + public DatabaseOption withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java b/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java index e972c9c30a..f8888132f3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java @@ -14,9 +14,11 @@ 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; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; public class CreateSchema implements Statement { @@ -25,6 +27,7 @@ public class CreateSchema implements Statement { private String schemaName; private List schemaPath; private List statements = new ArrayList<>(); + private List databaseOptions; private boolean hasIfNotExists = false; @Override @@ -114,6 +117,19 @@ public List getStatements() { return statements; } + public List getDatabaseOptions() { + return databaseOptions; + } + + public void setDatabaseOptions(List databaseOptions) { + this.databaseOptions = databaseOptions; + } + + public Optional getDatabaseOption(DatabaseOption.Kind kind) { + return Optional.ofNullable(databaseOptions).orElseGet(Collections::emptyList).stream() + .filter(option -> option.getKind() == kind).findFirst(); + } + public boolean hasIfNotExists() { return hasIfNotExists; } @@ -131,11 +147,15 @@ public String toString() { if (schemaName != null) { sql += " "; - if (catalogName!=null) { + if (catalogName != null) { sql += catalogName + "."; } sql += schemaName; } + if (databaseOptions != null && !databaseOptions.isEmpty()) { + sql += " " + databaseOptions.stream().map(Object::toString) + .collect(Collectors.joining(" ")); + } if (authorization != null) { sql += " AUTHORIZATION " + authorization; } @@ -157,6 +177,18 @@ public CreateSchema withSchemaPath(List schemaPath) { return this; } + public CreateSchema withDatabaseOptions(List databaseOptions) { + setDatabaseOptions(databaseOptions); + return this; + } + + public CreateSchema addDatabaseOptions(DatabaseOption... databaseOptions) { + List collection = + Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, databaseOptions); + return withDatabaseOptions(collection); + } + public CreateSchema addSchemaPath(String... schemaPath) { List collection = Optional.ofNullable(getSchemaPath()).orElseGet(ArrayList::new); Collections.addAll(collection, schemaPath); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 52994191b4..8163f05a10 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -126,6 +126,12 @@ public class CCJSqlParser extends AbstractJSqlParser { return jjtree.rootNode(); } + 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; + } + private static class ObjectNames { private final List names; @@ -11448,6 +11454,7 @@ CreateSchema CreateSchema(): CreateSchema schema = new CreateSchema(); List schemaPath = null; List statements = new ArrayList(); + DatabaseOption databaseOption = null; } { @@ -11461,6 +11468,11 @@ CreateSchema CreateSchema(): )? ] + ( + LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) + databaseOption=MySqlDatabaseOption() { schema.addDatabaseOptions(databaseOption); } + )* + [ (tk= | tk=) { schema.setAuthorization(tk.image); } ] @@ -11489,6 +11501,38 @@ CreateSchema CreateSchema(): } } +DatabaseOption MySqlDatabaseOption(): +{ + boolean useDefault = false; + boolean useEquals = false; + DatabaseOption.Kind kind = DatabaseOption.Kind.OTHER; + String name = null; + String value = null; + Token valueToken = null; +} +{ + [ { useDefault = true; } ] + ( + { + kind = DatabaseOption.Kind.CHARACTER_SET; + name = "CHARACTER SET"; + } + | + { + kind = DatabaseOption.Kind.COLLATE; + name = "COLLATE"; + } + | + { + kind = DatabaseOption.Kind.ENCRYPTION; + name = "ENCRYPTION"; + } + ) + [ "=" { useEquals = true; } ] + ( value=RelObjectName() | valueToken= { value = valueToken.image; } ) + { return new DatabaseOption(kind, name, value, useDefault, useEquals); } +} + List PathSpecification(): { Token tk; @@ -11507,12 +11551,19 @@ CreateDatabase CreateDatabase(): CreateDatabase createDatabase = new CreateDatabase(); String databaseName = null; List databaseOptions = null; + DatabaseOption databaseOption = null; } { [ LOOKAHEAD(2) { createDatabase.setIfNotExists(true); } ] databaseName = RelObjectName() { createDatabase.setDatabaseName(databaseName); } - [ databaseOptions = captureRest() { createDatabase.setDatabaseOptions(databaseOptions); } ] + ( + LOOKAHEAD({ isMySqlDatabaseOptionAhead() }) + databaseOption=MySqlDatabaseOption() { createDatabase.addOptions(databaseOption); } + )* + [ databaseOptions = captureRest() { + createDatabase.addOptions(DatabaseOption.raw(databaseOptions)); + } ] { return createDatabase; } 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 724b5ac059..20f7db0ee4 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 @@ -59,6 +59,12 @@ void testCreateDatabaseWithOptions() throws JSQLParserException { Arrays.asList("DEFAULT", "CHARACTER", "SET", "utf8mb4", "COLLATE", "utf8mb4_unicode_ci"), createDatabase.getDatabaseOptions()); + DatabaseOption characterSet = + createDatabase.getOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow(); + assertEquals("utf8mb4", characterSet.getValue()); + assertTrue(characterSet.isUseDefault()); + assertEquals("utf8mb4_unicode_ci", + createDatabase.getOption(DatabaseOption.Kind.COLLATE).orElseThrow().getValue()); } @Test 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 51aec7a84a..5d6093f186 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 @@ -9,9 +9,14 @@ */ package net.sf.jsqlparser.statement.create.schema; -import net.sf.jsqlparser.JSQLParserException; 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.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.database.DatabaseOption; import org.junit.jupiter.api.Test; /** @@ -49,4 +54,27 @@ void testIfNotExistsIssue2061() throws JSQLParserException { String sqlStr = "CREATE SCHEMA IF NOT EXISTS sales_kpi"; assertSqlCanBeParsedAndDeparsed(sqlStr); } + + @Test + void testMySqlCreateSchemaOptions() throws JSQLParserException { + String sql = "CREATE SCHEMA IF NOT EXISTS schema_4 DEFAULT CHARACTER SET = utf8 " + + "COLLATE = utf8_general_ci DEFAULT ENCRYPTION = 'N'"; + CreateSchema schema = (CreateSchema) assertSqlCanBeParsedAndDeparsed(sql); + + DatabaseOption characterSet = schema + .getDatabaseOption(DatabaseOption.Kind.CHARACTER_SET).orElseThrow(); + assertEquals("utf8", characterSet.getValue()); + assertTrue(characterSet.isUseDefault()); + assertTrue(characterSet.isUseEquals()); + + DatabaseOption collate = + schema.getDatabaseOption(DatabaseOption.Kind.COLLATE).orElseThrow(); + assertEquals("utf8_general_ci", collate.getValue()); + + DatabaseOption encryption = + schema.getDatabaseOption(DatabaseOption.Kind.ENCRYPTION).orElseThrow(); + assertEquals("'N'", encryption.getValue()); + + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + } }