Skip to content

Commit

Permalink
Merge 'bindings/java: Implement JDBC ResultSet' from Kim Seon Woo
Browse files Browse the repository at this point in the history
## Purpose of this PR
Associate jdbc's `ResultSet` with the returned values from limbo's step
function.
## Changes
### Rust
- `Java_org_github_tursodatabase_core_LimboStatement_step` now returns
an object of java's `LimboStepResult.java`
### Java
- Added `LimboStepResult.java` in order to distinguish the type of
`StepResult`(which limbo returns) and to encapsulate the interpretation
of limbo's `StepResult`
- Change `JDBC4ResultSet` inheriting `LimboResultSet` to composition.
IMO when using inheritance, it's too burdensome to fit unmatching parts
together.
- Enhance `JDBC4Statement.java`'s `execute` method
  - By looking at the `ResultSet` created after executing the qury, it's
now able to determine the (boolean) result.
## Reference
- #615

Closes #743
  • Loading branch information
penberg committed Jan 20, 2025
2 parents a338a19 + ddfbf11 commit 39ceddc
Show file tree
Hide file tree
Showing 14 changed files with 446 additions and 99 deletions.
2 changes: 1 addition & 1 deletion bindings/java/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: test build_test

test: build_test
./gradlew test
./gradlew test --info

build_test:
CARGO_TARGET_DIR=src/test/resources/limbo cargo build
62 changes: 54 additions & 8 deletions bindings/java/rs_src/limbo_statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ use jni::sys::jlong;
use jni::JNIEnv;
use limbo_core::{Statement, StepResult};

pub const STEP_RESULT_ID_ROW: i32 = 10;
pub const STEP_RESULT_ID_IO: i32 = 20;
pub const STEP_RESULT_ID_DONE: i32 = 30;
pub const STEP_RESULT_ID_INTERRUPT: i32 = 40;
pub const STEP_RESULT_ID_BUSY: i32 = 50;
pub const STEP_RESULT_ID_ERROR: i32 = 60;

pub struct LimboStatement {
pub(crate) stmt: Statement,
}
Expand Down Expand Up @@ -50,26 +57,26 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboStatement_step<'l

match stmt.stmt.step() {
Ok(StepResult::Row(row)) => match row_to_obj_array(&mut env, &row) {
Ok(row) => row,
Ok(row) => to_limbo_step_result(&mut env, STEP_RESULT_ID_ROW, Some(row)),
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());

JObject::null()
to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None)
}
},
Ok(StepResult::IO) => match env.new_object_array(0, "java/lang/Object", JObject::null()) {
Ok(row) => row.into(),
Ok(row) => to_limbo_step_result(&mut env, STEP_RESULT_ID_IO, Some(row.into())),
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());

JObject::null()
to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None)
}
},
_ => JObject::null(),
Ok(StepResult::Done) => to_limbo_step_result(&mut env, STEP_RESULT_ID_DONE, None),
Ok(StepResult::Interrupt) => to_limbo_step_result(&mut env, STEP_RESULT_ID_INTERRUPT, None),
Ok(StepResult::Busy) => to_limbo_step_result(&mut env, STEP_RESULT_ID_BUSY, None),
_ => to_limbo_step_result(&mut env, STEP_RESULT_ID_ERROR, None),
}
}

#[allow(dead_code)]
fn row_to_obj_array<'local>(
env: &mut JNIEnv<'local>,
row: &limbo_core::Row,
Expand All @@ -96,3 +103,42 @@ fn row_to_obj_array<'local>(

Ok(obj_array.into())
}

