diff --git a/flyway-database-kingbase/pom.xml b/flyway-database-kingbase/pom.xml new file mode 100644 index 0000000..0abfe8b --- /dev/null +++ b/flyway-database-kingbase/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + org.flywaydb + flyway-community-db-support + 10.16.1 + + + flyway-database-kingbase + ${project.artifactId} + + + + ${project.groupId} + flyway-core + + + ${project.groupId} + flyway-database-postgresql + ${version.flyway} + + + org.flywaydb + flyway-core + + + + + org.projectlombok + lombok + provided + + + + + + + src/main/resources + true + + + + + maven-resources-plugin + + + maven-jar-plugin + + + + \ No newline at end of file diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/KingbaseDatabaseExtension.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/KingbaseDatabaseExtension.java new file mode 100644 index 0000000..5fb3923 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/KingbaseDatabaseExtension.java @@ -0,0 +1,59 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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; + +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 KingbaseDatabaseExtension implements PluginMetadata { + public String getDescription() { + return "Community-contributed kingbase database support extension " + readVersion() + " by Redgate"; + } + + public static String readVersion() { + try { + return FileUtils.copyToString( + KingbaseDatabaseExtension.class.getClassLoader().getResourceAsStream("org/flywaydb/community" + + "/database/kingbase/version.txt"), + StandardCharsets.UTF_8); + } catch (IOException e) { + throw new FlywayException("Unable to read extension version: " + e.getMessage(), e); + } + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlConnection.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlConnection.java new file mode 100644 index 0000000..8663a17 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlConnection.java @@ -0,0 +1,58 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.database.postgresql.PostgreSQLConnection; + +import java.util.concurrent.Callable; + +public class KingbaseMysqlConnection extends PostgreSQLConnection { + + KingbaseMysqlConnection(KingbaseMysqlDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + public Schema getSchema(String name) { + return new KingbaseMysqlSchema(jdbcTemplate, (KingbaseMysqlDatabase) database, name); + } + + @Override + public T lock(Table table, Callable callable) { + return new KingbaseMysqlExecutionTemplate(jdbcTemplate, table.toString()).execute(callable); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlDatabase.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlDatabase.java new file mode 100644 index 0000000..65d53f7 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlDatabase.java @@ -0,0 +1,136 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import lombok.CustomLog; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.database.postgresql.PostgreSQLDatabase; + +import java.sql.Connection; +import java.sql.SQLException; + + +@CustomLog +public class KingbaseMysqlDatabase extends PostgreSQLDatabase { + + public static final String LOCK_TABLE_NAME = "YB_FLYWAY_LOCK_TABLE"; + /** + * This table is used to enforce locking through SELECT ... FOR UPDATE on a + * token row inserted in this table. The token row is inserted with the name + * of the Flyway's migration history table as a token for simplicity. + */ + private static final String CREATE_LOCK_TABLE_DDL = "CREATE TABLE IF NOT EXISTS " + LOCK_TABLE_NAME + " (table_name varchar PRIMARY KEY, locked bool)"; + + public KingbaseMysqlDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + super(configuration, jdbcConnectionFactory, statementInterceptor); + createLockTable(); + } + + @Override + protected KingbaseMysqlConnection doGetConnection(Connection connection) { + return new KingbaseMysqlConnection(this, connection); + } + + @Override + public void ensureSupported(Configuration configuration) { + // Checks the Postgres version + ensureDatabaseIsRecentEnough("11.2"); + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @Override + public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE IF NOT EXISTS " + table + " (\n" + + " \"installed_rank\" INT NOT NULL PRIMARY KEY,\n" + + " \"version\" VARCHAR(50),\n" + + " \"description\" VARCHAR(200) NOT NULL,\n" + + " \"type\" VARCHAR(20) NOT NULL,\n" + + " \"script\" VARCHAR(1000) NOT NULL,\n" + + " \"checksum\" INTEGER,\n" + + " \"installed_by\" VARCHAR(100) NOT NULL,\n" + + " \"installed_on\" TIMESTAMP NOT NULL DEFAULT now(),\n" + + " \"execution_time\" INTEGER NOT NULL,\n" + + " \"success\" BOOLEAN NOT NULL\n" + + ");\n" + + (baseline ? getBaselineStatement(table) + ";\n" : "") + + "CREATE INDEX IF NOT EXISTS \"" + table.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + + @Override + public boolean useSingleConnection() { + return true; + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + @Override + public String getOpenQuote() { + return "`"; + } + + @Override + public String getCloseQuote() { + return "`"; + } + + @Override + public boolean catalogIsSchema() { + return true; + } + + private void createLockTable() { + try { + jdbcTemplate.execute(CREATE_LOCK_TABLE_DDL); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to initialize the lock table", e); + } + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlDatabaseType.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlDatabaseType.java new file mode 100644 index 0000000..bbeaefb --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlDatabaseType.java @@ -0,0 +1,144 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import org.flywaydb.community.database.KingbaseDatabaseExtension; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.ResourceProvider; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.extensibility.Tier; +import org.flywaydb.core.internal.database.base.CommunityDatabaseType; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.core.internal.license.FlywayEditionUpgradeRequiredException; +import org.flywaydb.core.internal.parser.Parser; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.database.postgresql.PostgreSQLDatabaseType; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class KingbaseMysqlDatabaseType extends PostgreSQLDatabaseType implements CommunityDatabaseType { + @Override + public String getName() { + return "Kingbase"; + } + + @Override + public boolean handlesJDBCUrl(String url) { + if (url.startsWith("jdbc-secretsmanager:kingbase8:")) { + + throw new FlywayEditionUpgradeRequiredException(Tier.ENTERPRISE, (Tier) null, "jdbc-secretsmanager"); + + } + return url.startsWith("jdbc:kingbase8:") || url.startsWith("jdbc:p6spy:kingbase8:"); + } + + + @Override + public int getPriority() { + // Should be checked before plain PostgreSQL + return 1; + } + +// @Override +// public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { +// // The YB is what distinguishes Yugabyte +// return databaseProductName.startsWith("Kingbase"); +// } + + @Override + public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { + if (!databaseProductName.contains("Kingbase")) { + return false; // 先确保是 Kingbase 数据库 + } + + boolean result = checkDatabaseMode(connection, "mysql"); +// System.out.println("Checking Kingbase MySQL mode: " + result + ", databaseProductName: " + databaseProductName); + return result; + } + + private boolean checkDatabaseMode(Connection connection, String expectedMode) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW DATABASE_MODE;")) { + + if (rs.next()) { + String mode = rs.getString(1).trim().toLowerCase(); + return expectedMode.equals(mode); + } + } catch (SQLException e) { + throw new FlywayException("Failed to determine Kingbase database mode", e); + } + return false; + } + + + @Override + public Database createDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + return new KingbaseMysqlDatabase(configuration, jdbcConnectionFactory, statementInterceptor); + } + + @Override + public Parser createParser(Configuration configuration, ResourceProvider resourceProvider, ParsingContext parsingContext) { + return new KingbaseMysqlParser(configuration, parsingContext); + } + + @Override + public String getPluginVersion(Configuration config) { + return KingbaseDatabaseExtension.readVersion(); + } + + /** + * Returns the YugabyteDB Smart driver classname if the smart driver is + * being used. The plugin will work with the Postgresql JDBC driver also + * since the url in that case would start with 'jdbc:postgresql' which would + * return the PG JDBC driver class name. + * @param url + * @param classLoader + * @return "com.yugabyte.Driver" if url starts with "jdbc:yugabytedb:" + */ + @Override + public String getDriverClass(String url, ClassLoader classLoader) { + + if (url.startsWith("jdbc:p6spy:kingbase8:")) { + return "com.p6spy.engine.spy.P6SpyDriver"; + } + return "com.kingbase8.Driver"; + } + +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlExecutionTemplate.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlExecutionTemplate.java new file mode 100644 index 0000000..f0b8b93 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlExecutionTemplate.java @@ -0,0 +1,188 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import lombok.CustomLog; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.strategy.RetryStrategy; +import org.flywaydb.core.internal.util.FlywayDbWebsiteLinks; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; + +@CustomLog +public class KingbaseMysqlExecutionTemplate { + + private final JdbcTemplate jdbcTemplate; + private final String tableName; + private static final Map tableEntries = new ConcurrentHashMap<>(); + + KingbaseMysqlExecutionTemplate(JdbcTemplate jdbcTemplate, String tableName) { + this.jdbcTemplate = jdbcTemplate; + this.tableName = tableName; + } + + public T execute(Callable callable) { + Exception error = null; + try { + lock(); + return callable.call(); + } catch (RuntimeException e) { + error = e; + throw e; + } catch (Exception e) { + error = e; + throw new FlywayException(e); + } finally { + unlock(error); + } + } + + private void lock() throws SQLException { + RetryStrategy strategy = new RetryStrategy(); + strategy.doWithRetries(this::tryLock, "Interrupted while attempting to acquire lock through SELECT ... FOR UPDATE", + "Number of retries exceeded while attempting to acquire lock through SELECT ... FOR UPDATE. " + + "Configure the number of retries with the 'lockRetryCount' configuration option: " + FlywayDbWebsiteLinks.LOCK_RETRY_COUNT); + + } + + private boolean tryLock() { + Exception exception = null; + boolean txStarted = false, success = false; + Statement statement = null; + try { + statement = jdbcTemplate.getConnection().createStatement(); + + if (!tableEntries.containsKey(tableName)) { + try { + statement.executeUpdate("INSERT INTO " + + KingbaseMysqlDatabase.LOCK_TABLE_NAME + + " VALUES ('" + tableName + "', 'false')"); + tableEntries.put(tableName, true); + LOG.info(Thread.currentThread().getName() + "> Inserted a token row for " + tableName + " in " + KingbaseMysqlDatabase.LOCK_TABLE_NAME); + } catch (SQLException e) { + if ("23505".equals(e.getSQLState())) { + // 23505 == UNIQUE_VIOLATION + LOG.debug(Thread.currentThread().getName() + "> Token row already added for " + tableName); + } else { + throw new FlywaySqlException("Could not add token row for " + tableName + " in table " + KingbaseMysqlDatabase.LOCK_TABLE_NAME, e); + } + } + } + + boolean locked; + String selectForUpdate = "SELECT locked FROM " + + KingbaseMysqlDatabase.LOCK_TABLE_NAME + + " WHERE table_name = '" + + tableName + + "' FOR UPDATE"; + String updateLocked = "UPDATE " + KingbaseMysqlDatabase.LOCK_TABLE_NAME + + " SET locked = true WHERE table_name = '" + + tableName + "'"; + + statement.execute("BEGIN"); + txStarted = true; + ResultSet rs = statement.executeQuery(selectForUpdate); + if (rs.next()) { + locked = rs.getBoolean("locked"); + + if (locked) { + statement.execute("COMMIT"); + txStarted = false; + LOG.debug(Thread.currentThread().getName() + "> Another Flyway operation is in progress. Allowing it to complete"); + } else { + LOG.debug(Thread.currentThread().getName() + "> Setting locked = true"); + statement.executeUpdate(updateLocked); + success = true; + } + } else { + // For some reason the record was not found, retry + tableEntries.remove(tableName); + } + + } catch (SQLException e) { + LOG.warn(Thread.currentThread().getName() + "> Unable to perform lock action, SQLState: " + e.getSQLState()); + if (!"40001".equalsIgnoreCase(e.getSQLState())) { + exception = new FlywaySqlException("Unable to perform lock action", e); + throw (FlywaySqlException) exception; + } // else retry + } finally { + if (txStarted) { + try { + statement.execute("COMMIT"); + LOG.debug(Thread.currentThread().getName() + "> Completed the tx to set locked = true"); + } catch (SQLException e) { + if (exception == null) { + throw new FlywaySqlException("Failed to commit the tx to set locked = true", e); + } + LOG.warn(Thread.currentThread().getName() + "> Failed to commit the tx to set locked = true: " + e); + } + } + } + return success; + } + + private void unlock(Exception rethrow) { + Statement statement = null; + try { + statement = jdbcTemplate.getConnection().createStatement(); + statement.execute("BEGIN"); + ResultSet rs = statement.executeQuery("SELECT locked FROM " + KingbaseMysqlDatabase.LOCK_TABLE_NAME + " WHERE table_name = '" + tableName + "' FOR UPDATE"); + + if (rs.next()) { + boolean locked = rs.getBoolean("locked"); + if (locked) { + statement.executeUpdate("UPDATE " + KingbaseMysqlDatabase.LOCK_TABLE_NAME + " SET locked = false WHERE table_name = '" + tableName + "'"); + } else { + // Unexpected. This may happen only when callable took too long to complete + // and another thread forcefully reset it. + String msg = "Unlock failed but the Flyway operation may have succeeded. Check your Flyway operation before re-trying"; + LOG.warn(Thread.currentThread().getName() + "> " + msg); + if (rethrow == null) { + throw new FlywayException(msg); + } + } + } + } catch (SQLException e) { + if (rethrow == null) { + rethrow = new FlywayException("Unable to perform unlock action", e); + throw (FlywaySqlException) rethrow; + } + LOG.warn("Unable to perform unlock action " + e); + } finally { + try { + statement.execute("COMMIT"); + LOG.debug(Thread.currentThread().getName() + "> Completed the tx to set locked = false"); + } catch (SQLException e) { + if (rethrow == null) { + throw new FlywaySqlException("Failed to commit unlock action", e); + } + LOG.warn("Failed to commit unlock action: " + e); + } + } + } + +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlParser.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlParser.java new file mode 100644 index 0000000..417cc47 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlParser.java @@ -0,0 +1,186 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +import static java.lang.Character.isDigit; + +public class KingbaseMysqlParser extends Parser { + private static final char ALTERNATIVE_SINGLE_LINE_COMMENT = '#'; + + private static final Pattern STORED_PROGRAM_REGEX = Pattern.compile( + "^CREATE\\s(((DEFINER\\s(\\w+\\s)?@\\s(\\w+\\s)?)?(PROCEDURE|FUNCTION|EVENT))|TRIGGER)", Pattern.CASE_INSENSITIVE); + private static final StatementType STORED_PROGRAM_STATEMENT = new StatementType(); + + public KingbaseMysqlParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 8); + } + + @Override + protected void resetDelimiter(ParserContext context) { + // Do not reset delimiter as delimiter changes survive beyond a single statement + } + + @Override + protected Token handleKeyword(PeekingReader reader, ParserContext context, int pos, int line, int col, String keyword) throws IOException { + if ("DELIMITER".equalsIgnoreCase(keyword)) { + String text = ""; + while (text.isEmpty()) { + text = reader.readUntilExcluding('\n', '\r').trim(); + reader.swallow(1); + } + return new Token(TokenType.NEW_DELIMITER, pos, line, col, text, text, context.getParensDepth()); + } + return super.handleKeyword(reader, context, pos, line, col, keyword); + } + + @Override + protected char getIdentifierQuote() { + return '`'; + } + + @Override + protected char getAlternativeStringLiteralQuote() { + return '"'; + } + + @Override + protected boolean isSingleLineComment(String peek, ParserContext context, int col) { + return (super.isSingleLineComment(peek, context, col) + // Normally MySQL treats # as a comment, but this may have been overridden by DELIMITER # directive + || (peek.charAt(0) == ALTERNATIVE_SINGLE_LINE_COMMENT && !isDelimiter(peek, context, col, 0))); + } + + @Override + protected Token handleStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException, IOException { + reader.swallow(); + reader.swallowUntilIncludingWithEscape('\'', true, '\\'); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + @Override + protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(); + reader.swallowUntilIncludingWithEscape('"', true, '\\'); + return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); + } + + @Override + protected Token handleCommentDirective(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { + reader.swallow(2); + String text = reader.readUntilExcluding("*/"); + reader.swallow(2); + return new Token(TokenType.MULTI_LINE_COMMENT_DIRECTIVE, pos, line, col, text, text, context.getParensDepth()); + } + + @Override + protected boolean isCommentDirective(String text) { + return text.length() >= 8 + && text.charAt(0) == '/' + && text.charAt(1) == '*' + && text.charAt(2) == '!' + && isDigit(text.charAt(3)) + && isDigit(text.charAt(4)) + && isDigit(text.charAt(5)) + && isDigit(text.charAt(6)) + && isDigit(text.charAt(7)); + } + + @Override + protected StatementType detectStatementType(String simplifiedStatement, ParserContext context, PeekingReader reader) { + if (STORED_PROGRAM_REGEX.matcher(simplifiedStatement).matches()) { + return STORED_PROGRAM_STATEMENT; + } + + return super.detectStatementType(simplifiedStatement, context, reader); + } + + @Override + protected boolean shouldAdjustBlockDepth(ParserContext context, List tokens, Token token) { + // we assume that any blocks opened or closed inside some parens + // can't affect block depth outside those parens + return token.getParensDepth() == 0; + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) { + String keywordText = keyword.getText(); + + int parensDepth = keyword.getParensDepth(); + + if ("BEGIN".equalsIgnoreCase(keywordText) && context.getStatementType() == STORED_PROGRAM_STATEMENT) { + // BEGIN ... END is the usual way to define a nested block + context.increaseBlockDepth(""); + } + + if ("CASE".equalsIgnoreCase(keywordText)) { + // CASE is treated specially compared to IF or LOOP since it can either be + // a statement or an expression. CASE statements are terminated with END CASE, + // while CASE expressions are only terminated with END. + // + // We need to decide if some END token ends a CASE statement or a block. Since + // we can't easily tell if we're in a CASE statement or expression, we don't + // know if we should be expecting END or END CASE. So we'll just assume that + // END always closes a CASE, and then prevent END CASE from starting + // a new one: + if (!lastTokenIs(tokens, parensDepth, "END")) { + context.increaseBlockDepth(""); + } + + // we could do something similar for the other control flow keywords, but IF and + // REPEAT in particular would be tricky since they are also the names of functions + // (and functions don't have a matching END keyword). + } + + // END always ends a block, unless it's part of END IF or END LOOP etc + // + // this is a little tricky since we can't peek ahead at tokens, so we have to + // wait until one token *after* the END and decrease the block depth there. + if (context.getBlockDepth() > 0 + && lastTokenIs(tokens, parensDepth, "END") + && !"IF".equalsIgnoreCase(keywordText) + && !"LOOP".equalsIgnoreCase(keywordText) + && !"REPEAT".equalsIgnoreCase(keywordText) + && !"WHILE".equalsIgnoreCase(keywordText)) { + context.decreaseBlockDepth(); + } + } +} \ No newline at end of file diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlSchema.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlSchema.java new file mode 100644 index 0000000..11f5d38 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlSchema.java @@ -0,0 +1,55 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.database.postgresql.PostgreSQLSchema; + +public class KingbaseMysqlSchema extends PostgreSQLSchema { + /** + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + public KingbaseMysqlSchema(JdbcTemplate jdbcTemplate, KingbaseMysqlDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + public Table getTable(String tableName) { + return new KingbaseMysqlTable(jdbcTemplate, (KingbaseMysqlDatabase) database, this, tableName); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlTable.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlTable.java new file mode 100644 index 0000000..473e497 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/KingbaseMysqlTable.java @@ -0,0 +1,50 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasemysql; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.database.postgresql.PostgreSQLTable; + +public class KingbaseMysqlTable extends PostgreSQLTable { + /** + * @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 KingbaseMysqlTable(JdbcTemplate jdbcTemplate, KingbaseMysqlDatabase database, KingbaseMysqlSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/package-info.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/package-info.java new file mode 100644 index 0000000..7b5b662 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasemysql/package-info.java @@ -0,0 +1,23 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-postgresql + * ======================================================================== + * 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================================== + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.community.database.kingbasemysql; diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleConnection.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleConnection.java new file mode 100644 index 0000000..2e3003e --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleConnection.java @@ -0,0 +1,58 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.database.postgresql.PostgreSQLConnection; + +import java.util.concurrent.Callable; + +public class KingbaseOracleConnection extends PostgreSQLConnection { + + KingbaseOracleConnection(KingbaseOracleDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + public Schema getSchema(String name) { + return new KingbaseOracleSchema(jdbcTemplate, (KingbaseOracleDatabase) database, name); + } + + @Override + public T lock(Table table, Callable callable) { + return new KingbaseOracleExecutionTemplate(jdbcTemplate, table.toString()).execute(callable); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleDatabase.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleDatabase.java new file mode 100644 index 0000000..3c2aa87 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleDatabase.java @@ -0,0 +1,118 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +import lombok.CustomLog; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.database.postgresql.PostgreSQLDatabase; + +import java.sql.Connection; +import java.sql.SQLException; + + +@CustomLog +public class KingbaseOracleDatabase extends PostgreSQLDatabase { + + public static final String LOCK_TABLE_NAME = "YB_FLYWAY_LOCK_TABLE"; + /** + * This table is used to enforce locking through SELECT ... FOR UPDATE on a + * token row inserted in this table. The token row is inserted with the name + * of the Flyway's migration history table as a token for simplicity. + */ + private static final String CREATE_LOCK_TABLE_DDL = "CREATE TABLE IF NOT EXISTS " + LOCK_TABLE_NAME + " (table_name varchar PRIMARY KEY, locked bool)"; + + public KingbaseOracleDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + super(configuration, jdbcConnectionFactory, statementInterceptor); + createLockTable(); + } + + @Override + protected KingbaseOracleConnection doGetConnection(Connection connection) { + return new KingbaseOracleConnection(this, connection); + } + + @Override + public void ensureSupported(Configuration configuration) { + // Checks the Postgres version + ensureDatabaseIsRecentEnough("11.2"); + } + + @Override + public boolean supportsDdlTransactions() { + return false; + } + + @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.getName() + "_s_idx\" ON " + table + " (\"success\");"; + } + + + + @Override + public boolean useSingleConnection() { + return true; + } + + private void createLockTable() { + try { + jdbcTemplate.execute(CREATE_LOCK_TABLE_DDL); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to initialize the lock table", e); + } + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleDatabaseType.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleDatabaseType.java new file mode 100644 index 0000000..c8951af --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleDatabaseType.java @@ -0,0 +1,143 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +import org.flywaydb.community.database.KingbaseDatabaseExtension; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.ResourceProvider; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.extensibility.Tier; +import org.flywaydb.core.internal.database.base.CommunityDatabaseType; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.core.internal.license.FlywayEditionUpgradeRequiredException; +import org.flywaydb.core.internal.parser.Parser; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.database.postgresql.PostgreSQLDatabaseType; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class KingbaseOracleDatabaseType extends PostgreSQLDatabaseType implements CommunityDatabaseType { + @Override + public String getName() { + return "Kingbase"; + } + + @Override + public boolean handlesJDBCUrl(String url) { + if (url.startsWith("jdbc-secretsmanager:kingbase8:")) { + + + throw new FlywayEditionUpgradeRequiredException(Tier.ENTERPRISE, (Tier) null, "jdbc-secretsmanager"); + + } + return url.startsWith("jdbc:kingbase8:") || url.startsWith("jdbc:p6spy:kingbase8:"); + } + + @Override + public int getPriority() { + // Should be checked before plain PostgreSQL + return 1; + } + +// @Override +// public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { +// // The YB is what distinguishes Yugabyte +// return databaseProductName.startsWith("Kingbase"); +// } + + @Override + public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { + if (!databaseProductName.contains("Kingbase")) { + return false; // 先确保是 Kingbase 数据库 + } + + boolean result = checkDatabaseMode(connection, "oracle"); +// System.out.println("Checking Kingbase Oracle mode: " + result + ", databaseProductName: " + databaseProductName); + return result; + } + + private boolean checkDatabaseMode(Connection connection, String expectedMode) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW DATABASE_MODE;")) { + + if (rs.next()) { + String mode = rs.getString(1).trim().toLowerCase(); + return expectedMode.equals(mode); + } } catch (SQLException e) { + throw new FlywayException("Failed to determine Kingbase database mode", e); + } + return false; + } + + + @Override + public Database createDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + return new KingbaseOracleDatabase(configuration, jdbcConnectionFactory, statementInterceptor); + } + + @Override + public Parser createParser(Configuration configuration, ResourceProvider resourceProvider, ParsingContext parsingContext) { + return new KingbaseOracleParser(configuration, parsingContext); + } + + @Override + public String getPluginVersion(Configuration config) { + return KingbaseDatabaseExtension.readVersion(); + } + + /** + * Returns the YugabyteDB Smart driver classname if the smart driver is + * being used. The plugin will work with the Postgresql JDBC driver also + * since the url in that case would start with 'jdbc:postgresql' which would + * return the PG JDBC driver class name. + * @param url + * @param classLoader + * @return "com.yugabyte.Driver" if url starts with "jdbc:yugabytedb:" + */ + @Override + public String getDriverClass(String url, ClassLoader classLoader) { + + if (url.startsWith("jdbc:p6spy:kingbase8:")) { + return "com.p6spy.engine.spy.P6SpyDriver"; + } + return "com.kingbase8.Driver"; + } + +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleExecutionTemplate.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleExecutionTemplate.java new file mode 100644 index 0000000..aae12b7 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleExecutionTemplate.java @@ -0,0 +1,188 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +import lombok.CustomLog; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.strategy.RetryStrategy; +import org.flywaydb.core.internal.util.FlywayDbWebsiteLinks; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; + +@CustomLog +public class KingbaseOracleExecutionTemplate { + + private final JdbcTemplate jdbcTemplate; + private final String tableName; + private static final Map tableEntries = new ConcurrentHashMap<>(); + + KingbaseOracleExecutionTemplate(JdbcTemplate jdbcTemplate, String tableName) { + this.jdbcTemplate = jdbcTemplate; + this.tableName = tableName; + } + + public T execute(Callable callable) { + Exception error = null; + try { + lock(); + return callable.call(); + } catch (RuntimeException e) { + error = e; + throw e; + } catch (Exception e) { + error = e; + throw new FlywayException(e); + } finally { + unlock(error); + } + } + + private void lock() throws SQLException { + RetryStrategy strategy = new RetryStrategy(); + strategy.doWithRetries(this::tryLock, "Interrupted while attempting to acquire lock through SELECT ... FOR UPDATE", + "Number of retries exceeded while attempting to acquire lock through SELECT ... FOR UPDATE. " + + "Configure the number of retries with the 'lockRetryCount' configuration option: " + FlywayDbWebsiteLinks.LOCK_RETRY_COUNT); + + } + + private boolean tryLock() { + Exception exception = null; + boolean txStarted = false, success = false; + Statement statement = null; + try { + statement = jdbcTemplate.getConnection().createStatement(); + + if (!tableEntries.containsKey(tableName)) { + try { + statement.executeUpdate("INSERT INTO " + + KingbaseOracleDatabase.LOCK_TABLE_NAME + + " VALUES ('" + tableName + "', 'false')"); + tableEntries.put(tableName, true); + LOG.info(Thread.currentThread().getName() + "> Inserted a token row for " + tableName + " in " + KingbaseOracleDatabase.LOCK_TABLE_NAME); + } catch (SQLException e) { + if ("23505".equals(e.getSQLState())) { + // 23505 == UNIQUE_VIOLATION + LOG.debug(Thread.currentThread().getName() + "> Token row already added for " + tableName); + } else { + throw new FlywaySqlException("Could not add token row for " + tableName + " in table " + KingbaseOracleDatabase.LOCK_TABLE_NAME, e); + } + } + } + + boolean locked; + String selectForUpdate = "SELECT locked FROM " + + KingbaseOracleDatabase.LOCK_TABLE_NAME + + " WHERE table_name = '" + + tableName + + "' FOR UPDATE"; + String updateLocked = "UPDATE " + KingbaseOracleDatabase.LOCK_TABLE_NAME + + " SET locked = true WHERE table_name = '" + + tableName + "'"; + + statement.execute("BEGIN"); + txStarted = true; + ResultSet rs = statement.executeQuery(selectForUpdate); + if (rs.next()) { + locked = rs.getBoolean("locked"); + + if (locked) { + statement.execute("COMMIT"); + txStarted = false; + LOG.debug(Thread.currentThread().getName() + "> Another Flyway operation is in progress. Allowing it to complete"); + } else { + LOG.debug(Thread.currentThread().getName() + "> Setting locked = true"); + statement.executeUpdate(updateLocked); + success = true; + } + } else { + // For some reason the record was not found, retry + tableEntries.remove(tableName); + } + + } catch (SQLException e) { + LOG.warn(Thread.currentThread().getName() + "> Unable to perform lock action, SQLState: " + e.getSQLState()); + if (!"40001".equalsIgnoreCase(e.getSQLState())) { + exception = new FlywaySqlException("Unable to perform lock action", e); + throw (FlywaySqlException) exception; + } // else retry + } finally { + if (txStarted) { + try { + statement.execute("COMMIT"); + LOG.debug(Thread.currentThread().getName() + "> Completed the tx to set locked = true"); + } catch (SQLException e) { + if (exception == null) { + throw new FlywaySqlException("Failed to commit the tx to set locked = true", e); + } + LOG.warn(Thread.currentThread().getName() + "> Failed to commit the tx to set locked = true: " + e); + } + } + } + return success; + } + + private void unlock(Exception rethrow) { + Statement statement = null; + try { + statement = jdbcTemplate.getConnection().createStatement(); + statement.execute("BEGIN"); + ResultSet rs = statement.executeQuery("SELECT locked FROM " + KingbaseOracleDatabase.LOCK_TABLE_NAME + " WHERE table_name = '" + tableName + "' FOR UPDATE"); + + if (rs.next()) { + boolean locked = rs.getBoolean("locked"); + if (locked) { + statement.executeUpdate("UPDATE " + KingbaseOracleDatabase.LOCK_TABLE_NAME + " SET locked = false WHERE table_name = '" + tableName + "'"); + } else { + // Unexpected. This may happen only when callable took too long to complete + // and another thread forcefully reset it. + String msg = "Unlock failed but the Flyway operation may have succeeded. Check your Flyway operation before re-trying"; + LOG.warn(Thread.currentThread().getName() + "> " + msg); + if (rethrow == null) { + throw new FlywayException(msg); + } + } + } + } catch (SQLException e) { + if (rethrow == null) { + rethrow = new FlywayException("Unable to perform unlock action", e); + throw (FlywaySqlException) rethrow; + } + LOG.warn("Unable to perform unlock action " + e); + } finally { + try { + statement.execute("COMMIT"); + LOG.debug(Thread.currentThread().getName() + "> Completed the tx to set locked = false"); + } catch (SQLException e) { + if (rethrow == null) { + throw new FlywaySqlException("Failed to commit unlock action", e); + } + LOG.warn("Failed to commit unlock action: " + e); + } + } + } + +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleParser.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleParser.java new file mode 100644 index 0000000..8d83ed6 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleParser.java @@ -0,0 +1,397 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class KingbaseOracleParser 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 KingbaseOracleParser(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 + "'"; + } + } + + } \ No newline at end of file diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleSchema.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleSchema.java new file mode 100644 index 0000000..4f470dc --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleSchema.java @@ -0,0 +1,55 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.database.postgresql.PostgreSQLSchema; + +public class KingbaseOracleSchema extends PostgreSQLSchema { + /** + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + public KingbaseOracleSchema(JdbcTemplate jdbcTemplate, KingbaseOracleDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + public Table getTable(String tableName) { + return new KingbaseOracleTable(jdbcTemplate, (KingbaseOracleDatabase) database, this, tableName); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleTable.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleTable.java new file mode 100644 index 0000000..6aa2082 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/KingbaseOracleTable.java @@ -0,0 +1,50 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbaseoracle; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.database.postgresql.PostgreSQLTable; + +public class KingbaseOracleTable extends PostgreSQLTable { + /** + * @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 KingbaseOracleTable(JdbcTemplate jdbcTemplate, KingbaseOracleDatabase database, KingbaseOracleSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/package-info.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/package-info.java new file mode 100644 index 0000000..06ae537 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbaseoracle/package-info.java @@ -0,0 +1,23 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-postgresql + * ======================================================================== + * 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================================== + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.community.database.kingbaseoracle; diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerConnection.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerConnection.java new file mode 100644 index 0000000..fd08bdd --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerConnection.java @@ -0,0 +1,58 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasesqlserver; + +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.database.postgresql.PostgreSQLConnection; + +import java.util.concurrent.Callable; + +public class KingbaseSQLServerConnection extends PostgreSQLConnection { + + KingbaseSQLServerConnection(KingbaseSQLServerDatabase database, java.sql.Connection connection) { + super(database, connection); + } + + @Override + public Schema getSchema(String name) { + return new KingbaseSQLServerSchema(jdbcTemplate, (KingbaseSQLServerDatabase) database, name); + } + + @Override + public T lock(Table table, Callable callable) { + return new KingbaseSQLServerExecutionTemplate(jdbcTemplate, table.toString()).execute(callable); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerDatabase.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerDatabase.java new file mode 100644 index 0000000..61cb806 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerDatabase.java @@ -0,0 +1,144 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasesqlserver; + +import lombok.CustomLog; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import org.flywaydb.database.postgresql.PostgreSQLDatabase; + +import java.sql.Connection; +import java.sql.SQLException; + + +@CustomLog +public class KingbaseSQLServerDatabase extends PostgreSQLDatabase { + + public static final String LOCK_TABLE_NAME = "YB_FLYWAY_LOCK_TABLE"; + /** + * This table is used to enforce locking through SELECT ... FOR UPDATE on a + * token row inserted in this table. The token row is inserted with the name + * of the Flyway's migration history table as a token for simplicity. + */ +// private static final String CREATE_LOCK_TABLE_DDL = "CREATE TABLE IF NOT EXISTS " + LOCK_TABLE_NAME + " (table_name varchar PRIMARY KEY, locked bool)"; + private static final String CREATE_LOCK_TABLE_DDL = +// "CREATE TABLE IF NOT EXISTS " + LOCK_TABLE_NAME + +// " (table_name VARCHAR(255) PRIMARY KEY, locked BOOLEAN NOT NULL DEFAULT FALSE)"; + "CREATE TABLE IF NOT EXISTS " + LOCK_TABLE_NAME + + " (table_name VARCHAR(255) PRIMARY KEY, locked BIT NOT NULL DEFAULT 0)";//将locked的boolean类型修改为bit类型 + public KingbaseSQLServerDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + super(configuration, jdbcConnectionFactory, statementInterceptor); + createLockTable(); + } + + @Override + protected KingbaseSQLServerConnection doGetConnection(Connection connection) { + return new KingbaseSQLServerConnection(this, connection); + } + + @Override + public String getBooleanTrue() { + return "1"; + } + + @Override + public String getBooleanFalse() { + return "0"; + } + + @Override + public void ensureSupported(Configuration configuration) { + // Checks the Postgres version + ensureDatabaseIsRecentEnough("11.2"); + } + + + @Override + public boolean supportsDdlTransactions() { + return true; + } + +// 修改建表语句 在一个书事务之后加GO +@Override +public String getRawCreateScript(Table table, boolean baseline) { + return "CREATE TABLE " + table + " (\n" + + " [installed_rank] INT NOT NULL,\n" + + " [version] NVARCHAR(50),\n" + + " [description] NVARCHAR(200),\n" + + " [type] NVARCHAR(20) NOT NULL,\n" + + " [script] NVARCHAR(1000) NOT NULL,\n" + + " [checksum] INT,\n" + + " [installed_by] NVARCHAR(100) NOT NULL,\n" + + " [installed_on] DATETIME NOT NULL DEFAULT GETDATE(),\n" + + " [execution_time] INT NOT NULL,\n" + + " [success] BIT NOT NULL\n" + + ")" + ";\nGO\n" + // 每个 CREATE TABLE 后加 GO + (baseline ? getBaselineStatement(table) + ";\nGO\n" : "") + // 如果 baseline 需要执行,也加 GO + "ALTER TABLE " + table + " ADD CONSTRAINT [" + table.getName() + "_pk] PRIMARY KEY ([installed_rank]);\nGO\n" + // ALTER TABLE 后加 GO + "CREATE INDEX [" + table.getName() + "_s_idx] ON " + table + " ([success]);\nGO\n"; // CREATE INDEX 后加 GO +} + + //新增 + @Override + public String getOpenQuote() { return "["; } + @Override + public String getCloseQuote() { return "]"; } + @Override + public String getEscapedQuote() { return "]]"; } + + @Override + public Delimiter getDefaultDelimiter() { + return Delimiter.GO; + } + + @Override + public boolean useSingleConnection() { + return true; + } + + private void createLockTable() { + try { + jdbcTemplate.execute(CREATE_LOCK_TABLE_DDL); + } catch (SQLException e) { + throw new FlywaySqlException("Unable to initialize the lock table", e); + } + } +} + + diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerDatabaseType.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerDatabaseType.java new file mode 100644 index 0000000..158a529 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerDatabaseType.java @@ -0,0 +1,162 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasesqlserver; + +import org.flywaydb.community.database.KingbaseDatabaseExtension; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.ResourceProvider; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.extensibility.Tier; +import org.flywaydb.core.internal.database.base.CommunityDatabaseType; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.jdbc.JdbcConnectionFactory; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.core.internal.license.FlywayEditionUpgradeRequiredException; +import org.flywaydb.core.internal.parser.Parser; +import org.flywaydb.core.internal.parser.ParsingContext; +import org.flywaydb.database.postgresql.PostgreSQLDatabaseType; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +public class KingbaseSQLServerDatabaseType extends PostgreSQLDatabaseType implements CommunityDatabaseType { + @Override + public String getName() { + return "Kingbase"; + } + +// @Override +// public boolean handlesJDBCUrl(String url) { +// return url.startsWith("jdbc:kingbase8:") +// || url.startsWith("jdbc:postgresql:") || url.startsWith("jdbc:p6spy:postgresql:"); +// } + + @Override + public boolean handlesJDBCUrl(String url) { + if (url.startsWith("jdbc-secretsmanager:kingbase8:")) { + + throw new FlywayEditionUpgradeRequiredException(Tier.ENTERPRISE, (Tier) null, "jdbc-secretsmanager"); + + } + return url.startsWith("jdbc:kingbase8:") || url.startsWith("jdbc:p6spy:kingbase8:"); + } + + @Override + public int getPriority() { + // Should be checked before plain PostgreSQL + return 1; + } + +// @Override +// public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { +// // The YB is what distinguishes Yugabyte +// return databaseProductName.startsWith("Kingbase"); +// } + + @Override + public boolean handlesDatabaseProductNameAndVersion(String databaseProductName, String databaseProductVersion, Connection connection) { + if (!databaseProductName.contains("Kingbase")) { + return false; // 先确保是 Kingbase 数据库 + } + + boolean result = checkDatabaseMode(connection, "sqlserver"); +// System.out.println("Checking Kingbase SQL Server mode: " + result + ", databaseProductName: " + databaseProductName); + return result; + } + + private boolean checkDatabaseMode(Connection connection, String expectedMode) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW DATABASE_MODE;")) { + + if (rs.next()) { + String mode = rs.getString(1).trim().toLowerCase(); + return expectedMode.equals(mode); + } + } catch (SQLException e) { + throw new FlywayException("Failed to determine Kingbase database mode", e); + } + return false; + } + + + @Override + public Database createDatabase(Configuration configuration, JdbcConnectionFactory jdbcConnectionFactory, StatementInterceptor statementInterceptor) { + return new KingbaseSQLServerDatabase(configuration, jdbcConnectionFactory, statementInterceptor); + } + + + @Override + public Parser createParser(Configuration configuration, ResourceProvider resourceProvider, ParsingContext parsingContext) { + return new KingbaseSQLServerParser(configuration, parsingContext); + } + + @Override + public void setDefaultConnectionProps(String url, Properties props, ClassLoader classLoader) { + props.put("applicationName", APPLICATION_NAME); + } + + + @Override + public String getPluginVersion(Configuration config) { + return KingbaseDatabaseExtension.readVersion(); + } + + + /** + * Returns the YugabyteDB Smart driver classname if the smart driver is + * being used. The plugin will work with the Postgresql JDBC driver also + * since the url in that case would start with 'jdbc:postgresql' which would + * return the PG JDBC driver class name. + * @param url + * @param classLoader + * @return "com.yugabyte.Driver" if url starts with "jdbc:yugabytedb:" + */ +// @Override +// public String getDriverClass(String url, ClassLoader classLoader) { +// return url.startsWith("com.kingbase8.Driver") ? "com.kingbase8.Driver" : super.getDriverClass(url, classLoader); +// } + @Override + public String getDriverClass(String url, ClassLoader classLoader) { + + if (url.startsWith("jdbc:p6spy:kingbase8:")) { + return "com.p6spy.engine.spy.P6SpyDriver"; + } + return "com.kingbase8.Driver"; + } + +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerExecutionTemplate.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerExecutionTemplate.java new file mode 100644 index 0000000..2185f57 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerExecutionTemplate.java @@ -0,0 +1,178 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasesqlserver; + +import lombok.CustomLog; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.internal.exception.FlywaySqlException; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.core.internal.strategy.RetryStrategy; +import org.flywaydb.core.internal.util.FlywayDbWebsiteLinks; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; + +@CustomLog +public class KingbaseSQLServerExecutionTemplate { + + private final JdbcTemplate jdbcTemplate; + private final String tableName; + private static final Map tableEntries = new ConcurrentHashMap<>(); + + KingbaseSQLServerExecutionTemplate(JdbcTemplate jdbcTemplate, String tableName) { + this.jdbcTemplate = jdbcTemplate; + this.tableName = tableName; + } + + public T execute(Callable callable) { + Exception error = null; + try { + lock(); + return callable.call(); + } catch (RuntimeException e) { + error = e; + throw e; + } catch (Exception e) { + error = e; + throw new FlywayException(e); + } finally { + unlock(error); + } + } + + private void lock() throws SQLException { + RetryStrategy strategy = new RetryStrategy(); + strategy.doWithRetries(this::tryLock, "Interrupted while attempting to acquire lock through SELECT ... FOR UPDATE", + "Number of retries exceeded while attempting to acquire lock through SELECT ... FOR UPDATE. " + + "Configure the number of retries with the 'lockRetryCount' configuration option: " + FlywayDbWebsiteLinks.LOCK_RETRY_COUNT); + } + + private boolean tryLock() { + Exception exception = null; + boolean txStarted = false, success = false; + Statement statement = null; + try { + statement = jdbcTemplate.getConnection().createStatement(); + + if (!tableEntries.containsKey(tableName)) { + try { +// statement.executeUpdate("INSERT INTO " + KingbaseDatabase.LOCK_TABLE_NAME + " (table_name, locked) VALUES ('" + tableName + "', false)"); + statement.executeUpdate("INSERT INTO " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME + " (table_name, locked) VALUES ('" + tableName + "', '0')");//修改locked为bit类型 + tableEntries.put(tableName, true); + LOG.info(Thread.currentThread().getName() + "> Inserted a token row for " + tableName + " in " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME); + } catch (SQLException e) { + if ("23505".equals(e.getSQLState())) { // UNIQUE_VIOLATION + LOG.debug(Thread.currentThread().getName() + "> Token row already added for " + tableName); + } else { + throw new FlywaySqlException("Could not add token row for " + tableName + " in table " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME, e); + } + } + } + + String selectForUpdate = "SELECT locked FROM " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME + " WHERE table_name = '" + tableName + "' FOR UPDATE"; +// String updateLocked = "UPDATE " + KingbaseDatabase.LOCK_TABLE_NAME + " SET locked = true WHERE table_name = '" + tableName + "'"; + String updateLocked = "UPDATE " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME + " SET locked = '1' WHERE table_name = '" + tableName + "'"; //修改locked值为bit类型 + + + statement.execute("BEGIN TRANSACTION"); // 修改 BEGIN 为 START TRANSACTION + txStarted = true; + ResultSet rs = statement.executeQuery(selectForUpdate); + if (rs.next()) { +// boolean locked = rs.getBoolean("locked"); + boolean locked = "1".equals(rs.getString("locked"));//修改locked为bit类型 + if (locked) { + statement.execute("COMMIT"); + txStarted = false; + LOG.debug(Thread.currentThread().getName() + "> Another Flyway operation is in progress. Allowing it to complete"); + } else { + LOG.debug(Thread.currentThread().getName() + "> Setting locked = true"); + statement.executeUpdate(updateLocked); + success = true; + } + } else { + tableEntries.remove(tableName); + } + + } catch (SQLException e) { + LOG.warn(Thread.currentThread().getName() + "> Unable to perform lock action, SQLState: " + e.getSQLState()); + if (!"40001".equalsIgnoreCase(e.getSQLState())) { + exception = new FlywaySqlException("Unable to perform lock action", e); + throw (FlywaySqlException) exception; + } + } finally { + if (txStarted) { + try { + statement.execute("COMMIT"); + LOG.debug(Thread.currentThread().getName() + "> Completed the tx to set locked = true"); + } catch (SQLException e) { + if (exception == null) { + throw new FlywaySqlException("Failed to commit the tx to set locked = true", e); + } + LOG.warn(Thread.currentThread().getName() + "> Failed to commit the tx to set locked = true: " + e); + } + } + } + return success; + } + + private void unlock(Exception rethrow) { + Statement statement = null; + try { + statement = jdbcTemplate.getConnection().createStatement(); + statement.execute("BEGIN TRANSACTION"); // 修改 BEGIN 为 START TRANSACTION + ResultSet rs = statement.executeQuery("SELECT locked FROM " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME + " WHERE table_name = '" + tableName + "' FOR UPDATE"); + + if (rs.next()) { + boolean locked = rs.getBoolean("locked"); + if (locked) { +// statement.executeUpdate("UPDATE " + KingbaseDatabase.LOCK_TABLE_NAME + " SET locked = false WHERE table_name = '" + tableName + "'"); + statement.executeUpdate("UPDATE " + KingbaseSQLServerDatabase.LOCK_TABLE_NAME + " SET locked = '0' WHERE table_name = '" + tableName + "'"); //修改为bit类型 + + } else { + String msg = "Unlock failed but the Flyway operation may have succeeded. Check your Flyway operation before re-trying"; + LOG.warn(Thread.currentThread().getName() + "> " + msg); + if (rethrow == null) { + throw new FlywayException(msg); + } + } + } + } catch (SQLException e) { + if (rethrow == null) { + rethrow = new FlywayException("Unable to perform unlock action", e); + throw (FlywaySqlException) rethrow; + } + LOG.warn("Unable to perform unlock action " + e); + } finally { + try { + statement.execute("COMMIT"); + LOG.debug(Thread.currentThread().getName() + "> Completed the tx to set locked = false"); + } catch (SQLException e) { + if (rethrow == null) { + throw new FlywaySqlException("Failed to commit unlock action", e); + } + LOG.warn("Failed to commit unlock action: " + e); + } + } + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerParser.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerParser.java new file mode 100644 index 0000000..9f8b4b1 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerParser.java @@ -0,0 +1,153 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-kingbase + * ======================================================================== + * Copyright (C) 2010 - 2025 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.kingbasesqlserver; + +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.internal.parser.*; +import org.flywaydb.core.internal.sqlscript.Delimiter; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class KingbaseSQLServerParser extends Parser { + private static final List SPROCS_INVALID_IN_TRANSACTIONS = Arrays.asList( + "SP_ADDSUBSCRIPTION", "SP_DROPSUBSCRIPTION", + "SP_ADDDISTRIBUTOR", "SP_DROPDISTRIBUTOR", + "SP_ADDDISTPUBLISHER", "SP_DROPDISTPUBLISHER", + "SP_ADDLINKEDSERVER", "SP_DROPLINKEDSERVER", + "SP_ADDLINKEDSRVLOGIN", "SP_DROPLINKEDSRVLOGIN", + "SP_SERVEROPTION", "SP_REPLICATIONDBOPTION", + "SP_FULLTEXT_DATABASE"); + + private static final Pattern BEGIN_SINGLE_STATEMENT_REGEX = Pattern.compile("TRAN(SACTION)?|CONVERSATION|DIALOG"); + private static final Pattern TRANSACTION_REGEX = Pattern.compile("TRAN(SACTION)?"); + + public KingbaseSQLServerParser(Configuration configuration, ParsingContext parsingContext) { + super(configuration, parsingContext, 3); + } + + @Override + protected Delimiter getDefaultDelimiter() { + return Delimiter.GO; + } + + @Override + protected boolean isDelimiter(String peek, ParserContext context, int col, int colIgnoringWhitespace) { + return peek.length() >= 2 + && (peek.charAt(0) == 'G' || peek.charAt(0) == 'g') + && (peek.charAt(1) == 'O' || peek.charAt(1) == 'o') + && (peek.length() == 2 || Character.isWhitespace(peek.charAt(2))); + } + + @Override + protected String readKeyword(PeekingReader reader, Delimiter delimiter, ParserContext context) throws IOException { + // #2414: Ignore delimiter as GO (unlike ;) can be part of a regular keyword + return "" + (char) reader.read() + reader.readKeywordPart(null, context); + } + + @Override + protected Boolean detectCanExecuteInTransaction(String simplifiedStatement, List keywords) { + if (keywords.size() == 0) { + return null; + } + + Token currentToken = keywords.get(keywords.size() - 1); + String current = currentToken.getText(); + + if (currentToken.getType() != TokenType.IDENTIFIER && + ("BACKUP".equals(current) || "RESTORE".equals(current) || "RECONFIGURE".equals(current))) { + return false; + } + + if (keywords.size() < 2) { + return null; + } + + String previous = keywords.get(keywords.size() - 2).getText(); + + if ("EXEC".equals(previous) && SPROCS_INVALID_IN_TRANSACTIONS.contains(current)) { + return false; + } + + // (CREATE|DROP|ALTER) (DATABASE|FULLTEXT (INDEX|CATALOG)) + if (("CREATE".equals(previous) || "ALTER".equals(previous) || "DROP".equals(previous)) + && ("DATABASE".equals(current) || "FULLTEXT".equals(current))) { + return false; + } + + return null; + } + + @Override + protected boolean shouldAdjustBlockDepth(ParserContext context, List tokens, Token token) { + TokenType tokenType = token.getType(); + if (TokenType.DELIMITER.equals(tokenType) || ";".equals(token.getText())) { + return true; + } else if (TokenType.EOF.equals(tokenType)) { + return true; + } + + return super.shouldAdjustBlockDepth(context, tokens, token); + } + + @Override + protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) throws IOException { + String keywordText = keyword.getText(); + + if ("BEGIN".equals(keywordText)) { + context.increaseBlockDepth(""); + } + + if (context.getBlockDepth() > 0 && ("END".equals(keywordText) || + isSingleStatementBegin(tokens, keyword, keywordText) || + isDistributedTransaction(tokens, keyword, keywordText))) { + context.decreaseBlockDepth(); + } + + super.adjustBlockDepth(context, tokens, keyword, reader); + } + + private boolean isSingleStatementBegin(List tokens, Token keyword, String keywordText) { + return keywordText != null && BEGIN_SINGLE_STATEMENT_REGEX.matcher(keywordText).matches() && + lastTokenIs(tokens, keyword.getParensDepth(), "BEGIN"); + } + + private boolean isDistributedTransaction(List tokens, Token keyword, String keywordText) { + return keywordText != null && TRANSACTION_REGEX.matcher(keywordText).matches() && + lastTokenIs(tokens, keyword.getParensDepth(), "DISTRIBUTED") && + tokenAtIndexIs(tokens, tokens.size() - 2, "BEGIN"); + } + + @Override + protected int getTransactionalDetectionCutoff() { + return Integer.MAX_VALUE; + } + + @Override + protected char getOpeningIdentifierSymbol() { + return '['; + } + + @Override + protected char getClosingIdentifierSymbol() { + return ']'; + } +} \ No newline at end of file diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerSchema.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerSchema.java new file mode 100644 index 0000000..0ef8e06 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerSchema.java @@ -0,0 +1,55 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasesqlserver; + +import org.flywaydb.core.internal.database.base.Table; +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.database.postgresql.PostgreSQLSchema; + +public class KingbaseSQLServerSchema extends PostgreSQLSchema { + /** + * @param jdbcTemplate The Jdbc Template for communicating with the DB. + * @param database The database-specific support. + * @param name The name of the schema. + */ + public KingbaseSQLServerSchema(JdbcTemplate jdbcTemplate, KingbaseSQLServerDatabase database, String name) { + super(jdbcTemplate, database, name); + } + + @Override + public Table getTable(String tableName) { + return new KingbaseSQLServerTable(jdbcTemplate, (KingbaseSQLServerDatabase) database, this, tableName); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerTable.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerTable.java new file mode 100644 index 0000000..196a021 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/KingbaseSQLServerTable.java @@ -0,0 +1,50 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-yugabytedb + * ======================================================================== + * 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.kingbasesqlserver; + +import org.flywaydb.core.internal.jdbc.JdbcTemplate; +import org.flywaydb.database.postgresql.PostgreSQLTable; + +public class KingbaseSQLServerTable extends PostgreSQLTable { + /** + * @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 KingbaseSQLServerTable(JdbcTemplate jdbcTemplate, KingbaseSQLServerDatabase database, KingbaseSQLServerSchema schema, String name) { + super(jdbcTemplate, database, schema, name); + } +} diff --git a/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/package-info.java b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/package-info.java new file mode 100644 index 0000000..ff92460 --- /dev/null +++ b/flyway-database-kingbase/src/main/java/org/flywaydb/community/database/kingbasesqlserver/package-info.java @@ -0,0 +1,23 @@ +/*- + * ========================LICENSE_START================================= + * flyway-database-postgresql + * ======================================================================== + * 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================================== + */ +/** + * Private API. No compatibility guarantees provided. + */ +package org.flywaydb.community.database.kingbasesqlserver; diff --git a/flyway-database-kingbase/src/main/resources/META-INF/services/org.flywaydb.core.extensibility.Plugin b/flyway-database-kingbase/src/main/resources/META-INF/services/org.flywaydb.core.extensibility.Plugin new file mode 100644 index 0000000..b944d81 --- /dev/null +++ b/flyway-database-kingbase/src/main/resources/META-INF/services/org.flywaydb.core.extensibility.Plugin @@ -0,0 +1,3 @@ +org.flywaydb.community.database.kingbasemysql.KingbaseMysqlDatabaseType +org.flywaydb.community.database.kingbaseoracle.KingbaseOracleDatabaseType +org.flywaydb.community.database.kingbasesqlserver.KingbaseSQLServerDatabaseType diff --git a/flyway-database-kingbase/src/main/resources/org/flywaydb/community/database/kingbasemysql/version.txt b/flyway-database-kingbase/src/main/resources/org/flywaydb/community/database/kingbasemysql/version.txt new file mode 100644 index 0000000..1785151 --- /dev/null +++ b/flyway-database-kingbase/src/main/resources/org/flywaydb/community/database/kingbasemysql/version.txt @@ -0,0 +1 @@ +${pom.version} \ No newline at end of file