예제 #1
0
  @Override
  public void execute() throws BuildException {
    LogFactory.setLogCreator(new AntLogCreator(getProject()));
    log = LogFactory.getLog(getClass());

    prepareClassPath();

    try {
      Flyway flyway = new Flyway();

      flyway.setDataSource(createDataSource());
      String schemasValue = useValueIfPropertyNotSet(schemas, "schemas");
      if (schemasValue != null) {
        flyway.setSchemas(StringUtils.tokenizeToStringArray(schemasValue, ","));
      }
      String tableValue = useValueIfPropertyNotSet(table, "table");
      if (tableValue != null) {
        flyway.setTable(tableValue);
      }
      String initialVersionValue = useValueIfPropertyNotSet(initialVersion, "initialVersion");
      if (initialVersionValue != null) {
        flyway.setInitialVersion(initialVersionValue);
      }
      String initialDescriptionValue =
          useValueIfPropertyNotSet(initialDescription, "initialDescription");
      if (initialDescriptionValue != null) {
        flyway.setInitialDescription(initialDescriptionValue);
      }
      String initVersionValue = useValueIfPropertyNotSet(initVersion, "initVersion");
      if (initVersionValue != null) {
        flyway.setInitVersion(initVersionValue);
      }
      String initDescriptionValue = useValueIfPropertyNotSet(initDescription, "initDescription");
      if (initDescriptionValue != null) {
        flyway.setInitDescription(initDescriptionValue);
      }

      doExecute(flyway);
    } catch (Exception e) {
      throw new BuildException("Flyway Error: " + e.toString(), ExceptionUtils.getRootCause(e));
    }
  }
예제 #2
0
/** Supports reading and writing to the metadata table. */
public class MetaDataTableImpl implements MetaDataTable {
  private static final Log LOG = LogFactory.getLog(MetaDataTableImpl.class);

  /** Flag indicating whether the upgrade has already been executed. */
  private boolean upgraded;

  /** Database-specific functionality. */
  private final DbSupport dbSupport;

  /** The metadata table used by flyway. */
  private final Table table;

  /** The migration resolver. */
  private final MigrationResolver migrationResolver;

  /** JdbcTemplate with ddl manipulation access to the database. */
  private final JdbcTemplate jdbcTemplate;

  /**
   * Creates a new instance of the metadata table support.
   *
   * @param dbSupport Database-specific functionality.
   * @param table The metadata table used by flyway.
   * @param migrationResolver For resolving available migrations.
   */
  public MetaDataTableImpl(DbSupport dbSupport, Table table, MigrationResolver migrationResolver) {
    this.jdbcTemplate = dbSupport.getJdbcTemplate();
    this.dbSupport = dbSupport;
    this.table = table;
    this.migrationResolver = migrationResolver;
  }

  /** Creates the metatable if it doesn't exist, upgrades it if it does. */
  private void createIfNotExists() {
    if (table.existsNoQuotes() || table.exists()) {
      if (!upgraded) {
        new MetaDataTableTo20FormatUpgrader(dbSupport, table, migrationResolver).upgrade();
        new MetaDataTableTo202FormatUpgrader(dbSupport, table).upgrade();
        upgraded = true;
      }
      return;
    }

    LOG.info("Creating Metadata table: " + table);

    final String source =
        new ClassPathResource(dbSupport.getScriptLocation() + "createMetaDataTable.sql")
            .loadAsString("UTF-8");

    Map<String, String> placeholders = new HashMap<String, String>();
    placeholders.put("schema", table.getSchema().getName());
    placeholders.put("table", table.getName());
    final String sourceNoPlaceholders =
        new PlaceholderReplacer(placeholders, "${", "}").replacePlaceholders(source);

    SqlScript sqlScript = new SqlScript(sourceNoPlaceholders, dbSupport);
    sqlScript.execute(jdbcTemplate);

    LOG.debug("Metadata table " + table + " created.");
  }

  public void lock() {
    createIfNotExists();
    table.lock();
  }

