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