From df41994eccaa4c525506e6b3bb2ee7b19f9fd474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Sun, 24 Aug 2025 09:15:07 +0900 Subject: [PATCH] Implement execute batch --- .../java/tech/turso/jdbc4/JDBC4Statement.java | 79 ++++++- .../tech/turso/jdbc4/JDBC4StatementTest.java | 215 ++++++++++++++++++ 2 files changed, 290 insertions(+), 4 deletions(-) diff --git a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java index b86b838f5..67225aac5 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java @@ -2,11 +2,16 @@ package tech.turso.jdbc4; import static java.util.Objects.requireNonNull; +import java.sql.BatchUpdateException; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLTimeoutException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.locks.ReentrantLock; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; @@ -33,6 +38,12 @@ public class JDBC4Statement implements Statement { private ReentrantLock connectionLock = new ReentrantLock(); + /** + * List of SQL statements to be executed as a batch. Used for batch processing as per JDBC + * specification. + */ + private List batchCommands = new ArrayList<>(); + public JDBC4Statement(JDBC4Connection connection) { this( connection, @@ -232,18 +243,78 @@ public class JDBC4Statement implements Statement { @Override public void addBatch(String sql) throws SQLException { - // TODO + ensureOpen(); + if (sql == null) { + throw new SQLException("SQL command cannot be null"); + } + batchCommands.add(sql); } @Override public void clearBatch() throws SQLException { - // TODO + ensureOpen(); + batchCommands.clear(); } @Override public int[] executeBatch() throws SQLException { - // TODO - return new int[0]; + ensureOpen(); + + int[] updateCounts = new int[batchCommands.size()]; + List failedCommands = new ArrayList<>(); + int[] successfulCounts = new int[batchCommands.size()]; + + // Execute each command in the batch + for (int i = 0; i < batchCommands.size(); i++) { + String sql = batchCommands.get(i); + try { + // Check if the statement returns a ResultSet (SELECT statements) + // In batch processing, SELECT statements should throw an exception + if (execute(sql)) { + // This means the statement returned a ResultSet, which is not allowed in batch + failedCommands.add(sql); + updateCounts[i] = EXECUTE_FAILED; + // Create a BatchUpdateException for the failed command + BatchUpdateException bue = + new BatchUpdateException( + "Batch entry " + + i + + " (" + + sql + + ") was aborted. " + + "Batch commands cannot return result sets.", + "HY000", // General error SQL state + 0, + updateCounts); + // Clear the batch after failure + clearBatch(); + throw bue; + } else { + // For DML statements, get the update count + updateCounts[i] = getUpdateCount(); + } + } catch (SQLException e) { + // Handle SQL exceptions during batch execution + failedCommands.add(sql); + updateCounts[i] = EXECUTE_FAILED; + + // Create a BatchUpdateException with the partial results + BatchUpdateException bue = + new BatchUpdateException( + "Batch entry " + i + " (" + sql + ") failed: " + e.getMessage(), + e.getSQLState(), + e.getErrorCode(), + updateCounts, + e.getCause()); + // Clear the batch after failure + clearBatch(); + throw bue; + } + } + + // Clear the batch after successful execution + clearBatch(); + return updateCounts; } @Override diff --git a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4StatementTest.java b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4StatementTest.java index e8266c76a..53572d3be 100644 --- a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4StatementTest.java +++ b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4StatementTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.sql.BatchUpdateException; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -120,4 +121,218 @@ class JDBC4StatementTest { assertThat(stmt.executeUpdate("DELETE FROM s1")).isEqualTo(3); } + + /** Tests for batch processing functionality */ + @Test + void testAddBatch_single_statement() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Add a single batch command + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + + // Execute batch + int[] updateCounts = stmt.executeBatch(); + + // Verify results + assertThat(updateCounts).hasSize(1); + assertThat(updateCounts[0]).isEqualTo(1); + + // Verify data was inserted + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); + assertTrue(rs.next()); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + @Test + void testAddBatch_multiple_statements() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Add multiple batch commands + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); + stmt.addBatch("INSERT INTO batch_test VALUES (3, 'test3');"); + + // Execute batch + int[] updateCounts = stmt.executeBatch(); + + // Verify results + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(1); + assertThat(updateCounts[1]).isEqualTo(1); + assertThat(updateCounts[2]).isEqualTo(1); + + // Verify all data was inserted + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); + assertTrue(rs.next()); + assertThat(rs.getInt(1)).isEqualTo(3); + } + + @Test + void testAddBatch_with_updates_and_deletes() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Insert initial data + stmt.execute( + "INSERT INTO batch_test VALUES (1, 'initial1'), (2, 'initial2'), (3, 'initial3');"); + + // Add batch commands with different operations + stmt.addBatch("UPDATE batch_test SET value = 'updated' WHERE id = 1;"); + stmt.addBatch("DELETE FROM batch_test WHERE id = 2;"); + stmt.addBatch("INSERT INTO batch_test VALUES (4, 'new');"); + + // Execute batch + int[] updateCounts = stmt.executeBatch(); + + // Verify update counts + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(1); // UPDATE affected 1 row + assertThat(updateCounts[1]).isEqualTo(1); // DELETE affected 1 row + assertThat(updateCounts[2]).isEqualTo(1); // INSERT affected 1 row + + // Verify final state + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); + assertTrue(rs.next()); + assertThat(rs.getInt(1)).isEqualTo(3); // 3 initial - 1 deleted + 1 inserted = 3 + } + + @Test + void testClearBatch() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Add batch commands + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); + + // Clear the batch + stmt.clearBatch(); + + // Execute batch should return empty array + int[] updateCounts = stmt.executeBatch(); + assertThat(updateCounts).isEmpty(); + + // Verify no data was inserted + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); + assertTrue(rs.next()); + assertThat(rs.getInt(1)).isEqualTo(0); + } + + @Test + void testBatch_with_DDL_statements() throws SQLException { + // DDL statements should work in batch + stmt.addBatch("CREATE TABLE batch_test1 (id INTEGER);"); + stmt.addBatch("CREATE TABLE batch_test2 (id INTEGER);"); + stmt.addBatch("CREATE TABLE batch_test3 (id INTEGER);"); + + // Execute batch + int[] updateCounts = stmt.executeBatch(); + + // DDL statements typically return 0 for update count + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(0); + assertThat(updateCounts[1]).isEqualTo(0); + assertThat(updateCounts[2]).isEqualTo(0); + + // Verify tables were created by inserting data + assertDoesNotThrow(() -> stmt.execute("INSERT INTO batch_test1 VALUES (1);")); + assertDoesNotThrow(() -> stmt.execute("INSERT INTO batch_test2 VALUES (1);")); + assertDoesNotThrow(() -> stmt.execute("INSERT INTO batch_test3 VALUES (1);")); + } + + @Test + void testBatch_with_SELECT_should_throw_exception() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + stmt.execute("INSERT INTO batch_test VALUES (1, 'test1');"); + + // Add a SELECT statement to batch (not allowed) + stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); + stmt.addBatch("SELECT * FROM batch_test;"); // This should cause an exception + stmt.addBatch("INSERT INTO batch_test VALUES (3, 'test3');"); + + // Execute batch should throw BatchUpdateException + BatchUpdateException exception = + assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + + // Verify exception message + assertTrue(exception.getMessage().contains("Batch commands cannot return result sets")); + + // Verify update counts for executed statements before the failure + int[] updateCounts = exception.getUpdateCounts(); + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(1); // First INSERT succeeded + assertThat(updateCounts[1]).isEqualTo(Statement.EXECUTE_FAILED); // SELECT failed + // The third statement may not have been executed depending on implementation + } + + @Test + void testBatch_with_null_command_should_throw_exception() throws SQLException { + // Adding null command should throw SQLException + assertThrows(SQLException.class, () -> stmt.addBatch(null)); + } + + @Test + void testBatch_operations_on_closed_statement_should_throw_exception() throws SQLException { + stmt.close(); + + // All batch operations should throw SQLException on closed statement + assertThrows(SQLException.class, () -> stmt.addBatch("INSERT INTO test VALUES (1);")); + assertThrows(SQLException.class, () -> stmt.clearBatch()); + assertThrows(SQLException.class, () -> stmt.executeBatch()); + } + + @Test + void testBatch_with_syntax_error_should_throw_exception() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Add batch commands with a syntax error + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + stmt.addBatch("INVALID SQL SYNTAX;"); // This should cause an exception + stmt.addBatch("INSERT INTO batch_test VALUES (3, 'test3');"); + + // Execute batch should throw BatchUpdateException + BatchUpdateException exception = + assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + + // Verify update counts show partial execution + int[] updateCounts = exception.getUpdateCounts(); + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(1); // First INSERT succeeded + assertThat(updateCounts[1]).isEqualTo(Statement.EXECUTE_FAILED); // Invalid SQL failed + } + + @Test + void testBatch_empty_batch_returns_empty_array() throws SQLException { + // Execute empty batch + int[] updateCounts = stmt.executeBatch(); + + // Should return empty array + assertThat(updateCounts).isEmpty(); + } + + @Test + void testBatch_clears_after_successful_execution() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Add and execute batch + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + stmt.executeBatch(); + + // Execute batch again should return empty array (batch was cleared) + int[] updateCounts = stmt.executeBatch(); + assertThat(updateCounts).isEmpty(); + } + + @Test + void testBatch_clears_after_failed_execution() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + // Add batch with SELECT statement that will fail + stmt.addBatch("SELECT * FROM batch_test;"); + + // Execute batch should fail + assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + + // Execute batch again should return empty array (batch was cleared after failure) + int[] updateCounts = stmt.executeBatch(); + assertThat(updateCounts).isEmpty(); + } }