  public void addAppliedMigration(AppliedMigration appliedMigration) {
    createIfNotExists();

    MigrationVersion version = appliedMigration.getVersion();
    try {
      int versionRank = calculateVersionRank(version);

      jdbcTemplate.update(
          "UPDATE "
              + table
              + " SET "
              + dbSupport.quote("version_rank")
              + " = "
              + dbSupport.quote("version_rank")
              + " + 1 WHERE "
              + dbSupport.quote("version_rank")
              + " >= ?",
          versionRank);
      jdbcTemplate.update(
          "INSERT INTO "
              + table
              + " ("
              + dbSupport.quote("version_rank")
              + ","
              + dbSupport.quote("installed_rank")
              + ","
              + dbSupport.quote("version")
              + ","
              + dbSupport.quote("description")
              + ","
              + dbSupport.quote("type")
              + ","
              + dbSupport.quote("script")
              + ","
              + dbSupport.quote("checksum")
              + ","
              + dbSupport.quote("installed_by")
              + ","
              + dbSupport.quote("execution_time")
              + ","
              + dbSupport.quote("success")
              + ")"
              + " VALUES (?, ?, ?, ?, ?, ?, ?, "
              + dbSupport.getCurrentUserFunction()
              + ", ?, ?)",
          versionRank,
          calculateInstalledRank(),
          version.toString(),
          appliedMigration.getDescription(),
          appliedMigration.getType().name(),
          appliedMigration.getScript(),
          appliedMigration.getChecksum(),
          appliedMigration.getExecutionTime(),
          appliedMigration.isSuccess());
      LOG.debug("MetaData table " + table + " successfully updated to reflect changes");
    } catch (SQLException e) {
      throw new FlywayException(
          "Unable to insert row for version '" + version + "' in metadata table " + table, e);
    }
  }

  /**
   * Calculates the installed rank for the new migration to be inserted.
   *
   * @return The installed rank.
   */
  private int calculateInstalledRank() throws SQLException {
    int currentMax =
        jdbcTemplate.queryForInt(
            "SELECT MAX(" + dbSupport.quote("installed_rank") + ")" + " FROM " + table);
    return currentMax + 1;
  }

  /**
   * Calculate the rank for this new version about to be inserted.
   *
   * @param version The version to calculated for.
   * @return The rank.
   */
  private int calculateVersionRank(MigrationVersion version) throws SQLException {
    List<String> versions =
        jdbcTemplate.queryForStringList("select " + dbSupport.quote("version") + " from " + table);

    List<MigrationVersion> migrationVersions = new ArrayList<MigrationVersion>();
    for (String versionStr : versions) {
      migrationVersions.add(new MigrationVersion(versionStr));
    }

    Collections.sort(migrationVersions);

    for (int i = 0; i < migrationVersions.size(); i++) {
      if (version.compareTo(migrationVersions.get(i)) < 0) {
        return i + 1;
      }
    }

    return migrationVersions.size() + 1;
  }

  public List<AppliedMigration> allAppliedMigrations() {
    if (!table.existsNoQuotes() && !table.exists()) {
      return new ArrayList<AppliedMigration>();
    }

    createIfNotExists();

    String query =
        "SELECT "
            + dbSupport.quote("version_rank")
            + ","
            + dbSupport.quote("installed_rank")
            + ","
            + dbSupport.quote("version")
            + ","
            + dbSupport.quote("description")
            + ","
            + dbSupport.quote("type")
            + ","
            + dbSupport.quote("script")
            + ","
            + dbSupport.quote("checksum")
            + ","
            + dbSupport.quote("installed_on")
            + ","
            + dbSupport.quote("installed_by")
            + ","
            + dbSupport.quote("execution_time")
            + ","
            + dbSupport.quote("success")
            + " FROM "
            + table
            + " ORDER BY "
            + dbSupport.quote("version_rank");

    try {
      return jdbcTemplate.query(
          query,
          new RowMapper<AppliedMigration>() {
            public AppliedMigration mapRow(final ResultSet rs) throws SQLException {
              return new AppliedMigration(
                  rs.getInt("version_rank"),
                  rs.getInt("installed_rank"),
                  new MigrationVersion(rs.getString("version")),
                  rs.getString("description"),
                  MigrationType.valueOf(rs.getString("type")),
                  rs.getString("script"),
                  toInteger((Number) rs.getObject("checksum")),
                  rs.getTimestamp("installed_on"),
                  rs.getString("installed_by"),
                  toInteger((Number) rs.getObject("execution_time")),
                  rs.getBoolean("success"));
            }
          });
    } catch (SQLException e) {
      throw new FlywayException(
          "Error while retrieving the list of applied migrations from metadata table " + table, e);
    }
  }

