diff --git a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java index a3f8b3d4d..60b5316fe 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java @@ -176,6 +176,11 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep // TODO } + @Override + public void addBatch(String sql) throws SQLException { + throw new SQLException("addBatch(String) cannot be called on a PreparedStatement"); + } + @Override public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {} 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..eb31c8d0b 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java @@ -2,12 +2,16 @@ package tech.turso.jdbc4; import static java.util.Objects.requireNonNull; +import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoResultSet; @@ -15,6 +19,20 @@ import tech.turso.core.TursoStatement; public class JDBC4Statement implements Statement { + private static final Pattern BATCH_COMPATIBLE_PATTERN = + Pattern.compile( + "^\\s*" + + // Leading whitespace + "(?:/\\*.*?\\*/\\s*)*" + + // Optional C-style comments + "(?:--[^\\n]*\\n\\s*)*" + + // Optional SQL line comments + "(?:" + + // Start of keywords group + "INSERT|UPDATE|DELETE" + + ")\\b", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private final JDBC4Connection connection; @Nullable protected TursoStatement statement = null; @@ -33,6 +51,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 +256,82 @@ 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(); } + // TODO: let's make this batch operation atomic @Override public int[] executeBatch() throws SQLException { - // TODO - return new int[0]; + ensureOpen(); + + int[] updateCounts = new int[batchCommands.size()]; + List failedCommands = new ArrayList<>(); + + // Execute each command in the batch + for (int i = 0; i < batchCommands.size(); i++) { + String sql = batchCommands.get(i); + try { + if (!isBatchCompatibleStatement(sql)) { + failedCommands.add(sql); + updateCounts[i] = EXECUTE_FAILED; + 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; + } + + execute(sql); + // For DML statements, get the update count + updateCounts[i] = getUpdateCount(); + } catch (SQLException e) { + 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; + } + + boolean isBatchCompatibleStatement(String sql) { + if (sql == null || sql.trim().isEmpty()) { + return false; + } + + return BATCH_COMPATIBLE_PATTERN.matcher(sql).find(); } @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..f12fa07a7 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,277 @@ 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);"); + + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + + int[] updateCounts = stmt.executeBatch(); + + assertThat(updateCounts).hasSize(1); + assertThat(updateCounts[0]).isEqualTo(1); + + 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);"); + + 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');"); + + int[] updateCounts = stmt.executeBatch(); + + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(1); + assertThat(updateCounts[1]).isEqualTo(1); + assertThat(updateCounts[2]).isEqualTo(1); + + 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);"); + + stmt.execute( + "INSERT INTO batch_test VALUES (1, 'initial1'), (2, 'initial2'), (3, 'initial3');"); + + stmt.addBatch("UPDATE batch_test SET value = 'updated';"); + stmt.addBatch("DELETE FROM batch_test WHERE id = 2;"); + stmt.addBatch("INSERT INTO batch_test VALUES (4, 'new');"); + + int[] updateCounts = stmt.executeBatch(); + + assertThat(updateCounts).hasSize(3); + assertThat(updateCounts[0]).isEqualTo(3); // UPDATE affected 3 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);"); + + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); + + stmt.clearBatch(); + + int[] updateCounts = stmt.executeBatch(); + assertThat(updateCounts).isEmpty(); + + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); + assertTrue(rs.next()); + assertThat(rs.getInt(1)).isEqualTo(0); + } + + @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');"); + + 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');"); + + BatchUpdateException exception = + assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + + assertTrue(exception.getMessage().contains("Batch commands cannot return result sets")); + + 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 + } + + @Test + void testBatch_with_null_command_should_throw_exception() { + assertThrows(SQLException.class, () -> stmt.addBatch(null)); + } + + @Test + void testBatch_operations_on_closed_statement_should_throw_exception() throws SQLException { + stmt.close(); + + 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);"); + + 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');"); + + BatchUpdateException exception = + assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + + 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 { + int[] updateCounts = stmt.executeBatch(); + assertThat(updateCounts).isEmpty(); + } + + @Test + void testBatch_clears_after_successful_execution() throws SQLException { + stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); + + stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); + stmt.executeBatch(); + + 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);"); + + stmt.addBatch("SELECT * FROM batch_test;"); + + assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + + int[] updateCounts = stmt.executeBatch(); + assertThat(updateCounts).isEmpty(); + } + + /** Tests for isBatchCompatibleStatement method */ + @Test + void testIsBatchCompatibleStatement_compatible_statements() { + JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("INSERT INTO table VALUES (1, 2);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("insert into table values (1, 2);")); + assertTrue( + jdbc4Stmt.isBatchCompatibleStatement("INSERT INTO table (col1, col2) VALUES (1, 2);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("INSERT OR REPLACE INTO table VALUES (1);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("INSERT OR IGNORE INTO table VALUES (1);")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" INSERT INTO table VALUES (1);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("\t\nINSERT INTO table VALUES (1);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" \n\t INSERT INTO table VALUES (1);")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ INSERT INTO table VALUES (1);")); + assertTrue( + jdbc4Stmt.isBatchCompatibleStatement( + "/* multi\nline\ncomment */ INSERT INTO table VALUES (1);")); + assertTrue( + jdbc4Stmt.isBatchCompatibleStatement("-- line comment\nINSERT INTO table VALUES (1);")); + assertTrue( + jdbc4Stmt.isBatchCompatibleStatement( + "-- comment 1\n-- comment 2\nINSERT INTO table VALUES (1);")); + + assertTrue( + jdbc4Stmt.isBatchCompatibleStatement( + " /* comment */ -- another\n INSERT INTO table VALUES (1);")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UPDATE table SET col = 1;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("update table set col = 1;")); + assertTrue( + jdbc4Stmt.isBatchCompatibleStatement("UPDATE table SET col1 = 1, col2 = 2 WHERE id = 3;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UPDATE OR REPLACE table SET col = 1;")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" UPDATE table SET col = 1;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("\t\nUPDATE table SET col = 1;")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ UPDATE table SET col = 1;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("-- comment\nUPDATE table SET col = 1;")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("DELETE FROM table;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("delete from table;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("DELETE FROM table WHERE id = 1;")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" DELETE FROM table;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("\t\nDELETE FROM table;")); + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ DELETE FROM table;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("-- comment\nDELETE FROM table;")); + } + + @Test + void testIsBatchCompatibleStatement_non_compatible_statements() { + JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("SELECT * FROM table;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("select * from table;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement(" SELECT * FROM table;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ SELECT * FROM table;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("-- comment\nSELECT * FROM table;")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("EXPLAIN SELECT * FROM table;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("EXPLAIN QUERY PLAN SELECT * FROM table;")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("PRAGMA table_info(table);")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("PRAGMA foreign_keys = ON;")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("ANALYZE;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("ANALYZE table;")); + + assertFalse( + jdbc4Stmt.isBatchCompatibleStatement( + "WITH cte AS (SELECT * FROM table) SELECT * FROM cte;")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("VACUUM;")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("VALUES (1, 2), (3, 4);")); + } + + @Test + void testIsBatchCompatibleStatement_edge_cases() { + JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement(null)); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement(" ")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("\t\n")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("/* comment only */")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("-- comment only")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ -- another comment")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("SELECT * FROM table WHERE name = 'INSERT';")); + assertFalse( + jdbc4Stmt.isBatchCompatibleStatement("SELECT * FROM table WHERE action = 'DELETE';")); + + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("INSER INTO table VALUES (1);")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("UPDAT table SET col = 1;")); + assertFalse(jdbc4Stmt.isBatchCompatibleStatement("DELET FROM table;")); + } + + @Test + void testIsBatchCompatibleStatement_case_insensitive() { + JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; + + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("Insert INTO table VALUES (1);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("InSeRt INTO table VALUES (1);")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UPDATE table SET col = 1;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UpDaTe table SET col = 1;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("Delete FROM table;")); + assertTrue(jdbc4Stmt.isBatchCompatibleStatement("DeLeTe FROM table;")); + } } diff --git a/bindings/java/src/test/resources/turso/.rustc_info.json b/bindings/java/src/test/resources/turso/.rustc_info.json deleted file mode 100644 index b01291daa..000000000 --- a/bindings/java/src/test/resources/turso/.rustc_info.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc_fingerprint":11551670960185020797,"outputs":{"14427667104029986310":{"success":true,"status":"","code":0,"stdout":"rustc 1.83.0 (90b35a623 2024-11-26)\nbinary: rustc\ncommit-hash: 90b35a6239c3d8bdabc530a6a0816f7ff89a0aaf\ncommit-date: 2024-11-26\nhost: x86_64-unknown-linux-gnu\nrelease: 1.83.0\nLLVM version: 19.1.1\n","stderr":""},"11399821309745579047":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/merlin/.rustup/toolchains/1.83.0-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file