diff --git a/flyway-database-oceanbase/pom.xml b/flyway-database-oceanbase/pom.xml index e85d451..8bb0096 100644 --- a/flyway-database-oceanbase/pom.xml +++ b/flyway-database-oceanbase/pom.xml @@ -45,6 +45,10 @@ + + org.projectlombok + lombok + diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseConnection.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseConnection.java deleted file mode 100644 index 7d09cd1..0000000 --- a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseConnection.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) Red Gate Software Ltd 2010-2024 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.flywaydb.community.database.oceanbase; - -import org.flywaydb.database.mysql.MySQLConnection; -import org.flywaydb.database.mysql.MySQLDatabase; - -import java.sql.Connection; - -public class OceanBaseConnection extends MySQLConnection { - - public OceanBaseConnection(MySQLDatabase database, Connection connection) { - super(database, connection); - } - - @Override - protected boolean canUseNamedLockTemplate() { - return false; - } -} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/OceanBaseDatabaseExtension.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseExtension.java similarity index 60% rename from flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/OceanBaseDatabaseExtension.java rename to flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseExtension.java index 83f87f8..c7db359 100644 --- a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/OceanBaseDatabaseExtension.java +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseExtension.java @@ -1,3 +1,22 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ /* * Copyright (C) Red Gate Software Ltd 2010-2024 * @@ -13,15 +32,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.flywaydb.community.database; +package org.flywaydb.community.database.oceanbase; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; import org.flywaydb.core.api.FlywayException; import org.flywaydb.core.extensibility.PluginMetadata; import org.flywaydb.core.internal.util.FileUtils; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - public class OceanBaseDatabaseExtension implements PluginMetadata { public String getDescription() { return "Community-contributed OceanBase database support extension " + readVersion() + " by Redgate"; diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseType.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseType.java index 53a6642..8407e3d 100644 --- a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseType.java +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabaseType.java @@ -1,3 +1,22 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ /* * Copyright (C) Red Gate Software Ltd 2010-2024 * @@ -15,9 +34,17 @@ */ package org.flywaydb.community.database.oceanbase; -import org.flywaydb.community.database.OceanBaseDatabaseExtension; +import org.flywaydb.community.database.oceanbase.mysql.OceanBaseMysqlModeDatabase; +import org.flywaydb.community.database.oceanbase.oracle.OceanBaseOracleModeDatabase; +import org.flywaydb.community.database.oceanbase.oracle.OceanBaseOracleModeParser; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Types; + import org.flywaydb.core.api.ResourceProvider; import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.BaseDatabaseType; import org.flywaydb.core.internal.database.base.CommunityDatabaseType; import org.flywaydb.core.internal.database.base.Database; import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; @@ -25,17 +52,13 @@ import org.flywaydb.core.internal.parser.Parser; import org.flywaydb.core.internal.parser.ParsingContext; import org.flywaydb.core.internal.util.ClassUtils; -import org.flywaydb.database.mysql.MySQLDatabaseType; import org.flywaydb.database.mysql.MySQLParser; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Types; - -public class OceanBaseDatabaseType extends MySQLDatabaseType implements CommunityDatabaseType { +public class OceanBaseDatabaseType extends BaseDatabaseType implements CommunityDatabaseType { private static final String OB_JDBC_DRIVER = "com.oceanbase.jdbc.Driver"; private static final String OB_LEGACY_JDBC_DRIVER = "com.alipay.oceanbase.jdbc.Driver"; + private static JdbcConnectionFactory jdbcConnectionFactory; public String getName() { return "OceanBase"; @@ -59,24 +82,19 @@ public boolean handlesJDBCUrl(String url) { @Override public String getDriverClass(String url, ClassLoader classLoader) { - return url.startsWith("jdbc:oceanbase:")? - OB_JDBC_DRIVER : - super.getDriverClass(url, classLoader); + return url.startsWith("jdbc:oceanbase:") ? OB_JDBC_DRIVER : OceanBaseJdbcUtils.getDriverClass(url, classLoader); } @Override public String getBackupDriverClass(String url, ClassLoader classLoader) { - return (url.startsWith("jdbc:oceanbase:") && ClassUtils.isPresent(OB_LEGACY_JDBC_DRIVER, classLoader)? - OB_LEGACY_JDBC_DRIVER : - super.getBackupDriverClass(url, classLoader)); + return (url.startsWith("jdbc:oceanbase:") && ClassUtils.isPresent(OB_LEGACY_JDBC_DRIVER, classLoader) ? OB_LEGACY_JDBC_DRIVER : OceanBaseJdbcUtils.getBackupDriverClass(url, classLoader)); } @Override public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { - if (!databaseProductName.contains("MySQL") && !databaseProductName.contains("OceanBase")) { + if (!databaseProductName.contains("MySQL") && !databaseProductName.contains("Oracle") && !databaseProductName.contains("OceanBase")) { return false; } - String versionComment; try { versionComment = OceanBaseJdbcUtils.getVersionComment(connection); @@ -88,7 +106,31 @@ public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, @Override public Database createDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { - return new OceanBaseDatabase(configuration, jdbcConnectionFactory, statementInterceptor); + OceanBaseDatabaseType.jdbcConnectionFactory = jdbcConnectionFactory; + String compatibleMode = getCompatibleMode(jdbcConnectionFactory); + if (isMySQLMode(compatibleMode)) { + return new OceanBaseMysqlModeDatabase(configuration, jdbcConnectionFactory, statementInterceptor); + } else { + return new OceanBaseOracleModeDatabase(configuration, jdbcConnectionFactory, statementInterceptor); + } + } + + @Override + public Parser createParser(Configuration configuration, ResourceProvider resourceProvider, ParsingContext parsingContext) { + String compatibleMode = getCompatibleMode(jdbcConnectionFactory); + if (isMySQLMode(compatibleMode)) { + return new MySQLParser(configuration, parsingContext); + } else { + return new OceanBaseOracleModeParser(configuration, parsingContext); + } + } + + private String getCompatibleMode(JdbcConnectionFactory jdbcConnectionFactory) { + return OceanBaseJdbcUtils.getCompatibleMode(jdbcConnectionFactory.openConnection()); + } + + private boolean isMySQLMode(String compatibleMode) { + return "MySQL".equalsIgnoreCase(compatibleMode); } @Override diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseJdbcUtils.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseJdbcUtils.java index 85f0752..9cb8c93 100644 --- a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseJdbcUtils.java +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseJdbcUtils.java @@ -1,3 +1,22 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ /* * Copyright (C) Red Gate Software Ltd 2010-2024 * @@ -15,13 +34,14 @@ */ package org.flywaydb.community.database.oceanbase; -import org.flywaydb.core.internal.util.StringUtils; - import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import org.flywaydb.core.internal.util.ClassUtils; +import org.flywaydb.core.internal.util.StringUtils; + public class OceanBaseJdbcUtils { public static String getVersionComment(Connection connection) throws SQLException { @@ -50,4 +70,31 @@ private static String queryVariable(Connection connection, String variable) thro } return null; } + + public static String getCompatibleMode(Connection connection) { + String mode; + try (Connection conn = connection; Statement statement = conn.createStatement()) { + ResultSet rs = statement.executeQuery("SHOW VARIABLES LIKE 'ob_compatibility_mode'"); + mode = rs.next() ? rs.getString("VALUE") : null; + } catch (SQLException e) { + throw new RuntimeException("Failed to get oceanbase compatible mode", e); + } + return mode; + } + + public static String getDriverClass(String url, ClassLoader classLoader) { + if (!url.startsWith("jdbc:p6spy:mysql:") && !url.startsWith("jdbc:p6spy:google:")) { + return url.startsWith("jdbc:mysql:") ? "com.mysql.cj.jdbc.Driver" : "com.mysql.jdbc.GoogleDriver"; + } else { + return "com.p6spy.engine.spy.P6SpyDriver"; + } + } + + public static String getBackupDriverClass(String url, ClassLoader classLoader) { + if (ClassUtils.isPresent("com.mysql.jdbc.Driver", classLoader)) { + return "com.mysql.jdbc.Driver"; + } else { + return ClassUtils.isPresent("org.mariadb.jdbc.Driver", classLoader) && !url.contains("disableMariaDbDriver") ? "org.mariadb.jdbc.Driver" : null; + } + } } diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/OceanBaseMysqlModeConnection.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/OceanBaseMysqlModeConnection.java new file mode 100644 index 0000000..8f9b8f7 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/OceanBaseMysqlModeConnection.java @@ -0,0 +1,52 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +/* + * Copyright (C) Red Gate Software Ltd 2010-2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flywaydb.community.database.oceanbase.mysql; + +import java.sql.Connection; + +import org.flywaydb.database.mysql.MySQLConnection; +import org.flywaydb.database.mysql.MySQLDatabase; + +public class OceanBaseMysqlModeConnection extends MySQLConnection { + + public OceanBaseMysqlModeConnection(MySQLDatabase database, Connection connection) { + super(database, connection); + } + + @Override + protected boolean canUseNamedLockTemplate() { + return false; + } +} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabase.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/OceanBaseMysqlModeDatabase.java similarity index 58% rename from flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabase.java rename to flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/OceanBaseMysqlModeDatabase.java index c5f70e1..40d91c1 100644 --- a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/OceanBaseDatabase.java +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/OceanBaseMysqlModeDatabase.java @@ -1,3 +1,22 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ /* * Copyright (C) Red Gate Software Ltd 2010-2024 * @@ -13,7 +32,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.flywaydb.community.database.oceanbase; +package org.flywaydb.community.database.oceanbase.mysql; + +import org.flywaydb.community.database.oceanbase.OceanBaseJdbcUtils; + +import java.sql.Connection; +import java.sql.SQLException; import org.flywaydb.core.api.FlywayException; import org.flywaydb.core.api.MigrationVersion; @@ -23,18 +47,15 @@ import org.flywaydb.database.mysql.MySQLConnection; import org.flywaydb.database.mysql.MySQLDatabase; -import java.sql.Connection; -import java.sql.SQLException; - -public class OceanBaseDatabase extends MySQLDatabase { +public class OceanBaseMysqlModeDatabase extends MySQLDatabase { - public OceanBaseDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + public OceanBaseMysqlModeDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { super(configuration, jdbcConnectionFactory, statementInterceptor); } @Override protected MySQLConnection doGetConnection(Connection connection) { - return new OceanBaseConnection(this, connection); + return new OceanBaseMysqlModeConnection(this, connection); } @Override diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/package-info.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/package-info.java new file mode 100644 index 0000000..5174e31 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/mysql/package-info.java @@ -0,0 +1,38 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +/* + * Copyright (C) Red Gate Software Ltd 2010-2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Community-supported package. No compatibility guarantees provided. + */ +package org.flywaydb.community.database.oceanbase.mysql; diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeConnection.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeConnection.java new file mode 100644 index 0000000..88b0828 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeConnection.java @@ -0,0 +1,48 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oracle + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.flywaydb.community.database.oceanbase.oracle; + +import java.sql.SQLException; + +import org.flywaydb.core.internal.database.base.Connection; +import org.flywaydb.core.internal.database.base.Schema; + +public class OceanBaseOracleModeConnection extends Connection { + + OceanBaseOracleModeConnection(OceanBaseOracleModeDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + protected String getCurrentSchemaNameOrSearchPath() throws SQLException { + return jdbcTemplate.queryForString("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL"); + } + + @Override + public void doChangeCurrentSchemaOrSearchPathTo(String schema) throws SQLException { + jdbcTemplate.execute("ALTER SESSION SET CURRENT_SCHEMA=" + database.quote(schema)); + } + + @Override + public Schema getSchema(String name) { + return new OceanBaseOracleModeSchema(jdbcTemplate, database, name); + } + +} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeDatabase.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeDatabase.java new file mode 100644 index 0000000..83395b8 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeDatabase.java @@ -0,0 +1,180 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oracle + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.flywaydb.community.database.oceanbase.oracle; + +import org.flywaydb.community.database.oceanbase.OceanBaseJdbcUtils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; + +public class OceanBaseOracleModeDatabase extends Database { + + public OceanBaseOracleModeDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + super(configuration, jdbcConnectionFactory, statementInterceptor); + } + + @Override + protected OceanBaseOracleModeConnection doGetConnection(Connection connection) { + return new OceanBaseOracleModeConnection(this, connection); + } + + @Override + public void ensureSupported(Configuration configuration) { + ensureDatabaseIsRecentEnough("1.4"); + recommendFlywayUpgradeIfNecessary("5.0"); + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + String tablespace = configuration.getTablespace() == null ? "" : " TABLESPACE \"" + configuration.getTablespace() + "\""; + + return "CREATE TABLE " + table + " (\n" + " \"installed_rank\" INT NOT NULL,\n" + " \"version\" VARCHAR2(50),\n" + " \"description\" VARCHAR2(200) NOT NULL,\n" + " \"type\" VARCHAR2(20) NOT NULL,\n" + " \"script\" VARCHAR2(1000) NOT NULL,\n" + " \"checksum\" INT,\n" + " \"installed_by\" VARCHAR2(100) NOT NULL,\n" + " \"installed_on\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n" + " \"execution_time\" INT NOT NULL,\n" + " \"success\" NUMBER(1) NOT NULL,\n" + " CONSTRAINT \"" + table.getName() + "_pk\" PRIMARY KEY (\"installed_rank\")\n" + ")" + tablespace + ";\n" + (baseline ? getBaselineStatement(table) + ";\n" : "") + "CREATE INDEX \"" + table.getSchema().getName() + "\".\"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");\n"; + } + + @Override + public boolean supportsEmptyMigrationDescription() { + // OceanBase Oracle mode will convert the empty string to NULL implicitly, and throw an exception as the column is NOT NULL + return false; + } + + @Override + protected String doGetCurrentUser() throws SQLException { + return getMainConnection().getJdbcTemplate().queryForString("SELECT USER FROM DUAL"); + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public boolean catalogIsSchema() { + return false; + } + + @Override + protected MigrationVersion determineVersion() { + String versionNumber; + try { + versionNumber = OceanBaseJdbcUtils.getVersionNumber(rawMainJdbcConnection); + } catch (SQLException e) { + throw new FlywayException("Failed to get version number", e); + } + return MigrationVersion.fromVersion(versionNumber); + } + + /** + * Checks whether the specified query returns rows or not. Wraps the query in EXISTS() SQL function and executes it. + * This is more preferable to opening a cursor for the original query, because a query inside EXISTS() is implicitly + * optimized to return the first row and because the client never fetches more than 1 row despite the fetch size + * value. + * + * @param query The query to check. + * @param params The query parameters. + * @return {@code true} if the query returns rows, {@code false} if not. + * @throws SQLException when the query execution failed. + */ + boolean queryReturnsRows(String query, String... params) throws SQLException { + return getMainConnection().getJdbcTemplate().queryForBoolean("SELECT CASE WHEN EXISTS(" + query + ") THEN 1 ELSE 0 END FROM DUAL", params); + } + + /** + * Checks whether the specified data dictionary view in the specified system schema is accessible (directly or + * through a role) or not. + * + * @param owner the schema name, unquoted case-sensitive. + * @param name the data dictionary view name to check, unquoted case-sensitive. + * @return {@code true} if it is accessible, {@code false} if not. + * @throws SQLException if the check failed. + */ + private boolean isDataDictViewAccessible(String owner, String name) throws SQLException { + return queryReturnsRows("SELECT * FROM ALL_TAB_PRIVS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?" + " AND PRIVILEGE = 'SELECT'", owner, name); + } + + /** + * Checks whether the specified SYS view is accessible (directly or through a role) or not. + * + * @param name the data dictionary view name to check, unquoted case-sensitive. + * @return {@code true} if it is accessible, {@code false} if not. + * @throws SQLException if the check failed. + */ + boolean isDataDictViewAccessible(String name) throws SQLException { + return isDataDictViewAccessible("SYS", name); + } + + /** + * Returns the specified data dictionary view name prefixed with DBA_ or ALL_ depending on its accessibility. + * + * @param baseName the data dictionary view base name, unquoted case-sensitive, e.g. OBJECTS, TABLES. + * @return the full name of the view with the proper prefix. + * @throws SQLException if the check failed. + */ + String dbaOrAll(String baseName) throws SQLException { + return isDataDictViewAccessible("DBA_" + baseName) ? "DBA_" + baseName : "ALL_" + baseName; + } + + + /** + * Checks whether XDB component is available or not. + * + * @return {@code true} if it is available, {@code false} if not. + * @throws SQLException when checking availability of the component failed. + */ + boolean isXmlDbAvailable() throws SQLException { + return isDataDictViewAccessible("ALL_XML_TABLES"); + } + + /** + * Returns the list of schemas that were created and are maintained by oceanbase-supplied scripts and must not be + * changed in any other way. + * + * @return the set of system schema names + */ + Set getSystemSchemas() throws SQLException { + + // The list of known default system schemas + Set result = new HashSet<>(Arrays.asList("SYS", "LBACSYS", "ORAAUDITOR")); + + result.addAll(getMainConnection().getJdbcTemplate().queryForStringList("SELECT USERNAME FROM ALL_USERS WHERE REGEXP_LIKE(USERNAME, '^(APEX|FLOWS)_\\d+$') OR ORACLE_MAINTAINED = 'Y'")); + + return result; + } +} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeParser.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeParser.java new file mode 100644 index 0000000..8430338 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeParser.java @@ -0,0 +1,350 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oracle + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.flywaydb.community.database.oceanbase.oracle; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import static org.flywaydb.core.internal.util.FlywayDbWebsiteLinks.ORACLE_DATABASE; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.core.internal.sqlscript.ParsedSqlStatement; +import org.flywaydb.core.internal.util.StringUtils; + +public class OceanBaseOracleModeParser extends Parser { + /** + * Delimiter of PL/SQL blocks and statements. + */ + private static final Delimiter PLSQL_DELIMITER = new Delimiter("/", true); + + // accessible by ( keywordoptionalidentifier ) + private static final String ACCESSIBLE_BY_REGEX = "ACCESSIBLE\\sBY\\s\\(?(((FUNCTION|PROCEDURE|PACKAGE|TRIGGER|TYPE)\\s)?[^\\s]\\s?+)*\\)?"; + + private static final Pattern PLSQL_TYPE_BODY_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sTYPE\\sBODY\\s([^\\s]*\\s)?(IS|AS)"); + + private static final Pattern PLSQL_PACKAGE_BODY_REGEX = Pattern.compile("^CREATE(\\s*OR\\s*REPLACE)?(\\s*(NON)?EDITIONABLE)?\\s*PACKAGE\\s*BODY\\s*([^\\s]*\\s)?(IS|AS)"); + private static final StatementType PLSQL_PACKAGE_BODY_STATEMENT = new StatementType(); + + private static final Pattern PLSQL_PACKAGE_DEFINITION_REGEX = Pattern.compile("^CREATE(\\s*OR\\s*REPLACE)?(\\s*(NON)?EDITIONABLE)?\\s*PACKAGE\\s([^\\s*]*\\s*)?(AUTHID\\s*[^\\s*]*\\s*|" + ACCESSIBLE_BY_REGEX + ")*(IS|AS)"); + + private static final Pattern PLSQL_VIEW_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?((\\sNO)?\\sFORCE)?(\\s(NON)?EDITIONABLE)?\\sVIEW\\s([^\\s]*\\s)?AS\\sWITH\\s(PROCEDURE|FUNCTION)"); + private static final StatementType PLSQL_VIEW_STATEMENT = new StatementType(); + + private static final Pattern PLSQL_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\s(FUNCTION(\\s\\S*)|PROCEDURE|TYPE|TRIGGER)"); + private static final Pattern DECLARE_BEGIN_REGEX = Pattern.compile("^DECLARE|BEGIN|WITH"); + private static final StatementType PLSQL_STATEMENT = new StatementType(); + + private static final Pattern JAVA_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?(\\sAND\\s(RESOLVE|COMPILE))?(\\sNOFORCE)?\\sJAVA\\s(SOURCE|RESOURCE|CLASS)"); + private static final StatementType PLSQL_JAVA_STATEMENT = new StatementType(); + + private static final Pattern PLSQL_PACKAGE_BODY_WRAPPED_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sPACKAGE\\sBODY(\\s\\S*)?\\sWRAPPED(\\s\\S*)*"); + private static final Pattern PLSQL_PACKAGE_DEFINITION_WRAPPED_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sPACKAGE(\\s\\S*)?\\sWRAPPED(\\s\\S*)*"); + private static final Pattern PLSQL_WRAPPED_REGEX = Pattern.compile("^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\s(FUNCTION|PROCEDURE|TYPE)(\\s\\S*)?\\sWRAPPED(\\s\\S*)*"); + + private static final StatementType PLSQL_WRAPPED_STATEMENT = new StatementType(); + private int initialWrappedBlockDepth = -1; + + private static Pattern toRegex(String... commands) { + return Pattern.compile(toRegexPattern(commands)); + } + + private static String toRegexPattern(String... commands) { + return "^(" + StringUtils.arrayToDelimitedString("|", commands) + ")"; + } + + + public OceanBaseOracleModeParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected ParsedSqlStatement createStatement(PeekingReader reader, Recorder recorder, int statementPos, int statementLine, int statementCol, int nonCommentPartPos, int nonCommentPartLine, int nonCommentPartCol, StatementType statementType, boolean canExecuteInTransaction, Delimiter delimiter, String sql, boolean batchable) throws IOException { + if (PLSQL_VIEW_STATEMENT == statementType) { + sql = sql.trim(); + // Strip extra semicolon to avoid issues with WITH statements containing PL/SQL + if (sql.endsWith(";")) { + sql = sql.substring(0, sql.length() - 1); + } + } + + return super.createStatement(reader, recorder, statementPos, statementLine, statementCol, nonCommentPartPos, nonCommentPartLine, nonCommentPartCol, statementType, canExecuteInTransaction, delimiter, sql, batchable); + } + + @Override + protected StatementType detectStatementType(String simplifiedStatement, ParserContext context, PeekingReader reader) { + if (PLSQL_PACKAGE_BODY_WRAPPED_REGEX.matcher(simplifiedStatement).matches() || PLSQL_PACKAGE_DEFINITION_WRAPPED_REGEX.matcher(simplifiedStatement).matches() || PLSQL_WRAPPED_REGEX.matcher(simplifiedStatement).matches()) { + if (initialWrappedBlockDepth == -1) { + initialWrappedBlockDepth = context.getBlockDepth(); + } + return PLSQL_WRAPPED_STATEMENT; + } + + if (PLSQL_PACKAGE_BODY_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_PACKAGE_BODY_STATEMENT; + } + + if (PLSQL_REGEX.matcher(simplifiedStatement).matches() || PLSQL_PACKAGE_DEFINITION_REGEX.matcher(simplifiedStatement).matches() || DECLARE_BEGIN_REGEX.matcher(simplifiedStatement).matches()) { + try { + String wrappedKeyword = " WRAPPED"; + if (!reader.peek(wrappedKeyword.length()).equalsIgnoreCase(wrappedKeyword)) { + return PLSQL_STATEMENT; + } + } catch (IOException e) { + return PLSQL_STATEMENT; + } + } + + if (JAVA_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_JAVA_STATEMENT; + } + + if (PLSQL_VIEW_REGEX.matcher(simplifiedStatement).matches()) { + return PLSQL_VIEW_STATEMENT; + } + + + return super.detectStatementType(simplifiedStatement, context, reader); + } + + @Override + protected boolean shouldDiscard(Token token, boolean nonCommentPartSeen) { + // Discard dangling PL/SQL '/' delimiters + return ("/".equals(token.getText()) && !nonCommentPartSeen) || super.shouldDiscard(token, nonCommentPartSeen); + } + + @Override + protected void adjustDelimiter(ParserContext context, StatementType statementType) { + if (statementType == PLSQL_STATEMENT || statementType == PLSQL_VIEW_STATEMENT || statementType == PLSQL_JAVA_STATEMENT || statementType == PLSQL_PACKAGE_BODY_STATEMENT) { + context.setDelimiter(PLSQL_DELIMITER); + + + } else { + context.setDelimiter(Delimiter.SEMICOLON); + } + } + + @Override + protected boolean shouldAdjustBlockDepth(ParserContext context, List tokens, Token token) { + // Package bodies can have an unbalanced BEGIN without END in the initialisation section. + TokenType tokenType = token.getType(); + if (context.getStatementType() == PLSQL_PACKAGE_BODY_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType)) { + return true; + } + + // Handle wrapped SQL on these token types to ensure it gets treated as one block + if (context.getStatementType() == PLSQL_WRAPPED_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType)) { + return true; + } + + // In Oracle, symbols { } affect the block depth in embedded Java code + if (token.getType() == TokenType.SYMBOL && context.getStatementType() == PLSQL_JAVA_STATEMENT) { + return true; + } + final Token previousToken = getPreviousToken(tokens, token.getParensDepth()); + if (previousToken != null && "CASE".equals(token.getText()) && "FROM".equals(previousToken.getText())) { + return false; + } + + return super.shouldAdjustBlockDepth(context, tokens, token); + } + + // These words increase the block depth - unless preceded by END (in which case the END will decrease the block depth) + private static final List CONTROL_FLOW_KEYWORDS = Arrays.asList("IF", "LOOP", "CASE"); + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) { + TokenType tokenType = keyword.getType(); + String keywordText = keyword.getText(); + int parensDepth = keyword.getParensDepth(); + + if (lastTokenIs(tokens, parensDepth, "GOTO")) { + return; + } + + if (context.getStatementType() == PLSQL_WRAPPED_STATEMENT) { + // ensure wrapped SQL has an increased block depth so it gets treated as one statement + if (context.getBlockDepth() == initialWrappedBlockDepth) { + context.increaseBlockDepth("WRAPPED"); + } + // decrease block depth at the end to step out of a wrapped SQL block + if ((TokenType.EOF == tokenType || (TokenType.DELIMITER == tokenType && "/".equals(keywordText))) && context.getBlockDepth() > 0) { + context.decreaseBlockDepth(); + } + // return early as we don't need to parse the contents of wrapped SQL - it's all one statement anyways + return; + } else { + // decrease block depth when wrapped SQL ends to step out of wrapped SQL block + if (context.getBlockDepth() > initialWrappedBlockDepth && context.getBlockInitiator().equals("WRAPPED")) { + initialWrappedBlockDepth = -1; + context.decreaseBlockDepth(); + } + } + + // In embedded Java code we judge the end of a class definition by the depth of braces. + // We ignore normal SQL keywords as Java code can contain arbitrary identifiers. + if (context.getStatementType() == PLSQL_JAVA_STATEMENT) { + if ("{".equals(keywordText)) { + context.increaseBlockDepth("PLSQL_JAVA_STATEMENT"); + } else if ("}".equals(keywordText)) { + context.decreaseBlockDepth(); + } + return; + } + + if ("BEGIN".equals(keywordText) || (CONTROL_FLOW_KEYWORDS.contains(keywordText) && !precedingEndAttachesToThisKeyword(tokens, parensDepth, context, keyword)) || ("TRIGGER".equals(keywordText) && lastTokenIs(tokens, parensDepth, "COMPOUND")) || (context.getBlockDepth() == 0 && (doTokensMatchPattern(tokens, keyword, PLSQL_PACKAGE_BODY_REGEX) || doTokensMatchPattern(tokens, keyword, PLSQL_PACKAGE_DEFINITION_REGEX) || doTokensMatchPattern(tokens, keyword, PLSQL_TYPE_BODY_REGEX)))) { + context.increaseBlockDepth(keywordText); + } else if ("END".equals(keywordText)) { + context.decreaseBlockDepth(); + } + + // Package bodies can have an unbalanced BEGIN without END in the initialisation section. This allows us + // to exit the package even though we are still at block depth 1 due to the BEGIN. + if (context.getStatementType() == PLSQL_PACKAGE_BODY_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType) && context.getBlockDepth() == 1) { + context.decreaseBlockDepth(); + } + } + + private boolean precedingEndAttachesToThisKeyword(List tokens, int parensDepth, ParserContext context, Token keyword) { + // Normally IF, LOOP and CASE all pair up with END IF, END LOOP, END CASE + // However, CASE ... END is valid in expressions, so in code such as + // FOR i IN 1 .. CASE WHEN foo THEN 5 ELSE 6 END + // LOOP + // ... + // END LOOP + // the first END does *not* attach to the subsequent LOOP. The same is possible with $IF ... $END constructions + return lastTokenIs(tokens, parensDepth, "END") && lastTokenIsOnLine(tokens, parensDepth, keyword.getLine()) && keyword.getText().equals(context.getLastClosedBlockInitiator()); + } + + @Override + protected boolean doTokensMatchPattern(List previousTokens, Token current, Pattern regex) { + if (regex == PLSQL_PACKAGE_DEFINITION_REGEX && previousTokens.stream().anyMatch(t -> t.getType() == TokenType.KEYWORD && t.getText().equalsIgnoreCase("ACCESSIBLE"))) { + ArrayList tokenStrings = new ArrayList<>(); + tokenStrings.add(current.getText()); + + for (int i = previousTokens.size() - 1; i >= 0; i--) { + Token prevToken = previousTokens.get(i); + if (prevToken.getType() == TokenType.KEYWORD) { + tokenStrings.add(prevToken.getText()); + } + } + + StringBuilder builder = new StringBuilder(); + for (int i = tokenStrings.size() - 1; i >= 0; i--) { + builder.append(tokenStrings.get(i)); + if (i != 0) { + builder.append(" "); + } + } + + return regex.matcher(builder.toString()).matches() || super.doTokensMatchPattern(previousTokens, current, regex); + } + + return super.doTokensMatchPattern(previousTokens, current, regex); + } + + @Override + protected boolean isDelimiter(String peek, ParserContext context, int col, int colIgnoringWhitespace) { + Delimiter delimiter = context.getDelimiter(); + + if (peek.startsWith(delimiter.getEscape() + delimiter.getDelimiter())) { + return true; + } + + if (delimiter.shouldBeAloneOnLine()) { + // Only consider alone-on-line delimiters (such as "/" for PL/SQL) if + // it's the first character on the line + return colIgnoringWhitespace == 1 && peek.trim().equals(delimiter.getDelimiter()); + } else { + if (colIgnoringWhitespace == 1 && "/".equals(peek.trim())) { + return true; + } + } + + return super.isDelimiter(peek, context, col, colIgnoringWhitespace); + } + + @Override + protected Token handleMultilineComment(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow("/*".length()); + String text = reader.readUntilExcluding("*/"); + reader.swallow("*/".length()); + return new Token(TokenType.COMMENT, pos, line, col, text, text, context.getParensDepth()); + } + + @Override + protected Token handleDelimiter(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + + + if (reader.peek('/')) { + reader.swallow(1); + return new Token(TokenType.DELIMITER, pos, line, col, "/", "/", context.getParensDepth()); + } + + return super.handleDelimiter(reader, context, pos, line, col); + } + + @Override + protected boolean isAlternativeStringLiteral(String peek) { + if (peek.length() < 3) { + return false; + } + // Oracle's quoted-literal syntax is introduced by q (case-insensitive) followed by a literal surrounded by + // any of !!, [], {}, (), <> provided the selected pair do not appear in the literal string; the others may do. + char firstChar = peek.charAt(0); + return (firstChar == 'q' || firstChar == 'Q') && peek.charAt(1) == '\''; + } + + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(2); + String closeQuote = computeAlternativeCloseQuote((char) reader.read()); + reader.swallowUntilExcluding(closeQuote); + reader.swallow(closeQuote.length()); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + private String computeAlternativeCloseQuote(char specialChar) { + switch (specialChar) { + case '!': + return "!'"; + case '[': + return "]'"; + case '(': + return ")'"; + case '{': + return "}'"; + case '<': + return ">'"; + default: + return specialChar + "'"; + } + } + + + @Override + protected String getAdditionalParsingErrorInfo() { + return "For Oracle-specific information about syntax and limitations, see " + ORACLE_DATABASE + ". "; + } +} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeSchema.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeSchema.java new file mode 100644 index 0000000..d9ecced --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeSchema.java @@ -0,0 +1,549 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oracle + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.flywaydb.community.database.oceanbase.oracle; + +import org.flywaydb.community.database.oceanbase.OceanBaseJdbcUtils; + +import java.sql.SQLException; +import java.util.*; + +import lombok.CustomLog; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.util.StringUtils; + +/** + * OceanBase implementation of Schema. + */ +@CustomLog +public class OceanBaseOracleModeSchema extends Schema { + /** + * Creates a new OceanBase Oracle mode schema. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + OceanBaseOracleModeSchema(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + private boolean greaterThanCurrentVersion(String versionNumber) { + boolean isGreat = false; + try { + String versionStr = OceanBaseJdbcUtils.getVersionNumber(this.jdbcTemplate.getConnection()); + assert versionStr != null; + String[] split = versionStr.split("\\."); + int curMajorVersion = Integer.parseInt(split[0]); + int curMinorVersion = Integer.parseInt(split[1]); + + String[] versionSplit = versionNumber.split("\\."); + int majorVersion = Integer.parseInt(versionSplit[0]); + int minorVersion = Integer.parseInt(versionSplit[1]); + + if (curMajorVersion >= majorVersion && curMinorVersion >= minorVersion) { + isGreat = true; + } + } catch (Exception e) { + throw new RuntimeException("Fail to Compare version numbers.", e); + } + return isGreat; + } + + /** + * Checks whether the schema is system. + * + * @return {@code true} if it is system, {@code false} if not. + */ + public boolean isSystem() throws SQLException { + return database.getSystemSchemas().contains(name); + } + + /** + * Checks whether this schema is default for the current user. + * + * @return {@code true} if it is default, {@code false} if not. + */ + boolean isDefaultSchemaForUser() throws SQLException { + return name.equals(database.doGetCurrentUser()); + } + + @Override + protected boolean doExists() throws SQLException { + return database.queryReturnsRows("SELECT * FROM ALL_USERS WHERE USERNAME = ?", name); + } + + @Override + protected boolean doEmpty() throws SQLException { + return !ObjectType.supportedTypesExist(jdbcTemplate, database, this); + } + + @Override + protected void doCreate() throws SQLException { + jdbcTemplate.execute("CREATE USER " + database.quote(name) + " IDENTIFIED BY " + database.quote("FFllyywwaayy00!!")); + jdbcTemplate.execute("GRANT RESOURCE TO " + database.quote(name)); + jdbcTemplate.execute("GRANT UNLIMITED TABLESPACE TO " + database.quote(name)); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP USER " + database.quote(name) + " CASCADE"); + } + + @Override + protected void doClean() throws SQLException { + if (isSystem()) { + throw new FlywayException("Clean not supported on OceanBase Oracle mode for system schema " + database.quote(name) + "! " + "It must not be changed in any way except by running an OceanBase-supplied script!"); + } + + // Get existing object types in the schema. + Set objectTypeNames = ObjectType.getObjectTypeNames(jdbcTemplate, database, this); + + // Define the list of types to process, order is important. + List objectTypesToClean = Arrays.asList( + // Types to drop. + ObjectType.TRIGGER, ObjectType.FILE_WATCHER, ObjectType.SCHEDULER_CHAIN, ObjectType.SCHEDULER_JOB, ObjectType.SCHEDULER_PROGRAM, ObjectType.SCHEDULE, ObjectType.RULE_SET, ObjectType.RULE, ObjectType.EVALUATION_CONTEXT, ObjectType.FILE_GROUP, ObjectType.REWRITE_EQUIVALENCE, ObjectType.SQL_TRANSLATION_PROFILE, ObjectType.MATERIALIZED_VIEW, ObjectType.MATERIALIZED_VIEW_LOG, ObjectType.VIEW, ObjectType.DOMAIN_INDEX, ObjectType.DOMAIN_INDEX_TYPE, ObjectType.TABLE, ObjectType.INDEX, ObjectType.CLUSTER, ObjectType.SEQUENCE, ObjectType.OPERATOR, ObjectType.FUNCTION, ObjectType.PROCEDURE, ObjectType.PACKAGE, ObjectType.PACKAGE_BODY, ObjectType.CONTEXT, ObjectType.LIBRARY, ObjectType.TYPE, ObjectType.SYNONYM, ObjectType.JAVA_SOURCE, ObjectType.JAVA_CLASS, ObjectType.JAVA_RESOURCE, + + // Object types with sensitive information (passwords), skip intentionally, print warning if found. + ObjectType.DATABASE_LINK, ObjectType.CREDENTIAL, + + // Unsupported types, print warning if found + ObjectType.DATABASE_DESTINATION, ObjectType.SCHEDULER_GROUP, ObjectType.CUBE, ObjectType.CUBE_DIMENSION, ObjectType.CUBE_BUILD_PROCESS, ObjectType.MEASURE_FOLDER, + + // Undocumented types, print warning if found + ObjectType.ASSEMBLY, ObjectType.JAVA_DATA); + + for (ObjectType objectType : objectTypesToClean) { + if (objectTypeNames.contains(objectType.getName())) { + LOG.debug("Cleaning objects of type " + objectType + " ..."); + objectType.dropObjects(jdbcTemplate, database, this); + } + } + + if (isDefaultSchemaForUser()) { + jdbcTemplate.execute("PURGE RECYCLEBIN"); + } + } + + + @Override + protected OceanBaseOracleModeTable[] doAllTables() throws SQLException { + List tableNames = ObjectType.TABLE.getObjectNames(jdbcTemplate, database, this); + + OceanBaseOracleModeTable[] tables = new OceanBaseOracleModeTable[tableNames.size()]; + for (int i = 0; i < tableNames.size(); i++) { + tables[i] = new OceanBaseOracleModeTable(jdbcTemplate, database, this, tableNames.get(i)); + } + return tables; + } + + @Override + public Table getTable(String tableName) { + return new OceanBaseOracleModeTable(jdbcTemplate, database, this, tableName); + } + + /** + * OceanBase Oracle object types. + */ + public enum ObjectType { + // Tables, including XML tables, except for nested tables, IOT overflow tables and other secondary objects. + TABLE("TABLE", "CASCADE CONSTRAINTS PURGE") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + boolean referencePartitionedTablesExist = database.queryReturnsRows("SELECT * FROM ALL_PART_TABLES WHERE OWNER = ? AND PARTITIONING_TYPE = 'REFERENCE'", schema.getName()); + boolean xmlDbAvailable = database.isXmlDbAvailable(); + + StringBuilder tablesQuery = new StringBuilder(); + tablesQuery.append("WITH TABLES AS (\n" + " SELECT TABLE_NAME, OWNER\n" + " FROM ALL_TABLES\n" + " WHERE OWNER = ?\n" + " AND (IOT_TYPE IS NULL OR IOT_TYPE NOT LIKE '%OVERFLOW%')\n" + " AND NESTED != 'YES'\n" + " AND SECONDARY != 'Y'\n"); + + tablesQuery.append(")\n" + "SELECT t.TABLE_NAME\n" + "FROM TABLES t\n"); + + // Reference partitioned tables should be dropped in child-to-parent order. + if (referencePartitionedTablesExist) { + tablesQuery.append(" LEFT JOIN ALL_PART_TABLES pt\n" + " ON t.OWNER = pt.OWNER\n" + " AND t.TABLE_NAME = pt.TABLE_NAME\n" + " AND pt.PARTITIONING_TYPE = 'REFERENCE'\n" + " LEFT JOIN ALL_CONSTRAINTS fk\n" + " ON pt.OWNER = fk.OWNER\n" + " AND pt.TABLE_NAME = fk.TABLE_NAME\n" + " AND pt.REF_PTN_CONSTRAINT_NAME = fk.CONSTRAINT_NAME\n" + " AND fk.CONSTRAINT_TYPE = 'R'\n" + " LEFT JOIN ALL_CONSTRAINTS puk\n" + " ON fk.R_OWNER = puk.OWNER\n" + " AND fk.R_CONSTRAINT_NAME = puk.CONSTRAINT_NAME\n" + " AND puk.CONSTRAINT_TYPE IN ('P', 'U')\n" + " LEFT JOIN TABLES p\n" + " ON puk.OWNER = p.OWNER\n" + " AND puk.TABLE_NAME = p.TABLE_NAME\n" + "START WITH p.TABLE_NAME IS NULL\n" + "CONNECT BY PRIOR t.TABLE_NAME = p.TABLE_NAME\n" + "ORDER BY LEVEL DESC"); + } + + int n = 1 + (xmlDbAvailable ? 1 : 0); + String[] params = new String[n]; + Arrays.fill(params, schema.getName()); + + return jdbcTemplate.queryForStringList(tablesQuery.toString(), params); + } + }, + + // Materialized view logs. + MATERIALIZED_VIEW_LOG("MATERIALIZED VIEW LOG") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList("SELECT MASTER FROM ALL_MVIEW_LOGS WHERE LOG_OWNER = ?", schema.getName()); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "DROP " + this.getName() + " ON " + database.quote(schema.getName(), objectName); + } + }, + + // All indexes, except for domain indexes, should be dropped after tables (if any left). + INDEX("INDEX") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList("SELECT INDEX_NAME FROM ALL_INDEXES WHERE OWNER = ?" + + //" AND INDEX_NAME NOT LIKE 'SYS_C%'"+ + " AND INDEX_TYPE NOT LIKE '%DOMAIN%'", schema.getName()); + } + }, + + // Domain indexes, have related objects and should be dropped separately prior to tables. + DOMAIN_INDEX("INDEX", "FORCE") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList("SELECT INDEX_NAME FROM ALL_INDEXES WHERE OWNER = ? AND INDEX_TYPE LIKE '%DOMAIN%'", schema.getName()); + } + }, + + // Domain index types. + DOMAIN_INDEX_TYPE("INDEXTYPE", "FORCE"), + + // Operators. + OPERATOR("OPERATOR", "FORCE"), + + // Clusters. + CLUSTER("CLUSTER", "INCLUDING TABLES CASCADE CONSTRAINTS"), + + // Views, including XML views. + VIEW("VIEW", "CASCADE CONSTRAINTS"), + + // Materialized views, keep tables as they may be referenced. + MATERIALIZED_VIEW("MATERIALIZED VIEW", "PRESERVE TABLE"), + + // Local synonyms. + SYNONYM("SYNONYM", "FORCE"), + + // Sequences, no filtering for identity sequences, since they get dropped along with master tables. + SEQUENCE("SEQUENCE"), + + // Procedures, functions, packages. + PROCEDURE("PROCEDURE"), FUNCTION("FUNCTION"), PACKAGE("PACKAGE"), PACKAGE_BODY("PACKAGE BODY"), + + // Contexts, seen in DBA_CONTEXT view, may remain if DBA_CONTEXT is not accessible. + CONTEXT("CONTEXT") { + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList("SELECT NAMESPACE FROM " + database.dbaOrAll("CONTEXT") + " WHERE SCHEMA = ?", schema.getName()); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "DROP " + this.getName() + " " + database.quote(objectName); // no owner + } + }, + + // Triggers of all types, should be dropped at first, because invalid DDL triggers may break the whole clean. + TRIGGER("TRIGGER"), + + // Types. + TYPE("TYPE", "FORCE"), + + // Java sources, classes, resources. + JAVA_SOURCE("JAVA SOURCE"), JAVA_CLASS("JAVA CLASS"), JAVA_RESOURCE("JAVA RESOURCE"), + + // Libraries. + LIBRARY("LIBRARY"), + + // Rewrite equivalences. + REWRITE_EQUIVALENCE("REWRITE EQUIVALENCE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN SYS.DBMS_ADVANCED_REWRITE.DROP_REWRITE_EQUIVALENCE('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + + // SQL translation profiles. + SQL_TRANSLATION_PROFILE("SQL TRANSLATION PROFILE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SQL_TRANSLATOR.DROP_PROFILE('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + + // Scheduler objects. + SCHEDULER_JOB("JOB") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_JOB('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, SCHEDULER_PROGRAM("PROGRAM") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_PROGRAM('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, SCHEDULE("SCHEDULE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_SCHEDULE('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, SCHEDULER_CHAIN("CHAIN") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_CHAIN('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, FILE_WATCHER("FILE WATCHER") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_FILE_WATCHER('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // Streams/rule objects. + RULE_SET("RULE SET") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_RULE_ADM.DROP_RULE_SET('" + database.quote(schema.getName(), objectName) + "', DELETE_RULES => FALSE); END;"; + } + }, RULE("RULE") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_RULE_ADM.DROP_RULE('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, EVALUATION_CONTEXT("EVALUATION CONTEXT") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_RULE_ADM.DROP_EVALUATION_CONTEXT('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, FILE_GROUP("FILE GROUP") { + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_FILE_GROUP.DROP_FILE_GROUP('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, + + /*** Below are unsupported object types. They should be dropped explicitly in callbacks if used. ***/ + + // Database links and credentials, contain sensitive information (password) and hence not always can be re-created. + // Intentionally skip them and let the clean callbacks handle them if needed. + DATABASE_LINK("DATABASE LINK") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList("SELECT DB_LINK FROM " + database.dbaOrAll("DB_LINKS") + " WHERE OWNER = ?", schema.getName()); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "DROP " + this.getName() + " " + objectName; // db link name is case-insensitive and needs no owner + } + }, CREDENTIAL("CREDENTIAL") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_CREDENTIAL('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // Some scheduler types, not supported yet. + DATABASE_DESTINATION("DESTINATION") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_DATABASE_DESTINATION('" + database.quote(schema.getName(), objectName) + "'); END;"; + } + }, SCHEDULER_GROUP("SCHEDULER GROUP") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + + @Override + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "BEGIN DBMS_SCHEDULER.DROP_GROUP('" + database.quote(schema.getName(), objectName) + "', FORCE => TRUE); END;"; + } + }, + + // OLAP objects, not supported yet. + CUBE("CUBE") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, CUBE_DIMENSION("CUBE DIMENSION") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, CUBE_BUILD_PROCESS("CUBE BUILD PROCESS") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName()), "cube build processes"); + } + }, MEASURE_FOLDER("MEASURE FOLDER") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, + + // Undocumented objects. + ASSEMBLY("ASSEMBLY") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName()), "assemblies"); + } + }, JAVA_DATA("JAVA DATA") { + @Override + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) { + super.warnUnsupported(database.quote(schema.getName())); + } + }, + + // SYS-owned objects, cannot be dropped when a schema gets cleaned, simply ignore them. + CAPTURE("CAPTURE"), APPLY("APPLY"), DIRECTORY("DIRECTORY"), RESOURCE_PLAN("RESOURCE PLAN"), CONSUMER_GROUP("CONSUMER GROUP"), JOB_CLASS("JOB CLASS"), WINDOWS("WINDOW"), EDITION("EDITION"), AGENT_DESTINATION("DESTINATION"), UNIFIED_AUDIT_POLICY("UNIFIED AUDIT POLICY"); + + /** + * The name of the type as it mentioned in the Data Dictionary and the DROP statement. + */ + private final String name; + + /** + * The extra options used in the DROP statement to enforce the operation. + */ + private final String dropOptions; + + ObjectType(String name, String dropOptions) { + this.name = name; + this.dropOptions = dropOptions; + } + + ObjectType(String name) { + this(name, ""); + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return super.toString().replace('_', ' '); + } + + /** + * Returns the list of object names of this type. + * + * @throws SQLException if retrieving of objects failed. + */ + public List getObjectNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + return jdbcTemplate.queryForStringList("SELECT DISTINCT OBJECT_NAME FROM ALL_OBJECTS WHERE OWNER = ? AND OBJECT_TYPE = ?", schema.getName(), this.getName()); + } + + /** + * Generates the drop statement for the specified object. + */ + public String generateDropStatement(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String objectName) { + return "DROP " + this.getName() + " " + database.quote(schema.getName(), objectName) + (StringUtils.hasText(dropOptions) ? " " + dropOptions : ""); + } + + /** + * Drops all objects of this type in the specified schema. + * + * @throws SQLException if cleaning failed. + */ + public void dropObjects(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + for (String objectName : getObjectNames(jdbcTemplate, database, schema)) { + jdbcTemplate.execute(generateDropStatement(jdbcTemplate, database, schema, objectName)); + } + } + + private void warnUnsupported(String schemaName, String typeDesc) { + LOG.warn("Unable to clean " + typeDesc + " for schema " + schemaName + ": unsupported operation"); + } + + private void warnUnsupported(String schemaName) { + warnUnsupported(schemaName, this.toString().toLowerCase() + "s"); + } + + /** + * Returns the schema's existing object types. + * + * @return a set of object type names. + * @throws SQLException if retrieving of object types failed. + */ + public static Set getObjectTypeNames(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + boolean materializedViewEnable = schema.greaterThanCurrentVersion("4.3"); + StringBuilder queryBuilder = new StringBuilder(); + + // Base query to select distinct object types + queryBuilder.append("SELECT DISTINCT OBJECT_TYPE FROM ") + .append(database.dbaOrAll("OBJECTS")) + .append(" WHERE OWNER = ? "); + + // Add materialized view log conditionally + if (materializedViewEnable) { + queryBuilder.append("UNION SELECT '") + .append(MATERIALIZED_VIEW_LOG.getName()) + .append("' FROM DUAL WHERE EXISTS (SELECT * FROM ALL_MVIEW_LOGS WHERE LOG_OWNER = ?)"); + } + + String query = queryBuilder.toString(); + + // Prepare parameters + int paramCount = materializedViewEnable ? 2 : 1; + String[] params = new String[paramCount]; + Arrays.fill(params, schema.getName()); + + if (materializedViewEnable) { + params[1] = schema.getName(); // Set the same schema name for the second parameter + } + + // Execute the query and return the result as a Set + return new HashSet<>(jdbcTemplate.queryForStringList(query, params)); + } + + + /** + * Checks whether the specified schema contains object types that can be cleaned. + * + * @return {@code true} if it contains, {@code false} if not. + * @throws SQLException if retrieving of object types failed. + */ + public static boolean supportedTypesExist(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema) throws SQLException { + Set existingTypeNames = new HashSet<>(getObjectTypeNames(jdbcTemplate, database, schema)); + + // Remove unsupported types. + existingTypeNames.removeAll(Arrays.asList(DATABASE_LINK.getName(), CREDENTIAL.getName(), DATABASE_DESTINATION.getName(), SCHEDULER_GROUP.getName(), CUBE.getName(), CUBE_DIMENSION.getName(), CUBE_BUILD_PROCESS.getName(), MEASURE_FOLDER.getName(), ASSEMBLY.getName(), JAVA_DATA.getName())); + + return !existingTypeNames.isEmpty(); + } + } +} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeTable.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeTable.java new file mode 100644 index 0000000..2cf2532 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/OceanBaseOracleModeTable.java @@ -0,0 +1,57 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oracle + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.flywaydb.community.database.oceanbase.oracle; + +import java.sql.SQLException; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; + +/** + * Oracle-specific table. + */ +public class OceanBaseOracleModeTable extends Table { + /** + * Creates a new Oracle table. + * + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param schema The schema this table lives in. + * @param name The name of the table. + */ + public OceanBaseOracleModeTable(JdbcTemplate jdbcTemplate, OceanBaseOracleModeDatabase database, OceanBaseOracleModeSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } + + @Override + protected void doDrop() throws SQLException { + jdbcTemplate.execute("DROP TABLE " + database.quote(schema.getName(), name) + " CASCADE CONSTRAINTS PURGE"); + } + + @Override + protected boolean doExists() throws SQLException { + return exists(null, schema, name); + } + + @Override + protected void doLock() throws SQLException { + jdbcTemplate.execute("LOCK TABLE " + this + " IN EXCLUSIVE MODE"); + } +} diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/package-info.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/package-info.java new file mode 100644 index 0000000..8b7e0b1 --- /dev/null +++ b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/oracle/package-info.java @@ -0,0 +1,41 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-oceanbase + * ======================================================================== + * Copyright (C) 2010 - 2024 Red Gate Software Ltd + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +/* + * Copyright (C) Red Gate Software Ltd 2010-2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Community-supported package. No compatibility guarantees provided. + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.community.database.oceanbase.oracle; diff --git a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/package-info.java b/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/package-info.java deleted file mode 100644 index 6ae21ee..0000000 --- a/flyway-database-oceanbase/src/main/java/org/flywaydb/community/database/oceanbase/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (C) Red Gate Software Ltd 2010-2024 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Community-supported package. No compatibility guarantees provided. - */ -package org.flywaydb.community.database.oceanbase;