Merge 'bindings/java: Implement JDBC ResultSet' from Kim Seon Woo

## 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
- https://github.com/tursodatabase/limbo/issues/615

Closes #743
This commit is contained in:
Pekka Enberg
2025-01-20 09:33:33 +02:00
14 changed files with 450 additions and 103 deletions

View File

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

View File

@@ -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,
}
@@ -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,
@@ -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())
}

View File

@@ -8,8 +8,10 @@ import java.lang.annotation.Target;
/**
* 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 "";
}

View File

@@ -79,13 +79,13 @@ public abstract class LimboConnection implements Connection {
* @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;
@@ -133,7 +133,7 @@ public abstract class LimboConnection implements Connection {
* @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);
}

View File

@@ -113,7 +113,7 @@ public final class LimboDB extends AbstractDB {
* @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);
}

View File

@@ -2,26 +2,82 @@ package org.github.tursodatabase.core;
import java.sql.SQLException;
/**
* JDBC ResultSet.
*/
public abstract class LimboResultSet {
import org.github.tursodatabase.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
protected final LimboStatement statement;
/**
* 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 class LimboResultSet {
private static final Logger log = LoggerFactory.getLogger(LimboResultSet.class);
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;
protected LimboResultSet(LimboStatement statement) {
@Nullable
private LimboStepResult lastStepResult;
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.
*
@@ -34,9 +90,22 @@ public abstract class LimboResultSet {
/**
* @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 +
'}';
}
}

View File

@@ -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;
/**
* 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);
public abstract class LimboStatement {
private final String sql;
private final long statementPointer;
private final LimboResultSet resultSet;
protected final LimboConnection connection;
protected final LimboResultSet resultSet;
@Nullable
protected String sql = null;
protected LimboStatement(LimboConnection connection) {
this.connection = connection;
this.resultSet = new JDBC4ResultSet(this);
// 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 internalClose() throws SQLException {
// TODO
public LimboResultSet getResultSet() {
return resultSet;
}
protected void clearGeneratedKeys() 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();
}
protected void updateGeneratedKeys() throws SQLException {
// TODO
}
// 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 + '\'' +
'}';
}
}

View File

@@ -0,0 +1,70 @@
package org.github.tursodatabase.core;
import java.util.Arrays;
import org.github.tursodatabase.annotations.NativeInvocation;
import org.github.tursodatabase.annotations.Nullable;
/**
* Represents the step result of limbo's statement's step function.
*/
public class LimboStepResult {
private static final int STEP_RESULT_ID_ROW = 10;
private static final int STEP_RESULT_ID_IO = 20;
private static final int STEP_RESULT_ID_DONE = 30;
private static final int STEP_RESULT_ID_INTERRUPT = 40;
private static final int STEP_RESULT_ID_BUSY = 50;
private static final int STEP_RESULT_ID_ERROR = 60;
// Identifier for limbo's StepResult
private final int stepResultId;
@Nullable
private final Object[] result;
@NativeInvocation(invokedFrom = "limbo_statement.rs")
public LimboStepResult(int stepResultId) {
this.stepResultId = stepResultId;
this.result = null;
}
@NativeInvocation(invokedFrom = "limbo_statement.rs")
public LimboStepResult(int stepResultId, Object[] result) {
this.stepResultId = stepResultId;
this.result = result;
}
public boolean isRow() {
return stepResultId == STEP_RESULT_ID_ROW;
}
public boolean isDone() {
return stepResultId == STEP_RESULT_ID_DONE;
}
@Override
public String toString() {
return "LimboStepResult{" +
"stepResultName=" + getStepResultName() +
", result=" + Arrays.toString(result) +
'}';
}
private String getStepResultName() {
switch (stepResultId) {
case STEP_RESULT_ID_ROW:
return "ROW";
case STEP_RESULT_ID_IO:
return "IO";
case STEP_RESULT_ID_DONE:
return "DONE";
case STEP_RESULT_ID_INTERRUPT:
return "INTERRUPT";
case STEP_RESULT_ID_BUSY:
return "BUSY";
case STEP_RESULT_ID_ERROR:
return "ERROR";
default:
return "UNKNOWN";
}
}
}

View File

@@ -2,7 +2,6 @@ package org.github.tursodatabase.jdbc4;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import org.github.tursodatabase.core.LimboResultSet;
import org.github.tursodatabase.core.LimboStatement;
import java.io.InputStream;
import java.io.Reader;
@@ -12,16 +11,17 @@ import java.sql.*;
import java.util.Calendar;
import java.util.Map;
public class JDBC4ResultSet extends LimboResultSet implements ResultSet {
public class JDBC4ResultSet implements ResultSet {
public JDBC4ResultSet(LimboStatement statement) {
super(statement);
private final LimboResultSet resultSet;
public JDBC4ResultSet(LimboResultSet resultSet) {
this.resultSet = resultSet;
}
@Override
public boolean next() throws SQLException {
// TODO
return false;
return resultSet.next();
}
@Override

View File

@@ -1,18 +1,25 @@
package org.github.tursodatabase.jdbc4;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import org.github.tursodatabase.core.LimboConnection;
import org.github.tursodatabase.core.LimboStatement;
import static java.util.Objects.requireNonNull;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.concurrent.locks.ReentrantLock;
/**
* Implementation of the {@link Statement} interface for JDBC 4.
*/
public class JDBC4Statement extends LimboStatement implements Statement {
import org.github.tursodatabase.annotations.Nullable;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import org.github.tursodatabase.core.LimboConnection;
import org.github.tursodatabase.core.LimboResultSet;
import org.github.tursodatabase.core.LimboStatement;
public class JDBC4Statement implements Statement {
private final LimboConnection connection;
@Nullable
private LimboStatement statement = null;
private boolean closed;
private boolean closeOnCompletion;
@@ -28,26 +35,37 @@ public class JDBC4Statement extends LimboStatement implements Statement {
private ReentrantLock connectionLock = new ReentrantLock();
public JDBC4Statement(LimboConnection connection) {
this(connection, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT);
this(connection, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY,
ResultSet.CLOSE_CURSORS_AT_COMMIT);
}
public JDBC4Statement(LimboConnection connection, int resultSetType, int resultSetConcurrency, int resultSetHoldability) {
super(connection);
public JDBC4Statement(LimboConnection connection, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) {
this.connection = connection;
this.resultSetType = resultSetType;
this.resultSetConcurrency = resultSetConcurrency;
this.resultSetHoldability = resultSetHoldability;
}
@Override
@SkipNullableCheck
public ResultSet executeQuery(String sql) throws SQLException {
// TODO
return null;
execute(sql);
requireNonNull(statement, "statement should not be null after running execute method");
return new JDBC4ResultSet(statement.getResultSet());
}
@Override
public int executeUpdate(String sql) throws SQLException {
// TODO
execute(sql);
requireNonNull(statement, "statement should not be null after running execute method");
final LimboResultSet resultSet = statement.getResultSet();
while (resultSet.isOpen()) {
resultSet.next();
}
// TODO: return update count;
return 0;
}
@@ -121,6 +139,13 @@ public class JDBC4Statement extends LimboStatement implements Statement {
// TODO
}
/**
* The <code>execute</code> method executes an SQL statement and indicates the
* form of the first result. You must then use the methods
* <code>getResultSet</code> or <code>getUpdateCount</code>
* to retrieve the result, and <code>getMoreResults</code> to
* move to any subsequent result(s).
*/
@Override
public boolean execute(String sql) throws SQLException {
internalClose();
@@ -128,12 +153,14 @@ public class JDBC4Statement extends LimboStatement implements Statement {
return this.withConnectionTimeout(
() -> {
try {
// TODO: if sql is a readOnly query, do we still need the locks?
connectionLock.lock();
final long stmtPointer = connection.prepare(sql);
List<Object[]> result = execute(stmtPointer);
statement = connection.prepare(sql);
final boolean result = statement.execute();
updateGeneratedKeys();
exhaustedResults = false;
return !result.isEmpty();
return result;
} finally {
connectionLock.unlock();
}
@@ -142,10 +169,9 @@ public class JDBC4Statement extends LimboStatement implements Statement {
}
@Override
@SkipNullableCheck
public ResultSet getResultSet() throws SQLException {
// TODO
return null;
requireNonNull(statement, "statement is null");
return new JDBC4ResultSet(statement.getResultSet());
}
@Override
@@ -288,7 +314,7 @@ public class JDBC4Statement extends LimboStatement implements Statement {
@Override
public void closeOnCompletion() throws SQLException {
if (closed) throw new SQLException("statement is closed");
if (closed) {throw new SQLException("statement is closed");}
closeOnCompletion = true;
}
@@ -297,7 +323,7 @@ public class JDBC4Statement extends LimboStatement implements Statement {
*/
@Override
public boolean isCloseOnCompletion() throws SQLException {
if (closed) throw new SQLException("statement is closed");
if (closed) {throw new SQLException("statement is closed");}
return closeOnCompletion;
}
@@ -314,6 +340,18 @@ public class JDBC4Statement extends LimboStatement implements Statement {
return false;
}
protected void internalClose() throws SQLException {
// TODO
}
protected void clearGeneratedKeys() throws SQLException {
// TODO
}
protected void updateGeneratedKeys() throws SQLException {
// TODO
}
private <T> T withConnectionTimeout(SQLCallable<T> callable) throws SQLException {
final int originalBusyTimeoutMillis = connection.getBusyTimeout();
if (queryTimeoutSeconds > 0) {

View File

@@ -1,18 +1,18 @@
package org.github.tursodatabase.utils;
import static org.github.tursodatabase.utils.ByteArrayUtils.utf8ByteBufferToString;
import java.sql.SQLException;
import org.github.tursodatabase.LimboErrorCode;
import org.github.tursodatabase.annotations.Nullable;
import org.github.tursodatabase.exceptions.LimboException;
import java.sql.SQLException;
import static org.github.tursodatabase.utils.ByteArrayUtils.utf8ByteBufferToString;
public class LimboExceptionUtils {
/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorCode Error code.
* @param errorMessageBytes Error message.
*/
public static void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
@@ -23,10 +23,11 @@ public class LimboExceptionUtils {
/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorCode Error code.
* @param errorMessage Error message.
*/
public static LimboException buildLimboException(int errorCode, @Nullable String errorMessage) throws SQLException {
public static LimboException buildLimboException(int errorCode, @Nullable String errorMessage)
throws SQLException {
LimboErrorCode code = LimboErrorCode.getErrorCode(errorCode);
String msg;
if (code == LimboErrorCode.UNKNOWN_ERROR) {

View File

@@ -22,7 +22,6 @@ public class IntegrationTest {
}
@Test
@Disabled("Doesn't work on workflow. Need investigation.")
void create_table_multi_inserts_select() throws Exception {
Statement stmt = createDefaultStatement();
stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);");

View File

@@ -0,0 +1,61 @@
package org.github.tursodatabase.jdbc4;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import org.github.tursodatabase.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class JDBC4ResultSetTest {
private Statement stmt;
@BeforeEach
void setUp() throws Exception {
String filePath = TestUtils.createTempFile();
String url = "jdbc:sqlite:" + filePath;
final JDBC4Connection connection = new JDBC4Connection(url, filePath, new Properties());
stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY,
ResultSet.CLOSE_CURSORS_AT_COMMIT);
}
@Test
@Disabled("https://github.com/tursodatabase/limbo/pull/743#issuecomment-2600746904")
void invoking_next_before_the_last_row_should_return_true() throws Exception {
stmt.executeUpdate("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);");
stmt.executeUpdate("INSERT INTO users VALUES (1, 'sinwoo');");
stmt.executeUpdate("INSERT INTO users VALUES (2, 'seonwoo');");
// first call to next occur internally
stmt.executeQuery("SELECT * FROM users");
ResultSet resultSet = stmt.getResultSet();
assertTrue(resultSet.next());
}
@Test
@Disabled("https://github.com/tursodatabase/limbo/pull/743#issuecomment-2600746904")
void invoking_next_after_the_last_row_should_return_false() throws Exception {
stmt.executeUpdate("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);");
stmt.executeUpdate("INSERT INTO users VALUES (1, 'sinwoo');");
stmt.executeUpdate("INSERT INTO users VALUES (2, 'seonwoo');");
// first call to next occur internally
stmt.executeQuery("SELECT * FROM users");
ResultSet resultSet = stmt.getResultSet();
while (resultSet.next()) {
// run until next() returns false
}
// if the previous call to next() returned false, consecutive call to next() should return false as well
assertFalse(resultSet.next());
}
}

View File

@@ -0,0 +1,53 @@
package org.github.tursodatabase.jdbc4;
import static org.junit.jupiter.api.Assertions.*;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import org.github.tursodatabase.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class JDBC4StatementTest {
private Statement stmt;
@BeforeEach
void setUp() throws Exception {
String filePath = TestUtils.createTempFile();
String url = "jdbc:sqlite:" + filePath;
final JDBC4Connection connection = new JDBC4Connection(url, filePath, new Properties());
stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY,
ResultSet.CLOSE_CURSORS_AT_COMMIT);
}
@Test
void execute_ddl_should_return_false() throws Exception{
assertFalse(stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);"));
}
@Test
void execute_insert_should_return_false() throws Exception {
stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);");
assertFalse(stmt.execute("INSERT INTO users VALUES (1, 'limbo');"));
}
@Test
@Disabled("UPDATE not supported yet")
void execute_update_should_return_false() throws Exception {
stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);");
stmt.execute("INSERT INTO users VALUES (1, 'limbo');");
assertFalse(stmt.execute("UPDATE users SET username = 'seonwoo' WHERE id = 1;"));
}
@Test
void execute_select_should_return_true() throws Exception {
stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);");
stmt.execute("INSERT INTO users VALUES (1, 'limbo');");
assertTrue(stmt.execute("SELECT * FROM users;"));
}
}