diff --git a/bindings/java/Makefile b/bindings/java/Makefile index 5d091145d..6d303c38a 100644 --- a/bindings/java/Makefile +++ b/bindings/java/Makefile @@ -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 diff --git a/bindings/java/rs_src/limbo_statement.rs b/bindings/java/rs_src/limbo_statement.rs index 78eff1fc4..cdd8a5c75 100644 --- a/bindings/java/rs_src/limbo_statement.rs +++ b/bindings/java/rs_src/limbo_statement.rs @@ -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> { + 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()) +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/annotations/NativeInvocation.java b/bindings/java/src/main/java/org/github/tursodatabase/annotations/NativeInvocation.java index d3a905608..8f57c1bee 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/annotations/NativeInvocation.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/annotations/NativeInvocation.java @@ -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 ""; } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java index d6c3ab6af..8c77424b1 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboConnection.java @@ -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); } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java index 6229156ea..89d13b8cf 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboDB.java @@ -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); } 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 5d8b495ba..19d730727 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 @@ -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. + *

+ * 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 next 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 next method returns false, the cursor is positioned after the last row. + *

+ * Note that limbo only supports ResultSet.TYPE_FORWARD_ONLY, 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 + + '}'; + } } 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 592593df4..747c68c2e 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 @@ -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 resultSet object per LimboStatement can be open at the same time. + * Therefore, if the reading of one resultSet object is interleaved with the reading of another, each must + * have been generated by different LimboStatement objects. All execution method in the LimboStatement + * implicitly close the current resultSet 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 execute(long stmtPointer) throws SQLException { - List 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 + '\'' + + '}'; + } } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java new file mode 100644 index 000000000..7870cbeab --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java @@ -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"; + } + } +} 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 07bf6ed92..0d7cce084 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 @@ -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 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 4a06d20a1..24a751857 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 @@ -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 execute method executes an SQL statement and indicates the + * form of the first result. You must then use the methods + * getResultSet or getUpdateCount + * to retrieve the result, and getMoreResults 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 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 withConnectionTimeout(SQLCallable callable) throws SQLException { final int originalBusyTimeoutMillis = connection.getBusyTimeout(); if (queryTimeoutSeconds > 0) { diff --git a/bindings/java/src/main/java/org/github/tursodatabase/utils/LimboExceptionUtils.java b/bindings/java/src/main/java/org/github/tursodatabase/utils/LimboExceptionUtils.java index 9a45db040..1525fafc8 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/utils/LimboExceptionUtils.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/utils/LimboExceptionUtils.java @@ -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) { diff --git a/bindings/java/src/test/java/org/github/tursodatabase/IntegrationTest.java b/bindings/java/src/test/java/org/github/tursodatabase/IntegrationTest.java index 873c41476..be25ffdff 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/IntegrationTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/IntegrationTest.java @@ -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);"); 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 new file mode 100644 index 000000000..e717232a8 --- /dev/null +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -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()); + } +} 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 new file mode 100644 index 000000000..f81e9d482 --- /dev/null +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4StatementTest.java @@ -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;")); + } +}