All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.avaje.dbmigration.runner.MigrationTable Maven / Gradle / Ivy

There is a newer version: 1.3.2
Show newest version
package org.avaje.dbmigration.runner;

import org.avaje.dbmigration.MigrationConfig;
import org.avaje.dbmigration.util.IOUtils;
import org.avaje.dbmigration.util.JdbcClose;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Manages the migration table.
 */
public class MigrationTable {

  private static final Logger logger = LoggerFactory.getLogger(MigrationTable.class);

  private final Connection connection;

  private final String catalog;
  private final String schema;
  private final String table;
  private final String envUserName;

  private final Timestamp runOn = new Timestamp(System.currentTimeMillis());

  private final ScriptTransform scriptTransform;

  private final String insertSql;
  private final String selectSql;

  private final LinkedHashMap migrations;

  private MigrationMetaRow lastMigration;

  /**
   * Construct with server, configuration and jdbc connection (DB admin user).
   */
  public MigrationTable(MigrationConfig config, Connection connection) {

    this.connection = connection;
    this.migrations = new LinkedHashMap();

    this.catalog = null;
    this.schema = null;
    this.table = config.getMetaTable();
    this.selectSql = MigrationMetaRow.selectSql(table);
    this.insertSql = MigrationMetaRow.insertSql(table);
    this.scriptTransform = createScriptTransform(config);
    this.envUserName = System.getProperty("user.name");
  }

  /**
   * Return the number of migrations in the DB migration table.
   */
  public int size() {
    return migrations.size();
  }

  /**
   * Create the ScriptTransform for placeholder key/value replacement.
   */
  private ScriptTransform createScriptTransform(MigrationConfig config) {

    Map map = PlaceholderBuilder.build(config.getRunPlaceholders(), config.getRunPlaceholderMap());
    return new ScriptTransform(map);
  }

  /**
   * Create the table is it does not exist.
   */
  public void createIfNeeded() throws SQLException, IOException {

    if (!tableExists(connection)) {
      createTable(connection);
    }

    PreparedStatement query = connection.prepareStatement(selectSql);
    try {
      ResultSet resultSet = query.executeQuery();
      try {
        while (resultSet.next()) {
          MigrationMetaRow metaRow = new MigrationMetaRow(resultSet);
          addMigration(metaRow.getVersion(), metaRow);
        }
      } finally {
        JdbcClose.close(resultSet);
      }
    } finally {
      JdbcClose.close(query);
    }
  }


  private void createTable(Connection connection) throws IOException, SQLException {

    String script = ScriptTransform.table(table, getCreateTableScript());

    MigrationScriptRunner run = new MigrationScriptRunner(connection);
    run.runScript(false, script, "create migration table");
  }

  /**
   * Return the create table script.
   */
  private String getCreateTableScript() throws IOException {
    // supply a script to override the default table create script
    String script = readResource("migration-support/create-table.sql");
    if (script == null) {
      // no, just use the default script
      script = readResource("migration-support/default-create-table.sql");
    }
    return script;
  }

  private String readResource(String location) throws IOException {

    Enumeration resources = getClassLoader().getResources(location);
    if (resources.hasMoreElements()) {
      URL url = resources.nextElement();
      return IOUtils.readUtf8(url.openStream());
    }
    return null;
  }

  private ClassLoader getClassLoader() {
    return Thread.currentThread().getContextClassLoader();
  }

  /**
   * Return true if the table exists.
   */
  private boolean tableExists(Connection connection) throws SQLException {

    String migTable = table;

    DatabaseMetaData metaData = connection.getMetaData();
    if (metaData.storesUpperCaseIdentifiers()) {
      migTable = migTable.toUpperCase();
    }
    ResultSet tables = metaData.getTables(catalog, schema, migTable, null);
    try {
      return tables.next();
    } finally {
      JdbcClose.close(tables);
    }
  }

  /**
   * Return true if the migration ran successfully and false if the migration failed.
   */
  public boolean shouldRun(LocalMigrationResource localVersion, LocalMigrationResource priorVersion) throws SQLException {

    if (priorVersion != null && !localVersion.isRepeatable()) {
      if (!migrationExists(priorVersion)) {
        logger.error("Migration {} requires prior migration {} which has not been run", localVersion.getVersion(), priorVersion.getVersion());
        return false;
      }
    }

    MigrationMetaRow existing = migrations.get(localVersion.key());
    return runMigration(localVersion, existing);
  }

  /**
   * Run the migration script.
   *
   * @param local    The local migration resource
   * @param existing The information for this migration existing in the table
   *
   * @return True if the migrations should continue
   */
  private boolean runMigration(LocalMigrationResource local, MigrationMetaRow existing) throws SQLException {

    String script = convertScript(local.getContent());
    int checksum = Checksum.calculate(script);

    if (existing != null) {

      boolean matchChecksum = (existing.getChecksum() == checksum);

      if (!local.isRepeatable()) {
        if (!matchChecksum) {
          logger.error("Checksum mismatch on migration {}", local.getLocation());
        }
        return true;

      } else if (matchChecksum) {
        logger.trace("... skip unchanged repeatable migration {}", local.getLocation());
        return true;
      }
    }

    runMigration(local, script, checksum);
    return true;
  }

  /**
   * Run a migration script as new migration or update on existing repeatable migration.
   */
  private void runMigration(LocalMigrationResource local, String script, int checksum) throws SQLException {

    logger.debug("run migration {}", local.getLocation());

    long start = System.currentTimeMillis();
    MigrationScriptRunner run = new MigrationScriptRunner(connection);
    run.runScript(false, script, "run migration version: " + local.getVersion());

    long exeMillis = System.currentTimeMillis() - start;

    MigrationMetaRow metaRow = createMetaRow(local, checksum, exeMillis);
    PreparedStatement statement = connection.prepareStatement(insertSql);
    try {
      metaRow.bindInsert(statement);
      statement.executeUpdate();
      addMigration(local.key(), metaRow);
    } finally {
      JdbcClose.close(statement);
    }
  }

  /**
   * Create the MigrationMetaRow for this migration.
   */
  private MigrationMetaRow createMetaRow(LocalMigrationResource migration, int checksum, long exeMillis) {

    int nextId = 1;
    if (lastMigration != null) {
      nextId = lastMigration.getId() + 1;
    }

    String type = migration.getType();
    String runVersion = migration.key();
    String comment = migration.getComment();

    return new MigrationMetaRow(nextId, type, runVersion, comment, checksum, envUserName, runOn, exeMillis);
  }

  /**
   * Return true if the migration exists.
   */
  private boolean migrationExists(LocalMigrationResource priorVersion) {
    return migrations.containsKey(priorVersion.key());
  }

  /**
   * Apply the placeholder key/value replacement on the script.
   */
  private String convertScript(String script) {
    return scriptTransform.transform(script);
  }

  /**
   * Register the successfully executed migration (to allow dependant scripts to run).
   */
  private void addMigration(String key, MigrationMetaRow metaRow) {
    lastMigration = metaRow;
    if (metaRow.getVersion() == null) {
      throw new IllegalStateException("No runVersion in db migration table row? " + metaRow);
    }
    migrations.put(key, metaRow);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy