From f7cc402dfcd4a909756dea3ecf4e2fdfa45426d3 Mon Sep 17 00:00:00 2001 From: Duckulus Date: Sun, 16 Nov 2025 15:22:17 +0100 Subject: [PATCH 1/3] add batching support to JDBC4PreparedStatement --- bindings/java/rs_src/turso_statement.rs | 17 ++ .../java/tech/turso/core/TursoStatement.java | 43 ++++ .../turso/jdbc4/JDBC4PreparedStatement.java | 192 ++++++++++++------ .../java/tech/turso/jdbc4/JDBC4Statement.java | 4 +- .../tech/turso/core/TursoStatementTest.java | 14 +- .../jdbc4/JDBC4PreparedStatementTest.java | 66 +++++- 6 files changed, 265 insertions(+), 71 deletions(-) diff --git a/bindings/java/rs_src/turso_statement.rs b/bindings/java/rs_src/turso_statement.rs index ad81d5ef8..0f937f8fe 100644 --- a/bindings/java/rs_src/turso_statement.rs +++ b/bindings/java/rs_src/turso_statement.rs @@ -301,6 +301,23 @@ pub extern "system" fn Java_tech_turso_core_TursoStatement_changes<'local>( stmt.connection.conn.changes() } +#[no_mangle] +pub extern "system" fn Java_tech_turso_core_TursoStatement_parameterCount<'local>( + mut env: JNIEnv<'local>, + obj: JObject<'local>, + stmt_ptr: jlong, +) -> jint { + let stmt = match to_turso_statement(stmt_ptr) { + Ok(stmt) => stmt, + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); + return -1; + } + }; + + stmt.stmt.parameters_count() as jint +} + /// Converts an optional `JObject` into Java's `TursoStepResult`. /// /// This function takes an optional `JObject` and converts it into a Java object diff --git a/bindings/java/src/main/java/tech/turso/core/TursoStatement.java b/bindings/java/src/main/java/tech/turso/core/TursoStatement.java index 0c15f6586..9c9911623 100644 --- a/bindings/java/src/main/java/tech/turso/core/TursoStatement.java +++ b/bindings/java/src/main/java/tech/turso/core/TursoStatement.java @@ -215,6 +215,32 @@ public final class TursoStatement { private native int bindBlob(long statementPointer, int position, byte[] value) throws SQLException; + public void bindObject(int parameterIndex, Object x) throws SQLException { + if (x == null) { + this.bindNull(parameterIndex); + return; + } + if (x instanceof Byte) { + this.bindInt(parameterIndex, (Byte) x); + } else if (x instanceof Short) { + this.bindInt(parameterIndex, (Short) x); + } else if (x instanceof Integer) { + this.bindInt(parameterIndex, (Integer) x); + } else if (x instanceof Long) { + this.bindLong(parameterIndex, (Long) x); + } else if (x instanceof String) { + bindText(parameterIndex, (String) x); + } else if (x instanceof Float) { + bindDouble(parameterIndex, (Float) x); + } else if (x instanceof Double) { + bindDouble(parameterIndex, (Double) x); + } else if (x instanceof byte[]) { + bindBlob(parameterIndex, (byte[]) x); + } else { + throw new SQLException("Unsupported object type in bindObject: " + x.getClass().getName()); + } + } + /** * Returns total number of changes. * @@ -247,6 +273,23 @@ public final class TursoStatement { private native long changes(long statementPointer) throws SQLException; + /** + * Returns the number of parameters in this statement. Parameters are the `?`'s that get replaced + * by the provided arguments. + * + * @throws SQLException If a database access error occurs + */ + public int parameterCount() throws SQLException { + final int result = parameterCount(statementPointer); + if (result == -1) { + throw new SQLException("Exception while retrieving parameter count"); + } + + return result; + } + + private native int parameterCount(long statementPointer) throws SQLException; + /** * Checks if the statement is closed. * 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 04862b68c..48c5491d2 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java @@ -10,23 +10,11 @@ import java.math.BigDecimal; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.sql.Array; -import java.sql.Blob; -import java.sql.Clob; -import java.sql.Date; -import java.sql.NClob; -import java.sql.ParameterMetaData; -import java.sql.PreparedStatement; -import java.sql.Ref; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.RowId; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLXML; -import java.sql.Time; -import java.sql.Timestamp; +import java.sql.*; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoResultSet; @@ -34,7 +22,11 @@ import tech.turso.core.TursoResultSet; public final class JDBC4PreparedStatement extends JDBC4Statement implements PreparedStatement { private final String sql; - private final JDBC4ResultSet resultSet; + private JDBC4ResultSet resultSet; + + private final int paramCount; + private Object[] currentBatchParams; + private final ArrayList batchQueryParams = new ArrayList<>(); /** * Creates a new JDBC4PreparedStatement. @@ -48,97 +40,115 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep this.sql = sql; this.statement = connection.prepare(sql); this.resultSet = new JDBC4ResultSet(this.statement.getResultSet()); + this.paramCount = statement.parameterCount(); + this.currentBatchParams = new Object[paramCount]; + } + + private void reprepareStatement() throws SQLException { + this.statement = connection.prepare(sql); + this.resultSet = new JDBC4ResultSet(this.statement.getResultSet()); } @Override public ResultSet executeQuery() throws SQLException { // TODO: check bindings etc + bindParams(currentBatchParams); return this.resultSet; } @Override public int executeUpdate() throws SQLException { requireNonNull(this.statement); + bindParams(currentBatchParams); final TursoResultSet resultSet = statement.getResultSet(); resultSet.consumeAll(); return Math.toIntExact(statement.changes()); } + /** + * This helper method saves a parameter locally without binding it to the underlying native + * statement. We have to do this so we are able to switch between different sets of parameters + * when batching queries. + */ + private void setParam(int parameterIndex, @Nullable Object object) { + requireNonNull(this.statement); + currentBatchParams[parameterIndex - 1] = object; + } + @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { requireNonNull(this.statement); - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); } @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { requireNonNull(this.statement); - this.statement.bindInt(parameterIndex, x ? 1 : 0); + setParam(parameterIndex, x ? 1 : 0); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { requireNonNull(this.statement); - this.statement.bindInt(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { requireNonNull(this.statement); - this.statement.bindInt(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { requireNonNull(this.statement); - this.statement.bindInt(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setLong(int parameterIndex, long x) throws SQLException { requireNonNull(this.statement); - this.statement.bindLong(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { requireNonNull(this.statement); - this.statement.bindDouble(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { requireNonNull(this.statement); - this.statement.bindDouble(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { requireNonNull(this.statement); - this.statement.bindText(parameterIndex, x.toString()); + setParam(parameterIndex, x.toString()); } @Override public void setString(int parameterIndex, String x) throws SQLException { requireNonNull(this.statement); - this.statement.bindText(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setBytes(int parameterIndex, byte[] x) throws SQLException { requireNonNull(this.statement); - this.statement.bindBlob(parameterIndex, x); + setParam(parameterIndex, x); } @Override public void setDate(int parameterIndex, Date x) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); } else { long time = x.getTime(); - this.statement.bindBlob( - parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); + setParam(parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); } } @@ -146,11 +156,10 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setTime(int parameterIndex, Time x) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); } else { long time = x.getTime(); - this.statement.bindBlob( - parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); + setParam(parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); } } @@ -158,11 +167,10 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); } else { long time = x.getTime(); - this.statement.bindBlob( - parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); + setParam(parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); } } @@ -170,14 +178,14 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setAsciiStream length must be non-negative"); } if (length == 0) { - this.statement.bindText(parameterIndex, ""); + setParam(parameterIndex, ""); return; } try { @@ -188,7 +196,7 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep offset += read; } String ascii = new String(buffer, 0, offset, StandardCharsets.US_ASCII); - this.statement.bindText(parameterIndex, ascii); + setParam(parameterIndex, ascii); } catch (IOException e) { throw new SQLException("Error reading ASCII stream", e); } @@ -198,14 +206,14 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setUnicodeStream length must be non-negative"); } if (length == 0) { - this.statement.bindText(parameterIndex, ""); + setParam(parameterIndex, ""); return; } try { @@ -216,7 +224,7 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep offset += read; } String text = new String(buffer, 0, offset, StandardCharsets.UTF_8); - this.statement.bindText(parameterIndex, text); + setParam(parameterIndex, text); } catch (IOException e) { throw new SQLException("Error reading Unicode stream", e); } @@ -226,14 +234,14 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setBinaryStream length must be non-negative"); } if (length == 0) { - this.statement.bindBlob(parameterIndex, new byte[0]); + setParam(parameterIndex, new byte[0]); return; } try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { @@ -246,15 +254,21 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep totalRead += bytesRead; } byte[] data = baos.toByteArray(); - this.statement.bindBlob(parameterIndex, data); + setParam(parameterIndex, data); } catch (IOException e) { throw new SQLException("Error reading binary stream", e); } } @Override - public void clearParameters() throws SQLException { - // TODO + public void clearParameters() { + this.currentBatchParams = new Object[paramCount]; + } + + @Override + public void clearBatch() throws SQLException { + this.batchQueryParams.clear(); + this.currentBatchParams = new Object[paramCount]; } @Override @@ -266,7 +280,7 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setObject(int parameterIndex, Object x) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); return; } if (x instanceof String) { @@ -309,14 +323,78 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep @Override public boolean execute() throws SQLException { + return execute(currentBatchParams); + } + + /** This helper method runs the statement using the provided parameter values. */ + private boolean execute(Object[] params) throws SQLException { // TODO: check whether this is sufficient - requireNonNull(this.statement); - return statement.execute(); + requireNonNull(statement); + bindParams(params); + boolean result = statement.execute(); + updateCount = statement.changes(); + return result; } @Override - public void addBatch() throws SQLException { - // TODO + public int[] executeBatch() throws SQLException { + return Arrays.stream(executeLargeBatch()).mapToInt(l -> (int) l).toArray(); + } + + @Override + public long[] executeLargeBatch() throws SQLException { + requireNonNull(this.statement); + if (batchQueryParams.isEmpty()) { + return new long[0]; + } + long[] updateCounts = new long[batchQueryParams.size()]; + if (!isBatchCompatibleStatement(sql)) { + updateCounts[0] = EXECUTE_FAILED; + BatchUpdateException bue = + new BatchUpdateException( + "Batch commands cannot return result sets.", + "HY000", // General error SQL state + 0, + Arrays.stream(updateCounts).mapToInt(l -> (int) l).toArray()); + // Clear the batch after failure + clearBatch(); + throw bue; + } + for (int i = 0; i < batchQueryParams.size(); i++) { + try { + // TODO: do this without creating a new statement because this has unnecessary overhead + reprepareStatement(); + execute(batchQueryParams.get(i)); + updateCounts[i] = getUpdateCount(); + } catch (SQLException e) { + 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; + } + } + clearBatch(); + return updateCounts; + } + + /** Takes the given set of parameters and binds it to the underlying statement. */ + private void bindParams(Object[] params) throws SQLException { + requireNonNull(statement); + for (int paramIndex = 1; paramIndex <= params.length; paramIndex++) { + statement.bindObject(paramIndex, params[paramIndex - 1]); + } + } + + @Override + public void addBatch() { + batchQueryParams.add(currentBatchParams); + currentBatchParams = new Object[paramCount]; } @Override @@ -463,23 +541,23 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); return; } byte[] data = readBytes(x); String ascii = new String(data, StandardCharsets.US_ASCII); - this.statement.bindText(parameterIndex, ascii); + setParam(parameterIndex, ascii); } @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { requireNonNull(this.statement); if (x == null) { - this.statement.bindNull(parameterIndex); + setParam(parameterIndex, null); return; } byte[] data = readBytes(x); - this.statement.bindBlob(parameterIndex, data); + setParam(parameterIndex, data); } /** 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 8f1f50da3..6e5da6d67 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java @@ -34,7 +34,7 @@ public class JDBC4Statement implements Statement { + ")\\b", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - private final JDBC4Connection connection; + protected final JDBC4Connection connection; /** The underlying Turso statement. */ @Nullable protected TursoStatement statement = null; @@ -330,7 +330,7 @@ public class JDBC4Statement implements Statement { return updateCounts; } - boolean isBatchCompatibleStatement(String sql) { + protected boolean isBatchCompatibleStatement(String sql) { if (sql == null || sql.trim().isEmpty()) { return false; } diff --git a/bindings/java/src/test/java/tech/turso/core/TursoStatementTest.java b/bindings/java/src/test/java/tech/turso/core/TursoStatementTest.java index 1244ea58b..c03715f99 100644 --- a/bindings/java/src/test/java/tech/turso/core/TursoStatementTest.java +++ b/bindings/java/src/test/java/tech/turso/core/TursoStatementTest.java @@ -1,10 +1,6 @@ package tech.turso.core; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; @@ -119,6 +115,14 @@ class TursoStatementTest { selectStmt.close(); } + @Test + void test_parameterCount() throws Exception { + runSql("CREATE TABLE test (col1 INT);"); + assertEquals(0, connection.prepare("INSERT INTO test VALUES (1)").parameterCount()); + assertEquals(1, connection.prepare("INSERT INTO test VALUES (?)").parameterCount()); + assertEquals(2, connection.prepare("INSERT INTO test VALUES (?), (?)").parameterCount()); + } + private void runSql(String sql) throws Exception { TursoStatement stmt = connection.prepare(sql); while (stmt.execute()) { diff --git a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java index 6dc8fbe89..216c09e37 100644 --- a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java +++ b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java @@ -1,12 +1,6 @@ package tech.turso.jdbc4; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -871,4 +865,62 @@ class JDBC4PreparedStatementTest { preparedStatement.setInt(1, 1); assertEquals(preparedStatement.executeUpdate(), 1); } + + @Test + void testBatch_explicit_addBatch() throws Exception { + connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); + PreparedStatement preparedStatement = + connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (?, ?)"); + + preparedStatement.setInt(1, 1); + preparedStatement.setInt(2, 2); + preparedStatement.addBatch(); + preparedStatement.setInt(1, 3); + preparedStatement.setInt(2, 4); + preparedStatement.addBatch(); + + assertArrayEquals(new int[] {1, 1}, preparedStatement.executeBatch()); + + ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); + assertTrue(rs.next()); + assertEquals(1, rs.getInt(1)); + assertEquals(2, rs.getInt(2)); + assertTrue(rs.next()); + assertEquals(3, rs.getInt(1)); + assertEquals(4, rs.getInt(2)); + assertFalse(rs.next()); + } + + @Test + void testBatch_implicit_addBatch() throws Exception { + connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); + PreparedStatement preparedStatement = + connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (?, ?)"); + + preparedStatement.setInt(1, 1); + preparedStatement.setInt(2, 2); + preparedStatement.addBatch(); + // we set parameters but don't call addBatch afterward + // we should only get a result for the first insert statement to match sqlite-jdbc behavior + preparedStatement.setInt(1, 3); + preparedStatement.setInt(2, 4); + + assertArrayEquals(new int[] {1}, preparedStatement.executeBatch()); + + ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); + assertTrue(rs.next()); + assertEquals(1, rs.getInt(1)); + assertEquals(2, rs.getInt(2)); + assertFalse(rs.next()); + } + + @Test + void testBatch_invalid_command() throws Exception { + connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); + PreparedStatement preparedStatement = + connection.prepareStatement("SELECT * FROM test WHERE col1=?"); + preparedStatement.setInt(1, 1); + preparedStatement.addBatch(); + assertThrows(BatchUpdateException.class, preparedStatement::executeBatch); + } } From 66213612b823f146bda625288a59bcbc68b5b915 Mon Sep 17 00:00:00 2001 From: Duckulus Date: Wed, 19 Nov 2025 23:04:47 +0100 Subject: [PATCH 2/3] add unit tests for batch update,delete and adjust naming --- .../jdbc4/JDBC4PreparedStatementTest.java | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java index 216c09e37..526076b41 100644 --- a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java +++ b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java @@ -867,7 +867,7 @@ class JDBC4PreparedStatementTest { } @Test - void testBatch_explicit_addBatch() throws Exception { + void testBatchInsert() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (?, ?)"); @@ -892,7 +892,59 @@ class JDBC4PreparedStatementTest { } @Test - void testBatch_implicit_addBatch() throws Exception { + void testBatchUpdate() throws Exception { + connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); + connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (1, 1), (2, 2)").execute(); + + PreparedStatement preparedStatement = + connection.prepareStatement("UPDATE test SET col2=? WHERE col1=?"); + + preparedStatement.setInt(1, 5); + preparedStatement.setInt(2, 1); + preparedStatement.addBatch(); + preparedStatement.setInt(1, 6); + preparedStatement.setInt(2, 2); + preparedStatement.addBatch(); + preparedStatement.setInt(1, 7); + preparedStatement.setInt(2, 3); + preparedStatement.addBatch(); + + assertArrayEquals(new int[] {1, 1, 0}, preparedStatement.executeBatch()); + + ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); + assertTrue(rs.next()); + assertEquals(1, rs.getInt(1)); + assertEquals(5, rs.getInt(2)); + assertTrue(rs.next()); + assertEquals(2, rs.getInt(1)); + assertEquals(6, rs.getInt(2)); + assertFalse(rs.next()); + } + + @Test + void testBatchDelete() throws Exception { + connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); + connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (1, 1), (2, 2)").execute(); + + PreparedStatement preparedStatement = + connection.prepareStatement("DELETE FROM test WHERE col1=?"); + + preparedStatement.setInt(1, 1); + preparedStatement.addBatch(); + preparedStatement.setInt(1, 4); + preparedStatement.addBatch(); + + assertArrayEquals(new int[] {1, 0}, preparedStatement.executeBatch()); + + ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); + assertTrue(rs.next()); + assertEquals(2, rs.getInt(1)); + assertEquals(2, rs.getInt(2)); + assertFalse(rs.next()); + } + + @Test + void testBatch_implicitAddBatch_shouldIgnore() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (?, ?)"); @@ -915,7 +967,7 @@ class JDBC4PreparedStatementTest { } @Test - void testBatch_invalid_command() throws Exception { + void testBatch_select_shouldFail() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM test WHERE col1=?"); From 7e8977232615fd2e29fbfa452bbc0c52ec5a38d1 Mon Sep 17 00:00:00 2001 From: Duckulus Date: Wed, 19 Nov 2025 23:47:15 +0100 Subject: [PATCH 3/3] reset statement instead of recreating it when executing preparedstatement batch --- bindings/java/rs_src/turso_statement.rs | 18 ++++++++++++++++++ .../java/tech/turso/core/TursoStatement.java | 13 ++++++++++++- .../turso/jdbc4/JDBC4PreparedStatement.java | 10 ++-------- core/lib.rs | 1 + 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/bindings/java/rs_src/turso_statement.rs b/bindings/java/rs_src/turso_statement.rs index 0f937f8fe..cb8f33def 100644 --- a/bindings/java/rs_src/turso_statement.rs +++ b/bindings/java/rs_src/turso_statement.rs @@ -318,6 +318,24 @@ pub extern "system" fn Java_tech_turso_core_TursoStatement_parameterCount<'local stmt.stmt.parameters_count() as jint } +#[no_mangle] +pub extern "system" fn Java_tech_turso_core_TursoStatement_reset<'local>( + mut env: JNIEnv<'local>, + obj: JObject<'local>, + stmt_ptr: jlong, +) -> jint { + let stmt = match to_turso_statement(stmt_ptr) { + Ok(stmt) => stmt, + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); + return -1; + } + }; + + stmt.stmt.reset(); + 0 +} + /// Converts an optional `JObject` into Java's `TursoStepResult`. /// /// This function takes an optional `JObject` and converts it into a Java object diff --git a/bindings/java/src/main/java/tech/turso/core/TursoStatement.java b/bindings/java/src/main/java/tech/turso/core/TursoStatement.java index 9c9911623..585d86321 100644 --- a/bindings/java/src/main/java/tech/turso/core/TursoStatement.java +++ b/bindings/java/src/main/java/tech/turso/core/TursoStatement.java @@ -20,7 +20,7 @@ public final class TursoStatement { private final String sql; private final long statementPointer; - private final TursoResultSet resultSet; + private TursoResultSet resultSet; private boolean closed; @@ -290,6 +290,17 @@ public final class TursoStatement { private native int parameterCount(long statementPointer) throws SQLException; + /** Resets this statement so it's ready for re-execution */ + public void reset() throws SQLException { + final int result = reset(statementPointer); + if (result == -1) { + throw new SQLException("Exception while resetting statement"); + } + this.resultSet = TursoResultSet.of(this); + } + + private native int reset(long statementPointer) throws SQLException; + /** * Checks if the statement is closed. * 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 48c5491d2..c3f999070 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java @@ -22,7 +22,7 @@ import tech.turso.core.TursoResultSet; public final class JDBC4PreparedStatement extends JDBC4Statement implements PreparedStatement { private final String sql; - private JDBC4ResultSet resultSet; + private final JDBC4ResultSet resultSet; private final int paramCount; private Object[] currentBatchParams; @@ -44,11 +44,6 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep this.currentBatchParams = new Object[paramCount]; } - private void reprepareStatement() throws SQLException { - this.statement = connection.prepare(sql); - this.resultSet = new JDBC4ResultSet(this.statement.getResultSet()); - } - @Override public ResultSet executeQuery() throws SQLException { // TODO: check bindings etc @@ -362,8 +357,7 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep } for (int i = 0; i < batchQueryParams.size(); i++) { try { - // TODO: do this without creating a new statement because this has unnecessary overhead - reprepareStatement(); + statement.reset(); execute(batchQueryParams.get(i)); updateCounts[i] = getUpdateCount(); } catch (SQLException e) { diff --git a/core/lib.rs b/core/lib.rs index a32e5497b..3862756e3 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -2888,6 +2888,7 @@ impl Statement { &mut self.state.auto_txn_cleanup, ); self.state.reset(max_registers, max_cursors); + self.program.n_change.store(0, Ordering::SeqCst); self.busy = false; self.busy_timeout = None; }