  /**
   * Converts this number into an Integer.
   *
   * @param number The Number to convert.
   * @return The matching Integer.
   */
  private Integer toInteger(Number number) {
    if (number == null) {
      return null;
    }

    return number.intValue();
  }

  public MigrationVersion getCurrentSchemaVersion() {
    if (!table.existsNoQuotes() && !table.exists()) {
      return MigrationVersion.EMPTY;
    }

    try {
      if (jdbcTemplate.queryForInt("SELECT COUNT(*) FROM " + table) == 0) {
        return MigrationVersion.EMPTY;
      }
    } catch (SQLException e) {
      throw new FlywayException(
          "Error checking if the metadata table " + table + " has at least one row", e);
    }

    createIfNotExists();

    // Determine the version associated with the highest version_rank
    String query =
        "SELECT t1."
            + dbSupport.quote("version")
            + " FROM "
            + table
            + " t1"
            + " LEFT OUTER JOIN "
            + table
            + " t2 ON"
            + " (t1."
            + dbSupport.quote("version")
            + " = t2."
            + dbSupport.quote("version")
            + " AND t1."
            + dbSupport.quote("version_rank")
            + " < t2."
            + dbSupport.quote("version_rank")
            + ")"
            + " WHERE t2."
            + dbSupport.quote("version")
            + " IS NULL";
    try {
      String version = jdbcTemplate.queryForString(query);
      return new MigrationVersion(version);
    } catch (SQLException e) {
      throw new FlywayException(
          "Error determining current schema version from metadata table " + table, e);
    }
  }

  public void init(final MigrationVersion initVersion, final String initDescription) {
    addAppliedMigration(
        new AppliedMigration(
            initVersion, initDescription, MigrationType.INIT, initDescription, null, 0, true));
  }

  public void repair() {
    if (!table.existsNoQuotes() && !table.exists()) {
      LOG.info(
          "Repair of metadata table " + table + " not necessary. No failed migration detected.");
      return;
    }

    createIfNotExists();

    try {
      int failedCount =
          jdbcTemplate.queryForInt(
              "SELECT COUNT(*) FROM "
                  + table
                  + " WHERE "
                  + dbSupport.quote("success")
                  + "="
                  + dbSupport.getBooleanFalse());
      if (failedCount == 0) {
        LOG.info(
            "Repair of metadata table " + table + " not necessary. No failed migration detected.");
        return;
      }
    } catch (SQLException e) {
      throw new FlywayException(
          "Unable to check the metadata table " + table + " for failed migrations", e);
    }

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    try {
      jdbcTemplate.execute(
          "DELETE FROM "
              + table
              + " WHERE "
              + dbSupport.quote("success")
              + " = "
              + dbSupport.getBooleanFalse());
    } catch (SQLException e) {
      throw new FlywayException("Unable to repair metadata table " + table, e);
    }

    stopWatch.stop();

    LOG.info(
        "Metadata table "
            + table
            + " successfully repaired (execution time "
            + TimeFormat.format(stopWatch.getTotalTimeMillis())
            + ").");
    LOG.info("Manual cleanup of the remaining effects the failed migration may still be required.");
  }

  public void addSchemasMarker(final Schema[] schemas) {
    createIfNotExists();

    addAppliedMigration(
        new AppliedMigration(
            new MigrationVersion("0"),
            "<< Flyway Schema Creation >>",
            MigrationType.SCHEMA,
            StringUtils.arrayToCommaDelimitedString(schemas),
            null,
            0,
            true));
  }

