From f59d944e2a5f1655d5142c7d87323a52a32cb9cd Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 13:54:39 +0100 Subject: [PATCH 01/11] Refactoring: extract batch handling --- .../jdbc/SimplePreparedStatement.java | 9 +-- .../java/org/itsallcode/jdbc/batch/Batch.java | 66 +++++++++++++++++++ .../itsallcode/jdbc/batch/BatchInsert.java | 38 ++--------- .../jdbc/SimplePreparedStatementTest.java | 4 +- 4 files changed, 78 insertions(+), 39 deletions(-) create mode 100644 src/main/java/org/itsallcode/jdbc/batch/Batch.java diff --git a/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java b/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java index b0ad295..e2fbd86 100644 --- a/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java @@ -1,6 +1,7 @@ package org.itsallcode.jdbc; import java.sql.*; +import java.util.Objects; import org.itsallcode.jdbc.dialect.DbDialect; import org.itsallcode.jdbc.resultset.*; @@ -16,10 +17,10 @@ public class SimplePreparedStatement implements AutoCloseable { SimplePreparedStatement(final Context context, final DbDialect dialect, final PreparedStatement statement, final String sql) { - this.context = context; - this.dialect = dialect; - this.statement = statement; - this.sql = sql; + this.context = Objects.requireNonNull(context, "context"); + this.dialect = Objects.requireNonNull(dialect, "dialect"); + this.statement = Objects.requireNonNull(statement, "statement"); + this.sql = Objects.requireNonNull(sql, "sql"); } SimpleResultSet executeQuery(final ContextRowMapper rowMapper) { diff --git a/src/main/java/org/itsallcode/jdbc/batch/Batch.java b/src/main/java/org/itsallcode/jdbc/batch/Batch.java new file mode 100644 index 0000000..3244269 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/batch/Batch.java @@ -0,0 +1,66 @@ +package org.itsallcode.jdbc.batch; + +import java.time.Duration; +import java.time.Instant; +import java.util.logging.Logger; + +/** + * A generic batch handler that takes care of maximum batch size and executing + * the last batch before closing. + */ +class Batch implements AutoCloseable { + private static final Logger LOG = Logger.getLogger(Batch.class.getName()); + + private final Runnable batchExecutor; + private final AutoCloseable resource; + private final int maxBatchSize; + private final Instant start; + + private int totalCount; + private int currentBatchSize; + + Batch(final int maxBatchSize, final AutoCloseable resource, final Runnable batchExecutor) { + this.maxBatchSize = maxBatchSize; + this.resource = resource; + this.batchExecutor = batchExecutor; + this.start = Instant.now(); + } + + void addBatch() { + this.currentBatchSize++; + this.totalCount++; + if (this.currentBatchSize >= this.maxBatchSize) { + this.executeBatch(); + } + } + + private void executeBatch() { + if (currentBatchSize == 0) { + LOG.finest("No items added to batch, skip"); + return; + } + final Instant batchStart = Instant.now(); + batchExecutor.run(); + final Duration duration = Duration.between(batchStart, Instant.now()); + LOG.finest(() -> "Execute batch of %d after %d took %d ms".formatted(currentBatchSize, totalCount, + duration.toMillis())); + currentBatchSize = 0; + } + + @Override + public void close() { + executeBatch(); + closeResource(); + LOG.fine(() -> "Batch processing of %d items with batch size %d completed in %s".formatted(totalCount, + maxBatchSize, + Duration.between(start, Instant.now()))); + } + + private void closeResource() { + try { + resource.close(); + } catch (final Exception e) { + throw new IllegalStateException("Failed to close resource", e); + } + } +} diff --git a/src/main/java/org/itsallcode/jdbc/batch/BatchInsert.java b/src/main/java/org/itsallcode/jdbc/batch/BatchInsert.java index 2b6f122..08ff528 100644 --- a/src/main/java/org/itsallcode/jdbc/batch/BatchInsert.java +++ b/src/main/java/org/itsallcode/jdbc/batch/BatchInsert.java @@ -1,10 +1,7 @@ package org.itsallcode.jdbc.batch; import java.sql.PreparedStatement; -import java.time.Duration; -import java.time.Instant; import java.util.Objects; -import java.util.logging.Logger; import org.itsallcode.jdbc.*; @@ -13,19 +10,12 @@ * using {@link SimpleConnection#batchInsert()}. */ public class BatchInsert implements AutoCloseable { - private static final Logger LOG = Logger.getLogger(BatchInsert.class.getName()); - - private final int maxBatchSize; + private final Batch batch; private final SimplePreparedStatement statement; - private final Instant start; - - private int rows; - private int currentBatchSize; BatchInsert(final SimplePreparedStatement statement, final int maxBatchSize) { this.statement = Objects.requireNonNull(statement, "statement"); - this.maxBatchSize = maxBatchSize; - this.start = Instant.now(); + this.batch = new Batch(maxBatchSize, statement, statement::executeBatch); } /** @@ -66,31 +56,11 @@ public PreparedStatement getStatement() { */ public void addBatch() { statement.addBatch(); - currentBatchSize++; - rows++; - if (rows % maxBatchSize == 0) { - executeBatch(); - } - } - - private void executeBatch() { - if (currentBatchSize == 0) { - LOG.finest("No rows added to batch, skip"); - return; - } - final Instant batchStart = Instant.now(); - final int[] result = statement.executeBatch(); - final Duration duration = Duration.between(batchStart, Instant.now()); - LOG.finest(() -> "Execute batch of %d after %d took %d ms, result length: %s".formatted(currentBatchSize, rows, - duration.toMillis(), result.length)); - currentBatchSize = 0; + batch.addBatch(); } @Override public void close() { - executeBatch(); - LOG.fine(() -> "Batch insert of %d rows with batch size %d completed in %s".formatted(rows, maxBatchSize, - Duration.between(start, Instant.now()))); - statement.close(); + batch.close(); } } diff --git a/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java b/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java index 7303de4..460d8a9 100644 --- a/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java @@ -7,6 +7,7 @@ import java.sql.*; +import org.itsallcode.jdbc.dialect.GenericDialect; import org.itsallcode.jdbc.resultset.ContextRowMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -135,7 +136,8 @@ void getStatement() { } SimplePreparedStatement testee() { - return new SimplePreparedStatement(null, null, statementMock, SQL_QUERY); + return new SimplePreparedStatement(Context.builder().build(), GenericDialect.INSTANCE, statementMock, + SQL_QUERY); } record RowType() { From 2a52257089c2267d8dd069ee1e23498c584e1035 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 14:07:49 +0100 Subject: [PATCH 02/11] Rename executeStatement methods to executeUpdate --- .../org/itsallcode/jdbc/ExasolTypeTest.java | 4 +- .../itsallcode/jdbc/ConnectionWrapper.java | 28 ++++-- .../org/itsallcode/jdbc/DbOperations.java | 16 ++- .../org/itsallcode/jdbc/SimpleConnection.java | 4 +- .../jdbc/SimplePreparedStatement.java | 7 +- .../org/itsallcode/jdbc/SimpleStatement.java | 97 +++++++++++++++++++ .../java/org/itsallcode/jdbc/Transaction.java | 4 +- .../itsallcode/jdbc/batch/StatementBatch.java | 24 +++++ .../jdbc/batch/StatementBatchBuilder.java | 40 ++++++++ .../jdbc/ConnectionWrapperTest.java | 18 ++-- .../jdbc/SimpleConnectionITest.java | 25 ++--- .../itsallcode/jdbc/SimpleConnectionTest.java | 6 +- .../jdbc/SimplePreparedStatementTest.java | 10 +- .../org/itsallcode/jdbc/TransactionITest.java | 24 ++--- .../org/itsallcode/jdbc/TransactionTest.java | 6 +- .../itsallcode/jdbc/example/ExampleTest.java | 4 +- 16 files changed, 250 insertions(+), 67 deletions(-) create mode 100644 src/main/java/org/itsallcode/jdbc/SimpleStatement.java create mode 100644 src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java create mode 100644 src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java diff --git a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java index d77a9b2..5c680f2 100644 --- a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java +++ b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java @@ -180,8 +180,8 @@ record TypeTest(String value, String type, Object expectedValue, JDBCType expect void batchInsert() { final LocalDate date = LocalDate.parse("2024-10-20"); try (final SimpleConnection connection = connect()) { - connection.executeStatement("create schema test"); - connection.executeStatement("create table tab(col date)"); + connection.executeUpdate("create schema test"); + connection.executeUpdate("create table tab(col date)"); connection.batchInsert(LocalDate.class).into("TAB", List.of("COL")) .mapping((row, stmt) -> stmt.setObject(1, row)).rows(Stream.of(date)).start(); try (SimpleResultSet resultSet = connection.query("select * from tab", diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index 17c2c15..db2b858 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -34,15 +34,16 @@ class ConnectionWrapper implements AutoCloseable { this.paramSetterProvider = new ParamSetterProvider(dialect); } - void executeStatement(final String sql) { - this.executeStatement(sql, ps -> { + void executeUpdate(final String sql) { + // TODO + this.executeUpdate(sql, ps -> { }); } - void executeStatement(final String sql, final PreparedStatementSetter preparedStatementSetter) { + int executeUpdate(final String sql, final PreparedStatementSetter preparedStatementSetter) { try (SimplePreparedStatement statement = prepareStatement(sql)) { statement.setValues(preparedStatementSetter); - statement.execute(); + return statement.executeUpdate(); } } @@ -50,7 +51,7 @@ void executeScript(final String sqlScript) { Arrays.stream(sqlScript.split(";")) .map(String::trim) .filter(not(String::isEmpty)) - .forEach(this::executeStatement); + .forEach(this::executeUpdate); } SimpleResultSet query(final String sql) { @@ -66,12 +67,13 @@ SimpleResultSet query(final String sql, final PreparedStatementSetter pre return statement.executeQuery(ContextRowMapper.create(rowMapper)); } - SimplePreparedStatement prepareStatement(final String sql) { - return new SimplePreparedStatement(context, dialect, wrap(prepare(sql)), sql); + private SimplePreparedStatement prepareStatement(final String sql) { + return new SimplePreparedStatement(context, dialect, + new ConvertingPreparedStatement(prepare(sql), paramSetterProvider), sql); } - private PreparedStatement wrap(final PreparedStatement preparedStatement) { - return new ConvertingPreparedStatement(preparedStatement, paramSetterProvider); + private SimpleStatement createSimpleStatement() { + return new SimpleStatement(context, dialect, createStatement()); } BatchInsertBuilder batchInsert() { @@ -90,6 +92,14 @@ private PreparedStatement prepare(final String sql) { } } + private Statement createStatement() { + try { + return connection.createStatement(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error creating statement", e); + } + } + void setAutoCommit(final boolean autoCommit) { try { connection.setAutoCommit(autoCommit); diff --git a/src/main/java/org/itsallcode/jdbc/DbOperations.java b/src/main/java/org/itsallcode/jdbc/DbOperations.java index d7b305a..b63747c 100644 --- a/src/main/java/org/itsallcode/jdbc/DbOperations.java +++ b/src/main/java/org/itsallcode/jdbc/DbOperations.java @@ -29,9 +29,11 @@ public interface DbOperations extends AutoCloseable { * Execute a single SQL statement. * * @param sql SQL statement + * @return either the row count for SQL Data Manipulation Language (DML) + * statements or 0 for SQL statements that return nothing */ - default void executeStatement(final String sql) { - this.executeStatement(sql, stmt -> { + default int executeUpdate(final String sql) { + return this.executeUpdate(sql, stmt -> { }); } @@ -44,9 +46,11 @@ default void executeStatement(final String sql) { * * @param sql SQL statement * @param parameters parameters to set in the prepared statement + * @return either the row count for SQL Data Manipulation Language (DML) + * statements or 0 for SQL statements that return nothing */ - default void executeStatement(final String sql, final List parameters) { - this.executeStatement(sql, new GenericParameterSetter(parameters)); + default int executeUpdate(final String sql, final List parameters) { + return this.executeUpdate(sql, new GenericParameterSetter(parameters)); } /** @@ -54,8 +58,10 @@ default void executeStatement(final String sql, final List parameters) { * * @param sql SQL statement * @param preparedStatementSetter prepared statement setter + * @return either the row count for SQL Data Manipulation Language (DML) + * statements or 0 for SQL statements that return nothing */ - void executeStatement(final String sql, PreparedStatementSetter preparedStatementSetter); + int executeUpdate(final String sql, PreparedStatementSetter preparedStatementSetter); /** * Execute a SQL query and return a {@link SimpleResultSet result set} with diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index 09f6c65..695ed75 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -76,9 +76,9 @@ public void executeScript(final String sqlScript) { } @Override - public void executeStatement(final String sql, final PreparedStatementSetter preparedStatementSetter) { + public int executeUpdate(final String sql, final PreparedStatementSetter preparedStatementSetter) { checkOperationAllowed(); - connection.executeStatement(sql, preparedStatementSetter); + return connection.executeUpdate(sql, preparedStatementSetter); } @Override diff --git a/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java b/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java index e2fbd86..36d49eb 100644 --- a/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java @@ -37,9 +37,12 @@ private ResultSet doExecuteQuery() { } } - boolean execute() { + /** + * @see PreparedStatement#executeUpdate() + */ + int executeUpdate() { try { - return statement.execute(); + return statement.executeUpdate(); } catch (final SQLException e) { throw new UncheckedSQLException("Error executing statement '" + sql + "'", e); } diff --git a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java new file mode 100644 index 0000000..ef3958d --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java @@ -0,0 +1,97 @@ +package org.itsallcode.jdbc; + +import java.sql.*; +import java.util.Objects; + +import org.itsallcode.jdbc.dialect.DbDialect; +import org.itsallcode.jdbc.resultset.*; + +/** + * Simple wrapper for a JDBC {@link Statement}. + */ +public class SimpleStatement implements AutoCloseable { + private final Context context; + private final DbDialect dialect; + private final Statement statement; + + SimpleStatement(final Context context, final DbDialect dialect, final Statement statement) { + this.context = Objects.requireNonNull(context, "context"); + this.dialect = Objects.requireNonNull(dialect, "dialect"); + this.statement = Objects.requireNonNull(statement, "statement"); + } + + SimpleResultSet executeQuery(final String sql, final ContextRowMapper rowMapper) { + final ResultSet resultSet = doExecuteQuery(sql); + final ResultSet convertingResultSet = ConvertingResultSet.create(dialect, resultSet); + return new SimpleResultSet<>(context, convertingResultSet, rowMapper); + } + + private ResultSet doExecuteQuery(final String sql) { + try { + return statement.executeQuery(sql); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error executing query '" + sql + "'", e); + } + } + + boolean execute(final String sql) { + try { + return statement.execute(sql); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error executing statement '" + sql + "'", e); + } + } + + int getUpdateCount() { + try { + return statement.getUpdateCount(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error getting update count", e); + } + } + + @Override + public void close() { + try { + statement.close(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error closing statement", e); + } + } + + /** + * Execute the batch statement. + * + * @return array of update counts + * @see Statement#executeBatch() + */ + public int[] executeBatch() { + try { + return statement.executeBatch(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error executing batch", e); + } + } + + /** + * Add the SQL statement to the batch. + * + * @see Statement#addBatch(String) + */ + public void addBatch(final String sql) { + try { + this.statement.addBatch(sql); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error adding batch", e); + } + } + + /** + * Get the underlying {@link Statement}. + * + * @return the underlying {@link Statement} + */ + public Statement getStatement() { + return statement; + } +} diff --git a/src/main/java/org/itsallcode/jdbc/Transaction.java b/src/main/java/org/itsallcode/jdbc/Transaction.java index 5530a7e..e0b83d1 100644 --- a/src/main/java/org/itsallcode/jdbc/Transaction.java +++ b/src/main/java/org/itsallcode/jdbc/Transaction.java @@ -75,9 +75,9 @@ public void rollback() { } @Override - public void executeStatement(final String sql, final PreparedStatementSetter preparedStatementSetter) { + public int executeUpdate(final String sql, final PreparedStatementSetter preparedStatementSetter) { checkOperationAllowed(); - connection.executeStatement(sql, preparedStatementSetter); + return connection.executeUpdate(sql, preparedStatementSetter); } @Override diff --git a/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java b/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java new file mode 100644 index 0000000..f540b0f --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java @@ -0,0 +1,24 @@ +package org.itsallcode.jdbc.batch; + +import org.itsallcode.jdbc.SimpleStatement; + +public class StatementBatch implements AutoCloseable { + + private final Batch batch; + private final SimpleStatement statement; + + public StatementBatch(final SimpleStatement statement, final int maxBatchSize) { + this.statement = statement; + this.batch = new Batch(maxBatchSize, statement, statement::executeBatch); + } + + public void addBatch(final String sql) { + statement.addBatch(sql); + this.batch.addBatch(); + } + + @Override + public void close() { + this.batch.close(); + } +} diff --git a/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java b/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java new file mode 100644 index 0000000..bf9681f --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java @@ -0,0 +1,40 @@ +package org.itsallcode.jdbc.batch; + +import java.util.function.Supplier; + +import org.itsallcode.jdbc.SimpleStatement; + +public class StatementBatchBuilder { + private final Supplier statementFactory; + private int maxBatchSize = BatchInsertBuilder.DEFAULT_MAX_BATCH_SIZE; + + /** + * Create a new instance. + * + * @param statementFactory factory for creating {@link SimpleStatement}. + */ + public StatementBatchBuilder(final Supplier statementFactory) { + this.statementFactory = statementFactory; + } + + /** + * Define maximum batch size, using {@link #DEFAULT_MAX_BATCH_SIZE} as default. + * + * @param maxBatchSize maximum batch size + * @return {@code this} for fluent programming + */ + public StatementBatchBuilder maxBatchSize(final int maxBatchSize) { + this.maxBatchSize = maxBatchSize; + return this; + } + + /** + * Build the batch inserter. + * + * @return the statement batch + */ + public StatementBatch build() { + final SimpleStatement statement = statementFactory.get(); + return new StatementBatch(statement, this.maxBatchSize); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java index e12b448..0260e13 100644 --- a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java +++ b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java @@ -22,6 +22,8 @@ class ConnectionWrapperTest { @Mock Connection connectionMock; @Mock + Statement statementMock; + @Mock PreparedStatement preparedStatementMock; ConnectionWrapper testee() { @@ -32,7 +34,7 @@ ConnectionWrapper testee() { void executeStatementPrepareFails() throws SQLException { when(connectionMock.prepareStatement("sql")).thenThrow(new SQLException("expected")); final ConnectionWrapper testee = testee(); - assertThatThrownBy(() -> testee.executeStatement("sql")) + assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error preparing statement 'sql': expected"); } @@ -40,9 +42,9 @@ void executeStatementPrepareFails() throws SQLException { @Test void executeStatementExecuteFails() throws SQLException { when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); - when(preparedStatementMock.execute()).thenThrow(new SQLException("expected")); + when(preparedStatementMock.executeUpdate()).thenThrow(new SQLException("expected")); final ConnectionWrapper testee = testee(); - assertThatThrownBy(() -> testee.executeStatement("sql")) + assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error executing statement 'sql': expected"); } @@ -52,7 +54,7 @@ void executeStatementCloseFails() throws SQLException { when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); doThrow(new SQLException("expected")).when(preparedStatementMock).close(); final ConnectionWrapper testee = testee(); - assertThatThrownBy(() -> testee.executeStatement("sql")) + assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error closing statement: expected"); } @@ -60,10 +62,10 @@ void executeStatementCloseFails() throws SQLException { @Test void executeStatement() throws SQLException { when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); - testee().executeStatement("sql"); + testee().executeUpdate("sql"); final InOrder inOrder = inOrder(connectionMock, preparedStatementMock); inOrder.verify(connectionMock).prepareStatement("sql"); - inOrder.verify(preparedStatementMock).execute(); + inOrder.verify(preparedStatementMock).executeUpdate(); inOrder.verify(preparedStatementMock).close(); inOrder.verifyNoMoreInteractions(); } @@ -71,13 +73,13 @@ void executeStatement() throws SQLException { @Test void executeStatementWithPreparedStatementSetter() throws SQLException { when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); - testee().executeStatement("sql", ps -> { + testee().executeUpdate("sql", ps -> { ps.setString(1, "one"); }); final InOrder inOrder = inOrder(connectionMock, preparedStatementMock); inOrder.verify(connectionMock).prepareStatement("sql"); inOrder.verify(preparedStatementMock).setString(1, "one"); - inOrder.verify(preparedStatementMock).execute(); + inOrder.verify(preparedStatementMock).executeUpdate(); inOrder.verify(preparedStatementMock).close(); inOrder.verifyNoMoreInteractions(); } diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java index f2051ef..de3203b 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java @@ -25,24 +25,25 @@ class SimpleConnectionITest { void wrap() throws SQLException { try (Connection existingConnection = DriverManager.getConnection(H2TestFixture.H2_MEM_JDBC_URL); SimpleConnection connection = SimpleConnection.wrap(existingConnection, new H2Dialect())) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); - assertDoesNotThrow(() -> connection.executeStatement("select count(*) from test")); + assertThat(connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))")).isZero(); + assertDoesNotThrow(() -> connection.query("select count(*) from test")); } } @Test void executeStatement() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); - assertDoesNotThrow(() -> connection.executeStatement("select count(*) from test")); + assertThat(connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))")).isZero(); + assertDoesNotThrow(() -> connection.query("select count(*) from test")); } } @Test void executeStatementWithParameter() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); - connection.executeStatement("INSERT INTO TEST VALUES (?,?), (?,?)", List.of(1, "a", 2, "b")); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + assertThat(connection.executeUpdate("INSERT INTO TEST VALUES (?,?), (?,?)", List.of(1, "a", 2, "b"))) + .isEqualTo(2); assertThat(connection.query("select count(*) from test").toList().get(0).get(0).getValue()).isEqualTo(2L); } } @@ -50,7 +51,7 @@ void executeStatementWithParameter() { @Test void executeStatementFails() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - assertThatThrownBy(() -> connection.executeStatement("select count(*) from missing_table")) + assertThatThrownBy(() -> connection.executeUpdate("select count(*) from missing_table")) .isInstanceOf(UncheckedSQLException.class) .hasMessage( "Error preparing statement 'select count(*) from missing_table': Table \"MISSING_TABLE\" not found (this database is empty); SQL statement:\n" @@ -64,7 +65,7 @@ void executeScript() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { connection.executeScript("CREATE TABLE TEST(ID INT, NAME VARCHAR(255));" + "insert into test (id, name) values (1, 'test');"); - assertDoesNotThrow(() -> connection.executeStatement("select count(*) from test")); + assertDoesNotThrow(() -> connection.query("select count(*) from test")); } } @@ -185,8 +186,8 @@ void executeQuerySingleRow() { @Test void executeQueryWithParameter() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); - connection.executeStatement("INSERT INTO TEST VALUES (?,?), (?,?)", List.of(1, "a", 2, "b")); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("INSERT INTO TEST VALUES (?,?), (?,?)", List.of(1, "a", 2, "b")); final List result = connection.query("select * from test where id=?", List.of(2), (rs, idx) -> idx + "-" + rs.getString(2) + "-" + rs.getInt(1)).toList(); assertThat(result.get(0)).isEqualTo("0-b-2"); @@ -260,13 +261,13 @@ void multipleTransactions() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { connection.executeScript("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("INSERT INTO TEST VALUES (?,?), (?,?)", List.of(1, "a", 2, "b")); + tx.executeUpdate("INSERT INTO TEST VALUES (?,?), (?,?)", List.of(1, "a", 2, "b")); assertThat(getRowCount(tx, "test")).isEqualTo(2); tx.commit(); } assertThat(getRowCount(connection, "test")).isEqualTo(2); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("DELETE FROM TEST WHERE ID = ?", List.of(1)); + tx.executeUpdate("DELETE FROM TEST WHERE ID = ?", List.of(1)); assertThat(getRowCount(tx, "test")).isEqualTo(1); } assertThat(getRowCount(connection, "test")).isEqualTo(2); diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java index fe3c8a1..ed4ac26 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java @@ -64,9 +64,9 @@ static Stream operations() { return Stream.of( operation(con -> con.startTransaction()), operation(con -> con.executeScript("script")), - operation(con -> con.executeStatement("sql")), - operation(con -> con.executeStatement("sql", preparedStatementSetterMock)), - operation(con -> con.executeStatement("sql", List.of())), + operation(con -> con.executeUpdate("sql")), + operation(con -> con.executeUpdate("sql", preparedStatementSetterMock)), + operation(con -> con.executeUpdate("sql", List.of())), operation(con -> con.query("sql")), operation(con -> con.query("sql", rowMapperMock)), operation(con -> con.query("sql", preparedStatementSetterMock, rowMapperMock)), diff --git a/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java b/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java index 460d8a9..dd0f22e 100644 --- a/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimplePreparedStatementTest.java @@ -49,16 +49,16 @@ void executeQueryFails() throws SQLException { @Test void executeStatement() throws SQLException { - when(statementMock.execute()).thenReturn(true); - assertThat(testee().execute()).isTrue(); - verify(statementMock).execute(); + when(statementMock.executeUpdate()).thenReturn(2); + assertThat(testee().executeUpdate()).isEqualTo(2); + verify(statementMock).executeUpdate(); } @Test void executeStatementFails() throws SQLException { - when(statementMock.execute()).thenThrow(new SQLException("expected")); + when(statementMock.executeUpdate()).thenThrow(new SQLException("expected")); final SimplePreparedStatement testee = testee(); - assertThatThrownBy(testee::execute).isInstanceOf(UncheckedSQLException.class) + assertThatThrownBy(testee::executeUpdate).isInstanceOf(UncheckedSQLException.class) .hasMessage("Error executing statement 'query': expected"); } diff --git a/src/test/java/org/itsallcode/jdbc/TransactionITest.java b/src/test/java/org/itsallcode/jdbc/TransactionITest.java index 1481a31..4b76f5e 100644 --- a/src/test/java/org/itsallcode/jdbc/TransactionITest.java +++ b/src/test/java/org/itsallcode/jdbc/TransactionITest.java @@ -13,9 +13,9 @@ class TransactionITest { @Test void commit() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("insert into test (id, name) values (1, 'test')"); + tx.executeUpdate("insert into test (id, name) values (1, 'test')"); final List resultSet = tx.query("select count(*) as result from test").toList(); assertEquals(1L, resultSet.get(0).get(0).getValue(Long.class)); tx.commit(); @@ -28,9 +28,9 @@ void commit() { @Test void explicitRollback() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("insert into test (id, name) values (1, 'test')"); + tx.executeUpdate("insert into test (id, name) values (1, 'test')"); final List resultSet = tx.query("select count(*) as result from test").toList(); assertEquals(1L, resultSet.get(0).get(0).getValue(Long.class)); tx.rollback(); @@ -43,9 +43,9 @@ void explicitRollback() { @Test void implicitRollback() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("insert into test (id, name) values (1, 'test')"); + tx.executeUpdate("insert into test (id, name) values (1, 'test')"); final List resultSet = tx.query("select count(*) as result from test").toList(); assertEquals(1L, resultSet.get(0).get(0).getValue(Long.class)); } @@ -57,9 +57,9 @@ void implicitRollback() { @Test void executeStatementWithParamSetter() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("insert into test (id, name) values (?, ?)", stmt -> { + tx.executeUpdate("insert into test (id, name) values (?, ?)", stmt -> { stmt.setInt(1, 1); stmt.setString(2, "a"); }); @@ -85,9 +85,9 @@ void executeScript() { @Test void queryWithRowMapper() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (Transaction tx = connection.startTransaction()) { - tx.executeStatement("insert into test (id, name) values (1, 'test')"); + tx.executeUpdate("insert into test (id, name) values (1, 'test')"); final List resultSet = tx.query("select * from test", (final ResultSet rs, final int rowNum) -> rs.getString("name")).toList(); assertEquals(List.of("test"), resultSet); @@ -98,9 +98,9 @@ void queryWithRowMapper() { @Test void queryWithParamSetter() { try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { - connection.executeStatement("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + connection.executeUpdate("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); try (final Transaction tx = connection.startTransaction()) { - tx.executeStatement("insert into test (id, name) values (1, 'test')"); + tx.executeUpdate("insert into test (id, name) values (1, 'test')"); final List resultSet = tx.query("select * from test where id = ?", stmt -> stmt.setInt(1, 1), (final ResultSet rs, final int rowNum) -> rs.getString("name")).toList(); diff --git a/src/test/java/org/itsallcode/jdbc/TransactionTest.java b/src/test/java/org/itsallcode/jdbc/TransactionTest.java index a2e52e5..13116df 100644 --- a/src/test/java/org/itsallcode/jdbc/TransactionTest.java +++ b/src/test/java/org/itsallcode/jdbc/TransactionTest.java @@ -67,9 +67,9 @@ void closeRestoresAutoCommit() { static Stream operations() { return Stream.of( operation(tx -> tx.executeScript("script")), - operation(tx -> tx.executeStatement("sql")), - operation(tx -> tx.executeStatement("sql", preparedStatementSetterMock)), - operation(tx -> tx.executeStatement("sql", List.of())), + operation(tx -> tx.executeUpdate("sql")), + operation(tx -> tx.executeUpdate("sql", preparedStatementSetterMock)), + operation(tx -> tx.executeUpdate("sql", List.of())), operation(tx -> tx.query("sql")), operation(tx -> tx.query("sql", rowMapperMock)), operation(tx -> tx.query("sql", preparedStatementSetterMock, rowMapperMock)), diff --git a/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java b/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java index 19705c8..bf4c164 100644 --- a/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java +++ b/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java @@ -30,7 +30,7 @@ void exampleInsertSelect() { .create(Context.builder().build()); try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:", "user", "password")) { connection.executeScript(readResource("/schema.sql")); - connection.executeStatement("insert into names (id, name) values (1, 'a'), (2, 'b'), (3, 'c')"); + connection.executeUpdate("insert into names (id, name) values (1, 'a'), (2, 'b'), (3, 'c')"); try (SimpleResultSet rs = connection.query("select * from names order by id")) { final List result = rs.stream().toList(); @@ -46,7 +46,7 @@ void examplePreparedStatementWithRowMapper() { .create(Context.builder().build()); try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:", "user", "password")) { connection.executeScript(readResource("/schema.sql")); - connection.executeStatement("insert into names (id, name) values (1, 'a'), (2, 'b'), (3, 'c')"); + connection.executeUpdate("insert into names (id, name) values (1, 'a'), (2, 'b'), (3, 'c')"); try (SimpleResultSet result = connection.query("select id, name from names where id = ?", ps -> ps.setInt(1, 2), From 39693bb0a6d37708efffa384ae2c4229da3f69d7 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 14:08:58 +0100 Subject: [PATCH 03/11] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c41e538..23f26f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR #42](https://github.com/itsallcode/simple-jdbc/pull/42): Allow direct access to `Connection` - [PR #43](https://github.com/itsallcode/simple-jdbc/pull/43): Allow direct access to `Connection` from `DbOperations` - [PR #44](https://github.com/itsallcode/simple-jdbc/pull/44): Add `GenericDialect` for unsupported databases +- [PR #45](https://github.com/itsallcode/simple-jdbc/pull/45): Rename `executeStatement()` to `executeUpdate()` and return row count (**Breaking change**) ## [0.9.0] - 2024-12-23 From 725bd884806ee4f17bc10dffe82568b81e1ab0a9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 19:07:56 +0100 Subject: [PATCH 04/11] Fix javadoc & integration tests --- .../jdbc/BatchInsertPerformanceTest.java | 12 +++++++----- src/main/java/module-info.java | 6 +++--- .../java/org/itsallcode/jdbc/DbOperations.java | 4 ++-- .../java/org/itsallcode/jdbc/SimpleStatement.java | 1 + .../org/itsallcode/jdbc/batch/StatementBatch.java | 14 ++++++++++++++ .../jdbc/batch/StatementBatchBuilder.java | 6 +++++- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/integrationTest/java/org/itsallcode/jdbc/BatchInsertPerformanceTest.java b/src/integrationTest/java/org/itsallcode/jdbc/BatchInsertPerformanceTest.java index c6b9b8f..5f6405f 100644 --- a/src/integrationTest/java/org/itsallcode/jdbc/BatchInsertPerformanceTest.java +++ b/src/integrationTest/java/org/itsallcode/jdbc/BatchInsertPerformanceTest.java @@ -9,6 +9,7 @@ import java.util.stream.Stream; import org.itsallcode.jdbc.batch.*; +import org.itsallcode.jdbc.dialect.GenericDialect; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,15 +36,16 @@ void rowStatementSetter() { } private RowBatchInsertBuilder rowTestee() { - final PreparedStatement stmt = createNoopPreparedStatement(); - return new RowBatchInsertBuilder(sql -> new SimplePreparedStatement(null, null, stmt, "sql")) - .maxBatchSize(MAX_BATCH_SIZE); + return new RowBatchInsertBuilder(this::prepareStatement).maxBatchSize(MAX_BATCH_SIZE); } private BatchInsertBuilder testee() { + return new BatchInsertBuilder(this::prepareStatement).maxBatchSize(MAX_BATCH_SIZE); + } + + private SimplePreparedStatement prepareStatement(final String sql) { final PreparedStatement stmt = createNoopPreparedStatement(); - return new BatchInsertBuilder(sql -> new SimplePreparedStatement(null, null, stmt, "sql")) - .maxBatchSize(MAX_BATCH_SIZE); + return new SimplePreparedStatement(Context.builder().build(), GenericDialect.INSTANCE, stmt, sql); } private PreparedStatement createNoopPreparedStatement() { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5bb8344..0a4395a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -15,11 +15,11 @@ *
  • Execute statements *
      *
    • ... single statement: - * {@link org.itsallcode.jdbc.DbOperations#executeStatement(String)}
    • + * {@link org.itsallcode.jdbc.DbOperations#executeUpdate(String)} *
    • ... with a prepared statement and generic parameters: - * {@link org.itsallcode.jdbc.DbOperations#executeStatement(String, List)}
    • + * {@link org.itsallcode.jdbc.DbOperations#executeUpdate(String, List)} *
    • ... with a prepared statement and custom parameter setter: - * {@link org.itsallcode.jdbc.DbOperations#executeStatement(String, org.itsallcode.jdbc.PreparedStatementSetter)}
    • + * {@link org.itsallcode.jdbc.DbOperations#executeUpdate(String, org.itsallcode.jdbc.PreparedStatementSetter)} * *
    • ... semicolon separated SQL script: * {@link org.itsallcode.jdbc.DbOperations#executeScript(String)}
    • diff --git a/src/main/java/org/itsallcode/jdbc/DbOperations.java b/src/main/java/org/itsallcode/jdbc/DbOperations.java index b63747c..6678134 100644 --- a/src/main/java/org/itsallcode/jdbc/DbOperations.java +++ b/src/main/java/org/itsallcode/jdbc/DbOperations.java @@ -42,7 +42,7 @@ default int executeUpdate(final String sql) { *

      * This will use {@link PreparedStatement#setObject(int, Object)} for setting * parameters. If you need more control, use - * {@link #executeStatement(String, PreparedStatementSetter)}. + * {@link #executeUpdate(String, PreparedStatementSetter)}. * * @param sql SQL statement * @param parameters parameters to set in the prepared statement @@ -92,7 +92,7 @@ default SimpleResultSet query(final String sql, final RowMapper rowMap *

      * This will use {@link PreparedStatement#setObject(int, Object)} for setting * parameters. If you need more control, use - * {@link #executeStatement(String, PreparedStatementSetter)}. + * {@link #executeUpdate(String, PreparedStatementSetter)}. * * @param generic row type * @param sql SQL query diff --git a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java index ef3958d..196efbb 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java @@ -76,6 +76,7 @@ public int[] executeBatch() { /** * Add the SQL statement to the batch. * + * @param sql SQL statement * @see Statement#addBatch(String) */ public void addBatch(final String sql) { diff --git a/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java b/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java index f540b0f..84871eb 100644 --- a/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java +++ b/src/main/java/org/itsallcode/jdbc/batch/StatementBatch.java @@ -2,16 +2,30 @@ import org.itsallcode.jdbc.SimpleStatement; +/** + * A batch handler for SQL statements. + */ public class StatementBatch implements AutoCloseable { private final Batch batch; private final SimpleStatement statement; + /** + * Create a new instance. + * + * @param statement the statement + * @param maxBatchSize maximum batch size + */ public StatementBatch(final SimpleStatement statement, final int maxBatchSize) { this.statement = statement; this.batch = new Batch(maxBatchSize, statement, statement::executeBatch); } + /** + * Add a new SQL statement to the batch. + * + * @param sql SQL statement + */ public void addBatch(final String sql) { statement.addBatch(sql); this.batch.addBatch(); diff --git a/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java b/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java index bf9681f..66c9f55 100644 --- a/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java +++ b/src/main/java/org/itsallcode/jdbc/batch/StatementBatchBuilder.java @@ -4,6 +4,9 @@ import org.itsallcode.jdbc.SimpleStatement; +/** + * A builder for {@link StatementBatch}. + */ public class StatementBatchBuilder { private final Supplier statementFactory; private int maxBatchSize = BatchInsertBuilder.DEFAULT_MAX_BATCH_SIZE; @@ -18,7 +21,8 @@ public StatementBatchBuilder(final Supplier statementFactory) { } /** - * Define maximum batch size, using {@link #DEFAULT_MAX_BATCH_SIZE} as default. + * Define maximum batch size, using + * {@link BatchInsertBuilder#DEFAULT_MAX_BATCH_SIZE} as default. * * @param maxBatchSize maximum batch size * @return {@code this} for fluent programming From c81a07f3fde078245e02acadf2cb76e6b2fbe206 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 19:20:44 +0100 Subject: [PATCH 05/11] Add batch() method to DbOperations --- src/main/java/module-info.java | 2 ++ .../itsallcode/jdbc/ConnectionWrapper.java | 2 +- .../org/itsallcode/jdbc/DbOperations.java | 11 ++++++-- .../org/itsallcode/jdbc/SimpleConnection.java | 9 +++++-- .../java/org/itsallcode/jdbc/Transaction.java | 9 +++++-- .../jdbc/SimpleConnectionITest.java | 20 +++++++++++++++ .../itsallcode/jdbc/SimpleConnectionTest.java | 1 + .../org/itsallcode/jdbc/TransactionITest.java | 25 +++++++++++++++++++ .../org/itsallcode/jdbc/TransactionTest.java | 1 + 9 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 0a4395a..0e77de5 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -20,6 +20,8 @@ * {@link org.itsallcode.jdbc.DbOperations#executeUpdate(String, List)} *

    • ... with a prepared statement and custom parameter setter: * {@link org.itsallcode.jdbc.DbOperations#executeUpdate(String, org.itsallcode.jdbc.PreparedStatementSetter)}
    • + *
    • ... multiple statements in a batch: + * {@link org.itsallcode.jdbc.DbOperations#batch()}
    • * *
    • ... semicolon separated SQL script: * {@link org.itsallcode.jdbc.DbOperations#executeScript(String)}
    • diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index db2b858..b6525c6 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -72,7 +72,7 @@ private SimplePreparedStatement prepareStatement(final String sql) { new ConvertingPreparedStatement(prepare(sql), paramSetterProvider), sql); } - private SimpleStatement createSimpleStatement() { + SimpleStatement createSimpleStatement() { return new SimpleStatement(context, dialect, createStatement()); } diff --git a/src/main/java/org/itsallcode/jdbc/DbOperations.java b/src/main/java/org/itsallcode/jdbc/DbOperations.java index 6678134..374b1a0 100644 --- a/src/main/java/org/itsallcode/jdbc/DbOperations.java +++ b/src/main/java/org/itsallcode/jdbc/DbOperations.java @@ -6,8 +6,7 @@ import java.util.List; import java.util.stream.Stream; -import org.itsallcode.jdbc.batch.BatchInsertBuilder; -import org.itsallcode.jdbc.batch.RowBatchInsertBuilder; +import org.itsallcode.jdbc.batch.*; import org.itsallcode.jdbc.resultset.RowMapper; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.itsallcode.jdbc.resultset.generic.Row; @@ -118,6 +117,14 @@ default SimpleResultSet query(final String sql, final List parame SimpleResultSet query(final String sql, final PreparedStatementSetter preparedStatementSetter, final RowMapper rowMapper); + /** + * Create a batch statement builder for executing multiple statements in a + * batch. + * + * @return batch statement builder + */ + StatementBatchBuilder batch(); + /** * Create a batch insert builder for inserting rows by directly setting values * of a {@link PreparedStatement}. diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index 695ed75..a82ded6 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -2,8 +2,7 @@ import java.sql.Connection; -import org.itsallcode.jdbc.batch.BatchInsertBuilder; -import org.itsallcode.jdbc.batch.RowBatchInsertBuilder; +import org.itsallcode.jdbc.batch.*; import org.itsallcode.jdbc.dialect.DbDialect; import org.itsallcode.jdbc.resultset.RowMapper; import org.itsallcode.jdbc.resultset.SimpleResultSet; @@ -94,6 +93,12 @@ public SimpleResultSet query(final String sql, final PreparedStatementSet return connection.query(sql, preparedStatementSetter, rowMapper); } + @Override + public StatementBatchBuilder batch() { + checkOperationAllowed(); + return new StatementBatchBuilder(connection::createSimpleStatement); + } + @Override public BatchInsertBuilder batchInsert() { checkOperationAllowed(); diff --git a/src/main/java/org/itsallcode/jdbc/Transaction.java b/src/main/java/org/itsallcode/jdbc/Transaction.java index e0b83d1..3aeac76 100644 --- a/src/main/java/org/itsallcode/jdbc/Transaction.java +++ b/src/main/java/org/itsallcode/jdbc/Transaction.java @@ -3,8 +3,7 @@ import java.sql.Connection; import java.util.function.Consumer; -import org.itsallcode.jdbc.batch.BatchInsertBuilder; -import org.itsallcode.jdbc.batch.RowBatchInsertBuilder; +import org.itsallcode.jdbc.batch.*; import org.itsallcode.jdbc.resultset.RowMapper; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.itsallcode.jdbc.resultset.generic.Row; @@ -99,6 +98,12 @@ public SimpleResultSet query(final String sql, final PreparedStatementSet return connection.query(sql, preparedStatementSetter, rowMapper); } + @Override + public StatementBatchBuilder batch() { + checkOperationAllowed(); + return new StatementBatchBuilder(connection::createSimpleStatement); + } + @Override public BatchInsertBuilder batchInsert() { checkOperationAllowed(); diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java index de3203b..142a6b0 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java @@ -9,6 +9,7 @@ import java.util.*; import java.util.stream.Stream; +import org.itsallcode.jdbc.batch.StatementBatch; import org.itsallcode.jdbc.dialect.H2Dialect; import org.itsallcode.jdbc.resultset.RowMapper; import org.itsallcode.jdbc.resultset.SimpleResultSet; @@ -256,6 +257,25 @@ void insert() { } } + @Test + void batchStatement() { + try (SimpleConnection connection = H2TestFixture.createMemConnection()) { + try (StatementBatch batch = connection.batch().maxBatchSize(3).build()) { + batch.addBatch("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + batch.addBatch("INSERT INTO TEST VALUES (1, 'a')"); + batch.addBatch("INSERT INTO TEST VALUES (2, 'b')"); + batch.addBatch("INSERT INTO TEST VALUES (3, 'c')"); + batch.addBatch("INSERT INTO TEST VALUES (4, 'd')"); + } + + final List result = connection.query("select count(*) from test").stream().toList(); + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).columnValues()).hasSize(1), + () -> assertThat(result.get(0).get(0).value()).isEqualTo(4L)); + } + } + @Test void multipleTransactions() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java index ed4ac26..a56a659 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java @@ -72,6 +72,7 @@ static Stream operations() { operation(con -> con.query("sql", preparedStatementSetterMock, rowMapperMock)), operation(con -> con.query("sql", List.of(), rowMapperMock)), operation(con -> con.getOriginalConnection()), + operation(con -> con.batch()), operation(con -> con.batchInsert()), operation(con -> con.batchInsert(null))); } diff --git a/src/test/java/org/itsallcode/jdbc/TransactionITest.java b/src/test/java/org/itsallcode/jdbc/TransactionITest.java index 4b76f5e..8005c5e 100644 --- a/src/test/java/org/itsallcode/jdbc/TransactionITest.java +++ b/src/test/java/org/itsallcode/jdbc/TransactionITest.java @@ -1,11 +1,14 @@ package org.itsallcode.jdbc; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import java.sql.ResultSet; import java.util.List; import java.util.stream.Stream; +import org.itsallcode.jdbc.batch.StatementBatch; import org.itsallcode.jdbc.resultset.generic.Row; import org.junit.jupiter.api.Test; @@ -109,6 +112,28 @@ void queryWithParamSetter() { } } + @Test + void batchStatement() { + try (SimpleConnection connection = H2TestFixture.createMemConnection()) { + try (Transaction tx = connection.startTransaction()) { + try (StatementBatch batch = tx.batch().maxBatchSize(3).build()) { + batch.addBatch("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + batch.addBatch("INSERT INTO TEST VALUES (1, 'a')"); + batch.addBatch("INSERT INTO TEST VALUES (2, 'b')"); + batch.addBatch("INSERT INTO TEST VALUES (3, 'c')"); + batch.addBatch("INSERT INTO TEST VALUES (4, 'd')"); + } + tx.commit(); + } + + final List result = connection.query("select count(*) from test").stream().toList(); + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).columnValues()).hasSize(1), + () -> assertThat(result.get(0).get(0).value()).isEqualTo(4L)); + } + } + @Test void batchInsert() { try (SimpleConnection connection = H2TestFixture.createMemConnection()) { diff --git a/src/test/java/org/itsallcode/jdbc/TransactionTest.java b/src/test/java/org/itsallcode/jdbc/TransactionTest.java index 13116df..8ccf520 100644 --- a/src/test/java/org/itsallcode/jdbc/TransactionTest.java +++ b/src/test/java/org/itsallcode/jdbc/TransactionTest.java @@ -74,6 +74,7 @@ static Stream operations() { operation(tx -> tx.query("sql", rowMapperMock)), operation(tx -> tx.query("sql", preparedStatementSetterMock, rowMapperMock)), operation(tx -> tx.query("sql", List.of(), rowMapperMock)), + operation(tx -> tx.batch()), operation(tx -> tx.batchInsert()), operation(tx -> tx.batchInsert(null)), operation(tx -> tx.getOriginalConnection()), From 3db7ec9e1e5c46a44344f40360be47dd61d74bb3 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 20:30:53 +0100 Subject: [PATCH 06/11] Use executeUpdate() instead of execute() --- .../itsallcode/jdbc/ConnectionWrapper.java | 30 +++++++---- .../org/itsallcode/jdbc/DbOperations.java | 5 +- .../org/itsallcode/jdbc/SimpleConnection.java | 8 ++- .../org/itsallcode/jdbc/SimpleStatement.java | 10 ++++ .../java/org/itsallcode/jdbc/Transaction.java | 8 ++- .../jdbc/ConnectionWrapperTest.java | 52 ++++++++++--------- .../jdbc/SimpleConnectionITest.java | 2 +- .../itsallcode/jdbc/SimpleConnectionTest.java | 4 ++ .../itsallcode/jdbc/example/ExampleTest.java | 24 +++++++++ 9 files changed, 101 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index b6525c6..4fc9ee7 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -3,12 +3,10 @@ import static java.util.function.Predicate.not; import java.sql.*; -import java.util.Arrays; -import java.util.Objects; +import java.util.*; import java.util.logging.Logger; -import org.itsallcode.jdbc.batch.BatchInsertBuilder; -import org.itsallcode.jdbc.batch.RowBatchInsertBuilder; +import org.itsallcode.jdbc.batch.*; import org.itsallcode.jdbc.dialect.DbDialect; import org.itsallcode.jdbc.resultset.*; import org.itsallcode.jdbc.resultset.generic.Row; @@ -34,10 +32,10 @@ class ConnectionWrapper implements AutoCloseable { this.paramSetterProvider = new ParamSetterProvider(dialect); } - void executeUpdate(final String sql) { - // TODO - this.executeUpdate(sql, ps -> { - }); + int executeUpdate(final String sql) { + try (SimpleStatement statement = createSimpleStatement()) { + return statement.executeUpdate(sql); + } } int executeUpdate(final String sql, final PreparedStatementSetter preparedStatementSetter) { @@ -48,10 +46,16 @@ int executeUpdate(final String sql, final PreparedStatementSetter preparedStatem } void executeScript(final String sqlScript) { - Arrays.stream(sqlScript.split(";")) + final List statements = Arrays.stream(sqlScript.split(";")) .map(String::trim) .filter(not(String::isEmpty)) - .forEach(this::executeUpdate); + .toList(); + if (statements.isEmpty()) { + return; + } + try (StatementBatch batch = this.batch().build()) { + statements.forEach(batch::addBatch); + } } SimpleResultSet query(final String sql) { @@ -72,7 +76,11 @@ private SimplePreparedStatement prepareStatement(final String sql) { new ConvertingPreparedStatement(prepare(sql), paramSetterProvider), sql); } - SimpleStatement createSimpleStatement() { + StatementBatchBuilder batch() { + return new StatementBatchBuilder(this::createSimpleStatement); + } + + private SimpleStatement createSimpleStatement() { return new SimpleStatement(context, dialect, createStatement()); } diff --git a/src/main/java/org/itsallcode/jdbc/DbOperations.java b/src/main/java/org/itsallcode/jdbc/DbOperations.java index 374b1a0..f1f0db7 100644 --- a/src/main/java/org/itsallcode/jdbc/DbOperations.java +++ b/src/main/java/org/itsallcode/jdbc/DbOperations.java @@ -31,10 +31,7 @@ public interface DbOperations extends AutoCloseable { * @return either the row count for SQL Data Manipulation Language (DML) * statements or 0 for SQL statements that return nothing */ - default int executeUpdate(final String sql) { - return this.executeUpdate(sql, stmt -> { - }); - } + int executeUpdate(final String sql); /** * Execute a single SQL statement as a prepared statement with placeholders. diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index a82ded6..7718b02 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -68,6 +68,12 @@ private void checkOperationAllowed() { } } + @Override + public int executeUpdate(final String sql) { + checkOperationAllowed(); + return connection.executeUpdate(sql); + } + @Override public void executeScript(final String sqlScript) { checkOperationAllowed(); @@ -96,7 +102,7 @@ public SimpleResultSet query(final String sql, final PreparedStatementSet @Override public StatementBatchBuilder batch() { checkOperationAllowed(); - return new StatementBatchBuilder(connection::createSimpleStatement); + return connection.batch(); } @Override diff --git a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java index 196efbb..f6a653c 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java @@ -34,6 +34,7 @@ private ResultSet doExecuteQuery(final String sql) { } } + @Deprecated boolean execute(final String sql) { try { return statement.execute(sql); @@ -42,6 +43,15 @@ boolean execute(final String sql) { } } + int executeUpdate(final String sql) { + try { + return statement.executeUpdate(sql); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error executing statement '" + sql + "'", e); + } + } + + @Deprecated int getUpdateCount() { try { return statement.getUpdateCount(); diff --git a/src/main/java/org/itsallcode/jdbc/Transaction.java b/src/main/java/org/itsallcode/jdbc/Transaction.java index 3aeac76..4b44dfb 100644 --- a/src/main/java/org/itsallcode/jdbc/Transaction.java +++ b/src/main/java/org/itsallcode/jdbc/Transaction.java @@ -73,6 +73,12 @@ public void rollback() { this.transactionFinishedCallback.accept(this); } + @Override + public int executeUpdate(final String sql) { + checkOperationAllowed(); + return connection.executeUpdate(sql); + } + @Override public int executeUpdate(final String sql, final PreparedStatementSetter preparedStatementSetter) { checkOperationAllowed(); @@ -101,7 +107,7 @@ public SimpleResultSet query(final String sql, final PreparedStatementSet @Override public StatementBatchBuilder batch() { checkOperationAllowed(); - return new StatementBatchBuilder(connection::createSimpleStatement); + return connection.batch(); } @Override diff --git a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java index 0260e13..a8b8fc0 100644 --- a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java +++ b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java @@ -2,12 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.sql.*; -import org.itsallcode.jdbc.dialect.H2Dialect; +import org.itsallcode.jdbc.dialect.GenericDialect; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -27,22 +26,22 @@ class ConnectionWrapperTest { PreparedStatement preparedStatementMock; ConnectionWrapper testee() { - return new ConnectionWrapper(connectionMock, Context.builder().build(), new H2Dialect()); + return new ConnectionWrapper(connectionMock, Context.builder().build(), GenericDialect.INSTANCE); } @Test void executeStatementPrepareFails() throws SQLException { - when(connectionMock.prepareStatement("sql")).thenThrow(new SQLException("expected")); + when(connectionMock.createStatement()).thenThrow(new SQLException("expected")); final ConnectionWrapper testee = testee(); assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) - .hasMessage("Error preparing statement 'sql': expected"); + .hasMessage("Error creating statement: expected"); } @Test void executeStatementExecuteFails() throws SQLException { - when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); - when(preparedStatementMock.executeUpdate()).thenThrow(new SQLException("expected")); + when(connectionMock.createStatement()).thenReturn(statementMock); + when(statementMock.executeUpdate("sql")).thenThrow(new SQLException("expected")); final ConnectionWrapper testee = testee(); assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) @@ -51,8 +50,8 @@ void executeStatementExecuteFails() throws SQLException { @Test void executeStatementCloseFails() throws SQLException { - when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); - doThrow(new SQLException("expected")).when(preparedStatementMock).close(); + when(connectionMock.createStatement()).thenReturn(statementMock); + doThrow(new SQLException("expected")).when(statementMock).close(); final ConnectionWrapper testee = testee(); assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) @@ -61,12 +60,12 @@ void executeStatementCloseFails() throws SQLException { @Test void executeStatement() throws SQLException { - when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); + when(connectionMock.createStatement()).thenReturn(statementMock); testee().executeUpdate("sql"); - final InOrder inOrder = inOrder(connectionMock, preparedStatementMock); - inOrder.verify(connectionMock).prepareStatement("sql"); - inOrder.verify(preparedStatementMock).executeUpdate(); - inOrder.verify(preparedStatementMock).close(); + final InOrder inOrder = inOrder(connectionMock, statementMock); + inOrder.verify(connectionMock).createStatement(); + inOrder.verify(statementMock).executeUpdate("sql"); + inOrder.verify(statementMock).close(); inOrder.verifyNoMoreInteractions(); } @@ -84,9 +83,10 @@ void executeStatementWithPreparedStatementSetter() throws SQLException { inOrder.verifyNoMoreInteractions(); } - @Test - void executeScriptEmptyString() { - testee().executeScript(""); + @ParameterizedTest + @ValueSource(strings = { "", " ", "\n", "\t", " \n\t", ";" }) + void executeScriptEmptyString(final String script) { + testee().executeScript(script); verifyNoInteractions(connectionMock); } @@ -95,20 +95,24 @@ void executeScriptEmptyString() { "sql script;", "sql script;\n", "sql script\n;", "sql script;;", "sql script;;", ";sql script", " ; ; sql script" }) void executeScriptWithoutTrailingSemicolon(final String script) throws SQLException { - when(connectionMock.prepareStatement("sql script")).thenReturn(preparedStatementMock); + when(connectionMock.createStatement()).thenReturn(statementMock); testee().executeScript(script); - verify(connectionMock).prepareStatement("sql script"); + verify(statementMock).addBatch("sql script"); + verify(statementMock).executeBatch(); verifyNoMoreInteractions(connectionMock); } @Test void executeScriptRunsMultipleCommands() throws SQLException { - when(connectionMock.prepareStatement(anyString())).thenReturn(preparedStatementMock); + when(connectionMock.createStatement()).thenReturn(statementMock); testee().executeScript("script 1; script 2; script 3"); - verify(connectionMock).prepareStatement("script 1"); - verify(connectionMock).prepareStatement("script 2"); - verify(connectionMock).prepareStatement("script 3"); - verifyNoMoreInteractions(connectionMock); + final InOrder inOrder = inOrder(connectionMock, statementMock); + inOrder.verify(statementMock).addBatch("script 1"); + inOrder.verify(statementMock).addBatch("script 2"); + inOrder.verify(statementMock).addBatch("script 3"); + inOrder.verify(statementMock).executeBatch(); + inOrder.verify(statementMock).close(); + inOrder.verifyNoMoreInteractions(); } @Test diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java index 142a6b0..3818b3f 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java @@ -55,7 +55,7 @@ void executeStatementFails() { assertThatThrownBy(() -> connection.executeUpdate("select count(*) from missing_table")) .isInstanceOf(UncheckedSQLException.class) .hasMessage( - "Error preparing statement 'select count(*) from missing_table': Table \"MISSING_TABLE\" not found (this database is empty); SQL statement:\n" + "Error executing statement 'select count(*) from missing_table': Table \"MISSING_TABLE\" not found (this database is empty); SQL statement:\n" + "select count(*) from missing_table [42104-232]") .hasCauseInstanceOf(SQLException.class); } diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java index a56a659..85e9668 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java @@ -87,6 +87,7 @@ void operationSucceedsForOpenConnection(final Consumer operati final SimpleConnection testee = testee(); lenient().when(connectionMock.prepareStatement(anyString())) .thenReturn(mock(PreparedStatement.class, RETURNS_DEEP_STUBS)); + lenient().when(connectionMock.createStatement()).thenReturn(mock(Statement.class, RETURNS_DEEP_STUBS)); assertDoesNotThrow(() -> operation.accept(testee)); } @@ -116,6 +117,7 @@ void operationSucceedsAfterTransactionIsClosed(final Consumer final SimpleConnection testee = testee(); lenient().when(connectionMock.prepareStatement(anyString())) .thenReturn(mock(PreparedStatement.class, RETURNS_DEEP_STUBS)); + lenient().when(connectionMock.createStatement()).thenReturn(mock(Statement.class, RETURNS_DEEP_STUBS)); testee.startTransaction().close(); assertDoesNotThrow(() -> operation.accept(testee)); } @@ -126,6 +128,7 @@ void operationSucceedsAfterTransactionIsCommitted(final Consumer operation.accept(testee)); } @@ -136,6 +139,7 @@ void operationSucceedsAfterTransactionIsRolledBack(final Consumer operation.accept(testee)); } diff --git a/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java b/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java index bf4c164..8f2ae34 100644 --- a/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java +++ b/src/test/java/org/itsallcode/jdbc/example/ExampleTest.java @@ -1,5 +1,7 @@ package org.itsallcode.jdbc.example; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.*; @@ -12,6 +14,7 @@ import org.itsallcode.jdbc.*; import org.itsallcode.jdbc.batch.BatchInsert; +import org.itsallcode.jdbc.batch.StatementBatch; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.itsallcode.jdbc.resultset.generic.Row; import org.junit.jupiter.api.Test; @@ -58,6 +61,27 @@ void examplePreparedStatementWithRowMapper() { } } + @Test + void exampleBatchStatement() { + final ConnectionFactory connectionFactory = ConnectionFactory + .create(Context.builder().build()); + try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:", "user", "password")) { + try (StatementBatch batch = connection.batch().maxBatchSize(3).build()) { + batch.addBatch("CREATE TABLE TEST(ID INT, NAME VARCHAR(255))"); + batch.addBatch("INSERT INTO TEST VALUES (1, 'a')"); + batch.addBatch("INSERT INTO TEST VALUES (2, 'b')"); + batch.addBatch("INSERT INTO TEST VALUES (3, 'c')"); + batch.addBatch("INSERT INTO TEST VALUES (4, 'd')"); + } + + final List result = connection.query("select count(*) from test").stream().toList(); + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).columnValues()).hasSize(1), + () -> assertThat(result.get(0).get(0).value()).isEqualTo(4L)); + } + } + @Test void exampleRowBatchInsert() { final ConnectionFactory connectionFactory = ConnectionFactory From 40f99c6c05e5a6628315059863dc03aee822ca36 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 5 Jan 2025 20:42:20 +0100 Subject: [PATCH 07/11] Use simple statement for queries without parameter --- .../org/itsallcode/jdbc/ConnectionWrapper.java | 7 +++++-- .../org/itsallcode/jdbc/SimpleStatement.java | 18 ------------------ .../itsallcode/jdbc/SimpleConnectionITest.java | 2 +- .../itsallcode/jdbc/SimpleConnectionTest.java | 4 ++-- 4 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index 4fc9ee7..aa852c2 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -59,8 +59,10 @@ void executeScript(final String sqlScript) { } SimpleResultSet query(final String sql) { - return this.query(sql, ps -> { - }, ContextRowMapper.generic(dialect)); + LOG.finest(() -> "Executing query '" + sql + "'..."); + final SimpleStatement statement = createSimpleStatement(); + // TODO: close statement when resultset is closed + return statement.executeQuery(sql, ContextRowMapper.create(ContextRowMapper.generic(dialect))); } SimpleResultSet query(final String sql, final PreparedStatementSetter preparedStatementSetter, @@ -68,6 +70,7 @@ SimpleResultSet query(final String sql, final PreparedStatementSetter pre LOG.finest(() -> "Executing query '" + sql + "'..."); final SimplePreparedStatement statement = prepareStatement(sql); statement.setValues(preparedStatementSetter); + // TODO: close statement when resultset is closed return statement.executeQuery(ContextRowMapper.create(rowMapper)); } diff --git a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java index f6a653c..3ddc7c1 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java @@ -34,15 +34,6 @@ private ResultSet doExecuteQuery(final String sql) { } } - @Deprecated - boolean execute(final String sql) { - try { - return statement.execute(sql); - } catch (final SQLException e) { - throw new UncheckedSQLException("Error executing statement '" + sql + "'", e); - } - } - int executeUpdate(final String sql) { try { return statement.executeUpdate(sql); @@ -51,15 +42,6 @@ int executeUpdate(final String sql) { } } - @Deprecated - int getUpdateCount() { - try { - return statement.getUpdateCount(); - } catch (final SQLException e) { - throw new UncheckedSQLException("Error getting update count", e); - } - } - @Override public void close() { try { diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java index 3818b3f..6cf7222 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java @@ -76,7 +76,7 @@ void executeQueryFails() { assertThatThrownBy( () -> connection.query("select count(*) from missing_table")) .isInstanceOf(UncheckedSQLException.class).hasMessage( - "Error preparing statement 'select count(*) from missing_table': Table \"MISSING_TABLE\" not found (this database is empty); SQL statement:\n" + "Error executing query 'select count(*) from missing_table': Table \"MISSING_TABLE\" not found (this database is empty); SQL statement:\n" + "select count(*) from missing_table [42104-232]"); } } diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java index 85e9668..31919a1 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java @@ -34,10 +34,10 @@ class SimpleConnectionTest { @Test void queryPrepareStatementFails() throws SQLException { - when(connectionMock.prepareStatement(SQL_STATEMENT)).thenThrow(new SQLException("expected")); + when(connectionMock.createStatement()).thenThrow(new SQLException("expected")); final SimpleConnection testee = testee(); assertThatThrownBy(() -> testee.query(SQL_STATEMENT)).isInstanceOf(UncheckedSQLException.class) - .hasMessage("Error preparing statement 'query': expected"); + .hasMessage("Error creating statement: expected"); } @Test From 440cd533d95cf95ec507c43ead491877e708a433 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 6 Jan 2025 10:47:19 +0100 Subject: [PATCH 08/11] Add missing tests --- .../jdbc/ConnectionWrapperTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java index a8b8fc0..caa1152 100644 --- a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java +++ b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java @@ -5,8 +5,14 @@ import static org.mockito.Mockito.*; import java.sql.*; +import java.util.List; +import java.util.stream.Stream; +import org.itsallcode.jdbc.batch.BatchInsert; import org.itsallcode.jdbc.dialect.GenericDialect; +import org.itsallcode.jdbc.resultset.ContextRowMapper; +import org.itsallcode.jdbc.resultset.SimpleResultSet; +import org.itsallcode.jdbc.resultset.generic.Row; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -83,6 +89,73 @@ void executeStatementWithPreparedStatementSetter() throws SQLException { inOrder.verifyNoMoreInteractions(); } + @Test + void queryWithPreparedStatementSetterAndRowMapper() throws SQLException { + when(connectionMock.prepareStatement("sql")).thenReturn(preparedStatementMock); + when(preparedStatementMock.executeQuery()).thenReturn(mock(ResultSet.class, RETURNS_DEEP_STUBS)); + final SimpleResultSet result = testee().query("sql", ps -> { + ps.setString(1, "one"); + }, ContextRowMapper.generic(GenericDialect.INSTANCE)); + assertThat(result).isNotNull(); + final InOrder inOrder = inOrder(connectionMock, preparedStatementMock); + inOrder.verify(connectionMock).prepareStatement("sql"); + inOrder.verify(preparedStatementMock).setString(1, "one"); + inOrder.verify(preparedStatementMock).executeQuery(); + inOrder.verify(preparedStatementMock, never()).close(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void preparedStatementFails() throws SQLException { + when(connectionMock.prepareStatement("sql")).thenThrow(new SQLException("expected")); + final ConnectionWrapper testee = testee(); + assertThatThrownBy(() -> testee.executeUpdate("sql", ps -> { + ps.setString(1, "one"); + })).isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error preparing statement 'sql': expected"); + } + + @Test + void query() throws SQLException { + when(connectionMock.createStatement()).thenReturn(statementMock); + when(statementMock.executeQuery("sql")).thenReturn(mock(ResultSet.class, RETURNS_DEEP_STUBS)); + final SimpleResultSet result = testee().query("sql"); + assertThat(result).isNotNull(); + } + + @Test + void batchInsert() throws SQLException { + when(connectionMock.prepareStatement("insert into \"tab\" (\"c1\",\"c2\") values (?,?)")) + .thenReturn(preparedStatementMock); + final BatchInsert batch = testee().batchInsert().into("tab", List.of("c1", "c2")).maxBatchSize(4).build(); + batch.add(ps -> { + ps.setString(1, "one"); + }); + batch.close(); + final InOrder inOrder = inOrder(connectionMock, preparedStatementMock); + inOrder.verify(connectionMock).prepareStatement("insert into \"tab\" (\"c1\",\"c2\") values (?,?)"); + inOrder.verify(preparedStatementMock).setString(1, "one"); + inOrder.verify(preparedStatementMock).addBatch(); + inOrder.verify(preparedStatementMock).executeBatch(); + inOrder.verify(preparedStatementMock).close(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void rowBatchInsert() throws SQLException { + when(connectionMock.prepareStatement("insert into \"tab\" (\"c1\",\"c2\") values (?,?)")) + .thenReturn(preparedStatementMock); + testee().rowBatchInsert().into("tab", List.of("c1", "c2")).mapping(row -> new Object[] { row }) + .rows(Stream.of("one")).maxBatchSize(4).start(); + final InOrder inOrder = inOrder(connectionMock, preparedStatementMock); + inOrder.verify(connectionMock).prepareStatement("insert into \"tab\" (\"c1\",\"c2\") values (?,?)"); + inOrder.verify(preparedStatementMock).setObject(1, "one"); + inOrder.verify(preparedStatementMock).addBatch(); + inOrder.verify(preparedStatementMock).executeBatch(); + inOrder.verify(preparedStatementMock, times(2)).close(); // TODO: why twice? + inOrder.verifyNoMoreInteractions(); + } + @ParameterizedTest @ValueSource(strings = { "", " ", "\n", "\t", " \n\t", ";" }) void executeScriptEmptyString(final String script) { From 898574b121579057a5c491087e15125ca2afc9e6 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 6 Jan 2025 19:48:59 +0100 Subject: [PATCH 09/11] Add unit tests --- .../org/itsallcode/jdbc/SimpleStatement.java | 18 +-- .../java/org/itsallcode/jdbc/batch/Batch.java | 2 +- .../itsallcode/jdbc/SimpleStatementTest.java | 106 ++++++++++++++++++ .../org/itsallcode/jdbc/batch/BatchTest.java | 72 ++++++++++++ .../jdbc/batch/StatementBatchTest.java | 93 +++++++++++++++ 5 files changed, 281 insertions(+), 10 deletions(-) create mode 100644 src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java create mode 100644 src/test/java/org/itsallcode/jdbc/batch/BatchTest.java create mode 100644 src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java diff --git a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java index 3ddc7c1..69e9f5e 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleStatement.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleStatement.java @@ -42,15 +42,6 @@ int executeUpdate(final String sql) { } } - @Override - public void close() { - try { - statement.close(); - } catch (final SQLException e) { - throw new UncheckedSQLException("Error closing statement", e); - } - } - /** * Execute the batch statement. * @@ -87,4 +78,13 @@ public void addBatch(final String sql) { public Statement getStatement() { return statement; } + + @Override + public void close() { + try { + statement.close(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error closing statement", e); + } + } } diff --git a/src/main/java/org/itsallcode/jdbc/batch/Batch.java b/src/main/java/org/itsallcode/jdbc/batch/Batch.java index 3244269..00c5bb6 100644 --- a/src/main/java/org/itsallcode/jdbc/batch/Batch.java +++ b/src/main/java/org/itsallcode/jdbc/batch/Batch.java @@ -60,7 +60,7 @@ private void closeResource() { try { resource.close(); } catch (final Exception e) { - throw new IllegalStateException("Failed to close resource", e); + throw new IllegalStateException("Failed to close resource: " + e.getMessage(), e); } } } diff --git a/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java b/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java new file mode 100644 index 0000000..c3097aa --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java @@ -0,0 +1,106 @@ +package org.itsallcode.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import java.sql.*; + +import org.itsallcode.jdbc.dialect.GenericDialect; +import org.itsallcode.jdbc.resultset.ContextRowMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SimpleStatementTest { + @Mock + Statement stmtMock; + @Mock + ResultSet resultSetMock; + @Mock + ResultSetMetaData metaDataMock; + @Mock + ContextRowMapper rowMapperMock; + + SimpleStatement testee() { + return new SimpleStatement(Context.builder().build(), GenericDialect.INSTANCE, stmtMock); + } + + @Test + void executeQuery() throws SQLException { + when(stmtMock.executeQuery("sql")).thenReturn(resultSetMock); + when(resultSetMock.getMetaData()).thenReturn(metaDataMock); + assertThat(testee().executeQuery("sql", rowMapperMock)).isNotNull(); + verify(stmtMock).executeQuery("sql"); + } + + @Test + void executeQueryFails() throws SQLException { + when(stmtMock.executeQuery("sql")).thenThrow(new SQLException("expected")); + assertThatThrownBy(() -> testee().executeQuery("sql", rowMapperMock)) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error executing query 'sql': expected"); + } + + @Test + void executeUpdate() throws SQLException { + when(stmtMock.executeUpdate("sql")).thenReturn(2); + assertThat(testee().executeUpdate("sql")).isEqualTo(2); + } + + @Test + void executeUpdateFails() throws SQLException { + when(stmtMock.executeUpdate("sql")).thenThrow(new SQLException("expected")); + assertThatThrownBy(() -> testee().executeUpdate("sql")) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error executing statement 'sql': expected"); + } + + @Test + void addBatch() throws SQLException { + testee().addBatch("sql"); + verify(stmtMock).addBatch("sql"); + } + + @Test + void addBatchFails() throws SQLException { + doThrow(new SQLException("expected")).when(stmtMock).addBatch("sql"); + assertThatThrownBy(() -> testee().addBatch("sql")) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error adding batch: expected"); + } + + @Test + void executeBatch() throws SQLException { + when(stmtMock.executeBatch()).thenReturn(new int[] { 2 }); + assertThat(testee().executeBatch()).isEqualTo(new int[] { 2 }); + } + + @Test + void executeBatchFails() throws SQLException { + when(stmtMock.executeBatch()).thenThrow(new SQLException("expected")); + assertThatThrownBy(() -> testee().executeBatch()) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error executing batch: expected"); + } + + @Test + void getStatement() throws SQLException { + assertThat(testee().getStatement()).isSameAs(stmtMock); + } + + @Test + void close() throws SQLException { + testee().close(); + verify(stmtMock).close(); + } + + @Test + void closeFails() throws SQLException { + doThrow(new SQLException("expected")).when(stmtMock).close(); + assertThatThrownBy(testee()::close).isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error closing statement: expected"); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/batch/BatchTest.java b/src/test/java/org/itsallcode/jdbc/batch/BatchTest.java new file mode 100644 index 0000000..9c8b41e --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/batch/BatchTest.java @@ -0,0 +1,72 @@ +package org.itsallcode.jdbc.batch; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +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 BatchTest { + + @Mock + AutoCloseable resourceMock; + @Mock + Runnable batchExecutorMock; + + @Test + void addBatchDoesNotFlush() { + final Batch testee = testee(2); + testee.addBatch(); + + final InOrder inOrder = inOrder(batchExecutorMock, resourceMock); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void addBatchFlushesAfterBatchSizeReached() { + final Batch testee = testee(2); + + testee.addBatch(); + testee.addBatch(); + + final InOrder inOrder = inOrder(batchExecutorMock, resourceMock); + inOrder.verify(batchExecutorMock).run(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void closeClosesStatement() throws Exception { + final Batch testee = testee(2); + testee.close(); + verify(resourceMock).close(); + } + + @Test + void closeFlushes() throws Exception { + final Batch testee = testee(2); + + testee.addBatch(); + testee.close(); + + final InOrder inOrder = inOrder(batchExecutorMock, resourceMock); + inOrder.verify(batchExecutorMock).run(); + inOrder.verify(resourceMock).close(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void closeResourceFails() throws Exception { + final Batch testee = testee(2); + doThrow(new RuntimeException("expected")).when(resourceMock).close(); + assertThatThrownBy(testee::close).isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to close resource: expected"); + } + + Batch testee(final int maxBatchSize) { + return new Batch(maxBatchSize, resourceMock, batchExecutorMock); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java b/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java new file mode 100644 index 0000000..fa90bf6 --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java @@ -0,0 +1,93 @@ +package org.itsallcode.jdbc.batch; + +import static org.mockito.Mockito.*; + +import org.itsallcode.jdbc.SimpleStatement; +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 StatementBatchTest { + + @Mock + SimpleStatement stmtMock; + + @Test + void addDoesNotFlush() { + final StatementBatch testee = testee(2); + testee.addBatch("stmt1"); + + final InOrder inOrder = inOrder(stmtMock); + inOrder.verify(stmtMock).addBatch("stmt1"); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void addFlushesAfterBatchSizeReached() { + final StatementBatch testee = testee(2); + when(stmtMock.executeBatch()).thenReturn(new int[0]); + + testee.addBatch("stmt1"); + testee.addBatch("stmt2"); + + final InOrder inOrder = inOrder(stmtMock); + inOrder.verify(stmtMock).addBatch("stmt1"); + inOrder.verify(stmtMock).addBatch("stmt2"); + inOrder.verify(stmtMock).executeBatch(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void addBatchDoesNotFlush() { + final StatementBatch testee = testee(2); + testee.addBatch("stmt1"); + + final InOrder inOrder = inOrder(stmtMock); + inOrder.verify(stmtMock).addBatch("stmt1"); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void addBatchFlushesAfterBatchSizeReached() { + final StatementBatch testee = testee(2); + when(stmtMock.executeBatch()).thenReturn(new int[0]); + + testee.addBatch("stmt1"); + testee.addBatch("stmt2"); + + final InOrder inOrder = inOrder(stmtMock); + inOrder.verify(stmtMock).addBatch("stmt1"); + inOrder.verify(stmtMock).addBatch("stmt2"); + inOrder.verify(stmtMock).executeBatch(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void closeClosesStatement() { + final StatementBatch testee = testee(2); + testee.close(); + verify(stmtMock).close(); + } + + @Test + void closeFlushes() { + final StatementBatch testee = testee(2); + when(stmtMock.executeBatch()).thenReturn(new int[0]); + + testee.addBatch("stmt1"); + testee.close(); + + final InOrder inOrder = inOrder(stmtMock); + inOrder.verify(stmtMock).addBatch("stmt1"); + inOrder.verify(stmtMock).executeBatch(); + inOrder.verify(stmtMock).close(); + inOrder.verifyNoMoreInteractions(); + } + + StatementBatch testee(final int maxBatchSize) { + return new StatementBatch(stmtMock, maxBatchSize); + } +} From f5af2c47a115d419efd725154a16a7e1ff699c44 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 6 Jan 2025 19:57:34 +0100 Subject: [PATCH 10/11] Fix sonar findings --- .../itsallcode/jdbc/SimpleStatementTest.java | 15 +++++++---- .../jdbc/batch/StatementBatchTest.java | 25 ------------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java b/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java index c3097aa..1e286e1 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java @@ -39,7 +39,8 @@ void executeQuery() throws SQLException { @Test void executeQueryFails() throws SQLException { when(stmtMock.executeQuery("sql")).thenThrow(new SQLException("expected")); - assertThatThrownBy(() -> testee().executeQuery("sql", rowMapperMock)) + final SimpleStatement testee = testee(); + assertThatThrownBy(() -> testee.executeQuery("sql", rowMapperMock)) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error executing query 'sql': expected"); } @@ -53,7 +54,8 @@ void executeUpdate() throws SQLException { @Test void executeUpdateFails() throws SQLException { when(stmtMock.executeUpdate("sql")).thenThrow(new SQLException("expected")); - assertThatThrownBy(() -> testee().executeUpdate("sql")) + final SimpleStatement testee = testee(); + assertThatThrownBy(() -> testee.executeUpdate("sql")) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error executing statement 'sql': expected"); } @@ -67,7 +69,8 @@ void addBatch() throws SQLException { @Test void addBatchFails() throws SQLException { doThrow(new SQLException("expected")).when(stmtMock).addBatch("sql"); - assertThatThrownBy(() -> testee().addBatch("sql")) + final SimpleStatement testee = testee(); + assertThatThrownBy(() -> testee.addBatch("sql")) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error adding batch: expected"); } @@ -81,7 +84,8 @@ void executeBatch() throws SQLException { @Test void executeBatchFails() throws SQLException { when(stmtMock.executeBatch()).thenThrow(new SQLException("expected")); - assertThatThrownBy(() -> testee().executeBatch()) + final SimpleStatement testee = testee(); + assertThatThrownBy(testee::executeBatch) .isInstanceOf(UncheckedSQLException.class) .hasMessage("Error executing batch: expected"); } @@ -100,7 +104,8 @@ void close() throws SQLException { @Test void closeFails() throws SQLException { doThrow(new SQLException("expected")).when(stmtMock).close(); - assertThatThrownBy(testee()::close).isInstanceOf(UncheckedSQLException.class) + final SimpleStatement testee = testee(); + assertThatThrownBy(testee::close).isInstanceOf(UncheckedSQLException.class) .hasMessage("Error closing statement: expected"); } } diff --git a/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java b/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java index fa90bf6..108b6dc 100644 --- a/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java +++ b/src/test/java/org/itsallcode/jdbc/batch/StatementBatchTest.java @@ -15,31 +15,6 @@ class StatementBatchTest { @Mock SimpleStatement stmtMock; - @Test - void addDoesNotFlush() { - final StatementBatch testee = testee(2); - testee.addBatch("stmt1"); - - final InOrder inOrder = inOrder(stmtMock); - inOrder.verify(stmtMock).addBatch("stmt1"); - inOrder.verifyNoMoreInteractions(); - } - - @Test - void addFlushesAfterBatchSizeReached() { - final StatementBatch testee = testee(2); - when(stmtMock.executeBatch()).thenReturn(new int[0]); - - testee.addBatch("stmt1"); - testee.addBatch("stmt2"); - - final InOrder inOrder = inOrder(stmtMock); - inOrder.verify(stmtMock).addBatch("stmt1"); - inOrder.verify(stmtMock).addBatch("stmt2"); - inOrder.verify(stmtMock).executeBatch(); - inOrder.verifyNoMoreInteractions(); - } - @Test void addBatchDoesNotFlush() { final StatementBatch testee = testee(2); From e206dd261044e0bc594fd622477d5d6cb3303edd Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 6 Jan 2025 20:03:44 +0100 Subject: [PATCH 11/11] Fix sonar warning --- src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java b/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java index 1e286e1..a1926d5 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleStatementTest.java @@ -91,7 +91,7 @@ void executeBatchFails() throws SQLException { } @Test - void getStatement() throws SQLException { + void getStatement() { assertThat(testee().getStatement()).isSameAs(stmtMock); }