diff --git a/doc/changes/changes_3.6.0.md b/doc/changes/changes_3.6.0.md index 11e73f9..e3c94fc 100644 --- a/doc/changes/changes_3.6.0.md +++ b/doc/changes/changes_3.6.0.md @@ -1,4 +1,4 @@ -# Test Database Builder for Java 3.6.0, released 2024-??-?? +# Test Database Builder for Java 3.6.0, released 2024-09-24 Code name: Fix CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-java:3.25.1` @@ -6,7 +6,7 @@ Code name: Fix CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-ja This release fixes CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-java:3.25.1`. -The release also speeds up inserting rows into a table by using batch insert and allows specifying a charset when creating MySQL tables, see the [user guide](../user_guide/user_guide.md#mysql-specific-database-objects) for details. +The release also speeds up inserting rows into a table by using batch insert, allows specifying a charset when creating MySQL tables, see the [user guide](../user_guide/user_guide.md#mysql-specific-database-objects) for details and supports databases that don't support transactions. TDBJ will then insert rows without a transaction. ## Security @@ -16,6 +16,7 @@ The release also speeds up inserting rows into a table by using batch insert and * #137: Updated `AbstractImmediateDatabaseObjectWriter#write()` to use batching for inserting rows * #134: Allowed specifying charset for MySQL tables +* #136: Added support for databases without transaction support ## Dependency Updates diff --git a/error_code_config.yml b/error_code_config.yml index 799dcf0..347a000 100644 --- a/error_code_config.yml +++ b/error_code_config.yml @@ -2,4 +2,4 @@ error-tags: TDBJ: packages: - com.exasol.dbbuilder - highest-index: 35 + highest-index: 37 diff --git a/src/main/java/com/exasol/dbbuilder/dialects/AbstractImmediateDatabaseObjectWriter.java b/src/main/java/com/exasol/dbbuilder/dialects/AbstractImmediateDatabaseObjectWriter.java index 2b9c69d..a9b34a7 100644 --- a/src/main/java/com/exasol/dbbuilder/dialects/AbstractImmediateDatabaseObjectWriter.java +++ b/src/main/java/com/exasol/dbbuilder/dialects/AbstractImmediateDatabaseObjectWriter.java @@ -82,18 +82,14 @@ public void truncate(final Table table) { protected abstract String getQuotedColumnName(String columnName); @Override + @SuppressWarnings("try") // autoCommit never referenced in try block by intention public void write(final Table table, final Stream> rows) { final String valuePlaceholders = "?" + ", ?".repeat(table.getColumnCount() - 1); final String sql = "INSERT INTO " + table.getFullyQualifiedName() + " VALUES(" + valuePlaceholders + ")"; - try (final PreparedStatement preparedStatement = this.connection.prepareStatement(sql)) { - final boolean autoCommitOriginalState = this.connection.getAutoCommit(); - this.connection.setAutoCommit(false); + try (final AutoCommit autoCommit = AutoCommit.tryDeactivate(connection); + final PreparedStatement preparedStatement = this.connection.prepareStatement(sql)) { rows.forEach(row -> addBatch(table, preparedStatement, row)); preparedStatement.executeBatch(); - if (autoCommitOriginalState) { - this.connection.commit(); - this.connection.setAutoCommit(true); - } } catch (final SQLException exception) { throw new DatabaseObjectException(table, ExaError.messageBuilder("E-TDBJ-2") diff --git a/src/main/java/com/exasol/dbbuilder/dialects/AutoCommit.java b/src/main/java/com/exasol/dbbuilder/dialects/AutoCommit.java new file mode 100644 index 0000000..ca0f71d --- /dev/null +++ b/src/main/java/com/exasol/dbbuilder/dialects/AutoCommit.java @@ -0,0 +1,61 @@ +package com.exasol.dbbuilder.dialects; + +import java.sql.*; +import java.util.logging.Logger; + +import com.exasol.errorreporting.ExaError; + +/** + * This class allows temporarily deactivating AutoCommit for a given {@link Connection} and restores the original state + * in {@link #close()}. If the database does not support deactivating AutoCommit (i.e. throws a + * {@link SQLFeatureNotSupportedException}), this class will silently ignore it. + */ +class AutoCommit implements AutoCloseable { + private static final Logger LOG = Logger.getLogger(AutoCommit.class.getName()); + private final Connection connection; + + private AutoCommit(final Connection connection) { + this.connection = connection; + } + + static AutoCommit tryDeactivate(final Connection connection) { + try { + final boolean originalState = connection.getAutoCommit(); + if (!originalState) { + return new AutoCommit(null); + } + if (deactivatingAutoCommitSuccessful(connection)) { + return new AutoCommit(connection); + } else { + return new AutoCommit(null); + } + } catch (final SQLException exception) { + throw new DatabaseObjectException( + ExaError.messageBuilder("E-TDBJ-36").message("Failed to check AutoCommit state").toString(), + exception); + } + } + + private static boolean deactivatingAutoCommitSuccessful(final Connection connection) throws SQLException { + try { + connection.setAutoCommit(false); + return true; + } catch (final SQLFeatureNotSupportedException exception) { + LOG.fine("Database does not support deactivating AutoCommit: " + exception.getMessage()); + return false; + } + } + + @Override + public void close() { + if (connection != null) { + try { + connection.setAutoCommit(true); + } catch (final SQLException exception) { + throw new DatabaseObjectException( + ExaError.messageBuilder("E-TDBJ-37").message("Failed to re-enable AutoCommit").toString(), + exception); + } + } + } +} diff --git a/src/test/java/com/exasol/dbbuilder/dialects/AutoCommitTest.java b/src/test/java/com/exasol/dbbuilder/dialects/AutoCommitTest.java new file mode 100644 index 0000000..279d3b1 --- /dev/null +++ b/src/test/java/com/exasol/dbbuilder/dialects/AutoCommitTest.java @@ -0,0 +1,68 @@ +package com.exasol.dbbuilder.dialects; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.*; + +import java.sql.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AutoCommitTest { + @Mock + Connection connectionMock; + + @Test + void autoCommitAlreadyDeactivated() throws SQLException { + when(connectionMock.getAutoCommit()).thenReturn(false); + AutoCommit.tryDeactivate(connectionMock).close(); + verify(connectionMock, never()).setAutoCommit(anyBoolean()); + verifyNoMoreInteractions(connectionMock); + } + + @Test + void autoCommitEnabledAndSupported() throws SQLException { + when(connectionMock.getAutoCommit()).thenReturn(true); + AutoCommit.tryDeactivate(connectionMock).close(); + final InOrder inOrder = inOrder(connectionMock); + inOrder.verify(connectionMock).setAutoCommit(false); + inOrder.verify(connectionMock).setAutoCommit(true); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void autoCommitEnabledAndNotSupported() throws SQLException { + when(connectionMock.getAutoCommit()).thenReturn(true); + doThrow(new SQLFeatureNotSupportedException("unsupported")).when(connectionMock).setAutoCommit(false); + AutoCommit.tryDeactivate(connectionMock).close(); + verify(connectionMock).setAutoCommit(false); + verifyNoMoreInteractions(connectionMock); + } + + @Test + void settingAutoCommitFailsWithOtherException() throws SQLException { + when(connectionMock.getAutoCommit()).thenReturn(true); + doThrow(new SQLException("mock")).when(connectionMock).setAutoCommit(false); + final DatabaseObjectException exception = assertThrows(DatabaseObjectException.class, + () -> AutoCommit.tryDeactivate(connectionMock)); + assertThat(exception.getMessage(), equalTo("E-TDBJ-36: Failed to check AutoCommit state")); + assertThat(exception.getCause().getMessage(), equalTo("mock")); + } + + @Test + void reactivatingAutoCommitFails() throws SQLException { + when(connectionMock.getAutoCommit()).thenReturn(true); + final AutoCommit autoCommit = AutoCommit.tryDeactivate(connectionMock); + doThrow(new SQLException("mock")).when(connectionMock).setAutoCommit(true); + final DatabaseObjectException exception = assertThrows(DatabaseObjectException.class, autoCommit::close); + assertThat(exception.getMessage(), equalTo("E-TDBJ-37: Failed to re-enable AutoCommit")); + assertThat(exception.getCause().getMessage(), equalTo("mock")); + } +}