Skip to content

Commit db6e18d

Browse files
#136: Support for databases without transaction support (#142)
Co-authored-by: Sebastian Bär <[email protected]>
1 parent 53de628 commit db6e18d

File tree

5 files changed

+136
-10
lines changed

5 files changed

+136
-10
lines changed

doc/changes/changes_3.6.0.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# Test Database Builder for Java 3.6.0, released 2024-??-??
1+
# Test Database Builder for Java 3.6.0, released 2024-09-24
22

33
Code name: Fix CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-java:3.25.1`
44

55
## Summary
66

77
This release fixes CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-java:3.25.1`.
88

9-
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.
9+
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.
1010

1111
## Security
1212

@@ -16,6 +16,7 @@ The release also speeds up inserting rows into a table by using batch insert and
1616

1717
* #137: Updated `AbstractImmediateDatabaseObjectWriter#write()` to use batching for inserting rows
1818
* #134: Allowed specifying charset for MySQL tables
19+
* #136: Added support for databases without transaction support
1920

2021
## Dependency Updates
2122

error_code_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ error-tags:
22
TDBJ:
33
packages:
44
- com.exasol.dbbuilder
5-
highest-index: 35
5+
highest-index: 37

src/main/java/com/exasol/dbbuilder/dialects/AbstractImmediateDatabaseObjectWriter.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,14 @@ public void truncate(final Table table) {
8282
protected abstract String getQuotedColumnName(String columnName);
8383

8484
@Override
85+
@SuppressWarnings("try") // autoCommit never referenced in try block by intention
8586
public void write(final Table table, final Stream<List<Object>> rows) {
8687
final String valuePlaceholders = "?" + ", ?".repeat(table.getColumnCount() - 1);
8788
final String sql = "INSERT INTO " + table.getFullyQualifiedName() + " VALUES(" + valuePlaceholders + ")";
88-
try (final PreparedStatement preparedStatement = this.connection.prepareStatement(sql)) {
89-
final boolean autoCommitOriginalState = this.connection.getAutoCommit();
90-
this.connection.setAutoCommit(false);
89+
try (final AutoCommit autoCommit = AutoCommit.tryDeactivate(connection);
90+
final PreparedStatement preparedStatement = this.connection.prepareStatement(sql)) {
9191
rows.forEach(row -> addBatch(table, preparedStatement, row));
9292
preparedStatement.executeBatch();
93-
if (autoCommitOriginalState) {
94-
this.connection.commit();
95-
this.connection.setAutoCommit(true);
96-
}
9793
} catch (final SQLException exception) {
9894
throw new DatabaseObjectException(table,
9995
ExaError.messageBuilder("E-TDBJ-2")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.exasol.dbbuilder.dialects;
2+
3+
import java.sql.*;
4+
import java.util.logging.Logger;
5+
6+
import com.exasol.errorreporting.ExaError;
7+
8+
/**
9+
* This class allows temporarily deactivating AutoCommit for a given {@link Connection} and restores the original state
10+
* in {@link #close()}. If the database does not support deactivating AutoCommit (i.e. throws a
11+
* {@link SQLFeatureNotSupportedException}), this class will silently ignore it.
12+
*/
13+
class AutoCommit implements AutoCloseable {
14+
private static final Logger LOG = Logger.getLogger(AutoCommit.class.getName());
15+
private final Connection connection;
16+
17+
private AutoCommit(final Connection connection) {
18+
this.connection = connection;
19+
}
20+
21+
static AutoCommit tryDeactivate(final Connection connection) {
22+
try {
23+
final boolean originalState = connection.getAutoCommit();
24+
if (!originalState) {
25+
return new AutoCommit(null);
26+
}
27+
if (deactivatingAutoCommitSuccessful(connection)) {
28+
return new AutoCommit(connection);
29+
} else {
30+
return new AutoCommit(null);
31+
}
32+
} catch (final SQLException exception) {
33+
throw new DatabaseObjectException(
34+
ExaError.messageBuilder("E-TDBJ-36").message("Failed to check AutoCommit state").toString(),
35+
exception);
36+
}
37+
}
38+
39+
private static boolean deactivatingAutoCommitSuccessful(final Connection connection) throws SQLException {
40+
try {
41+
connection.setAutoCommit(false);
42+
return true;
43+
} catch (final SQLFeatureNotSupportedException exception) {
44+
LOG.fine("Database does not support deactivating AutoCommit: " + exception.getMessage());
45+
return false;
46+
}
47+
}
48+
49+
@Override
50+
public void close() {
51+
if (connection != null) {
52+
try {
53+
connection.setAutoCommit(true);
54+
} catch (final SQLException exception) {
55+
throw new DatabaseObjectException(
56+
ExaError.messageBuilder("E-TDBJ-37").message("Failed to re-enable AutoCommit").toString(),
57+
exception);
58+
}
59+
}
60+
}
61+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.exasol.dbbuilder.dialects;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.equalTo;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
import static org.mockito.ArgumentMatchers.anyBoolean;
7+
import static org.mockito.Mockito.*;
8+
9+
import java.sql.*;
10+
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.mockito.InOrder;
14+
import org.mockito.Mock;
15+
import org.mockito.junit.jupiter.MockitoExtension;
16+
17+
@ExtendWith(MockitoExtension.class)
18+
class AutoCommitTest {
19+
@Mock
20+
Connection connectionMock;
21+
22+
@Test
23+
void autoCommitAlreadyDeactivated() throws SQLException {
24+
when(connectionMock.getAutoCommit()).thenReturn(false);
25+
AutoCommit.tryDeactivate(connectionMock).close();
26+
verify(connectionMock, never()).setAutoCommit(anyBoolean());
27+
verifyNoMoreInteractions(connectionMock);
28+
}
29+
30+
@Test
31+
void autoCommitEnabledAndSupported() throws SQLException {
32+
when(connectionMock.getAutoCommit()).thenReturn(true);
33+
AutoCommit.tryDeactivate(connectionMock).close();
34+
final InOrder inOrder = inOrder(connectionMock);
35+
inOrder.verify(connectionMock).setAutoCommit(false);
36+
inOrder.verify(connectionMock).setAutoCommit(true);
37+
inOrder.verifyNoMoreInteractions();
38+
}
39+
40+
@Test
41+
void autoCommitEnabledAndNotSupported() throws SQLException {
42+
when(connectionMock.getAutoCommit()).thenReturn(true);
43+
doThrow(new SQLFeatureNotSupportedException("unsupported")).when(connectionMock).setAutoCommit(false);
44+
AutoCommit.tryDeactivate(connectionMock).close();
45+
verify(connectionMock).setAutoCommit(false);
46+
verifyNoMoreInteractions(connectionMock);
47+
}
48+
49+
@Test
50+
void settingAutoCommitFailsWithOtherException() throws SQLException {
51+
when(connectionMock.getAutoCommit()).thenReturn(true);
52+
doThrow(new SQLException("mock")).when(connectionMock).setAutoCommit(false);
53+
final DatabaseObjectException exception = assertThrows(DatabaseObjectException.class,
54+
() -> AutoCommit.tryDeactivate(connectionMock));
55+
assertThat(exception.getMessage(), equalTo("E-TDBJ-36: Failed to check AutoCommit state"));
56+
assertThat(exception.getCause().getMessage(), equalTo("mock"));
57+
}
58+
59+
@Test
60+
void reactivatingAutoCommitFails() throws SQLException {
61+
when(connectionMock.getAutoCommit()).thenReturn(true);
62+
final AutoCommit autoCommit = AutoCommit.tryDeactivate(connectionMock);
63+
doThrow(new SQLException("mock")).when(connectionMock).setAutoCommit(true);
64+
final DatabaseObjectException exception = assertThrows(DatabaseObjectException.class, autoCommit::close);
65+
assertThat(exception.getMessage(), equalTo("E-TDBJ-37: Failed to re-enable AutoCommit"));
66+
assertThat(exception.getCause().getMessage(), equalTo("mock"));
67+
}
68+
}

0 commit comments

Comments
 (0)