Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ void tearDown() throws SQLException {
}
}

@Test
@DisplayName("container-backed setup stops the container when datasource creation fails")
void containerBackedSetupStopsContainerWhenDataSourceCreationFails() {
JdbcDatabaseContainer<?> container = org.mockito.Mockito.mock(JdbcDatabaseContainer.class);
RuntimeException setupFailure = new IllegalStateException("datasource creation failed");
org.mockito.Mockito.when(container.isRunning()).thenReturn(true);

try (MockedStatic<SqlAuditTestHelper> helper = org.mockito.Mockito.mockStatic(SqlAuditTestHelper.class)) {
helper.when(() -> SqlAuditTestHelper.createContainer("informix")).thenReturn(container);
helper.when(() -> SqlAuditTestHelper.createDataSource(container)).thenThrow(setupFailure);

RuntimeException thrown = assertThrows(RuntimeException.class,
() -> setupTest(SqlDialect.INFORMIX, "informix"));

assertSame(setupFailure, thrown);
helper.verify(() -> SqlAuditTestHelper.createDataSource(container));
org.mockito.Mockito.verify(container).start();
org.mockito.Mockito.verify(container).stop();
}
}

private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws SQLException {
if ("h2".equals(dialectName)) {
HikariConfig config = new HikariConfig();
Expand Down Expand Up @@ -145,22 +166,24 @@ private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws
JdbcDatabaseContainer<?> container = SqlAuditTestHelper.createContainer(dialectName);
container.start();

HikariConfig config = new HikariConfig();
config.setJdbcUrl(container.getJdbcUrl());
config.setUsername(container.getUsername());
config.setPassword(container.getPassword());
config.setDriverClassName(container.getDriverClassName());
DataSource dataSource = new HikariDataSource(config);
TestContext testContext = new TestContext(dataSource, container, sqlDialect);

DataSource dataSource = null;
TestContext testContext = null;
try {
dataSource = SqlAuditTestHelper.createDataSource(container);
testContext = new TestContext(dataSource, container, sqlDialect);
SqlAuditTestHelper.createTables(dataSource, sqlDialect);
} catch (SQLException exception) {
testContext.cleanup();
return testContext;
} catch (SQLException | RuntimeException exception) {
TestContext cleanupContext = testContext != null
? testContext
: new TestContext(dataSource, container, sqlDialect);
try {
cleanupContext.cleanup();
} catch (SQLException cleanupFailure) {
exception.addSuppressed(cleanupFailure);
}
throw exception;
}

return testContext;
}

private Class<?>[] getChangeClasses(String dialectName, String scenario) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,34 +83,50 @@ public String getId() {
* Applies a change operation with session-scoped dependency injection.
* <p>
* This method is the entry point for non-transactional change execution.
* It calls {@link #enhanceExecutionRuntime(RuntimeContext, boolean)} to allow
* subclasses to inject session-scoped dependencies before executing the change.
* It delegates lifecycle ownership to {@link #nonTxWrapper(Function, ExecutionRuntime)},
* which enhances the runtime before invoking the change.
*
* @param <T> the return type of the change operation
* @param changeApplier the function that executes the actual change
* @param executionRuntime the runtime context for dependency resolution
* @return the result of the change operation
*/
public final <T> T applyChange(Function<ExecutionRuntime, T> changeApplier, ExecutionRuntime executionRuntime) {
enhanceExecutionRuntime(executionRuntime, false);
return changeApplier.apply(executionRuntime);
return nonTxWrapper(changeApplier, executionRuntime);
}

/**
* Rolls back (reverts) a previously applied change with session-scoped dependency injection.
* <p>
* This method is the entry point for non-transactional rollback execution.
* It calls {@link #enhanceExecutionRuntime(RuntimeContext, boolean)} to allow
* subclasses to inject session-scoped dependencies before executing the rollback.
* It delegates lifecycle ownership to {@link #nonTxWrapper(Function, ExecutionRuntime)},
* which enhances the runtime before invoking the rollback.
*
* @param <T> the return type of the rollback operation
* @param changeRollbacker the function that executes the actual rollback
* @param executionRuntime the runtime context for dependency resolution
* @return the result of the rollback operation
*/
public final <T> T rollbackChange(Function<ExecutionRuntime, T> changeRollbacker, ExecutionRuntime executionRuntime) {
return nonTxWrapper(changeRollbacker, executionRuntime);
}


/**
* Executes a non-transactional callback and owns its runtime lifecycle.
* <p>
* The default implementation enhances the runtime once before executing the callback.
* Subclasses that manage resources for non-transactional execution must enhance the runtime
* and release those resources within their override.
*
* @param changeFunc the callback to execute
* @param executionRuntime the runtime to enhance and pass to the callback
* @param <T> the callback return type
* @return the callback result
*/
protected <T> T nonTxWrapper(Function<ExecutionRuntime, T> changeFunc, ExecutionRuntime executionRuntime) {
enhanceExecutionRuntime(executionRuntime, false);
return changeRollbacker.apply(executionRuntime);
return changeFunc.apply(executionRuntime);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
import io.flamingock.internal.core.builder.FlamingockEdition;
import io.flamingock.internal.core.external.targets.TransactionalTargetSystem;
import io.flamingock.internal.core.external.targets.mark.NoOpTargetSystemAuditMarker;
import io.flamingock.internal.core.runtime.ExecutionRuntime;
import io.flamingock.internal.core.transaction.TransactionManager;
import io.flamingock.internal.common.core.transaction.TransactionWrapper;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.function.Function;

import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY;

Expand Down Expand Up @@ -79,17 +81,23 @@ public TransactionWrapper getTxWrapper() {
return txWrapper;
}

/**
* Acquires one connection for a non-transactional callback, injects it into the runtime,
* and closes that same connection after the callback completes or fails.
*
* @param changeFunc the callback to execute
* @param executionRuntime the runtime to receive the connection dependency
* @param <T> the callback return type
* @return the callback result
*/
@Override
protected void enhanceExecutionRuntime(RuntimeContext executionRuntime, boolean isTransactional) {
//if transactional, the connection is injected in the wrapInTransaction
if (!isTransactional) {
try {
executionRuntime.addDependency(dataSource.getConnection());
} catch (SQLException e) {
throw new FlamingockException(e);
}
protected <T> T nonTxWrapper(Function<ExecutionRuntime, T> changeFunc, ExecutionRuntime executionRuntime) {
try (Connection connection = dataSource.getConnection()) {
executionRuntime.addDependency(connection);
return changeFunc.apply(executionRuntime);
} catch (SQLException e) {
throw new FlamingockException(e);
}

}

private SqlTxWrapper createTxWrapper(TransactionManager<Connection> txManager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.flamingock.internal.common.core.context.ContextResolver;
import io.flamingock.internal.core.builder.FlamingockEdition;
import io.flamingock.internal.core.runtime.ExecutionRuntime;
import io.flamingock.internal.core.transaction.TransactionManager;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -33,6 +34,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class SqlTargetSystemSharedTxManagerTest {
Expand All @@ -58,6 +60,61 @@ void txWrapperAndAuditMarkerShouldShareSameTxManager() throws Exception {
"SqlTxWrapper and SqlTargetSystemAuditMarker must share the same TransactionManager instance");
}

@Test
@DisplayName("Should close SQL connection after non-transactional apply")
void shouldCloseConnectionAfterNonTransactionalApply() throws Exception {
Connection connection = mock(Connection.class);
DataSource dataSource = mock(DataSource.class);
when(dataSource.getConnection()).thenReturn(connection);

SqlTargetSystem targetSystem = new SqlTargetSystem("test-sql", dataSource);
targetSystem.applyChange(runtime -> null, mockRuntimeWith(connection));

verify(dataSource).getConnection();
verify(connection).close();
}

@Test
@DisplayName("Should close SQL connection after non-transactional rollback")
void shouldCloseConnectionAfterNonTransactionalRollback() throws Exception {
Connection connection = mock(Connection.class);
DataSource dataSource = mock(DataSource.class);
when(dataSource.getConnection()).thenReturn(connection);

SqlTargetSystem targetSystem = new SqlTargetSystem("test-sql", dataSource);
targetSystem.rollbackChange(runtime -> null, mockRuntimeWith(connection));

verify(dataSource).getConnection();
verify(connection).close();
}

@Test
@DisplayName("Should close SQL connection when non-transactional callback fails")
void shouldCloseConnectionWhenNonTransactionalCallbackFails() throws Exception {
Connection connection = mock(Connection.class);
DataSource dataSource = mock(DataSource.class);
when(dataSource.getConnection()).thenReturn(connection);
RuntimeException callbackFailure = new RuntimeException("callback failed");

SqlTargetSystem targetSystem = new SqlTargetSystem("test-sql", dataSource);
RuntimeException thrown = assertThrows(RuntimeException.class,
() -> targetSystem.applyChange(runtime -> {
throw callbackFailure;
}, mockRuntimeWith(connection)));

assertSame(callbackFailure, thrown);
verify(dataSource).getConnection();
verify(connection).close();
}

private static ExecutionRuntime mockRuntimeWith(Connection connection) {
ExecutionRuntime executionRuntime = mock(ExecutionRuntime.class);
ContextResolver contextResolver = mock(ContextResolver.class);
when(contextResolver.getDependencyValue(Connection.class)).thenReturn(Optional.of(connection));
when(executionRuntime.getContext()).thenReturn(contextResolver);
return executionRuntime;
}

private static DataSource mockDataSource() throws Exception {
ResultSet emptyResultSet = mock(ResultSet.class);
when(emptyResultSet.next()).thenReturn(false);
Expand Down
Loading