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); + } +}