001package org.avaje.dbmigration.runner; 002 003import org.avaje.dbmigration.MigrationConfig; 004import org.avaje.dbmigration.util.IOUtils; 005import org.avaje.dbmigration.util.JdbcClose; 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009import java.io.IOException; 010import java.net.URL; 011import java.sql.Connection; 012import java.sql.DatabaseMetaData; 013import java.sql.PreparedStatement; 014import java.sql.ResultSet; 015import java.sql.SQLException; 016import java.sql.Timestamp; 017import java.util.Enumeration; 018import java.util.LinkedHashMap; 019import java.util.Map; 020 021/** 022 * Manages the migration table. 023 */ 024public class MigrationTable { 025 026 private static final Logger logger = LoggerFactory.getLogger(MigrationTable.class); 027 028 private final Connection connection; 029 030 private final String catalog; 031 private final String schema; 032 private final String table; 033 private final String envUserName; 034 035 private final Timestamp runOn = new Timestamp(System.currentTimeMillis()); 036 037 private final ScriptTransform scriptTransform; 038 039 private final String insertSql; 040 private final String selectSql; 041 042 private final LinkedHashMap<String, MigrationMetaRow> migrations; 043 044 private MigrationMetaRow lastMigration; 045 046 /** 047 * Construct with server, configuration and jdbc connection (DB admin user). 048 */ 049 public MigrationTable(MigrationConfig config, Connection connection) { 050 051 this.connection = connection; 052 this.migrations = new LinkedHashMap<String, MigrationMetaRow>(); 053 054 this.catalog = null; 055 this.schema = config.getDbSchema(); 056 this.table = config.getMetaTable(); 057 this.selectSql = MigrationMetaRow.selectSql(table); 058 this.insertSql = MigrationMetaRow.insertSql(table); 059 this.scriptTransform = createScriptTransform(config); 060 this.envUserName = System.getProperty("user.name"); 061 } 062 063 /** 064 * Return the number of migrations in the DB migration table. 065 */ 066 public int size() { 067 return migrations.size(); 068 } 069 070 /** 071 * Create the ScriptTransform for placeholder key/value replacement. 072 */ 073 private ScriptTransform createScriptTransform(MigrationConfig config) { 074 075 Map<String, String> map = PlaceholderBuilder.build(config.getRunPlaceholders(), config.getRunPlaceholderMap()); 076 return new ScriptTransform(map); 077 } 078 079 /** 080 * Create the table is it does not exist. 081 */ 082 public void createIfNeeded() throws SQLException, IOException { 083 084 if (!tableExists(connection)) { 085 createTable(connection); 086 } 087 088 PreparedStatement query = connection.prepareStatement(selectSql); 089 try { 090 ResultSet resultSet = query.executeQuery(); 091 try { 092 while (resultSet.next()) { 093 MigrationMetaRow metaRow = new MigrationMetaRow(resultSet); 094 addMigration(metaRow.getVersion(), metaRow); 095 } 096 } finally { 097 JdbcClose.close(resultSet); 098 } 099 } finally { 100 JdbcClose.close(query); 101 } 102 } 103 104 105 private void createTable(Connection connection) throws IOException, SQLException { 106 107 String script = ScriptTransform.table(table, getCreateTableScript()); 108 109 MigrationScriptRunner run = new MigrationScriptRunner(connection); 110 run.runScript(false, script, "create migration table"); 111 } 112 113 /** 114 * Return the create table script. 115 */ 116 private String getCreateTableScript() throws IOException { 117 // supply a script to override the default table create script 118 String script = readResource("migration-support/create-table.sql"); 119 if (script == null) { 120 // no, just use the default script 121 script = readResource("migration-support/default-create-table.sql"); 122 } 123 return script; 124 } 125 126 private String readResource(String location) throws IOException { 127 128 Enumeration<URL> resources = getClassLoader().getResources(location); 129 if (resources.hasMoreElements()) { 130 URL url = resources.nextElement(); 131 return IOUtils.readUtf8(url.openStream()); 132 } 133 return null; 134 } 135 136 private ClassLoader getClassLoader() { 137 return Thread.currentThread().getContextClassLoader(); 138 } 139 140 /** 141 * Return true if the table exists. 142 */ 143 private boolean tableExists(Connection connection) throws SQLException { 144 145 String migTable = table; 146 147 DatabaseMetaData metaData = connection.getMetaData(); 148 if (metaData.storesUpperCaseIdentifiers()) { 149 migTable = migTable.toUpperCase(); 150 } 151 ResultSet tables = metaData.getTables(catalog, schema, migTable, null); 152 try { 153 return tables.next(); 154 } finally { 155 JdbcClose.close(tables); 156 } 157 } 158 159 /** 160 * Return true if the migration ran successfully and false if the migration failed. 161 */ 162 public boolean shouldRun(LocalMigrationResource localVersion, LocalMigrationResource priorVersion) throws SQLException { 163 164 if (priorVersion != null && !localVersion.isRepeatable()) { 165 if (!migrationExists(priorVersion)) { 166 logger.error("Migration {} requires prior migration {} which has not been run", localVersion.getVersion(), priorVersion.getVersion()); 167 return false; 168 } 169 } 170 171 MigrationMetaRow existing = migrations.get(localVersion.key()); 172 return runMigration(localVersion, existing); 173 } 174 175 /** 176 * Run the migration script. 177 * 178 * @param local The local migration resource 179 * @param existing The information for this migration existing in the table 180 * 181 * @return True if the migrations should continue 182 */ 183 private boolean runMigration(LocalMigrationResource local, MigrationMetaRow existing) throws SQLException { 184 185 String script = convertScript(local.getContent()); 186 int checksum = Checksum.calculate(script); 187 188 if (existing != null) { 189 190 boolean matchChecksum = (existing.getChecksum() == checksum); 191 192 if (!local.isRepeatable()) { 193 if (!matchChecksum) { 194 logger.error("Checksum mismatch on migration {}", local.getLocation()); 195 } 196 return true; 197 198 } else if (matchChecksum) { 199 logger.trace("... skip unchanged repeatable migration {}", local.getLocation()); 200 return true; 201 } 202 } 203 204 runMigration(local, script, checksum); 205 return true; 206 } 207 208 /** 209 * Run a migration script as new migration or update on existing repeatable migration. 210 */ 211 private void runMigration(LocalMigrationResource local, String script, int checksum) throws SQLException { 212 213 logger.debug("run migration {}", local.getLocation()); 214 215 long start = System.currentTimeMillis(); 216 MigrationScriptRunner run = new MigrationScriptRunner(connection); 217 run.runScript(false, script, "run migration version: " + local.getVersion()); 218 219 long exeMillis = System.currentTimeMillis() - start; 220 221 MigrationMetaRow metaRow = createMetaRow(local, checksum, exeMillis); 222 PreparedStatement statement = connection.prepareStatement(insertSql); 223 try { 224 metaRow.bindInsert(statement); 225 statement.executeUpdate(); 226 addMigration(local.key(), metaRow); 227 } finally { 228 JdbcClose.close(statement); 229 } 230 } 231 232 /** 233 * Create the MigrationMetaRow for this migration. 234 */ 235 private MigrationMetaRow createMetaRow(LocalMigrationResource migration, int checksum, long exeMillis) { 236 237 int nextId = 1; 238 if (lastMigration != null) { 239 nextId = lastMigration.getId() + 1; 240 } 241 242 String type = migration.getType(); 243 String runVersion = migration.key(); 244 String comment = migration.getComment(); 245 246 return new MigrationMetaRow(nextId, type, runVersion, comment, checksum, envUserName, runOn, exeMillis); 247 } 248 249 /** 250 * Return true if the migration exists. 251 */ 252 private boolean migrationExists(LocalMigrationResource priorVersion) { 253 return migrations.containsKey(priorVersion.key()); 254 } 255 256 /** 257 * Apply the placeholder key/value replacement on the script. 258 */ 259 private String convertScript(String script) { 260 return scriptTransform.transform(script); 261 } 262 263 /** 264 * Register the successfully executed migration (to allow dependant scripts to run). 265 */ 266 private void addMigration(String key, MigrationMetaRow metaRow) { 267 lastMigration = metaRow; 268 if (metaRow.getVersion() == null) { 269 throw new IllegalStateException("No runVersion in db migration table row? " + metaRow); 270 } 271 migrations.put(key, metaRow); 272 } 273}