package joist.migrations; import java.sql.Connection; import java.sql.SQLException; import java.util.Optional; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import joist.codegen.Config; import joist.jdbc.Jdbc; import joist.jdbc.JdbcException; public class Migrater { private static final Logger log = LoggerFactory.getLogger(Migrater.class); private static ThreadLocal<Connection> current = new ThreadLocal<Connection>(); private final Config config; private final SchemaVersionTable schemaInfoTable; private final MigrationLoader migrationClasses; public static Connection getConnection() { return Migrater.current.get(); } public Migrater(Config config) { this.config = config; this.schemaInfoTable = new SchemaVersionTable(config); this.migrationClasses = new MigrationLoader(this.config.packageNamesContainingMigrations); MigrationKeywords.config = config; } public void migrate() { boolean locked = this.schemaInfoTable.tryToLock(); if (!locked) { throw new RuntimeException("schema_info was already locked"); } try { DataSource ds = this.config.dbAppSaSettings.getDataSource(); this.applyNormalMigrations(ds); this.applyBranchMigrationsIfAllowed(ds); this.schemaInfoTable.vacuumIfAppropriate(); } finally { this.schemaInfoTable.unlock(); } } private void applyNormalMigrations(DataSource ds) { int version = Jdbc.inTransaction(ds, c -> this.schemaInfoTable.nextVersionNumber(c)); while (true) { Optional<Migration> migration = this.migrationClasses.get("m", version); if (migration.isPresent()) { this.apply(ds, migration.get(), Optional.of(version)); version++; } else { break; } } } private void applyBranchMigrationsIfAllowed(DataSource ds) { if (this.config.allowBranchMigrations) { this.applyBranchMigrations(ds); } else { Optional<Migration> b0 = this.migrationClasses.get("b", 0); if (b0.isPresent()) { throw new IllegalStateException("Found branch migration " + b0.get().getClass() + " but allowBranchMigrations=false"); } } } private void applyBranchMigrations(DataSource ds) { int version = 0; // branch migrations always start from zero while (true) { Optional<Migration> migration = this.migrationClasses.get("b", version); if (migration.isPresent()) { this.apply(ds, migration.get(), Optional.empty()); version++; } else { break; } } } private void apply(DataSource ds, Migration migration, Optional<Integer> version) { Jdbc.inTransaction(ds, connection -> { Migrater.current.set(connection); log.info("Applying {}: {}", migration.getClass().getSimpleName(), migration.toString()); if (this.config.db.isMySQL()) { // Set the updater in case any history triggers are listening Jdbc.update(connection, "set @updater=?", migration.getClass().getSimpleName()); } try { migration.apply(); } catch (SQLException se) { throw new JdbcException(se); } // Tick to the current version number version.ifPresent(v -> this.schemaInfoTable.updateVersionNumber(connection, v)); if (this.config.db.isMySQL()) { Jdbc.update(connection, "set @updater=null"); } return null; }); } }