diff --git a/ebean-migration/src/main/java/io/ebean/migration/MigrationContext.java b/ebean-migration/src/main/java/io/ebean/migration/MigrationContext.java new file mode 100644 index 0000000..a2417ad --- /dev/null +++ b/ebean-migration/src/main/java/io/ebean/migration/MigrationContext.java @@ -0,0 +1,41 @@ +package io.ebean.migration; + +import java.sql.Connection; + +/** + * The current context while a migration runs. + *

+ * This is used to provide meta-informations in JDBC migrations and mainly provides a read-only access + * to a subset of MigrationConfig. + *

+ * It is possible to provide an extended implementation in MigrationEngine.run(context), + * which is accessible in JdbcMigration. So you can create a EbeanMigrationContext, so that you can + * access the current ebean server in the JDBC migration. + * + * @author Roland Praml, FOCONIS AG + */ +public interface MigrationContext { + /** + * The current connection. Note: During migration, this connection is always the same. + * You must not close this connection! + */ + Connection connection(); + + /** + * The migration path of SQL migrations. You can use this, to load additional SQL resources + * in your JDBC migration or determine, if this JDBC migration is for a particular path. + * This can be used if you have multiple ebean servers for different databases. + */ + String migrationPath(); + + /** + * The platform of the current migration run. (e.g. sqlserver17) + */ + String platform(); + + /** + * The base platform of the current migration run. (e.g. sqlserver) + */ + String basePlatform(); + +} diff --git a/ebean-migration/src/main/java/io/ebean/migration/MigrationRunner.java b/ebean-migration/src/main/java/io/ebean/migration/MigrationRunner.java index 165febf..b3270f7 100644 --- a/ebean-migration/src/main/java/io/ebean/migration/MigrationRunner.java +++ b/ebean-migration/src/main/java/io/ebean/migration/MigrationRunner.java @@ -44,6 +44,13 @@ public List checkState(Connection connection) { return run(connection, true); } + /** + * Return the migrations that would be applied if the migration is run. + */ + public List checkState(MigrationContext context) { + return run(context, true); + } + /** * Run by creating a DB connection from driver, url, username defined in MigrationConfig. */ @@ -65,6 +72,13 @@ public void run(Connection connection) { run(connection, false); } + /** + * Run the migrations if there are any that need running. + */ + public void run(MigrationContext context) { + run(context, false); + } + private Connection connection(DataSource dataSource) { String username = migrationConfig.getDbUsername(); try { @@ -86,4 +100,10 @@ private List run(Connection connection, boolean checkStateOnl return new MigrationEngine(migrationConfig, checkStateOnly).run(connection); } + /** + * Run the migrations if there are any that need running. + */ + private List run(MigrationContext context, boolean checkStateOnly) { + return new MigrationEngine(migrationConfig, checkStateOnly).run(context); + } } diff --git a/ebean-migration/src/main/java/io/ebean/migration/runner/DefaultMigrationContext.java b/ebean-migration/src/main/java/io/ebean/migration/runner/DefaultMigrationContext.java new file mode 100644 index 0000000..96bc4c0 --- /dev/null +++ b/ebean-migration/src/main/java/io/ebean/migration/runner/DefaultMigrationContext.java @@ -0,0 +1,45 @@ +package io.ebean.migration.runner; + +import io.ebean.migration.MigrationConfig; +import io.ebean.migration.MigrationContext; + +import java.sql.Connection; + +/** + * A default implementation of the MigrationContext. + * + * @author Roland Praml, FOCONIS AG + */ +public class DefaultMigrationContext implements MigrationContext { + private final Connection connection; + private final String migrationPath; + private final String platform; + private final String basePlatform; + + public DefaultMigrationContext(MigrationConfig config, Connection connection) { + this.connection = connection; + this.migrationPath = config.getMigrationPath(); + this.platform = config.getPlatform(); + this.basePlatform = config.getBasePlatform(); + } + + @Override + public Connection connection() { + return connection; + } + + @Override + public String migrationPath() { + return migrationPath; + } + + @Override + public String platform() { + return platform; + } + + @Override + public String basePlatform() { + return basePlatform; + } +} diff --git a/ebean-migration/src/main/java/io/ebean/migration/runner/FirstCheck.java b/ebean-migration/src/main/java/io/ebean/migration/runner/FirstCheck.java index 72becb1..ad18657 100644 --- a/ebean-migration/src/main/java/io/ebean/migration/runner/FirstCheck.java +++ b/ebean-migration/src/main/java/io/ebean/migration/runner/FirstCheck.java @@ -1,6 +1,7 @@ package io.ebean.migration.runner; import io.ebean.migration.MigrationConfig; +import io.ebean.migration.MigrationContext; import java.sql.Connection; import java.sql.SQLException; @@ -15,17 +16,17 @@ final class FirstCheck { final MigrationConfig config; final MigrationPlatform platform; - final Connection connection; + final MigrationContext context; final String schema; final String table; final String sqlTable; boolean tableKnownToExist; private int count; - FirstCheck(MigrationConfig config, Connection connection, MigrationPlatform platform) { + FirstCheck(MigrationConfig config, MigrationContext context, MigrationPlatform platform) { this.config = config; this.platform = platform; - this.connection = connection; + this.context = context; this.schema = config.getDbSchema(); this.table = config.getMetaTable(); this.sqlTable = schema != null ? schema + '.' + table : table; @@ -80,7 +81,7 @@ private int checksumFor(LocalMigrationResource local) { } List fastRead() throws SQLException { - return platform.fastReadMigrations(sqlTable, connection); + return platform.fastReadMigrations(sqlTable, context.connection()); } int count() { diff --git a/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationEngine.java b/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationEngine.java index 55562aa..8de37fb 100644 --- a/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationEngine.java +++ b/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationEngine.java @@ -2,6 +2,7 @@ import io.avaje.applog.AppLog; import io.ebean.migration.MigrationConfig; +import io.ebean.migration.MigrationContext; import io.ebean.migration.MigrationException; import io.ebean.migration.MigrationResource; @@ -35,51 +36,63 @@ public MigrationEngine(MigrationConfig migrationConfig, boolean checkStateOnly) /** * Run the migrations if there are any that need running. + * + * @param connection the connection to run on. Note the connection will be closed. */ public List run(Connection connection) { try { - long startMs = System.currentTimeMillis(); - LocalMigrationResources resources = new LocalMigrationResources(migrationConfig); - if (!resources.readResources() && !resources.readInitResources()) { - log.log(DEBUG, "no migrations to check"); - return emptyList(); - } - long splitMs = System.currentTimeMillis() - startMs; - final var platform = derivePlatform(migrationConfig, connection); - final var firstCheck = new FirstCheck(migrationConfig, connection, platform); - if (fastMode && firstCheck.fastModeCheck(resources.versions())) { - long checkMs = System.currentTimeMillis() - startMs; - log.log(INFO, "DB migrations completed in {0}ms - totalMigrations:{1} readResources:{2}ms", checkMs, firstCheck.count(), splitMs); - return emptyList(); - } - // ensure running with autoCommit false - setAutoCommitFalse(connection); - - final MigrationTable table = initialiseMigrationTable(firstCheck, connection); - try { - List result = runMigrations(table, resources.versions()); - connection.commit(); - if (!checkStateOnly) { - long commitMs = System.currentTimeMillis(); - log.log(INFO, "DB migrations completed in {0}ms - executed:{1} totalMigrations:{2} mode:{3}", (commitMs - startMs), table.count(), table.size(), table.mode()); - int countNonTransactional = table.runNonTransactional(); - if (countNonTransactional > 0) { - log.log(INFO, "Non-transactional DB migrations completed in {0}ms - executed:{1}", (System.currentTimeMillis() - commitMs), countNonTransactional); - } + return run(new DefaultMigrationContext(migrationConfig, connection)); + } finally { + close(connection); + } + } + + /** + * Run the migrations if there are any that need running. (Does not close connection) + */ + public List run(MigrationContext context) { + + long startMs = System.currentTimeMillis(); + LocalMigrationResources resources = new LocalMigrationResources(migrationConfig); + if (!resources.readResources() && !resources.readInitResources()) { + log.log(DEBUG, "no migrations to check"); + return emptyList(); + } + + var connection = context.connection(); + long splitMs = System.currentTimeMillis() - startMs; + final var platform = derivePlatform(migrationConfig, connection); + final var firstCheck = new FirstCheck(migrationConfig, context, platform); + if (fastMode && firstCheck.fastModeCheck(resources.versions())) { + long checkMs = System.currentTimeMillis() - startMs; + log.log(INFO, "DB migrations completed in {0}ms - totalMigrations:{1} readResources:{2}ms", checkMs, firstCheck.count(), splitMs); + return emptyList(); + } + // ensure running with autoCommit false + setAutoCommitFalse(connection); + + final MigrationTable table = initialiseMigrationTable(firstCheck, connection); + try { + List result = runMigrations(table, resources.versions()); + connection.commit(); + if (!checkStateOnly) { + long commitMs = System.currentTimeMillis(); + log.log(INFO, "DB migrations completed in {0}ms - executed:{1} totalMigrations:{2} mode:{3}", (commitMs - startMs), table.count(), table.size(), table.mode()); + int countNonTransactional = table.runNonTransactional(); + if (countNonTransactional > 0) { + log.log(INFO, "Non-transactional DB migrations completed in {0}ms - executed:{1}", (System.currentTimeMillis() - commitMs), countNonTransactional); } - return result; - } catch (MigrationException e) { - rollback(connection); - throw e; - } catch (Throwable e) { - log.log(ERROR, "Perform rollback due to DB migration error", e); - rollback(connection); - throw new MigrationException("Error running DB migrations", e); - } finally { - table.unlockMigrationTable(); } + return result; + } catch (MigrationException e) { + rollback(connection); + throw e; + } catch (Throwable e) { + log.log(ERROR, "Perform rollback due to DB migration error", e); + rollback(connection); + throw new MigrationException("Error running DB migrations", e); } finally { - close(connection); + table.unlockMigrationTable(); } } diff --git a/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationTable.java b/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationTable.java index 93550fe..8dea179 100644 --- a/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationTable.java +++ b/ebean-migration/src/main/java/io/ebean/migration/runner/MigrationTable.java @@ -26,7 +26,7 @@ final class MigrationTable { private static final int AUTO_PATCH_CHECKSUM = -1; private final MigrationConfig config; - private final Connection connection; + private final MigrationContext context; private final boolean checkStateOnly; private boolean earlyChecksumMode; private final MigrationPlatform platform; @@ -72,18 +72,18 @@ final class MigrationTable { private int executionCount; private boolean patchLegacyChecksums; private MigrationMetaRow initMetaRow; - private boolean tableKnownToExist; + private final boolean tableKnownToExist; public MigrationTable(FirstCheck firstCheck, boolean checkStateOnly) { this.config = firstCheck.config; this.platform = firstCheck.platform; - this.connection = firstCheck.connection; + this.context = firstCheck.context; this.schema = firstCheck.schema; this.table = firstCheck.table; this.sqlTable = firstCheck.sqlTable; this.tableKnownToExist = firstCheck.tableKnownToExist; - this.scriptRunner = new MigrationScriptRunner(connection, platform); + this.scriptRunner = new MigrationScriptRunner(context.connection(), platform); this.checkStateOnly = checkStateOnly; this.earlyChecksumMode = config.isEarlyChecksumMode(); this.migrations = new LinkedHashMap<>(); @@ -142,7 +142,7 @@ private ScriptTransform createScriptTransform(MigrationConfig config) { void createIfNeededAndLock() throws SQLException, IOException { SQLException suppressedException = null; if (!tableKnownToExist) { - MigrationSchema.createIfNeeded(config, connection); + MigrationSchema.createIfNeeded(config, context.connection()); if (!tableExists()) { try { createTable(); @@ -174,14 +174,14 @@ void createIfNeededAndLock() throws SQLException, IOException { * contain all the executed migrations in that case. */ private void obtainLockWithWait() throws SQLException { - platform.lockMigrationTable(sqlTable, connection); + platform.lockMigrationTable(sqlTable, context.connection()); } /** * Release a lock on the migration table (MySql, MariaDB only). */ void unlockMigrationTable() { - platform.unlockMigrationTable(sqlTable, connection); + platform.unlockMigrationTable(sqlTable, context.connection()); } /** @@ -191,12 +191,13 @@ void unlockMigrationTable() { * executed during the wait for the lock. */ private void readExistingMigrations() throws SQLException { - for (MigrationMetaRow metaRow : platform.readExistingMigrations(sqlTable, connection)) { + for (MigrationMetaRow metaRow : platform.readExistingMigrations(sqlTable, context.connection())) { addMigration(metaRow.version(), metaRow); } } void createTable() throws IOException, SQLException { + Connection connection = context.connection(); try { scriptRunner.runScript(createTableDdl(), "create migration table"); createInitMetaRow().executeInsert(connection, insertSql); @@ -253,6 +254,7 @@ private ClassLoader classLoader() { * Return true if the table exists. */ boolean tableExists() throws SQLException { + Connection connection = context.connection(); String migTable = table; DatabaseMetaData metaData = connection.getMetaData(); if (metaData.storesUpperCaseIdentifiers()) { @@ -351,7 +353,7 @@ boolean skipMigration(int checksum, int checksum2, LocalMigrationResource local, } else if (patchLegacyChecksums && (existing.checksum() == checksum2 || checksum2 == AUTO_PATCH_CHECKSUM)) { if (!checkStateOnly) { log.log(INFO, "Auto patch migration, set early mode checksum on {0} to {1,number} from {2,number}", local.location(), checksum, existing.checksum()); - existing.resetChecksum(checksum, connection, updateChecksumSql); + existing.resetChecksum(checksum, context.connection(), updateChecksumSql); } return true; @@ -373,7 +375,7 @@ boolean skipMigration(int checksum, int checksum2, LocalMigrationResource local, private boolean patchResetChecksum(MigrationMetaRow existing, int newChecksum) throws SQLException { if (isResetOnVersion(existing.version())) { if (!checkStateOnly) { - existing.resetChecksum(newChecksum, connection, updateChecksumSql); + existing.resetChecksum(newChecksum, context.connection(), updateChecksumSql); } return true; } else { @@ -405,7 +407,7 @@ private void executeMigration(LocalMigrationResource local, String script, int c } if (existing != null) { existing.rerun(checksum, exeMillis, envUserName, runOn); - existing.executeUpdate(connection, updateSql); + existing.executeUpdate(context.connection(), updateSql); } else { insertIntoHistory(local, checksum, exeMillis); } @@ -424,7 +426,7 @@ private long executeMigration(LocalMigrationResource local, String script) throw if (local instanceof LocalJdbcMigrationResource) { JdbcMigration migration = ((LocalJdbcMigrationResource) local).migration(); log.log(INFO, "Executing jdbc migration version: {0} - {1}", local.version(), migration); - migration.migrate(connection); + migration.migrate(context.connection()); } else { log.log(DEBUG, "run migration {0}", local.location()); scriptRunner.runScript(script, "run migration version: " + local.version()); @@ -435,7 +437,7 @@ private long executeMigration(LocalMigrationResource local, String script) throw private void insertIntoHistory(LocalMigrationResource local, int checksum, long exeMillis) throws SQLException { MigrationMetaRow metaRow = createMetaRow(local, checksum, exeMillis); - metaRow.executeInsert(connection, insertSql); + metaRow.executeInsert(context.connection(), insertSql); addMigration(local.key(), metaRow); } @@ -525,7 +527,7 @@ List runAll(List localVersions) throw } if (patchLegacyChecksums && !checkStateOnly) { // only patch the legacy checksums once - initMetaRow.resetChecksum(EARLY_MODE_CHECKSUM, connection, updateChecksumSql); + initMetaRow.resetChecksum(EARLY_MODE_CHECKSUM, context.connection(), updateChecksumSql); } return checkMigrations; } diff --git a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable1Test.java b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable1Test.java index 99723e6..81b72c5 100644 --- a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable1Test.java +++ b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable1Test.java @@ -44,7 +44,7 @@ public void shutdown() { private MigrationTable migrationTable(Connection conn) { - var fc = new FirstCheck(config, conn, platform); + var fc = new FirstCheck(config, new DefaultMigrationContext(config, conn), platform); return new MigrationTable(fc, false); } diff --git a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable2Test.java b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable2Test.java index f916dd0..4b1e6bb 100644 --- a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable2Test.java +++ b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTable2Test.java @@ -12,7 +12,7 @@ public class MigrationTable2Test { private static MigrationTable migrationTable(MigrationConfig config) { - var fc = new FirstCheck(config, null, new MigrationPlatform()); + var fc = new FirstCheck(config, new DefaultMigrationContext(config, null), new MigrationPlatform()); return new MigrationTable(fc, false); } diff --git a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableAsyncTest.java b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableAsyncTest.java index 38bc0b6..a6795d8 100644 --- a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableAsyncTest.java +++ b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableAsyncTest.java @@ -204,7 +204,7 @@ private void runTest(boolean withExisting) throws SQLException, InterruptedExcep } private static MigrationTable migrationTable(MigrationPlatform platform, Connection connection) { - var fc = new FirstCheck(config, connection, platform); + var fc = new FirstCheck(config, new DefaultMigrationContext(config, connection), platform); return new MigrationTable(fc, false); } diff --git a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableCreateTableRaceTest.java b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableCreateTableRaceTest.java index b5d226c..99b3d83 100644 --- a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableCreateTableRaceTest.java +++ b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableCreateTableRaceTest.java @@ -51,7 +51,7 @@ void testRaceCondition_expect_loserOfCreateTableCanPerformTableExistsCheck() thr try (Connection conn = dataSource.getConnection()) { dropTable(conn); - var fc = new FirstCheck(config, conn, platform); + var fc = new FirstCheck(config, new DefaultMigrationContext(config, conn), platform); MigrationTable table = new MigrationTable(fc, false); table.createTable(); try { diff --git a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableTestDb2.java b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableTestDb2.java index 4777ed4..5a35392 100644 --- a/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableTestDb2.java +++ b/ebean-migration/src/test/java/io/ebean/migration/runner/MigrationTableTestDb2.java @@ -44,7 +44,7 @@ public void testMigrationTableBase() throws Exception { config.setMigrationPath("dbmig"); try (Connection conn = dataSource.getConnection()) { - var fc = new FirstCheck(config, conn, platform); + var fc = new FirstCheck(config, new DefaultMigrationContext(config, conn), platform); MigrationTable table = new MigrationTable(fc, false); table.createIfNeededAndLock(); table.unlockMigrationTable();