diff --git a/core/src/main/java/jef/platform/base/migration/MigrationApplier.java b/core/src/main/java/jef/platform/base/migration/MigrationApplier.java index da4f7a8..fdbb974 100644 --- a/core/src/main/java/jef/platform/base/migration/MigrationApplier.java +++ b/core/src/main/java/jef/platform/base/migration/MigrationApplier.java @@ -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()) { - getAppliedMigrations().forEach(a -> migrations.removeIf(m -> m.getClass().getSimpleName().equals(a))); - } else { - createMigrationsTable(); - } + ensureMigrationsTableExists(); + getAppliedMigrations().forEach(a -> migrations.removeIf(m -> m.getClass().getSimpleName().equals(a))); 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 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 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 getAppliedMigrations() throws SQLException; + protected abstract List 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 } diff --git a/core/src/test/java/jef/platform/base/migration/MigrationApplierTest.java b/core/src/test/java/jef/platform/base/migration/MigrationApplierTest.java new file mode 100644 index 0000000..37f2c90 --- /dev/null +++ b/core/src/test/java/jef/platform/base/migration/MigrationApplierTest.java @@ -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 +} \ No newline at end of file