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 = null; 243 int checksum; 244 if (local instanceof LocalDdlMigrationResource) { 245 script = convertScript(((LocalDdlMigrationResource) local).getContent()); 246 checksum = Checksum.calculate(script); 247 } else { 248 checksum = ((LocalJdbcMigrationResource) local).getChecksum(); 249 } 250 251 if (existing == null && patchInsertMigration(local, checksum)) { 252 return true; 253 } 254 if (existing != null && skipMigration(checksum, local, existing)) { 255 return true; 256 } 257 executeMigration(local, script, checksum, existing); 258 return true; 259 } 260 261 /** 262 * Return true if we 'patch history' inserting a DB migration without running it. 263 */ 264 private boolean patchInsertMigration(LocalMigrationResource local, int checksum) throws SQLException { 265 if (patchInsertVersions != null && patchInsertVersions.contains(local.key())) { 266 logger.info("patch migration - insert into history {}", local.getLocation()); 267 if (!checkState) { 268 insertIntoHistory(local, checksum, 0); 269 } 270 return true; 271 } 272 return false; 273 } 274 275 /** 276 * Return true if the migration should be skipped. 277 */ 278 boolean skipMigration(int checksum, LocalMigrationResource local, MigrationMetaRow existing) throws SQLException { 279 280 boolean matchChecksum = (existing.getChecksum() == checksum); 281 if (matchChecksum) { 282 logger.trace("... skip unchanged migration {}", local.getLocation()); 283 return true; 284 285 } else if (patchResetChecksum(existing, checksum)) { 286 logger.info("patch migration - reset checksum on {}", local.getLocation()); 287 return true; 288 289 } else if (local.isRepeatable() || skipChecksum) { 290 // re-run the migration 291 return false; 292 } else { 293 throw new MigrationException("Checksum mismatch on migration " + local.getLocation()); 294 } 295 } 296 297 /** 298 * Return true if the checksum is reset on the existing migration. 299 */ 300 private boolean patchResetChecksum(MigrationMetaRow existing, int newChecksum) throws SQLException { 301 302 if (isResetOnVersion(existing.getVersion())) { 303 if (!checkState) { 304 existing.resetChecksum(newChecksum, connection, updateChecksumSql); 305 } 306 return true; 307 } else { 308 return false; 309 } 310 } 311 312 private boolean isResetOnVersion(String version) { 313 return patchResetChecksumVersions != null && patchResetChecksumVersions.contains(version); 314 } 315 316 /** 317 * Run a migration script as new migration or update on existing repeatable migration. 318 */ 319 private void executeMigration(LocalMigrationResource local, String script, int checksum, MigrationMetaRow existing) throws SQLException { 320 321 if (checkState) { 322 checkMigrations.add(local); 323 // simulate the migration being run such that following migrations also match 324 addMigration(local.key(), createMetaRow(local, checksum, 1)); 325 return; 326 } 327 328 logger.debug("run migration {}", local.getLocation()); 329 330 long start = System.currentTimeMillis(); 331 if (local instanceof LocalDdlMigrationResource) { 332 MigrationScriptRunner run = new MigrationScriptRunner(connection); 333 run.runScript(false, script, "run migration version: " + local.getVersion()); 334 } else { 335 ((LocalJdbcMigrationResource)local).getMigration().migrate(connection); 336 } 337 long exeMillis = System.currentTimeMillis() - start; 338 339 if (existing != null) { 340 existing.rerun(checksum, exeMillis, envUserName, runOn); 341 existing.executeUpdate(connection, updateSql); 342 343 } else { 344 insertIntoHistory(local, checksum, exeMillis); 345 } 346 } 347 348 private void insertIntoHistory(LocalMigrationResource local, int checksum, long exeMillis) throws SQLException { 349 MigrationMetaRow metaRow = createMetaRow(local, checksum, exeMillis); 350 metaRow.executeInsert(connection, insertSql); 351 addMigration(local.key(), metaRow); 352 } 353 354 /** 355 * Create the MigrationMetaRow for this migration. 356 */ 357 private MigrationMetaRow createMetaRow(LocalMigrationResource migration, int checksum, long exeMillis) { 358 359 int nextId = 1; 360 if (lastMigration != null) { 361 nextId = lastMigration.getId() + 1; 362 } 363 364 String type = migration.getType(); 365 String runVersion = migration.key(); 366 String comment = migration.getComment(); 367 368 return new MigrationMetaRow(nextId, type, runVersion, comment, checksum, envUserName, runOn, exeMillis); 369 } 370 371 /** 372 * Return true if the migration exists. 373 */ 374 private boolean migrationExists(LocalMigrationResource priorVersion) { 375 return migrations.containsKey(priorVersion.key()); 376 } 377 378 /** 379 * Apply the placeholder key/value replacement on the script. 380 */ 381 private String convertScript(String script) { 382 return scriptTransform.transform(script); 383 } 384 385 /** 386 * Register the successfully executed migration (to allow dependant scripts to run). 387 */ 388 private void addMigration(String key, MigrationMetaRow metaRow) { 389 lastMigration = metaRow; 390 if (metaRow.getVersion() == null) { 391 throw new IllegalStateException("No runVersion in db migration table row? " + metaRow); 392 } 393 migrations.put(key, metaRow); 394 } 395}