diff --git a/bindings/java/Makefile b/bindings/java/Makefile
index e91fcc923..a8e979dbc 100644
--- a/bindings/java/Makefile
+++ b/bindings/java/Makefile
@@ -1,7 +1,7 @@
-java_run: lib
- export LIMBO_SYSTEM_PATH=../../target/debug && ./gradlew run
-
.PHONY: lib
-lib:
- cargo build
+run_test: build_test
+ ./gradlew test
+
+build_test:
+ CARGO_TARGET_DIR=src/test/resources/limbo cargo build
diff --git a/bindings/java/build.gradle.kts b/bindings/java/build.gradle.kts
index 331b4831f..f1349859d 100644
--- a/bindings/java/build.gradle.kts
+++ b/bindings/java/build.gradle.kts
@@ -13,6 +13,7 @@ repositories {
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
+ testImplementation("org.assertj:assertj-core:3.27.0")
}
application {
@@ -28,4 +29,6 @@ application {
tasks.test {
useJUnitPlatform()
+ // In order to find rust built file under resources, we need to set it as system path
+ systemProperty("java.library.path", "${System.getProperty("java.library.path")}:$projectDir/src/test/resources/limbo/debug")
}
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/LimboErrorCode.java b/bindings/java/src/main/java/org/github/tursodatabase/LimboErrorCode.java
new file mode 100644
index 000000000..0c65ba04f
--- /dev/null
+++ b/bindings/java/src/main/java/org/github/tursodatabase/LimboErrorCode.java
@@ -0,0 +1,34 @@
+package org.github.tursodatabase;
+
+public enum LimboErrorCode {
+ UNKNOWN_ERROR(-1, "Unknown error"),
+ ETC(9999, "Unclassified error");
+
+ public final int code;
+ public final String message;
+
+ /**
+ * @param code Error code
+ * @param message Message for the error.
+ */
+ LimboErrorCode(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public static LimboErrorCode getErrorCode(int errorCode) {
+ for (LimboErrorCode limboErrorCode: LimboErrorCode.values()) {
+ if (errorCode == limboErrorCode.code) return limboErrorCode;
+ }
+
+ return UNKNOWN_ERROR;
+ }
+
+ @Override
+ public String toString() {
+ return "LimboErrorCode{" +
+ "code=" + code +
+ ", message='" + message + '\'' +
+ '}';
+ }
+}
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/Main.java b/bindings/java/src/main/java/org/github/tursodatabase/Main.java
deleted file mode 100644
index de9b94e36..000000000
--- a/bindings/java/src/main/java/org/github/tursodatabase/Main.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.github.tursodatabase;
-
-import org.github.tursodatabase.limbo.Connection;
-import org.github.tursodatabase.limbo.Cursor;
-import org.github.tursodatabase.limbo.Limbo;
-
-/**
- * TODO: Remove Main class. We can use test code to verify behaviors.
- */
-public class Main {
- public static void main(String[] args) throws Exception {
- Limbo limbo = Limbo.create();
- Connection connection = limbo.getConnection("database.db");
-
- Cursor cursor = connection.cursor();
- cursor.execute("SELECT * FROM example_table;");
- System.out.println("result: " + cursor.fetchOne());
- }
-}
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/NativeInvocation.java b/bindings/java/src/main/java/org/github/tursodatabase/NativeInvocation.java
new file mode 100644
index 000000000..ee91caf53
--- /dev/null
+++ b/bindings/java/src/main/java/org/github/tursodatabase/NativeInvocation.java
@@ -0,0 +1,15 @@
+package org.github.tursodatabase;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark methods that are called by native functions.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface NativeInvocation {
+}
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/DB.java b/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java
similarity index 73%
rename from bindings/java/src/main/java/org/github/tursodatabase/core/DB.java
rename to bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java
index 4d82a7a92..adea4cb20 100644
--- a/bindings/java/src/main/java/org/github/tursodatabase/core/DB.java
+++ b/bindings/java/src/main/java/org/github/tursodatabase/core/AbstractDB.java
@@ -1,5 +1,9 @@
package org.github.tursodatabase.core;
+import org.github.tursodatabase.LimboErrorCode;
+import org.github.tursodatabase.NativeInvocation;
+import org.github.tursodatabase.exceptions.LimboException;
+
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -10,18 +14,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
* are not only to provide functionality, but to handle contractual
* differences between the JDBC specification and the Limbo API.
*/
-public abstract class DB {
+public abstract class AbstractDB {
private final String url;
private final String fileName;
private final AtomicBoolean closed = new AtomicBoolean(true);
- public DB(String url, String fileName) throws SQLException {
+ public AbstractDB(String url, String filaName) throws SQLException {
this.url = url;
- this.fileName = fileName;
- }
-
- public String getUrl() {
- return url;
+ this.fileName = filaName;
}
public boolean isClosed() {
@@ -36,7 +36,7 @@ public abstract class DB {
/**
* Executes an SQL statement.
*
- * @param sql SQL statement to be executed.
+ * @param sql SQL statement to be executed.
* @param autoCommit Whether to auto-commit the transaction.
* @throws SQLException if a database access error occurs.
*/
@@ -47,17 +47,16 @@ public abstract class DB {
/**
* Creates an SQLite interface to a database for the given connection.
- * @see SQLite Open Flags
*
- * @param fileName The database.
* @param openFlags Flags for opening the database.
* @throws SQLException if a database access error occurs.
*/
- public final synchronized void open(String fileName, int openFlags) throws SQLException {
- // TODO: add implementation
- throw new SQLFeatureNotSupportedException();
+ public final synchronized void open(int openFlags) throws SQLException {
+ _open(fileName, openFlags);
}
+ protected abstract void _open(String fileName, int openFlags) throws SQLException;
+
/**
* Closes a database connection and finalizes any remaining statements before the closing
* operation.
@@ -95,13 +94,13 @@ public abstract class DB {
/**
* Creates an SQLite interface to a database with the provided open flags.
- * @see SQLite Open Flags
*
- * @param filename The database to open.
+ * @param fileName The database to open.
* @param openFlags Flags for opening the database.
+ * @return pointer to database instance
* @throws SQLException if a database access error occurs.
*/
- protected abstract void _open(String filename, int openFlags) throws SQLException;
+ protected abstract long _open_utf8(byte[] fileName, int openFlags) throws SQLException;
/**
* Closes the SQLite interface to a database.
@@ -173,4 +172,35 @@ public abstract class DB {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
}
+
+ /**
+ * Throws SQL Exception with error code.
+ *
+ * @param errorCode Error code to be passed.
+ * @throws SQLException Formatted SQLException with error code
+ */
+ @NativeInvocation
+ private LimboException newSQLException(int errorCode, long errorMessagePointer) throws SQLException {
+ throw newSQLException(errorCode, getErrorMessage(errorMessagePointer));
+ }
+
+ /**
+ * Throws formatted SQLException with error code and message.
+ *
+ * @param errorCode Error code to be passed.
+ * @param errorMessage throw newSQLException(errorCode);Error message to be passed.
+ * @return Formatted SQLException with error code and message.
+ */
+ public static LimboException newSQLException(int errorCode, String errorMessage) {
+ LimboErrorCode code = LimboErrorCode.getErrorCode(errorCode);
+ String msg;
+ if (code == LimboErrorCode.UNKNOWN_ERROR) {
+ msg = String.format("%s:%s (%s)", code, errorCode, errorMessage);
+ } else {
+ msg = String.format("%s (%s)", code, errorMessage);
+ }
+ return new LimboException(msg, code);
+ }
+
+ protected abstract String getErrorMessage(long errorMessagePointer);
}
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 095da6910..2ed082ce6 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
@@ -1,65 +1,67 @@
package org.github.tursodatabase.core;
+import org.github.tursodatabase.LimboErrorCode;
+
+import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
/**
* This class provides a thin JNI layer over the SQLite3 C API.
*/
-public final class LimboDB extends DB {
- /**
- * SQLite connection handle.
- */
- private long pointer = 0;
+public final class LimboDB extends AbstractDB {
+
+ // Pointer to database instance
+ private long dbPtr;
+ private boolean isOpen;
private static boolean isLoaded;
- private static boolean loadSucceeded;
static {
if ("The Android Project".equals(System.getProperty("java.vm.vendor"))) {
- System.loadLibrary("sqlitejdbc");
- isLoaded = true;
- loadSucceeded = true;
+ // TODO
} else {
// continue with non Android execution path
isLoaded = false;
- loadSucceeded = false;
}
}
+ // url example: "jdbc:sqlite:{fileName}
+
+ /**
+ *
+ * @param url e.g. "jdbc:sqlite:fileName
+ * @param fileName e.g. path to file
+ */
+ public static LimboDB create(String url, String fileName) throws SQLException {
+ return new LimboDB(url, fileName);
+ }
+
// TODO: receive config as argument
- public LimboDB(String url, String fileName) throws SQLException {
+ private LimboDB(String url, String fileName) throws SQLException {
super(url, fileName);
}
/**
* Loads the SQLite interface backend.
- *
- * @return True if the SQLite JDBC driver is successfully loaded; false otherwise.
*/
- public static boolean load() throws Exception {
- if (isLoaded) return loadSucceeded;
+ public void load() {
+ if (isLoaded) return;
try {
System.loadLibrary("_limbo_java");
- loadSucceeded = true;
+
} finally {
isLoaded = true;
}
- return loadSucceeded;
}
// WRAPPER FUNCTIONS ////////////////////////////////////////////
- @Override
- protected synchronized void _open(String file, int openFlags) throws SQLException {
- // TODO: add implementation
- throw new SQLFeatureNotSupportedException();
- }
-
// TODO: add support for JNI
- synchronized native void _open_utf8(byte[] fileUtf8, int openFlags) throws SQLException;
+ @Override
+ protected synchronized native long _open_utf8(byte[] file, int openFlags) throws SQLException;
// TODO: add support for JNI
@Override
@@ -78,6 +80,15 @@ public final class LimboDB extends DB {
@Override
public native void interrupt();
+ @Override
+ protected void _open(String fileName, int openFlags) throws SQLException {
+ if (isOpen) {
+ throw newSQLException(LimboErrorCode.UNKNOWN_ERROR.code, "Already opened");
+ }
+ dbPtr = _open_utf8(stringToUtf8ByteArray(fileName), openFlags);
+ isOpen = true;
+ }
+
@Override
protected synchronized SafeStmtPtr prepare(String sql) throws SQLException {
// TODO: add implementation
@@ -91,4 +102,26 @@ public final class LimboDB extends DB {
// TODO: add support for JNI
@Override
public synchronized native int step(long stmt);
+
+ @Override
+ protected String getErrorMessage(long errorMessagePointer) {
+ return utf8ByteBufferToString(getErrorMessageUtf8(errorMessagePointer));
+ }
+
+ private native byte[] getErrorMessageUtf8(long errorMessagePointer);
+
+ private static String utf8ByteBufferToString(byte[] buffer) {
+ if (buffer == null) {
+ return null;
+ }
+
+ return new String(buffer, StandardCharsets.UTF_8);
+ }
+
+ private static byte[] stringToUtf8ByteArray(String str) {
+ if (str == null) {
+ return null;
+ }
+ return str.getBytes(StandardCharsets.UTF_8);
+ }
}
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/exceptions/LimboException.java b/bindings/java/src/main/java/org/github/tursodatabase/exceptions/LimboException.java
new file mode 100644
index 000000000..d4526a818
--- /dev/null
+++ b/bindings/java/src/main/java/org/github/tursodatabase/exceptions/LimboException.java
@@ -0,0 +1,18 @@
+package org.github.tursodatabase.exceptions;
+
+import org.github.tursodatabase.LimboErrorCode;
+
+import java.sql.SQLException;
+
+public class LimboException extends SQLException {
+ private final LimboErrorCode resultCode;
+
+ public LimboException(String message, LimboErrorCode resultCode) {
+ super(message, null, resultCode.code & 0xff);
+ this.resultCode = resultCode;
+ }
+
+ public LimboErrorCode getResultCode() {
+ return resultCode;
+ }
+}
diff --git a/bindings/java/src/test/java/org/github/tursodatabase/TestUtils.java b/bindings/java/src/test/java/org/github/tursodatabase/TestUtils.java
new file mode 100644
index 000000000..0d7e64488
--- /dev/null
+++ b/bindings/java/src/test/java/org/github/tursodatabase/TestUtils.java
@@ -0,0 +1,13 @@
+package org.github.tursodatabase;
+
+import java.io.IOException;
+import java.nio.file.Files;
+
+public class TestUtils {
+ /**
+ * Create temporary file and returns the path.
+ */
+ public static String createTempFile() throws IOException {
+ return Files.createTempFile("limbo_test_db", null).toAbsolutePath().toString();
+ }
+}
diff --git a/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java
new file mode 100644
index 000000000..8a62ea083
--- /dev/null
+++ b/bindings/java/src/test/java/org/github/tursodatabase/core/LimboDBTest.java
@@ -0,0 +1,29 @@
+package org.github.tursodatabase.core;
+
+import org.github.tursodatabase.TestUtils;
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLException;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class LimboDBTest {
+
+ @Test
+ void db_should_open_normally() throws Exception {
+ String dbPath = TestUtils.createTempFile();
+ LimboDB db = LimboDB.create("jdbc:sqlite" + dbPath, dbPath);
+ db.load();
+ db.open(0);
+ }
+
+ @Test
+ void should_throw_exception_when_opened_twice() throws Exception {
+ String dbPath = TestUtils.createTempFile();
+ LimboDB db = LimboDB.create("jdbc:sqlite:" + dbPath, dbPath);
+ db.load();
+ db.open(0);
+
+ assertThatThrownBy(() -> db.open(0)).isInstanceOf(SQLException.class);
+ }
+}