  public boolean hasSchemasMarker() {
    if (!table.existsNoQuotes() && !table.exists()) {
      return false;
    }

    createIfNotExists();

    try {
      int count =
          jdbcTemplate.queryForInt(
              "SELECT COUNT(*) FROM " + table + " WHERE " + dbSupport.quote("type") + "='SCHEMA'");
      return count > 0;
    } catch (SQLException e) {
      throw new FlywayException(
          "Unable to check whether the metadata table " + table + " has a schema marker migration",
          e);
    }
  }

  public boolean hasInitMarker() {
    if (!table.existsNoQuotes() && !table.exists()) {
      return false;
    }

    createIfNotExists();

    try {
      int count =
          jdbcTemplate.queryForInt(
              "SELECT COUNT(*) FROM " + table + " WHERE " + dbSupport.quote("type") + "='INIT'");
      return count > 0;
    } catch (SQLException e) {
      throw new FlywayException(
          "Unable to check whether the metadata table " + table + " has an init marker migration",
          e);
    }
  }

  public boolean hasAppliedMigrations() {
    if (!table.existsNoQuotes() && !table.exists()) {
      return false;
    }

    createIfNotExists();

    try {
      int count =
          jdbcTemplate.queryForInt(
              "SELECT COUNT(*) FROM "
                  + table
                  + " WHERE "
                  + dbSupport.quote("type")
                  + " NOT IN ('SCHEMA', 'INIT')");
      return count > 0;
    } catch (SQLException e) {
      throw new FlywayException(
          "Unable to check whether the metadata table " + table + " has applied migrations", e);
    }
  }

  @Override
  public String toString() {
    return table.toString();
  }
}
예제 #3
0
/** Oracle implementation of Schema. */
public class OracleSchema extends Schema {
  private static final Log LOG = LogFactory.getLog(OracleSchema.class);

  /**
   * Creates a new Oracle schema.
   *
   * @param jdbcTemplate The Jdbc Template for communicating with the DB.
   * @param dbSupport The database-specific support.
   * @param name The name of the schema.
   */
  public OracleSchema(JdbcTemplate jdbcTemplate, DbSupport dbSupport, String name) {
    super(jdbcTemplate, dbSupport, name);
  }

  @Override
  protected boolean doExists() throws SQLException {
    return jdbcTemplate.queryForInt("SELECT COUNT(*) FROM all_users WHERE username=?", name) > 0;
  }

  @Override
  protected boolean doEmpty() throws SQLException {
    return jdbcTemplate.queryForInt("SELECT count(*) FROM all_objects WHERE owner = ?", name) == 0;
  }

  @Override
  protected void doCreate() throws SQLException {
    jdbcTemplate.execute("CREATE USER " + dbSupport.quote(name) + " IDENTIFIED BY flyway");
    jdbcTemplate.execute("GRANT RESOURCE TO " + dbSupport.quote(name));
  }

  @Override
  protected void doDrop() throws SQLException {
    jdbcTemplate.execute("DROP USER " + dbSupport.quote(name) + " CASCADE");
  }

