refactor migration applier, added tests
This commit is contained in:
@@ -12,7 +12,9 @@ import lombok.Getter;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -25,22 +27,19 @@ public abstract class MigrationApplier {
|
||||
protected final SqlPlatform sqlPlatform;
|
||||
|
||||
public void migrate() throws MigrationException {
|
||||
var migrations = findMigrations(options.getMigrationsPackage()); //TODO find all migrations, support multiple db contexts
|
||||
var migrations = new ArrayList<>(findMigrations(options.getMigrationsPackage())); //TODO find all migrations, support multiple db contexts
|
||||
try {
|
||||
if (migrationsTableExists()) {
|
||||
ensureMigrationsTableExists();
|
||||
getAppliedMigrations().forEach(a -> migrations.removeIf(m -> m.getClass().getSimpleName().equals(a)));
|
||||
} else {
|
||||
createMigrationsTable();
|
||||
}
|
||||
for (Migration migration : migrations) {
|
||||
applyMigration(migration);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new MigrationException(e);
|
||||
throw new MigrationException("Error while applying migrations: " + e.getLocalizedMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void applyMigration(Migration m) throws MigrationException {
|
||||
protected void applyMigration(Migration m) throws MigrationException {//TODO verify transactions work
|
||||
var mb = new MigrationBuilder();
|
||||
m.up(mb);
|
||||
var operations = mb.getOperations().stream().map(MigrationOperation.Builder::build).toList();
|
||||
@@ -49,17 +48,7 @@ public abstract class MigrationApplier {
|
||||
connection.setAutoCommit(false);
|
||||
try {
|
||||
for (MigrationOperation op : operations) {
|
||||
var stmt = sqlPlatform.getTranslator().translate(connection, op);
|
||||
try {
|
||||
stmt.execute();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLException("Failed to execute query: " + stmt, e);
|
||||
} finally {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
}
|
||||
applyMigrationOperation(op);
|
||||
}
|
||||
insertMigrationLog(m);
|
||||
connection.commit();
|
||||
@@ -74,7 +63,26 @@ public abstract class MigrationApplier {
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Migration> findMigrations(String packageName) throws MigrationException {
|
||||
protected void applyMigrationOperation(MigrationOperation operation) throws SQLException {
|
||||
PreparedStatement stmt;
|
||||
try {
|
||||
stmt = sqlPlatform.getTranslator().translate(connection, operation);//TODO may return an object containing the prep stmt and a string rep of the query
|
||||
} catch (SQLException e) {
|
||||
throw new SQLException("Failed to build query: " + e.getLocalizedMessage(), e);
|
||||
}
|
||||
try {
|
||||
stmt.execute();
|
||||
} catch (SQLException e) {
|
||||
throw new SQLException("Failed to execute query: " + stmt, e);
|
||||
} finally {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Migration> findMigrations(String packageName) throws MigrationException {//TODO rewrite this function after adding annotations to migrations (@Context, @Name, ...), also get rid of the packageName restristrict and just read the classfiles with asm
|
||||
try (var is = getClass().getClassLoader().getResourceAsStream(packageName.replace(".", "/"));
|
||||
var reader = new BufferedReader(new InputStreamReader(is))) {
|
||||
return reader.lines()
|
||||
@@ -101,11 +109,21 @@ public abstract class MigrationApplier {
|
||||
}
|
||||
}
|
||||
|
||||
protected void ensureMigrationsTableExists() throws MigrationException {
|
||||
try {
|
||||
if (!migrationsTableExists()) {
|
||||
createMigrationsTable();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new MigrationException("Failed to create migrations log table", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract boolean migrationsTableExists() throws SQLException;
|
||||
|
||||
protected abstract void createMigrationsTable() throws SQLException;
|
||||
protected abstract void createMigrationsTable() throws SQLException;//TODO use a hardcoded migration
|
||||
|
||||
protected abstract List<String> getAppliedMigrations() throws SQLException;
|
||||
protected abstract List<String> getAppliedMigrations() throws SQLException;//TODO dbset to load later
|
||||
|
||||
protected abstract void insertMigrationLog(Migration m) throws SQLException;
|
||||
protected abstract void insertMigrationLog(Migration m) throws SQLException;//TODO dbset to insert later
|
||||
}
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
package jef.platform.base.migration;
|
||||
|
||||
import jef.MigrationException;
|
||||
import jef.model.migration.Migration;
|
||||
import jef.model.migration.MigrationBuilder;
|
||||
import jef.model.migration.operation.MigrationOperation;
|
||||
import jef.platform.SqlPlatform;
|
||||
import jef.platform.base.DatabaseOptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
class MigrationApplierTest {
|
||||
private String packageName;
|
||||
private DatabaseOptions options;
|
||||
private Connection connection;
|
||||
private SqlPlatform platform;
|
||||
private MigrationApplier applier;
|
||||
private Object[] allMocks;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
//setup
|
||||
packageName = "package";
|
||||
options = mock(DatabaseOptions.class);
|
||||
doReturn(packageName).when(options).getMigrationsPackage();
|
||||
|
||||
connection = mock(Connection.class);
|
||||
platform = mock(SqlPlatform.class);
|
||||
applier = mock(MigrationApplier.class, withSettings().useConstructor(connection, options, platform).defaultAnswer(CALLS_REAL_METHODS)); //spy
|
||||
|
||||
allMocks = List.of(options, connection, platform, applier).toArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrate_success() throws MigrationException, SQLException {
|
||||
interface Migration1 extends Migration {
|
||||
}
|
||||
interface Migration2 extends Migration {
|
||||
}
|
||||
var migration1 = mock(Migration1.class);
|
||||
var migration2 = mock(Migration2.class);
|
||||
doReturn(List.of(migration1, migration2)).when(applier).findMigrations(packageName);
|
||||
|
||||
doNothing().when(applier).ensureMigrationsTableExists();
|
||||
doReturn(List.of(migration1.getClass().getSimpleName())).when(applier).getAppliedMigrations();
|
||||
doNothing().when(applier).applyMigration(any(Migration.class));
|
||||
|
||||
//test
|
||||
applier.migrate();
|
||||
|
||||
//assert
|
||||
verify(applier, times(1)).migrate();
|
||||
verify(options, times(1)).getMigrationsPackage();
|
||||
verify(applier, times(1)).findMigrations(packageName);
|
||||
verify(applier, times(1)).ensureMigrationsTableExists();
|
||||
verify(applier, times(1)).getAppliedMigrations();
|
||||
verify(applier, times(1)).applyMigration(migration2);
|
||||
|
||||
verifyNoMoreInteractions(migration1, migration2);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrate_findMigrationsThrowsMigrationException() throws MigrationException {
|
||||
//setup
|
||||
var exception = new MigrationException("reason");
|
||||
doThrow(exception).when(applier).findMigrations(packageName);
|
||||
|
||||
//test
|
||||
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
|
||||
|
||||
//assert
|
||||
assertSame(exception, ex);
|
||||
|
||||
verify(applier, times(1)).migrate();
|
||||
verify(options, times(1)).getMigrationsPackage();
|
||||
verify(applier, times(1)).findMigrations(packageName);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrate_ensureMigrationsTableExistsThrowsMigrationException() throws MigrationException {
|
||||
//setup
|
||||
doReturn(List.of()).when(applier).findMigrations(packageName);
|
||||
var exception = new MigrationException("reason");
|
||||
doThrow(exception).when(applier).ensureMigrationsTableExists();
|
||||
|
||||
//test
|
||||
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
|
||||
|
||||
//assert
|
||||
assertSame(exception, ex);
|
||||
|
||||
verify(applier, times(1)).migrate();
|
||||
verify(options, times(1)).getMigrationsPackage();
|
||||
verify(applier, times(1)).findMigrations(packageName);
|
||||
verify(applier, times(1)).ensureMigrationsTableExists();
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrate_getAppliedMigrationsThrowsSqlException() throws MigrationException, SQLException {
|
||||
//setup
|
||||
doReturn(List.of()).when(applier).findMigrations(packageName);
|
||||
doNothing().when(applier).ensureMigrationsTableExists();
|
||||
var exception = new SQLException("reason");
|
||||
doThrow(exception).when(applier).getAppliedMigrations();
|
||||
|
||||
//test
|
||||
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
|
||||
|
||||
//assert
|
||||
assertEquals("Error while applying migrations: reason", ex.getMessage());
|
||||
assertSame(exception, ex.getCause());
|
||||
|
||||
verify(applier, times(1)).migrate();
|
||||
verify(options, times(1)).getMigrationsPackage();
|
||||
verify(applier, times(1)).findMigrations(packageName);
|
||||
verify(applier, times(1)).ensureMigrationsTableExists();
|
||||
verify(applier, times(1)).getAppliedMigrations();
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrate_applyMigrationThrowsMigrationException() throws MigrationException, SQLException {
|
||||
//setup
|
||||
var migration = mock(Migration.class);
|
||||
doReturn(List.of(migration)).when(applier).findMigrations(packageName);
|
||||
|
||||
doNothing().when(applier).ensureMigrationsTableExists();
|
||||
doReturn(List.of()).when(applier).getAppliedMigrations();
|
||||
|
||||
var exception = new MigrationException("reason");
|
||||
doThrow(exception).when(applier).applyMigration(migration);
|
||||
|
||||
//test
|
||||
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
|
||||
|
||||
//assert
|
||||
assertSame(exception, ex);
|
||||
|
||||
verify(applier, times(1)).migrate();
|
||||
verify(options, times(1)).getMigrationsPackage();
|
||||
verify(applier, times(1)).findMigrations(packageName);
|
||||
verify(applier, times(1)).ensureMigrationsTableExists();
|
||||
verify(applier, times(1)).getAppliedMigrations();
|
||||
verify(applier, times(1)).applyMigration(migration);
|
||||
verifyNoMoreInteractions(migration);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMigration_success() throws SQLException, MigrationException {
|
||||
//setup
|
||||
var migration = mock(Migration.class);
|
||||
var operationBuilder = new MigrationOperation.Builder[]{null};
|
||||
var operation = new MigrationOperation[]{null};
|
||||
doAnswer(invok -> {
|
||||
operationBuilder[0] = ((MigrationBuilder) invok.getArguments()[0]).addTable("test", List.of());
|
||||
operation[0] = operationBuilder[0].build();
|
||||
return null;
|
||||
}).when(migration).up(any(MigrationBuilder.class));
|
||||
doNothing().when(applier).applyMigrationOperation(argThat(e -> e.equals(operation[0])));//eq(operation[0]) does not work as var would have to be set already and isn't
|
||||
|
||||
//test
|
||||
applier.applyMigration(migration);
|
||||
|
||||
//assert
|
||||
verify(applier, times(1)).applyMigration(migration);
|
||||
verify(migration, times(1)).up(any(MigrationBuilder.class));
|
||||
verify(connection, times(1)).setAutoCommit(false);
|
||||
verify(applier, times(1)).applyMigrationOperation(operation[0]);
|
||||
verify(applier, times(1)).insertMigrationLog(migration);
|
||||
verify(connection, times(1)).commit();
|
||||
verify(connection, times(1)).setAutoCommit(true);
|
||||
verifyNoMoreInteractions(migration);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMigration_applyMigrationOperationThrowsSqlException() throws SQLException, MigrationException {
|
||||
//setup
|
||||
var migration = mock(Migration.class);
|
||||
var operationBuilder = new MigrationOperation.Builder[]{null};
|
||||
var operation = new MigrationOperation[]{null};
|
||||
doAnswer(invok -> {
|
||||
operationBuilder[0] = ((MigrationBuilder) invok.getArguments()[0]).addTable("test", List.of());
|
||||
operation[0] = operationBuilder[0].build();
|
||||
return null;
|
||||
}).when(migration).up(any(MigrationBuilder.class));
|
||||
var sqlException = new SQLException("reason");
|
||||
doThrow(sqlException).when(applier).applyMigrationOperation(argThat(e -> e.equals(operation[0])));//eq(operation[0]) does not work as var would have to be set already and isn't
|
||||
|
||||
//test
|
||||
var thrown = assertThrows(MigrationException.class, () -> applier.applyMigration(migration));
|
||||
|
||||
//assert
|
||||
assertTrue(thrown.getMessage().matches("Failed to apply migration '(.*?)' to the database: reason"));
|
||||
assertSame(sqlException, thrown.getCause());
|
||||
|
||||
verify(applier, times(1)).applyMigration(migration);
|
||||
verify(migration, times(1)).up(any(MigrationBuilder.class));
|
||||
verify(connection, times(1)).setAutoCommit(false);
|
||||
verify(applier, times(1)).applyMigrationOperation(operation[0]);
|
||||
verify(connection, times(1)).rollback();
|
||||
verify(connection, times(1)).setAutoCommit(true);
|
||||
verifyNoMoreInteractions(migration);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMigration_insertMigrationLogThrowsSqlException() throws SQLException, MigrationException {
|
||||
//setup
|
||||
var migration = mock(Migration.class);
|
||||
var operationBuilder = new MigrationOperation.Builder[]{null};
|
||||
var operation = new MigrationOperation[]{null};
|
||||
doAnswer(invok -> {
|
||||
operationBuilder[0] = ((MigrationBuilder) invok.getArguments()[0]).addTable("test", List.of());
|
||||
operation[0] = operationBuilder[0].build();
|
||||
return null;
|
||||
}).when(migration).up(any(MigrationBuilder.class));
|
||||
var sqlException = new SQLException("reason");
|
||||
doNothing().when(applier).applyMigrationOperation(argThat(e -> e.equals(operation[0])));//eq(operation[0]) does not work as var would have to be set already and isn't
|
||||
doThrow(sqlException).when(applier).insertMigrationLog(migration);
|
||||
|
||||
//test
|
||||
var thrown = assertThrows(MigrationException.class, () -> applier.applyMigration(migration));
|
||||
|
||||
//assert
|
||||
assertTrue(thrown.getMessage().matches("Failed to apply migration '(.*?)' to the database: reason"));
|
||||
assertSame(sqlException, thrown.getCause());
|
||||
|
||||
verify(applier, times(1)).applyMigration(migration);
|
||||
verify(migration, times(1)).up(any(MigrationBuilder.class));
|
||||
verify(connection, times(1)).setAutoCommit(false);
|
||||
verify(applier, times(1)).applyMigrationOperation(operation[0]);
|
||||
verify(applier, times(1)).insertMigrationLog(migration);
|
||||
verify(connection, times(1)).rollback();
|
||||
verify(connection, times(1)).setAutoCommit(true);
|
||||
verifyNoMoreInteractions(migration);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMigrationOperation_success() throws SQLException {
|
||||
//setup
|
||||
var translator = mock(MigrationOperationTranslator.class);
|
||||
doReturn(translator).when(platform).getTranslator();
|
||||
var operation = mock(MigrationOperation.class);
|
||||
var stmt = mock(PreparedStatement.class);
|
||||
doReturn(stmt).when(translator).translate(connection, operation);
|
||||
doReturn("query").when(stmt).toString();
|
||||
|
||||
//test
|
||||
applier.applyMigrationOperation(operation);
|
||||
|
||||
//assert
|
||||
verify(applier, times(1)).applyMigrationOperation(operation);
|
||||
verify(platform, times(1)).getTranslator();
|
||||
verify(translator, times(1)).translate(connection, operation);
|
||||
verify(stmt, times(1)).execute();
|
||||
verify(stmt, times(1)).close();
|
||||
verifyNoMoreInteractions(translator, operation, stmt);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMigrationOperation_translateThrowsSQLException() throws SQLException {
|
||||
//setup
|
||||
var translator = mock(MigrationOperationTranslator.class);
|
||||
doReturn(translator).when(platform).getTranslator();
|
||||
var operation = mock(MigrationOperation.class);
|
||||
var stmt = mock(PreparedStatement.class);
|
||||
var sqlException = new SQLException("reason");
|
||||
doThrow(sqlException).when(translator).translate(connection, operation);
|
||||
|
||||
//test
|
||||
var thrown = assertThrows(SQLException.class, () -> applier.applyMigrationOperation(operation));
|
||||
|
||||
//assert
|
||||
assertEquals("Failed to build query: reason", thrown.getMessage());
|
||||
assertSame(sqlException, thrown.getCause());
|
||||
|
||||
verify(applier, times(1)).applyMigrationOperation(operation);
|
||||
verify(platform, times(1)).getTranslator();
|
||||
verify(translator, times(1)).translate(connection, operation);
|
||||
verifyNoMoreInteractions(translator, operation, stmt);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMigrationOperation_stmtExecuteThrowsSQLException() throws SQLException {
|
||||
//setup
|
||||
var translator = mock(MigrationOperationTranslator.class);
|
||||
doReturn(translator).when(platform).getTranslator();
|
||||
var operation = mock(MigrationOperation.class);
|
||||
var stmt = mock(PreparedStatement.class);
|
||||
doReturn(stmt).when(translator).translate(connection, operation);
|
||||
doReturn("query").when(stmt).toString();
|
||||
var sqlException = new SQLException("reason");
|
||||
doThrow(sqlException).when(stmt).execute();
|
||||
|
||||
//test
|
||||
var thrown = assertThrows(SQLException.class, () -> applier.applyMigrationOperation(operation));
|
||||
|
||||
//assert
|
||||
assertEquals("Failed to execute query: query", thrown.getMessage());
|
||||
assertSame(sqlException, thrown.getCause());
|
||||
|
||||
verify(applier, times(1)).applyMigrationOperation(operation);
|
||||
verify(platform, times(1)).getTranslator();
|
||||
verify(translator, times(1)).translate(connection, operation);
|
||||
verify(stmt, times(1)).execute();
|
||||
verify(stmt, times(1)).close();
|
||||
verifyNoMoreInteractions(translator, operation, stmt);
|
||||
verifyNoMoreInteractions(allMocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findMigrations() {
|
||||
//setup
|
||||
|
||||
|
||||
//test
|
||||
|
||||
|
||||
//assert
|
||||
}//TODO after function rewrite
|
||||
}
|
||||
Reference in New Issue
Block a user