Merge 'bindings/java: Implement batch operations for JDBC4Statement' from Kim Seon Woo

Implement the following methods for JDBC4Statement:
- addBatch
- clearBatch
- executeBatch

Closes #2754
This commit is contained in:
Pekka Enberg
2025-08-24 08:54:44 +03:00
committed by GitHub
4 changed files with 371 additions and 5 deletions

View File

@@ -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 {}

View File

@@ -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<String> 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<String> 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

View File

@@ -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;"));
}
}

View File

@@ -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":{}}