/* * Copyright 2010-2017 Boxfuse GmbH * * 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.core.internal.command; import org.flywaydb.core.api.FlywayException; import org.flywaydb.core.api.MigrationInfo; import org.flywaydb.core.api.MigrationState; import org.flywaydb.core.api.MigrationVersion; import org.flywaydb.core.api.callback.FlywayCallback; import org.flywaydb.core.api.configuration.FlywayConfiguration; import org.flywaydb.core.api.resolver.MigrationExecutor; import org.flywaydb.core.api.resolver.MigrationResolver; import org.flywaydb.core.api.resolver.ResolvedMigration; import org.flywaydb.core.internal.dbsupport.DbSupport; import org.flywaydb.core.internal.dbsupport.DbSupportFactory; import org.flywaydb.core.internal.dbsupport.FlywaySqlScriptException; import org.flywaydb.core.internal.dbsupport.Schema; import org.flywaydb.core.internal.info.MigrationInfoImpl; import org.flywaydb.core.internal.info.MigrationInfoServiceImpl; import org.flywaydb.core.internal.metadatatable.AppliedMigration; import org.flywaydb.core.internal.metadatatable.MetaDataTable; import org.flywaydb.core.internal.util.StopWatch; import org.flywaydb.core.internal.util.StringUtils; import org.flywaydb.core.internal.util.TimeFormat; import org.flywaydb.core.internal.util.jdbc.TransactionTemplate; import org.flywaydb.core.internal.util.logging.Log; import org.flywaydb.core.internal.util.logging.LogFactory; import java.sql.Connection; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; /** * Main workflow for migrating the database. * * @author Axel Fontaine */ public class DbMigrate { private static final Log LOG = LogFactory.getLog(DbMigrate.class); /** * Database-specific functionality. */ private final DbSupport dbSupport; /** * The database metadata table. */ private final MetaDataTable metaDataTable; /** * The schema containing the metadata table. */ private final Schema schema; /** * The migration resolver. */ private final MigrationResolver migrationResolver; /** * The Flyway configuration. */ private final FlywayConfiguration configuration; /** * The connection to use to perform the actual database migrations. */ private final Connection connectionUserObjects; /** * Flag whether to ignore failed future migrations or not. */ private final boolean ignoreFailedFutureMigration; /** * The DB support for the user objects connection. */ private final DbSupport dbSupportUserObjects; /** * Creates a new database migrator. * * @param connectionUserObjects The connection to use to perform the actual database migrations. * @param dbSupport Database-specific functionality. * @param metaDataTable The database metadata table. * @param migrationResolver The migration resolver. * @param ignoreFailedFutureMigration Flag whether to ignore failed future migrations or not. * @param configuration The Flyway configuration. */ public DbMigrate(Connection connectionUserObjects, DbSupport dbSupport, MetaDataTable metaDataTable, Schema schema, MigrationResolver migrationResolver, boolean ignoreFailedFutureMigration, FlywayConfiguration configuration) { this.connectionUserObjects = connectionUserObjects; this.dbSupport = dbSupport; this.metaDataTable = metaDataTable; this.schema = schema; this.migrationResolver = migrationResolver; this.ignoreFailedFutureMigration = ignoreFailedFutureMigration; this.configuration = configuration; dbSupportUserObjects = DbSupportFactory.createDbSupport(connectionUserObjects, false); } /** * Starts the actual migration. * * @return The number of successfully applied migrations. * @throws FlywayException when migration failed. */ public int migrate() throws FlywayException { try { for (final FlywayCallback callback : configuration.getCallbacks()) { new TransactionTemplate(connectionUserObjects).execute(new Callable<Object>() { @Override public Object call() throws SQLException { dbSupportUserObjects.changeCurrentSchemaTo(schema); callback.beforeMigrate(connectionUserObjects); return null; } }); } StopWatch stopWatch = new StopWatch(); stopWatch.start(); int migrationSuccessCount = 0; while (true) { final boolean firstRun = migrationSuccessCount == 0; int count = metaDataTable.lock(new Callable<Integer>() { @Override public Integer call() { MigrationInfoServiceImpl infoService = new MigrationInfoServiceImpl(migrationResolver, metaDataTable, configuration.getTarget(), configuration.isOutOfOrder(), true, true, true); infoService.refresh(); MigrationVersion currentSchemaVersion = MigrationVersion.EMPTY; if (infoService.current() != null) { currentSchemaVersion = infoService.current().getVersion(); } if (firstRun) { LOG.info("Current version of schema " + schema + ": " + currentSchemaVersion); if (configuration.isOutOfOrder()) { LOG.warn("outOfOrder mode is active. Migration of schema " + schema + " may not be reproducible."); } } MigrationInfo[] future = infoService.future(); if (future.length > 0) { MigrationInfo[] resolved = infoService.resolved(); if (resolved.length == 0) { LOG.warn("Schema " + schema + " has version " + currentSchemaVersion + ", but no migration could be resolved in the configured locations !"); } else { int offset = resolved.length - 1; while (resolved[offset].getVersion() == null) { // Skip repeatable migrations offset--; } LOG.warn("Schema " + schema + " has a version (" + currentSchemaVersion + ") that is newer than the latest available migration (" + resolved[offset].getVersion() + ") !"); } } MigrationInfo[] failed = infoService.failed(); if (failed.length > 0) { if ((failed.length == 1) && (failed[0].getState() == MigrationState.FUTURE_FAILED) && (configuration.isIgnoreFutureMigrations() || ignoreFailedFutureMigration)) { LOG.warn("Schema " + schema + " contains a failed future migration to version " + failed[0].getVersion() + " !"); } else { if (failed[0].getVersion() == null) { throw new FlywayException("Schema " + schema + " contains a failed repeatable migration (" + failed[0].getDescription() + ") !"); } throw new FlywayException("Schema " + schema + " contains a failed migration to version " + failed[0].getVersion() + " !"); } } LinkedHashMap<MigrationInfoImpl, Boolean> group = new LinkedHashMap<MigrationInfoImpl, Boolean>(); for (MigrationInfoImpl pendingMigration : infoService.pending()) { boolean isOutOfOrder = pendingMigration.getVersion() != null && pendingMigration.getVersion().compareTo(currentSchemaVersion) < 0; group.put(pendingMigration, isOutOfOrder); if (!configuration.isGroup()) { // Only include one pending migration if group is disabled break; } } if (!group.isEmpty()) { applyMigrations(group); } return group.size(); } }); if (count == 0) { // No further migrations available break; } migrationSuccessCount += count; } stopWatch.stop(); logSummary(migrationSuccessCount, stopWatch.getTotalTimeMillis()); for (final FlywayCallback callback : configuration.getCallbacks()) { new TransactionTemplate(connectionUserObjects).execute(new Callable<Object>() { @Override public Object call() throws SQLException { dbSupportUserObjects.changeCurrentSchemaTo(schema); callback.afterMigrate(connectionUserObjects); return null; } }); } return migrationSuccessCount; } finally { dbSupportUserObjects.restoreCurrentSchema(); } } /** * Logs the summary of this migration run. * * @param migrationSuccessCount The number of successfully applied migrations. * @param executionTime The total time taken to perform this migration run (in ms). */ private void logSummary(int migrationSuccessCount, long executionTime) { if (migrationSuccessCount == 0) { LOG.info("Schema " + schema + " is up to date. No migration necessary."); return; } if (migrationSuccessCount == 1) { LOG.info("Successfully applied 1 migration to schema " + schema + " (execution time " + TimeFormat.format(executionTime) + ")."); } else { LOG.info("Successfully applied " + migrationSuccessCount + " migrations to schema " + schema + " (execution time " + TimeFormat.format(executionTime) + ")."); } } /** * Applies this migration to the database. The migration state and the execution time are updated accordingly. * * @param group The group of migrations to apply. */ private void applyMigrations(final LinkedHashMap<MigrationInfoImpl, Boolean> group) { boolean executeGroupInTransaction = isExecuteGroupInTransaction(group); final StopWatch stopWatch = new StopWatch(); try { if (executeGroupInTransaction) { new TransactionTemplate(connectionUserObjects).execute(new Callable<Object>() { @Override public Object call() throws SQLException { doMigrateGroup(group, stopWatch); return null; } }); } else { doMigrateGroup(group, stopWatch); } } catch (FlywayMigrateSqlException e) { MigrationInfoImpl migration = e.getMigration(); String failedMsg = "Migration of " + toMigrationText(migration, e.isOutOfOrder()) + " failed!"; if (dbSupport.supportsDdlTransactions() && executeGroupInTransaction) { LOG.error(failedMsg + " Changes successfully rolled back."); } else { LOG.error(failedMsg + " Please restore backups and roll back database and code!"); stopWatch.stop(); int executionTime = (int) stopWatch.getTotalTimeMillis(); AppliedMigration appliedMigration = new AppliedMigration(migration.getVersion(), migration.getDescription(), migration.getType(), migration.getScript(), migration.getResolvedMigration().getChecksum(), executionTime, false); metaDataTable.addAppliedMigration(appliedMigration); } throw e; } } private boolean isExecuteGroupInTransaction(LinkedHashMap<MigrationInfoImpl, Boolean> group) { boolean executeGroupInTransaction = true; boolean first = true; for (Map.Entry<MigrationInfoImpl, Boolean> entry : group.entrySet()) { ResolvedMigration resolvedMigration = entry.getKey().getResolvedMigration(); boolean inTransaction = resolvedMigration.getExecutor().executeInTransaction(); if (first) { executeGroupInTransaction = inTransaction; first = false; } else { if (!configuration.isMixed() && executeGroupInTransaction != inTransaction) { throw new FlywayException( "Detected both transactional and non-transactional migrations within the same migration group" + " (even though mixed is false). First offending migration:" + (resolvedMigration.getVersion() == null ? "" : " " + resolvedMigration.getVersion()) + (StringUtils.hasLength(resolvedMigration.getDescription()) ? " " + resolvedMigration.getDescription() : "") + (inTransaction ? "" : " [non-transactional]")); } executeGroupInTransaction = executeGroupInTransaction && inTransaction; } } return executeGroupInTransaction; } private void doMigrateGroup(LinkedHashMap<MigrationInfoImpl, Boolean> group, StopWatch stopWatch) { for (Map.Entry<MigrationInfoImpl, Boolean> entry : group.entrySet()) { final MigrationInfoImpl migration = entry.getKey(); boolean isOutOfOrder = entry.getValue(); final String migrationText = toMigrationText(migration, isOutOfOrder); stopWatch.start(); LOG.info("Migrating " + migrationText); dbSupportUserObjects.changeCurrentSchemaTo(schema); for (final FlywayCallback callback : configuration.getCallbacks()) { callback.beforeEachMigrate(connectionUserObjects, migration); } try { migration.getResolvedMigration().getExecutor().execute(connectionUserObjects); } catch (FlywaySqlScriptException e) { throw new FlywayMigrateSqlException(migration, isOutOfOrder, e); } catch (SQLException e) { throw new FlywayMigrateSqlException(migration, isOutOfOrder, e); } LOG.debug("Successfully completed migration of " + migrationText); for (final FlywayCallback callback : configuration.getCallbacks()) { callback.afterEachMigrate(connectionUserObjects, migration); } stopWatch.stop(); int executionTime = (int) stopWatch.getTotalTimeMillis(); AppliedMigration appliedMigration = new AppliedMigration(migration.getVersion(), migration.getDescription(), migration.getType(), migration.getScript(), migration.getResolvedMigration().getChecksum(), executionTime, true); metaDataTable.addAppliedMigration(appliedMigration); } } private String toMigrationText(MigrationInfoImpl migration, boolean isOutOfOrder) { final MigrationExecutor migrationExecutor = migration.getResolvedMigration().getExecutor(); final String migrationText; if (migration.getVersion() != null) { migrationText = "schema " + schema + " to version " + migration.getVersion() + " - " + migration.getDescription() + (isOutOfOrder ? " [out of order]" : "") + (migrationExecutor.executeInTransaction() ? "" : " [non-transactional]"); } else { migrationText = "schema " + schema + " with repeatable migration " + migration.getDescription() + (migrationExecutor.executeInTransaction() ? "" : " [non-transactional]"); } return migrationText; } public static class FlywayMigrateSqlException extends FlywaySqlScriptException { private final MigrationInfoImpl migration; private final boolean outOfOrder; FlywayMigrateSqlException(MigrationInfoImpl migration, boolean outOfOrder, SQLException e) { super(null, null, e); this.migration = migration; this.outOfOrder = outOfOrder; } FlywayMigrateSqlException(MigrationInfoImpl migration, boolean outOfOrder, FlywaySqlScriptException e) { super(e.getResource(), e.getSqlStatement(), (SQLException) e.getCause()); this.migration = migration; this.outOfOrder = outOfOrder; } public MigrationInfoImpl getMigration() { return migration; } public boolean isOutOfOrder() { return outOfOrder; } } }