  @Override
  protected void doClean() throws SQLException {
    if ("SYSTEM".equals(name.toUpperCase())) {
      throw new FlywayException(
          "Clean not supported on Oracle for user 'SYSTEM'! You should NEVER add your own objects to the SYSTEM schema!");
    }

    jdbcTemplate.execute("PURGE RECYCLEBIN");

    for (String statement : generateDropStatementsForSpatialExtensions()) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForQueueTables()) {
      // for dropping queue tables, a special grant is required:
      // GRANT EXECUTE ON DBMS_AQADM TO flyway;
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("SEQUENCE", "")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("FUNCTION", "")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement :
        generateDropStatementsForObjectType("MATERIALIZED VIEW", "PRESERVE TABLE")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("PACKAGE", "")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("PROCEDURE", "")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("SYNONYM", "")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("TRIGGER", "")) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("VIEW", "CASCADE CONSTRAINTS")) {
      jdbcTemplate.execute(statement);
    }

    for (Table table : allTables()) {
      table.drop();
    }

    for (String statement : generateDropStatementsForXmlTables()) {
      jdbcTemplate.execute(statement);
    }

    for (String statement : generateDropStatementsForObjectType("TYPE", "FORCE")) {
      jdbcTemplate.execute(statement);
    }
  }

  /**
   * Generates the drop statements for all xml tables.
   *
   * @return The complete drop statements, ready to execute.
   * @throws SQLException when the drop statements could not be generated.
   */
  private List<String> generateDropStatementsForXmlTables() throws SQLException {
    List<String> dropStatements = new ArrayList<String>();

    if (!xmlDBExtensionsAvailable()) {
      LOG.debug("Oracle XML DB Extensions are not available. No cleaning of XML tables.");
      return dropStatements;
    }

    List<String> objectNames =
        jdbcTemplate.queryForStringList(
            "SELECT table_name FROM all_xml_tables WHERE owner = ?", name);
    for (String objectName : objectNames) {
      dropStatements.add("DROP TABLE " + dbSupport.quote(name, objectName) + " PURGE");
    }
    return dropStatements;
  }

  /**
   * Checks whether Oracle XML DB extensions are available or not.
   *
   * @return {@code true} if they are available, {@code false} if not.
   * @throws SQLException when checking availability of the extensions failed.
   */
  private boolean xmlDBExtensionsAvailable() throws SQLException {
    return (jdbcTemplate.queryForInt("SELECT COUNT(*) FROM all_users WHERE username = '******'") > 0)
        && (jdbcTemplate.queryForInt(
                "SELECT COUNT(*) FROM all_views WHERE view_name = 'RESOURCE_VIEW'")
            > 0);
  }

  /**
   * Generates the drop statements for all database objects of this type.
   *
   * @param objectType The type of database object to drop.
   * @param extraArguments The extra arguments to add to the drop statement.
   * @return The complete drop statements, ready to execute.
   * @throws SQLException when the drop statements could not be generated.
   */
  private List<String> generateDropStatementsForObjectType(String objectType, String extraArguments)
      throws SQLException {
    String query =
        "SELECT object_name FROM all_objects WHERE object_type = ? AND owner = ?"
            // Ignore Spatial Index Sequences as they get dropped automatically when the index gets
            // dropped.
            + " AND object_name NOT LIKE 'MDRS_%$'";

    List<String> objectNames = jdbcTemplate.queryForStringList(query, objectType, name);
    List<String> dropStatements = new ArrayList<String>();
    for (String objectName : objectNames) {
      dropStatements.add(
          "DROP " + objectType + " " + dbSupport.quote(name, objectName) + " " + extraArguments);
    }
    return dropStatements;
  }

  /**
   * Generates the drop statements for Oracle Spatial Extensions-related database objects.
   *
   * @return The complete drop statements, ready to execute.
   * @throws SQLException when the drop statements could not be generated.
   */
  private List<String> generateDropStatementsForSpatialExtensions() throws SQLException {
    List<String> statements = new ArrayList<String>();

    if (!spatialExtensionsAvailable()) {
      LOG.debug(
          "Oracle Spatial Extensions are not available. No cleaning of MDSYS tables and views.");
      return statements;
    }
    if (!dbSupport.getCurrentSchema().getName().equalsIgnoreCase(name)) {
      int count =
          jdbcTemplate.queryForInt(
              "SELECT COUNT (*) FROM all_sdo_geom_metadata WHERE owner=?", name);
      count +=
          jdbcTemplate.queryForInt(
              "SELECT COUNT (*) FROM all_sdo_index_info WHERE sdo_index_owner=?", name);
      if (count > 0) {
        LOG.warn(
            "Unable to clean Oracle Spatial objects for schema '"
                + name
                + "' as they do not belong to the default schema for this connection!");
      }
      return statements;
    }

    statements.add("DELETE FROM mdsys.user_sdo_geom_metadata");

    List<String> indexNames =
        jdbcTemplate.queryForStringList("select INDEX_NAME from USER_SDO_INDEX_INFO");
    for (String indexName : indexNames) {
      statements.add("DROP INDEX \"" + indexName + "\"");
    }

    return statements;
  }

  /**
   * Generates the drop statements for queue tables.
   *
   * @return The complete drop statements, ready to execute.
   * @throws SQLException when the drop statements could not be generated.
   */
  private List<String> generateDropStatementsForQueueTables() throws SQLException {
    List<String> statements = new ArrayList<String>();

    List<String> queueTblNames =
        jdbcTemplate.queryForStringList("select QUEUE_TABLE from USER_QUEUE_TABLES");
    for (String queueTblName : queueTblNames) {
      statements.add(
          "begin DBMS_AQADM.drop_queue_table (queue_table=> '"
              + queueTblName
              + "', FORCE => TRUE); end;");
    }

    return statements;
  }

  /**
   * Checks whether Oracle Spatial extensions are available or not.
   *
   * @return {@code true} if they are available, {@code false} if not.
   * @throws SQLException when checking availability of the spatial extensions failed.
   */
  private boolean spatialExtensionsAvailable() throws SQLException {
    return jdbcTemplate.queryForInt(
            "SELECT COUNT(*) FROM all_views WHERE owner = 'MDSYS' AND view_name = 'USER_SDO_GEOM_METADATA'")
        > 0;
  }

  @Override
  protected Table[] doAllTables() throws SQLException {
    List<String> tableNames =
        jdbcTemplate.queryForStringList(
            "SELECT table_name FROM all_tables WHERE owner = ?"
                // Ignore Recycle bin objects
                + " AND table_name NOT LIKE 'BIN$%'"
                // Ignore Spatial Index Tables as they get dropped automatically when the index gets
                // dropped.
                + " AND table_name NOT LIKE 'MDRT_%$'"
                // Ignore Materialized View Logs
                + " AND table_name NOT LIKE 'MLOG$%' AND table_name NOT LIKE 'RUPD$%'"
                // Ignore Oracle Text Index Tables
                + " AND table_name NOT LIKE 'DR$%'"
                // Ignore Index Organized Tables
                + " AND table_name NOT LIKE 'SYS_IOT_OVER_%'"
                // Ignore Nested Tables
                + " AND nested != 'YES'"
                // Ignore Nested Tables
                + " AND secondary != 'Y'",
            name);

    Table[] tables = new Table[tableNames.size()];
    for (int i = 0; i < tableNames.size(); i++) {
      tables[i] = new OracleTable(jdbcTemplate, dbSupport, this, tableNames.get(i));
    }
    return tables;
  }

  @Override
  public Table getTable(String tableName) {
    return new OracleTable(jdbcTemplate, dbSupport, this, tableName);
  }
}
예제 #4
0
/**
 * Main workflow for migrating the database.
 *
 * @author Axel Fontaine
 */
