001package io.ebean.migration.runner; 002 003import io.ebean.migration.MigrationConfig; 004import io.ebean.migration.MigrationException; 005import io.ebean.migration.util.IOUtils; 006import io.ebean.migration.util.JdbcClose; 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009 010import java.io.IOException; 011import java.net.URL; 012import java.sql.Connection; 013import java.sql.DatabaseMetaData; 014import java.sql.PreparedStatement; 015import java.sql.ResultSet; 016import java.sql.SQLException; 017import java.sql.Timestamp; 018import java.util.ArrayList; 019import java.util.Enumeration; 020import java.util.LinkedHashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024 025/** 026 * Manages the migration table. 027 */ 028public class MigrationTable { 029 030 private static final Logger logger = LoggerFactory.getLogger(MigrationTable.class); 031 032 private final Connection connection; 033 private final boolean checkState; 034 035 private final String catalog; 036 private final String schema; 037 private final String table; 038 private final String sqlTable; 039 private final String envUserName; 040 private final String platformName; 041 042 private final Timestamp runOn = new Timestamp(System.currentTimeMillis()); 043 044 private final ScriptTransform scriptTransform; 045 046 private final String insertSql; 047 private final String updateSql; 048 private final String updateChecksumSql; 049 private final String selectSql; 050 051 private final LinkedHashMap<String, MigrationMetaRow> migrations; 052 private final boolean skipChecksum; 053 054 private final Set<String> patchInsertVersions; 055 private final Set<String> patchResetChecksumVersions; 056 057 private MigrationMetaRow lastMigration; 058 059 private final List<LocalMigrationResource> checkMigrations = new ArrayList<>(); 060 061 /** 062 * Construct with server, configuration and jdbc connection (DB admin user). 063 */ 064 public MigrationTable(MigrationConfig config, Connection connection, boolean checkState) { 065 066 this.connection = connection; 067 this.checkState = checkState; 068 this.migrations = new LinkedHashMap<>(); 069 070 this.catalog = null; 071 this.patchResetChecksumVersions = config.getPatchResetChecksumOn(); 072 this.patchInsertVersions = config.getPatchInsertOn(); 073 this.skipChecksum = config.isSkipChecksum(); 074 this.schema = config.getDbSchema(); 075 this.table = config.getMetaTable(); 076 this.platformName = config.getPlatformName(); 077 this.sqlTable = sqlTable(); 078 this.selectSql = MigrationMetaRow.selectSql(sqlTable, platformName); 079 this.insertSql = MigrationMetaRow.insertSql(sqlTable); 080 this.updateSql = MigrationMetaRow.updateSql(sqlTable); 081 this.updateChecksumSql = MigrationMetaRow.updateChecksumSql(sqlTable); 082 this.scriptTransform = createScriptTransform(config); 083 this.envUserName = System.getProperty("user.name"); 084 } 085 086 /** 087 * Return the migrations that have been run. 088 */ 089 public List<LocalMigrationResource> ran() { 090 return checkMigrations; 091 } 092 093 private String sqlTable() { 094 if (schema != null) { 095 return schema + "." + table; 096 } else { 097 return table; 098 } 099 } 100 101 private String sqlPrimaryKey() { 102 return "pk_" + table; 103 } 104 105 /** 106 * Return the number of migrations in the DB migration table. 107 */ 108 public int size() { 109 return migrations.size(); 110 } 111 112 /** 113 * Create the ScriptTransform for placeholder key/value replacement. 114 */ 115 private ScriptTransform createScriptTransform(MigrationConfig config) { 116 117 Map<String, String> map = PlaceholderBuilder.build(config.getRunPlaceholders(), config.getRunPlaceholderMap()); 118 return new ScriptTransform(map); 119 } 120 121 /** 122 * Create the table is it does not exist. 123 * <p> 124 * Also holds DB lock on migration table and loads existing migrations. 125 * </p> 126 */ 127 public void createIfNeededAndLock() throws SQLException, IOException { 128 129 if (!tableExists(connection)) { 130 createTable(connection); 131 } 132 133 // load existing migrations, hold DB lock on migration table 134 PreparedStatement query = connection.prepareStatement(selectSql); 135 try { 136 ResultSet resultSet = query.executeQuery(); 137 try { 138 while (resultSet.next()) { 139 MigrationMetaRow metaRow = new MigrationMetaRow(resultSet); 140 addMigration(metaRow.getVersion(), metaRow); 141 } 142 } finally { 143 JdbcClose.close(resultSet); 144 } 145 } finally { 146 JdbcClose.close(query); 147 } 148 } 149 150 private void createTable(Connection connection) throws IOException, SQLException { 151 152 String tableScript = createTableDdl(); 153 MigrationScriptRunner run = new MigrationScriptRunner(connection); 154 run.runScript(false, tableScript, "create migration table"); 155 } 156 157 /** 158 * Return the create table script. 159 */ 160 String createTableDdl() throws IOException { 161 String script = ScriptTransform.replace("${table}", sqlTable, getCreateTableScript()); 162 return ScriptTransform.replace("${pk_table}", sqlPrimaryKey(), script); 163 } 164 165 /** 166 * Return the create table script. 167 */ 168 private String getCreateTableScript() throws IOException { 169 // supply a script to override the default table create script 170 String script = readResource("migration-support/create-table.sql"); 171 if (script == null && platformName != null && !platformName.isEmpty()) { 172 // look for platform specific create table 173 script = readResource("migration-support/" + platformName + "-create-table.sql"); 174 } 175 if (script == null) { 176 // no, just use the default script 177 script = readResource("migration-support/default-create-table.sql"); 178 } 179 return script; 180 } 181 182 private String readResource(String location) throws IOException { 183 184 Enumeration<URL> resources = getClassLoader().getResources(location); 185 if (resources.hasMoreElements()) { 186 URL url = resources.nextElement(); 187 return IOUtils.readUtf8(url.openStream()); 188 } 189 return null; 190 } 191 192 private ClassLoader getClassLoader() { 193 return Thread.currentThread().getContextClassLoader(); 194 } 195 196 /** 197 * Return true if the table exists. 198 */ 199 private boolean tableExists(Connection connection) throws SQLException { 200 201 String migTable = table; 202 203 DatabaseMetaData metaData = connection.getMetaData(); 204 if (metaData.storesUpperCaseIdentifiers()) { 205 migTable = migTable.toUpperCase(); 206 } 207 String checkCatalog = (catalog != null) ? catalog : connection.getCatalog(); 208 String checkSchema = (schema != null) ? schema : connection.getSchema(); 209 ResultSet tables = metaData.getTables(checkCatalog, checkSchema, migTable, null); 210 try { 211 return tables.next(); 212 } finally { 213 JdbcClose.close(tables); 214 } 215 } 216 217 /** 218 * Return true if the migration ran successfully and false if the migration failed. 219 */ 220 public boolean shouldRun(LocalMigrationResource localVersion, LocalMigrationResource priorVersion) throws SQLException { 221 222 if (priorVersion != null && !localVersion.isRepeatable()) { 223 if (!migrationExists(priorVersion)) { 224 logger.error("Migration {} requires prior migration {} which has not been run", localVersion.getVersion(), priorVersion.getVersion()); 225 return false; 226 } 227 } 228 229 MigrationMetaRow existing = migrations.get(localVersion.key()); 230 return runMigration(localVersion, existing); 231 } 232 233 /** 234 * Run the migration script. 235 * 236 * @param local The local migration resource 237 * @param existing The information for this migration existing in the table 238 * @return True if the migrations should continue 239 */ 240 private boolean runMigration(LocalMigrationResource local, MigrationMetaRow existing) throws SQLException { 241 242 String script = convertScript(local.getContent()); 243 int checksum = Checksum.calculate(script); 244 245 if (existing == null && patchInsertMigration(local, checksum)) { 246 return true; 247 } 248 if (existing != null && skipMigration(checksum, local, existing)) { 249 return true; 250 } 251 executeMigration(local, script, checksum, existing); 252 return true; 253 } 254 255 /** 256 * Return true if we 'patch history' inserting a DB migration without running it. 257 */ 258 private boolean patchInsertMigration(LocalMigrationResource local, int checksum) throws SQLException { 259 if (patchInsertVersions != null && patchInsertVersions.contains(local.key())) { 260 logger.info("patch migration - insert into history {}", local.getLocation()); 261 if (!checkState) { 262 insertIntoHistory(local, checksum, 0); 263 } 264 return true; 265 } 266 return false; 267 } 268 269 /** 270 * Return true if the migration should be skipped. 271 */ 272 boolean skipMigration(int checksum, LocalMigrationResource local, MigrationMetaRow existing) throws SQLException { 273 274 boolean matchChecksum = (existing.getChecksum() == checksum); 275 if (matchChecksum) { 276 logger.trace("... skip unchanged migration {}", local.getLocation()); 277 return true; 278 279 } else if (patchResetChecksum(existing, checksum)) { 280 logger.info("patch migration - reset checksum on {}", local.getLocation()); 281 return true; 282 283 } else if (local.isRepeatable() || skipChecksum) { 284 // re-run the migration 285 return false; 286 } else { 287 throw new MigrationException("Checksum mismatch on migration " + local.getLocation()); 288 } 289 } 290 291 /** 292 * Return true if the checksum is reset on the existing migration. 293 */ 294 private boolean patchResetChecksum(MigrationMetaRow existing, int newChecksum) throws SQLException { 295 296 if (isResetOnVersion(existing.getVersion())) { 297 if (!checkState) { 298 existing.resetChecksum(newChecksum, connection, updateChecksumSql); 299 } 300 return true; 301 } else { 302 return false; 303 } 304 } 305 306 private boolean isResetOnVersion(String version) { 307 return patchResetChecksumVersions != null && patchResetChecksumVersions.contains(version); 308 } 309 310 /** 311 * Run a migration script as new migration or update on existing repeatable migration. 312 */ 313 private void executeMigration(LocalMigrationResource local, String script, int checksum, MigrationMetaRow existing) throws SQLException { 314 315 if (checkState) { 316 checkMigrations.add(local); 317 // simulate the migration being run such that following migrations also match 318 addMigration(local.key(), createMetaRow(local, checksum, 1)); 319 return; 320 } 321 322 logger.debug("run migration {}", local.getLocation()); 323 324 long start = System.currentTimeMillis(); 325 MigrationScriptRunner run = new MigrationScriptRunner(connection); 326 run.runScript(false, script, "run migration version: " + local.getVersion()); 327 328 long exeMillis = System.currentTimeMillis() - start; 329 330 if (existing != null) { 331 existing.rerun(checksum, exeMillis, envUserName, runOn); 332 existing.executeUpdate(connection, updateSql); 333 334 } else { 335 insertIntoHistory(local, checksum, exeMillis); 336 } 337 } 338 339 private void insertIntoHistory(LocalMigrationResource local, int checksum, long exeMillis) throws SQLException { 340 MigrationMetaRow metaRow = createMetaRow(local, checksum, exeMillis); 341 metaRow.executeInsert(connection, insertSql); 342 addMigration(local.key(), metaRow); 343 } 344 345 /** 346 * Create the MigrationMetaRow for this migration. 347 */ 348 private MigrationMetaRow createMetaRow(LocalMigrationResource migration, int checksum, long exeMillis) { 349 350 int nextId = 1; 351 if (lastMigration != null) { 352 nextId = lastMigration.getId() + 1; 353 } 354 355 String type = migration.getType(); 356 String runVersion = migration.key(); 357 String comment = migration.getComment(); 358 359 return new MigrationMetaRow(nextId, type, runVersion, comment, checksum, envUserName, runOn, exeMillis); 360 } 361 362 /** 363 * Return true if the migration exists. 364 */ 365 private boolean migrationExists(LocalMigrationResource priorVersion) { 366 return migrations.containsKey(priorVersion.key()); 367 } 368 369 /** 370 * Apply the placeholder key/value replacement on the script. 371 */ 372 private String convertScript(String script) { 373 return scriptTransform.transform(script); 374 } 375 376 /** 377 * Register the successfully executed migration (to allow dependant scripts to run). 378 */ 379 private void addMigration(String key, MigrationMetaRow metaRow) { 380 lastMigration = metaRow; 381 if (metaRow.getVersion() == null) { 382 throw new IllegalStateException("No runVersion in db migration table row? " + metaRow); 383 } 384 migrations.put(key, metaRow); 385 } 386}