/// Converts an optional `JObject` into Java's `LimboStepResult`.
///
/// This function takes an optional `JObject` and converts it into a Java object
/// of type `LimboStepResult`. The conversion is done by creating a new Java object with the
/// appropriate constructor arguments.
///
/// # Arguments
///
/// * `env` - A mutable reference to the JNI environment.
/// * `id` - An integer representing the type of `StepResult`.
/// * `result` - An optional `JObject` that contains the result data.
///
/// # Returns
///
/// A `JObject` representing the `LimboStepResult` in Java. If the object creation fails,
/// a null `JObject` is returned
fn to_limbo_step_result<'local>(
env: &mut JNIEnv<'local>,
id: i32,
result: Option<JObject<'local>>,
) -> JObject<'local> {
let mut ctor_args = vec![JValue::Int(id)];
if let Some(res) = result {
ctor_args.push(JValue::Object(&res));
env.new_object(
"org/github/tursodatabase/core/LimboStepResult",
"(I[Ljava/lang/Object;)V",
&ctor_args,
)
} else {
env.new_object(
"org/github/tursodatabase/core/LimboStepResult",
"(I)V",
&ctor_args,
)
}
.unwrap_or_else(|_| JObject::null())
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

/**
* Annotation to mark methods that are called by native functions.
* For example, throwing exceptions or creating java objects.
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface NativeInvocation {
String invokedFrom() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ public AbstractDB getDatabase() {
* @return Pointer to statement.
* @throws SQLException if a database access error occurs.
*/
public long prepare(String sql) throws SQLException {
public LimboStatement prepare(String sql) throws SQLException {
logger.trace("DriverManager [{}] [SQLite EXEC] {}", Thread.currentThread().getName(), sql);
byte[] sqlBytes = stringToUtf8ByteArray(sql);
if (sqlBytes == null) {
throw new SQLException("Failed to convert " + sql + " into bytes");
}
return prepareUtf8(connectionPtr, sqlBytes);
return new LimboStatement(sql, prepareUtf8(connectionPtr, sqlBytes));
}

private native long prepareUtf8(long connectionPtr, byte[] sqlUtf8) throws SQLException;
Expand Down Expand Up @@ -133,7 +133,7 @@ public void setBusyTimeout(int busyTimeout) {
* @param errorCode Error code.
* @param errorMessageBytes Error message.
*/
@NativeInvocation
@NativeInvocation(invokedFrom = "limbo_connection.rs")
private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public long connect() throws SQLException {
* @param errorCode Error code.
* @param errorMessageBytes Error message.
*/
@NativeInvocation
@NativeInvocation(invokedFrom = "limbo_db.rs")
private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,82 @@

import java.sql.SQLException;

import org.github.tursodatabase.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* JDBC ResultSet.
* A table of data representing limbo database result set, which is generated by executing a statement that queries the
* database.
* <p>
* A {@link LimboResultSet} object is automatically closed when the {@link LimboStatement} object that generated it is
* closed or re-executed.
*/
public abstract class LimboResultSet {
public class LimboResultSet {

private static final Logger log = LoggerFactory.getLogger(LimboResultSet.class);

protected final LimboStatement statement;
private final LimboStatement statement;

// Whether the result set does not have any rows.
protected boolean isEmptyResultSet = false;
private boolean isEmptyResultSet = false;
// If the result set is open. Doesn't mean it has results.
private boolean open = false;
private boolean open;
// Maximum number of rows as set by the statement
protected long maxRows;
private long maxRows;
// number of current row, starts at 1 (0 is used to represent loading data)
protected int row = 0;
private int row = 0;
private boolean pastLastRow = false;

@Nullable
private LimboStepResult lastStepResult;

protected LimboResultSet(LimboStatement statement) {
public static LimboResultSet of(LimboStatement statement) {
return new LimboResultSet(statement);
}

private LimboResultSet(LimboStatement statement) {
this.open = true;
this.statement = statement;
}

/**
* 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 <code>next</code> makes the first row the current row; the second call
* makes the second row the current row, and so on.
* When a call to the <code>next</code> method returns <code>false</code>, the cursor is positioned after the last row.
* <p>
* Note that limbo only supports <code>ResultSet.TYPE_FORWARD_ONLY</code>, which means that the cursor can only move forward.
*/
public boolean next() throws SQLException {
if (!open || isEmptyResultSet || pastLastRow) {
return false; // completed ResultSet
}

if (maxRows != 0 && row == maxRows) {
return false;
}

lastStepResult = this.statement.step();
log.debug("lastStepResult: {}", lastStepResult);
if (lastStepResult.isRow()) {
row++;
}

pastLastRow = lastStepResult.isDone();
if (pastLastRow) {
open = false;
}
return !pastLastRow;
}

/**
* Checks whether the last step result has returned row result.
*/
public boolean hasLastStepReturnedRow() {
return lastStepResult != null && lastStepResult.isRow();
}

/**
* Checks the status of the result set.
*
Expand All @@ -34,9 +90,22 @@ public boolean isOpen() {
/**
* @throws SQLException if not {@link #open}
*/
protected void checkOpen() throws SQLException {
public void checkOpen() throws SQLException {
if (!open) {
throw new SQLException("ResultSet closed");
}
}

@Override
public String toString() {
return "LimboResultSet{" +
"statement=" + statement +
", isEmptyResultSet=" + isEmptyResultSet +
", open=" + open +
", maxRows=" + maxRows +
", row=" + row +
", pastLastRow=" + pastLastRow +
", lastResult=" + lastStepResult +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -1,68 +1,76 @@
package org.github.tursodatabase.core;

import java.sql.SQLException;

import org.github.tursodatabase.annotations.NativeInvocation;
import org.github.tursodatabase.annotations.Nullable;
import org.github.tursodatabase.jdbc4.JDBC4ResultSet;
import org.github.tursodatabase.utils.LimboExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public abstract class LimboStatement {

protected final LimboConnection connection;
protected final LimboResultSet resultSet;

@Nullable
protected String sql = null;
/**
* By default, only one <code>resultSet</code> object per <code>LimboStatement</code> can be open at the same time.
* Therefore, if the reading of one <code>resultSet</code> object is interleaved with the reading of another, each must
* have been generated by different <code>LimboStatement</code> objects. All execution method in the <code>LimboStatement</code>
* implicitly close the current <code>resultSet</code> object of the statement if an open one exists.
*/
public class LimboStatement {
private static final Logger log = LoggerFactory.getLogger(LimboStatement.class);

protected LimboStatement(LimboConnection connection) {
this.connection = connection;
this.resultSet = new JDBC4ResultSet(this);
}
private final String sql;
private final long statementPointer;
private final LimboResultSet resultSet;

protected void internalClose() throws SQLException {
// TODO
// 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) {
this.sql = sql;
this.statementPointer = statementPointer;
this.resultSet = LimboResultSet.of(this);
log.debug("Creating statement with sql: {}", this.sql);
}

protected void clearGeneratedKeys() throws SQLException {
// TODO
public LimboResultSet getResultSet() {
return resultSet;
}

protected void updateGeneratedKeys() throws SQLException {
// TODO
/**
* Expects a clean statement created right after prepare method is called.
*
* @return true if the ResultSet has at least one row; false otherwise.
*/
public boolean execute() throws SQLException {
resultSet.next();
return resultSet.hasLastStepReturnedRow();
}

// TODO: associate the result with CoreResultSet
// TODO: we can make this async!!
// TODO: distinguish queries that return result or doesn't return result
protected List<Object[]> execute(long stmtPointer) throws SQLException {
List<Object[]> result = new ArrayList<>();
while (true) {
Object[] stepResult = step(stmtPointer);
if (stepResult != null) {
for (int i = 0; i < stepResult.length; i++) {
System.out.println("stepResult" + i + ": " + stepResult[i]);
}
}
if (stepResult == null) break;
result.add(stepResult);
LimboStepResult step() throws SQLException {
final LimboStepResult result = step(this.statementPointer);
if (result == null) {
throw new SQLException("step() returned null, which is only returned when an error occurs");
}

return result;
}

private native Object[] step(long stmtPointer) throws SQLException;
@Nullable
private native LimboStepResult step(long stmtPointer) throws SQLException;

/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorCode Error code.
* @param errorMessageBytes Error message.
*/
@NativeInvocation
@NativeInvocation(invokedFrom = "limbo_statement.rs")
private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}

@Override
public String toString() {
return "LimboStatement{" +
"statementPointer=" + statementPointer +
", sql='" + sql + '\'' +
'}';
}
}
Loading

0 comments on commit 39ceddc

Please sign in to comment.