diff --git a/bindings/java/rs_src/limbo_statement.rs b/bindings/java/rs_src/limbo_statement.rs index 7de4b2c19..aed8e7d99 100644 --- a/bindings/java/rs_src/limbo_statement.rs +++ b/bindings/java/rs_src/limbo_statement.rs @@ -29,7 +29,6 @@ impl LimboStatement { Box::into_raw(Box::new(self)) as jlong } - #[allow(dead_code)] pub fn drop(ptr: jlong) { let _boxed = unsafe { Box::from_raw(ptr as *mut LimboStatement) }; } @@ -88,6 +87,15 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboStatement_step<'l } } +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_core_LimboStatement__1close<'local>( + _env: JNIEnv<'local>, + _obj: JObject<'local>, + stmt_ptr: jlong, +) { + LimboStatement::drop(stmt_ptr); +} + fn row_to_obj_array<'local>( env: &mut JNIEnv<'local>, row: &limbo_core::Row, diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java index c6cb8d00e..b226c53d0 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java @@ -1,5 +1,6 @@ package org.github.tursodatabase.core; +import java.sql.ResultSet; import java.sql.SQLException; import org.github.tursodatabase.annotations.Nullable; import org.slf4j.Logger; @@ -39,6 +40,20 @@ public class LimboResultSet { this.statement = statement; } + /** + * Consumes all the rows in this {@link ResultSet} until the {@link #next()} method returns + * `false`. + * + * @throws SQLException if the result set is not open or if an error occurs while iterating. + */ + public void consumeAll() throws SQLException { + if (!open) { + throw new SQLException("The result set is not open"); + } + + while (next()) {} + } + /** * Moves the cursor forward one row from its current position. A {@link LimboResultSet} cursor is * initially positioned before the first fow; the first call to the method next makes @@ -50,7 +65,11 @@ public class LimboResultSet { * cursor can only move forward. */ public boolean next() throws SQLException { - if (!open || isEmptyResultSet || pastLastRow) { + if (!open) { + throw new SQLException("The resultSet is not open"); + } + + if (isEmptyResultSet || pastLastRow) { return false; // completed ResultSet } @@ -70,9 +89,6 @@ public class LimboResultSet { } pastLastRow = lastStepResult.isDone(); - if (pastLastRow) { - open = false; - } return !pastLastRow; } @@ -97,6 +113,10 @@ public class LimboResultSet { } } + public void close() throws SQLException { + this.open = false; + } + @Override public String toString() { return "LimboResultSet{" diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java index c749e27cc..8566c403e 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java @@ -21,6 +21,8 @@ public class LimboStatement { private final long statementPointer; private final LimboResultSet resultSet; + private boolean closed; + // TODO: what if the statement we ran was DDL, update queries and etc. Should we still create a // resultSet? public LimboStatement(String sql, long statementPointer) { @@ -67,6 +69,30 @@ public class LimboStatement { LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes); } + /** + * Closes the current statement and releases any resources associated with it. This method calls + * the native `_close` method to perform the actual closing operation. + */ + public void close() throws SQLException { + if (closed) { + return; + } + this.resultSet.close(); + _close(statementPointer); + closed = true; + } + + private native void _close(long statementPointer); + + /** + * Checks if the statement is closed. + * + * @return true if the statement is closed, false otherwise. + */ + public boolean isClosed() { + return closed; + } + @Override public String toString() { return "LimboStatement{" diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java index 867b2688e..092bf4d44 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java @@ -25,7 +25,7 @@ public class JDBC4ResultSet implements ResultSet { @Override public void close() throws SQLException { - // TODO + resultSet.close(); } @Override @@ -866,8 +866,7 @@ public class JDBC4ResultSet implements ResultSet { @Override public boolean isClosed() throws SQLException { - // TODO - return false; + return !resultSet.isOpen(); } @Override diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java index eee4c95a3..7cbf6f69d 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java @@ -19,6 +19,8 @@ public class JDBC4Statement implements Statement { private final LimboConnection connection; @Nullable private LimboStatement statement = null; + // Because JDBC4Statement has different life cycle in compared to LimboStatement, let's use this + // field to manage JDBC4Statement lifecycle private boolean closed; private boolean closeOnCompletion; @@ -65,9 +67,7 @@ public class JDBC4Statement implements Statement { requireNonNull(statement, "statement should not be null after running execute method"); final LimboResultSet resultSet = statement.getResultSet(); - while (resultSet.isOpen()) { - resultSet.next(); - } + resultSet.consumeAll(); // TODO: return update count; return 0; @@ -75,8 +75,14 @@ public class JDBC4Statement implements Statement { @Override public void close() throws SQLException { - clearGeneratedKeys(); - internalClose(); + if (closed) { + return; + } + + if (this.statement != null) { + this.statement.close(); + } + closed = true; } @@ -150,8 +156,7 @@ public class JDBC4Statement implements Statement { */ @Override public boolean execute(String sql) throws SQLException { - internalClose(); - + ensureOpen(); return this.withConnectionTimeout( () -> { try { @@ -298,8 +303,7 @@ public class JDBC4Statement implements Statement { @Override public boolean isClosed() throws SQLException { - // TODO - return false; + return this.closed; } @Override @@ -346,14 +350,6 @@ public class JDBC4Statement implements Statement { return false; } - protected void internalClose() throws SQLException { - // TODO - } - - protected void clearGeneratedKeys() throws SQLException { - // TODO - } - protected void updateGeneratedKeys() throws SQLException { // TODO } @@ -378,4 +374,10 @@ public class JDBC4Statement implements Statement { protected interface SQLCallable { T call() throws SQLException; } + + private void ensureOpen() throws SQLException { + if (closed) { + throw new SQLException("Statement is closed"); + } + } } diff --git a/bindings/java/src/test/java/org/github/tursodatabase/core/LimboStatementTest.java b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboStatementTest.java new file mode 100644 index 000000000..fe274b07e --- /dev/null +++ b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboStatementTest.java @@ -0,0 +1,31 @@ +package org.github.tursodatabase.core; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Properties; +import org.github.tursodatabase.TestUtils; +import org.github.tursodatabase.jdbc4.JDBC4Connection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LimboStatementTest { + + private JDBC4Connection connection; + + @BeforeEach + void setUp() throws Exception { + String filePath = TestUtils.createTempFile(); + String url = "jdbc:sqlite:" + filePath; + connection = new JDBC4Connection(url, filePath, new Properties()); + } + + @Test + void closing_statement_closes_related_resources() throws Exception { + LimboStatement stmt = connection.prepare("SELECT 1;"); + stmt.execute(); + + stmt.close(); + assertTrue(stmt.isClosed()); + assertFalse(stmt.getResultSet().isOpen()); + } +} diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index f764a9361..a16c096c9 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -1,9 +1,11 @@ package org.github.tursodatabase.jdbc4; 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.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.github.tursodatabase.TestUtils; @@ -57,4 +59,24 @@ class JDBC4ResultSetTest { // as well assertFalse(resultSet.next()); } + + @Test + void close_resultSet_test() throws Exception { + stmt.executeQuery("SELECT 1;"); + ResultSet resultSet = stmt.getResultSet(); + + assertFalse(resultSet.isClosed()); + resultSet.close(); + assertTrue(resultSet.isClosed()); + } + + @Test + void calling_methods_on_closed_resultSet_should_throw_exception() throws Exception { + stmt.executeQuery("SELECT 1;"); + ResultSet resultSet = stmt.getResultSet(); + resultSet.close(); + assertTrue(resultSet.isClosed()); + + assertThrows(SQLException.class, resultSet::next); + } } diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4StatementTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4StatementTest.java index 2a837629d..a48cedea9 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4StatementTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4StatementTest.java @@ -3,6 +3,7 @@ package org.github.tursodatabase.jdbc4; import static org.junit.jupiter.api.Assertions.*; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.github.tursodatabase.TestUtils; @@ -51,4 +52,22 @@ class JDBC4StatementTest { stmt.execute("INSERT INTO users VALUES (1, 'limbo');"); assertTrue(stmt.execute("SELECT * FROM users;")); } + + @Test + void close_statement_test() throws Exception { + stmt.close(); + assertTrue(stmt.isClosed()); + } + + @Test + void double_close_is_no_op() throws SQLException { + stmt.close(); + assertDoesNotThrow(() -> stmt.close()); + } + + @Test + void operations_on_closed_statement_should_throw_exception() throws Exception { + stmt.close(); + assertThrows(SQLException.class, () -> stmt.execute("SELECT 1;")); + } }