public class DbMigrate {

  private static final Log LOG = LogFactory.getLog(DbMigrate.class);

  /** The target version of the migration. */
  private final MigrationVersion target;

  /** 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 connection to use. */
  private final Connection connectionMetaDataTable;

  /** 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;

  /**
   * Allows migrations to be run "out of order".
   *
   * <p>If you already have versions 1 and 3 applied, and now a version 2 is found, it will be
   * applied too instead of being ignored.
   *
   * <p>(default: {@code false})
   */
  private boolean outOfOrder;

  /**
   * Creates a new database migrator.
   *
   * @param connectionMetaDataTable The connection to use.
   * @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 target The target version of the migration.
   * @param ignoreFailedFutureMigration Flag whether to ignore failed future migrations or not.
   * @param outOfOrder Allows migrations to be run "out of order".
   */
  public DbMigrate(
      Connection connectionMetaDataTable,
      Connection connectionUserObjects,
      DbSupport dbSupport,
      MetaDataTable metaDataTable,
      Schema schema,
      MigrationResolver migrationResolver,
      MigrationVersion target,
      boolean ignoreFailedFutureMigration,
      boolean outOfOrder) {
    this.connectionMetaDataTable = connectionMetaDataTable;
    this.connectionUserObjects = connectionUserObjects;
    this.dbSupport = dbSupport;
    this.metaDataTable = metaDataTable;
    this.schema = schema;
    this.migrationResolver = migrationResolver;
    this.target = target;
    this.ignoreFailedFutureMigration = ignoreFailedFutureMigration;
    this.outOfOrder = outOfOrder;
  }

