/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.aurora.scheduler.storage.db;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import javax.inject.Inject;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import org.apache.aurora.scheduler.storage.db.views.MigrationChangelogEntry;
import org.apache.ibatis.migration.Change;
import org.apache.ibatis.migration.DataSourceConnectionProvider;
import org.apache.ibatis.migration.MigrationLoader;
import org.apache.ibatis.migration.operations.UpOperation;
import org.apache.ibatis.migration.options.DatabaseOperationOption;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.util.Objects.requireNonNull;
public class MigrationManagerImpl implements MigrationManager {
private static final Logger LOG = LoggerFactory.getLogger(MigrationManagerImpl.class);
private final SqlSessionFactory sqlSessionFactory;
private final MigrationLoader migrationLoader;
@Inject
MigrationManagerImpl(SqlSessionFactory sqlSessionFactory, MigrationLoader migrationLoader) {
this.sqlSessionFactory = requireNonNull(sqlSessionFactory);
this.migrationLoader = requireNonNull(migrationLoader);
}
@Override
public void migrate() throws SQLException {
LOG.info("Running db migrations.");
try (SqlSession sqlSession = sqlSessionFactory.openSession(true /* auto commit */)) {
MigrationMapper mapper = sqlSession.getMapper(MigrationMapper.class);
LOG.info("Bootstrapping changelog table (if necessary).");
mapper.bootstrapChangelog();
if (!checkRollback(mapper, migrationLoader.getMigrations())) {
DatabaseOperationOption options = new DatabaseOperationOption();
options.setAutoCommit(true);
new UpOperation().operate(
new DataSourceConnectionProvider(
sqlSessionFactory.getConfiguration().getEnvironment().getDataSource()),
migrationLoader,
options,
null);
saveDowngradeScript(mapper);
}
}
}
/**
* Iterates applied changes to ensure they all exist on the classpath. For any changes that do not
* exist on the classpath, their downgrade script is run.
*
* @param mapper A {@link MigrationMapper} instance used to modify the changelog.
* @param changes The list of {@link Change}s found on the classpath.
* @return true if a rollback was detected, false otherwise.
* @throws SQLException in the event a SQL failure.
*/
private boolean checkRollback(MigrationMapper mapper, List<Change> changes) throws SQLException {
boolean rollback = false;
List<MigrationChangelogEntry> appliedChanges = mapper.selectAll();
for (MigrationChangelogEntry change : appliedChanges) {
// We cannot directly call changes.contains(...) since contains relies on Change#equals
// which includes class in its equality check rather than checking instanceof. Instead we just
// find the first element in changes whose id matches our applied change. If it does not exist
// then this must be a rollback.
if (changes.stream().anyMatch(c -> c.getId().equals(change.getId()))) {
LOG.info("Change " + change.getId() + " has been applied, no other downgrades are "
+ "necessary");
break;
}
LOG.info("No migration corresponding to change id " + change.getId() + " found. Assuming "
+ "this is a rollback.");
LOG.info("Downgrade SQL for " + change.getId() + " is: " + change.getDowngradeScript());
try (SqlSession session = sqlSessionFactory.openSession(true)) {
try (Connection c = session.getConnection()) {
try (PreparedStatement downgrade = c.prepareStatement(change.getDowngradeScript())) {
downgrade.execute();
rollback = true;
}
}
}
LOG.info("Deleting applied change: " + change.getId());
mapper.delete(change.getId());
}
return rollback;
}
private void saveDowngradeScript(MigrationMapper mapper) {
for (Change c : migrationLoader.getMigrations()) {
try {
String downgradeScript = CharStreams.toString(migrationLoader.getScriptReader(c, true));
LOG.info("Saving downgrade script for change id " + c.getId() + ": " + downgradeScript);
mapper.saveDowngradeScript(c.getId(), downgradeScript.getBytes(Charsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}