Skip to content

Commit

Permalink
#136: Support for databases without transaction support (#142)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Bär <[email protected]>
  • Loading branch information
kaklakariada and redcatbear authored Sep 24, 2024
1 parent 53de628 commit db6e18d
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 10 deletions.
5 changes: 3 additions & 2 deletions doc/changes/changes_3.6.0.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# 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`

## Summary

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

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion error_code_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ error-tags:
TDBJ:
packages:
- com.exasol.dbbuilder
highest-index: 35
highest-index: 37
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<Object>> 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")
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/exasol/dbbuilder/dialects/AutoCommit.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
68 changes: 68 additions & 0 deletions src/test/java/com/exasol/dbbuilder/dialects/AutoCommitTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}

0 comments on commit db6e18d

Please sign in to comment.