  /**
   * Starts the actual migration.
   *
   * @return The number of successfully applied migrations.
   * @throws FlywayException when migration failed.
   */
  public int migrate() throws FlywayException {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    int migrationSuccessCount = 0;
    while (true) {
      final boolean firstRun = migrationSuccessCount == 0;
      MigrationVersion result =
          new TransactionTemplate(connectionMetaDataTable, false)
              .execute(
                  new TransactionCallback<MigrationVersion>() {
                    public MigrationVersion doInTransaction() {
                      metaDataTable.lock();

                      MigrationInfoServiceImpl infoService =
                          new MigrationInfoServiceImpl(
                              migrationResolver, metaDataTable, target, outOfOrder);
                      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 (outOfOrder) {
                          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 {
                          LOG.warn(
                              "Schema "
                                  + schema
                                  + " has a version ("
                                  + currentSchemaVersion
                                  + ") that is newer than the latest available migration ("
                                  + resolved[resolved.length - 1].getVersion()
                                  + ") !");
                        }
                      }

                      MigrationInfo[] failed = infoService.failed();
                      if (failed.length > 0) {
                        if ((failed.length == 1)
                            && (failed[0].getState() == MigrationState.FUTURE_FAILED)
                            && ignoreFailedFutureMigration) {
                          LOG.warn(
                              "Schema "
                                  + schema
                                  + " contains a failed future migration to version "
                                  + failed[0].getVersion()
                                  + " !");
                        } else {
                          throw new FlywayException(
                              "Schema "
                                  + schema
                                  + " contains a failed migration to version "
                                  + failed[0].getVersion()
                                  + " !");
                        }
                      }

                      MigrationInfoImpl[] pendingMigrations = infoService.pending();

                      if (pendingMigrations.length == 0) {
                        return null;
                      }

                      boolean isOutOfOrder =
                          pendingMigrations[0].getVersion().compareTo(currentSchemaVersion) < 0;
                      return applyMigration(
                          pendingMigrations[0].getResolvedMigration(), isOutOfOrder);
                    }
                  });
      if (result == null) {
        // No further migrations available
        break;
      }

      migrationSuccessCount++;
    }

    stopWatch.stop();

    logSummary(migrationSuccessCount, stopWatch.getTotalTimeMillis());
    return migrationSuccessCount;
  }

  /**
   * 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 migration The migration to apply.
   * @param isOutOfOrder If this migration is being applied out of order.
   * @return The result of the migration.
   */
  private MigrationVersion applyMigration(final ResolvedMigration migration, boolean isOutOfOrder) {
    MigrationVersion version = migration.getVersion();
    if (isOutOfOrder) {
      LOG.info("Migrating schema " + schema + " to version " + version + " (out of order)");
    } else {
      LOG.info("Migrating schema " + schema + " to version " + version);
    }

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    try {
      new TransactionTemplate(connectionUserObjects)
          .execute(
              new TransactionCallback<Void>() {
                public Void doInTransaction() {
                  migration.getExecutor().execute(connectionUserObjects);
                  return null;
                }
              });
      LOG.debug(
          "Successfully completed and committed migration of schema "
              + schema
              + " to version "
              + version);
    } catch (FlywayException e) {
      String failedMsg = "Migration of schema " + schema + " to version " + version + " failed!";
      if (dbSupport.supportsDdlTransactions()) {
        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(
                version,
                migration.getDescription(),
                migration.getType(),
                migration.getScript(),
                migration.getChecksum(),
                executionTime,
                false);
        metaDataTable.addAppliedMigration(appliedMigration);
      }
      throw e;
    }

    stopWatch.stop();
    int executionTime = (int) stopWatch.getTotalTimeMillis();

    AppliedMigration appliedMigration =
        new AppliedMigration(
            version,
            migration.getDescription(),
            migration.getType(),
            migration.getScript(),
            migration.getChecksum(),
            executionTime,
            true);
    metaDataTable.addAppliedMigration(appliedMigration);

    return version;
  }
}