diff --git a/Cargo.lock b/Cargo.lock index fa685398..44978f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "cast" version = "0.3.0" @@ -220,6 +226,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -359,6 +371,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1020,6 +1042,39 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "java-limbo" +version = "0.0.11" +dependencies = [ + "anyhow", + "jni", + "lazy_static", + "limbo_core", + "rand", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.76" @@ -2417,6 +2472,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2444,6 +2508,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2475,6 +2554,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2487,6 +2572,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2499,6 +2590,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2517,6 +2614,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2529,6 +2632,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2541,6 +2650,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2553,6 +2668,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 95a991fe..12224d7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ [workspace] resolver = "2" members = [ + "bindings/java", "bindings/python", "bindings/wasm", "cli", diff --git a/bindings/java/.gitignore b/bindings/java/.gitignore new file mode 100644 index 00000000..581bf51f --- /dev/null +++ b/bindings/java/.gitignore @@ -0,0 +1,39 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml new file mode 100644 index 00000000..79de553a --- /dev/null +++ b/bindings/java/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "java-limbo" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +name = "_limbo_java" +crate-type = ["cdylib"] +path = "rs_src/lib.rs" + +[dependencies] +anyhow = "1.0" +limbo_core = { path = "../../core" } +jni = "0.21.1" +rand = { version = "0.8.5", features = [] } +lazy_static = "1.5.0" diff --git a/bindings/java/Makefile b/bindings/java/Makefile new file mode 100644 index 00000000..e91fcc92 --- /dev/null +++ b/bindings/java/Makefile @@ -0,0 +1,7 @@ +java_run: lib + export LIMBO_SYSTEM_PATH=../../target/debug && ./gradlew run + +.PHONY: lib + +lib: + cargo build diff --git a/bindings/java/build.gradle.kts b/bindings/java/build.gradle.kts new file mode 100644 index 00000000..331b4831 --- /dev/null +++ b/bindings/java/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + java + application +} + +group = "org.github.tursodatabase" +version = "0.0.1-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +application { + mainClass.set("org.github.tursodatabase.Main") + + val limboSystemLibraryPath = System.getenv("LIMBO_SYSTEM_PATH") + if (limboSystemLibraryPath != null) { + applicationDefaultJvmArgs = listOf( + "-Djava.library.path=${System.getProperty("java.library.path")}:$limboSystemLibraryPath" + ) + } +} + +tasks.test { + useJUnitPlatform() +} diff --git a/bindings/java/gradle/wrapper/gradle-wrapper.jar b/bindings/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/bindings/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bindings/java/gradle/wrapper/gradle-wrapper.properties b/bindings/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..9355b415 --- /dev/null +++ b/bindings/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/bindings/java/gradlew b/bindings/java/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/bindings/java/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/bindings/java/gradlew.bat b/bindings/java/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/bindings/java/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bindings/java/rs_src/connection.rs b/bindings/java/rs_src/connection.rs new file mode 100644 index 00000000..ef3a565b --- /dev/null +++ b/bindings/java/rs_src/connection.rs @@ -0,0 +1,84 @@ +use crate::cursor::Cursor; +use jni::objects::JClass; +use jni::sys::jlong; +use jni::JNIEnv; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct Connection { + pub(crate) conn: Arc>>, + pub(crate) io: Arc, +} + +/// Returns a pointer to a `Cursor` object. +/// +/// The Java application will pass this pointer to native functions, +/// which will use it to reference the `Cursor` object. +/// +/// # Arguments +/// +/// * `_env` - The JNI environment pointer. +/// * `_class` - The Java class calling this function. +/// * `connection_ptr` - A pointer to the `Connection` object. +/// +/// # Returns +/// +/// A `jlong` representing the pointer to the newly created `Cursor` object. +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_cursor<'local>( + _env: JNIEnv<'local>, + _class: JClass<'local>, + connection_ptr: jlong, +) -> jlong { + let connection = to_connection(connection_ptr); + let cursor = Cursor { + array_size: 1, + conn: connection.clone(), + description: None, + rowcount: -1, + smt: None, + }; + Box::into_raw(Box::new(cursor)) as jlong +} + +/// Closes the connection and releases the associated resources. +/// +/// This function is called from the Java side to close the connection +/// and free the memory allocated for the `Connection` object. +/// +/// # Arguments +/// +/// * `_env` - The JNI environment pointer. +/// * `_class` - The Java class calling this function. +/// * `connection_ptr` - A pointer to the `Connection` object to be closed. +#[no_mangle] +pub unsafe extern "system" fn Java_org_github_tursodatabase_limbo_Connection_close<'local>( + _env: JNIEnv<'local>, + _class: JClass<'local>, + connection_ptr: jlong, +) { + let _boxed_connection = Box::from_raw(connection_ptr as *mut Connection); +} + +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_commit<'local>( + _env: &mut JNIEnv<'local>, + _class: JClass<'local>, + _connection_id: jlong, +) { + unimplemented!() +} + +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_rollback<'local>( + _env: &mut JNIEnv<'local>, + _class: JClass<'local>, + _connection_id: jlong, +) { + unimplemented!() +} + +fn to_connection(connection_ptr: jlong) -> &'static mut Connection { + unsafe { &mut *(connection_ptr as *mut Connection) } +} diff --git a/bindings/java/rs_src/cursor.rs b/bindings/java/rs_src/cursor.rs new file mode 100644 index 00000000..da67292d --- /dev/null +++ b/bindings/java/rs_src/cursor.rs @@ -0,0 +1,240 @@ +use crate::connection::Connection; +use crate::errors::ErrorCode; +use crate::utils::row_to_obj_array; +use crate::{eprint_return, eprint_return_null}; +use jni::errors::JniError; +use jni::objects::{JClass, JObject, JString}; +use jni::sys::jlong; +use jni::JNIEnv; +use limbo_core::IO; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct Cursor { + /// This read/write attribute specifies the number of rows to fetch at a time with `.fetchmany()`. + /// It defaults to `1`, meaning it fetches a single row at a time. + pub(crate) array_size: i64, + + pub(crate) conn: Connection, + + /// The `.description` attribute is a read-only sequence of 7-item, each describing a column in the result set: + /// + /// - `name`: The column's name (always present). + /// - `type_code`: The data type code (always present). + /// - `display_size`: Column's display size (optional). + /// - `internal_size`: Column's internal size (optional). + /// - `precision`: Numeric precision (optional). + /// - `scale`: Numeric scale (optional). + /// - `null_ok`: Indicates if null values are allowed (optional). + /// + /// The `name` and `type_code` fields are mandatory; others default to `None` if not applicable. + /// + /// This attribute is `None` for operations that do not return rows or if no `.execute*()` method has been invoked. + pub(crate) description: Option, + + /// Read-only attribute that provides the number of modified rows for `INSERT`, `UPDATE`, `DELETE`, + /// and `REPLACE` statements; it is `-1` for other statements, including CTE queries. + /// It is only updated by the `execute()` and `executemany()` methods after the statement has run to completion. + /// This means any resulting rows must be fetched for `rowcount` to be updated. + pub(crate) rowcount: i64, + + pub(crate) smt: Option>>, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub(crate) struct Description { + _name: String, + _type_code: String, + _display_size: Option, + _internal_size: Option, + _precision: Option, + _scale: Option, + _null_ok: Option, +} + +impl Debug for Cursor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cursor") + .field("array_size", &self.array_size) + .field("description", &self.description) + .field("rowcount", &self.rowcount) + .finish() + } +} + +/// TODO: we should find a way to handle Error thrown by rust and how to handle those errors in java +#[no_mangle] +#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_execute<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + cursor_ptr: jlong, + sql: JString<'local>, +) -> Result<(), JniError> { + let sql: String = env + .get_string(&sql) + .expect("Could not extract query") + .into(); + + let stmt_is_dml = stmt_is_dml(&sql); + if stmt_is_dml { + return eprint_return!( + "DML statements (INSERT/UPDATE/DELETE) are not fully supported in this version", + JniError::Other(ErrorCode::STATEMENT_IS_DML) + ); + } + + let cursor = to_cursor(cursor_ptr); + let conn_lock = match cursor.conn.conn.lock() { + Ok(lock) => lock, + Err(_) => return eprint_return!("Failed to acquire connection lock", JniError::Other(-1)), + }; + + match conn_lock.prepare(&sql) { + Ok(statement) => { + cursor.smt = Some(Arc::new(Mutex::new(statement))); + Ok(()) + } + Err(e) => { + eprint_return!( + &format!("Failed to prepare statement: {:?}", e), + JniError::Other(-1) + ) + } + } +} + +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_fetchOne<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + cursor_ptr: jlong, +) -> JObject<'local> { + let cursor = to_cursor(cursor_ptr); + + if let Some(smt) = &cursor.smt { + loop { + let mut smt_lock = match smt.lock() { + Ok(lock) => lock, + Err(_) => { + return eprint_return_null!( + "Failed to acquire statement lock", + JniError::Other(-1) + ) + } + }; + + match smt_lock.step() { + Ok(limbo_core::StepResult::Row(row)) => { + return match row_to_obj_array(&mut env, &row) { + Ok(r) => r, + Err(e) => eprint_return_null!(&format!("{:?}", e), JniError::Other(-1)), + } + } + Ok(limbo_core::StepResult::IO) => { + if let Err(e) = cursor.conn.io.run_once() { + return eprint_return_null!( + &format!("IO Error: {:?}", e), + JniError::Other(-1) + ); + } + } + Ok(limbo_core::StepResult::Interrupt) => return JObject::null(), + Ok(limbo_core::StepResult::Done) => return JObject::null(), + Ok(limbo_core::StepResult::Busy) => { + return eprint_return_null!("Busy error", JniError::Other(-1)); + } + Err(e) => { + return eprint_return_null!( + format!("Step error: {:?}", e), + JniError::Other(-1) + ); + } + }; + } + } else { + eprint_return_null!("No statement prepared for execution", JniError::Other(-1)) + } +} + +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_fetchAll<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + cursor_ptr: jlong, +) -> JObject<'local> { + let cursor = to_cursor(cursor_ptr); + + if let Some(smt) = &cursor.smt { + let mut rows = Vec::new(); + loop { + let mut smt_lock = match smt.lock() { + Ok(lock) => lock, + Err(_) => { + return eprint_return_null!( + "Failed to acquire statement lock", + JniError::Other(-1) + ) + } + }; + + match smt_lock.step() { + Ok(limbo_core::StepResult::Row(row)) => match row_to_obj_array(&mut env, &row) { + Ok(r) => rows.push(r), + Err(e) => return eprint_return_null!(&format!("{:?}", e), JniError::Other(-1)), + }, + Ok(limbo_core::StepResult::IO) => { + if let Err(e) = cursor.conn.io.run_once() { + return eprint_return_null!( + &format!("IO Error: {:?}", e), + JniError::Other(-1) + ); + } + } + Ok(limbo_core::StepResult::Interrupt) => { + return JObject::null(); + } + Ok(limbo_core::StepResult::Done) => { + break; + } + Ok(limbo_core::StepResult::Busy) => { + return eprint_return_null!("Busy error", JniError::Other(-1)); + } + Err(e) => { + return eprint_return_null!( + format!("Step error: {:?}", e), + JniError::Other(-1) + ); + } + }; + } + + let array_class = env + .find_class("[Ljava/lang/Object;") + .expect("Failed to find Object array class"); + let result_array = env + .new_object_array(rows.len() as i32, array_class, JObject::null()) + .expect("Failed to create new object array"); + + for (i, row) in rows.into_iter().enumerate() { + env.set_object_array_element(&result_array, i as i32, row) + .expect("Failed to set object array element"); + } + + result_array.into() + } else { + eprint_return_null!("No statement prepared for execution", JniError::Other(-1)) + } +} + +fn to_cursor(cursor_ptr: jlong) -> &'static mut Cursor { + unsafe { &mut *(cursor_ptr as *mut Cursor) } +} + +fn stmt_is_dml(sql: &str) -> bool { + let sql = sql.trim(); + let sql = sql.to_uppercase(); + sql.starts_with("INSERT") || sql.starts_with("UPDATE") || sql.starts_with("DELETE") +} diff --git a/bindings/java/rs_src/errors.rs b/bindings/java/rs_src/errors.rs new file mode 100644 index 00000000..490fdfbd --- /dev/null +++ b/bindings/java/rs_src/errors.rs @@ -0,0 +1,35 @@ +use jni::errors::{Error, JniError}; + +#[derive(Debug, Clone)] +pub struct CustomError { + pub message: String, +} + +/// This struct defines error codes that correspond to the constants defined in the +/// Java package `org.github.tursodatabase.exceptions.ErrorCode`. +/// +/// These error codes are used to handle and represent specific error conditions +/// that may occur within the Rust code and need to be communicated to the Java side. +#[derive(Clone)] +pub struct ErrorCode; + +impl ErrorCode { + pub const CONNECTION_FAILURE: i32 = -1; + + pub const STATEMENT_IS_DML: i32 = -1; +} + +impl From for CustomError { + fn from(value: Error) -> Self { + CustomError { + message: value.to_string(), + } + } +} + +impl From for JniError { + fn from(value: CustomError) -> Self { + eprintln!("Error occurred: {:?}", value.message); + JniError::Other(-1) + } +} diff --git a/bindings/java/rs_src/lib.rs b/bindings/java/rs_src/lib.rs new file mode 100644 index 00000000..4ba9ba2c --- /dev/null +++ b/bindings/java/rs_src/lib.rs @@ -0,0 +1,66 @@ +mod connection; +mod cursor; +mod errors; +mod macros; +mod utils; + +use crate::connection::Connection; +use crate::errors::ErrorCode; +use jni::errors::JniError; +use jni::objects::{JClass, JString}; +use jni::sys::jlong; +use jni::JNIEnv; +use std::sync::{Arc, Mutex}; + +/// Establishes a connection to the database specified by the given path. +/// +/// This function is called from the Java side to create a connection to the database. +/// It returns a pointer to the `Connection` object, which can be used in subsequent +/// native function calls. +/// +/// # Arguments +/// +/// * `env` - The JNI environment pointer. +/// * `_class` - The Java class calling this function. +/// * `path` - A `JString` representing the path to the database file. +/// +/// # Returns +/// +/// A `jlong` representing the pointer to the newly created `Connection` object, +/// or [ErrorCode::CONNECTION_FAILURE] if the connection could not be established. +#[no_mangle] +pub extern "system" fn Java_org_github_tursodatabase_limbo_Limbo_connect<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + path: JString<'local>, +) -> jlong { + connect_internal(&mut env, path).unwrap_or_else(|_| ErrorCode::CONNECTION_FAILURE as jlong) +} + +#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)] // TODO: remove +fn connect_internal<'local>( + env: &mut JNIEnv<'local>, + path: JString<'local>, +) -> Result { + let io = Arc::new(limbo_core::PlatformIO::new().map_err(|e| { + println!("IO initialization failed: {:?}", e); + JniError::Unknown + })?); + + let path: String = env + .get_string(&path) + .expect("Failed to convert JString to Rust String") + .into(); + let db = limbo_core::Database::open_file(io.clone(), &path).map_err(|e| { + println!("Failed to open database: {:?}", e); + JniError::Unknown + })?; + + let conn = db.connect().clone(); + let connection = Connection { + conn: Arc::new(Mutex::new(conn)), + io, + }; + + Ok(Box::into_raw(Box::new(connection)) as jlong) +} diff --git a/bindings/java/rs_src/macros.rs b/bindings/java/rs_src/macros.rs new file mode 100644 index 00000000..967834f9 --- /dev/null +++ b/bindings/java/rs_src/macros.rs @@ -0,0 +1,16 @@ +// bindings/java/src/macros.rs +#[macro_export] +macro_rules! eprint_return { + ($log:expr, $error:expr) => {{ + eprintln!("{}", $log); + Err($error) + }}; +} + +#[macro_export] +macro_rules! eprint_return_null { + ($log:expr, $error:expr) => {{ + eprintln!("{}", $log); + JObject::null() + }}; +} diff --git a/bindings/java/rs_src/utils.rs b/bindings/java/rs_src/utils.rs new file mode 100644 index 00000000..4fde084f --- /dev/null +++ b/bindings/java/rs_src/utils.rs @@ -0,0 +1,30 @@ +use crate::errors::CustomError; +use jni::objects::{JObject, JValue}; +use jni::JNIEnv; + +pub(crate) fn row_to_obj_array<'local>( + env: &mut JNIEnv<'local>, + row: &limbo_core::Row, +) -> Result, CustomError> { + let obj_array = + env.new_object_array(row.values.len() as i32, "java/lang/Object", JObject::null())?; + + for (i, value) in row.values.iter().enumerate() { + let obj = match value { + limbo_core::Value::Null => JObject::null(), + limbo_core::Value::Integer(i) => { + env.new_object("java/lang/Long", "(J)V", &[JValue::Long(*i)])? + } + limbo_core::Value::Float(f) => { + env.new_object("java/lang/Double", "(D)V", &[JValue::Double(*f)])? + } + limbo_core::Value::Text(s) => env.new_string(s)?.into(), + limbo_core::Value::Blob(b) => env.byte_array_from_slice(b)?.into(), + }; + if let Err(e) = env.set_object_array_element(&obj_array, i as i32, obj) { + eprintln!("Error on parsing row: {:?}", e); + } + } + + Ok(obj_array.into()) +} diff --git a/bindings/java/settings.gradle.kts b/bindings/java/settings.gradle.kts new file mode 100644 index 00000000..1f763b4b --- /dev/null +++ b/bindings/java/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "limbo" diff --git a/bindings/java/src/main/java/org/github/tursodatabase/Main.java b/bindings/java/src/main/java/org/github/tursodatabase/Main.java new file mode 100644 index 00000000..caf33f00 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/Main.java @@ -0,0 +1,16 @@ +package org.github.tursodatabase; + +import org.github.tursodatabase.limbo.Connection; +import org.github.tursodatabase.limbo.Cursor; +import org.github.tursodatabase.limbo.Limbo; + +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/exceptions/ErrorCode.java b/bindings/java/src/main/java/org/github/tursodatabase/exceptions/ErrorCode.java new file mode 100644 index 00000000..9ec09e19 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/exceptions/ErrorCode.java @@ -0,0 +1,12 @@ +package org.github.tursodatabase.exceptions; + + +/** + * This class defines error codes that correspond to specific error conditions + * that may occur while communicating with the JNI. + *

+ * Refer to ErrorCode in rust package. + */ +public class ErrorCode { + public static int CONNECTION_FAILURE = -1; +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Connection.java b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Connection.java new file mode 100644 index 00000000..7c1f89a8 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Connection.java @@ -0,0 +1,66 @@ +package org.github.tursodatabase.limbo; + +import java.lang.Exception; + +/** + * Represents a connection to the database. + */ +public class Connection { + + // Pointer to the connection object + private final long connectionPtr; + + public Connection(long connectionPtr) { + this.connectionPtr = connectionPtr; + } + + /** + * Creates a new cursor object using this connection. + * + * @return A new Cursor object. + * @throws Exception If the cursor cannot be created. + */ + public Cursor cursor() throws Exception { + long cursorId = cursor(connectionPtr); + return new Cursor(cursorId); + } + + private native long cursor(long connectionPtr); + + /** + * Closes the connection to the database. + * + * @throws Exception If there is an error closing the connection. + */ + public void close() throws Exception { + close(connectionPtr); + } + + private native void close(long connectionPtr); + + /** + * Commits the current transaction. + * + * @throws Exception If there is an error during commit. + */ + public void commit() throws Exception { + try { + commit(connectionPtr); + } catch (Exception e) { + System.out.println("caught exception: " + e); + } + } + + private native void commit(long connectionPtr) throws Exception; + + /** + * Rolls back the current transaction. + * + * @throws Exception If there is an error during rollback. + */ + public void rollback() throws Exception { + rollback(connectionPtr); + } + + private native void rollback(long connectionPtr) throws Exception; +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Cursor.java b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Cursor.java new file mode 100644 index 00000000..f31ac9b9 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Cursor.java @@ -0,0 +1,85 @@ +package org.github.tursodatabase.limbo; + +/** + * Represents a database cursor. + */ +public class Cursor { + private long cursorPtr; + + public Cursor(long cursorPtr) { + this.cursorPtr = cursorPtr; + } + + // TODO: support parameters + public Cursor execute(String sql) { + var result = execute(cursorPtr, sql); + System.out.println("resut: " + result); + return this; + } + + private static native int execute(long cursorPtr, String sql); + + public Object fetchOne() throws Exception { + Object result = fetchOne(cursorPtr); + return processSingleResult(result); + } + + private static native Object fetchOne(long cursorPtr); + + public Object fetchAll() throws Exception { + Object result = fetchAll(cursorPtr); + return processArrayResult(result); + } + + private static native Object fetchAll(long cursorPtr); + + private Object processSingleResult(Object result) throws Exception { + if (result instanceof Object[]) { + System.out.println("The result is of type: Object[]"); + for (Object element : (Object[]) result) { + printElementType(element); + } + return result; + } else { + printElementType(result); + return result; + } + } + + private Object processArrayResult(Object result) throws Exception { + if (result instanceof Object[][]) { + System.out.println("The result is of type: Object[][]"); + Object[][] array = (Object[][]) result; + for (Object[] row : array) { + for (Object element : row) { + printElementType(element); + } + } + return array; + } else { + throw new Exception("result should be of type Object[][]. Maybe internal logic has error."); + } + } + + private void printElementType(Object element) { + if (element instanceof String) { + System.out.println("String: " + element); + } else if (element instanceof Integer) { + System.out.println("Integer: " + element); + } else if (element instanceof Double) { + System.out.println("Double: " + element); + } else if (element instanceof Boolean) { + System.out.println("Boolean: " + element); + } else if (element instanceof Long) { + System.out.println("Long: " + element); + } else if (element instanceof byte[]) { + System.out.print("byte[]: "); + for (byte b : (byte[]) element) { + System.out.print(b + " "); + } + System.out.println(); + } else { + System.out.println("Unknown type: " + element); + } + } +} diff --git a/bindings/java/src/main/java/org/github/tursodatabase/limbo/Limbo.java b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Limbo.java new file mode 100644 index 00000000..8d64df15 --- /dev/null +++ b/bindings/java/src/main/java/org/github/tursodatabase/limbo/Limbo.java @@ -0,0 +1,31 @@ +package org.github.tursodatabase.limbo; + +import org.github.tursodatabase.exceptions.ErrorCode; + +import java.lang.Exception; + +public class Limbo { + + private static volatile boolean initialized; + + private Limbo() { + if (!initialized) { + System.loadLibrary("_limbo_java"); + initialized = true; + } + } + + public static Limbo create() { + return new Limbo(); + } + + public Connection getConnection(String path) throws Exception { + long connectionId = connect(path); + if (connectionId == ErrorCode.CONNECTION_FAILURE) { + throw new Exception("Failed to initialize connection"); + } + return new Connection(connectionId); + } + + private static native long connect(String path); +}