Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#136: Support for databases without transaction support #142

Merged
merged 17 commits into from
Sep 24, 2024
Merged
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
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"));
}
}