Merge 'bindings/java: Implement minimal execute method ' from Kim Seon Woo

## Purpose of this PR
- Minimal implementation of `execute`
## Changes
### Java side
- Implement `execute`
- Along the way, rename classes and methods which have the same meanings
  - `fileName` -> `filePath`
  - unify file names for rust code to java code e.g.
`limbo_connection.rs` will match `LimboConnection.java`
### Rust side
- Replace `pointer to struct` and `struct to pointer` functions close
together
- Rename rust files to match java files
## Note
- Implementation differs from that of `sqlite-java`. It's because we can
easily add JNI code and thereby implementing bindings is less restriced.
## References
https://github.com/tursodatabase/limbo/issues/615

Closes #723
This commit is contained in:
Pekka Enberg
2025-01-18 09:05:37 +02:00
37 changed files with 2137 additions and 689 deletions

4
Cargo.lock generated
View File

@@ -1062,11 +1062,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
name = "java-limbo"
version = "0.0.12"
dependencies = [
"anyhow",
"jni",
"lazy_static",
"limbo_core",
"rand",
"thiserror 2.0.11",
]
[[package]]

View File

@@ -18,6 +18,11 @@ This product depends on AssertJ, distributed by the AssertJ authors:
* License: licenses/bindings/java/errorprone-license.md (Apache License v2.0)
* Homepage: https://joel-costigliola.github.io/assertj/
This product depends on logback, distributed by the logback authors:
* License: licenses/bindings/java/logback-license.md (Apache License v2.0)
* Homepage: https://github.com/qos-ch/logback?tab=License-1-ov-file
This product depends on serde, distributed by the serde-rs project:
* License: licenses/core/serde-apache-license.md (Apache License v2.0)

View File

@@ -12,8 +12,6 @@ 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"
thiserror = "2.0.9"

View File

@@ -1,5 +1,7 @@
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
plugins {
java
@@ -20,6 +22,9 @@ repositories {
}
dependencies {
implementation("ch.qos.logback:logback-classic:1.2.13")
implementation("ch.qos.logback:logback-core:1.2.13")
errorprone("com.uber.nullaway:nullaway:0.10.26") // maximum version which supports java 8
errorprone("com.google.errorprone:error_prone_core:2.10.0") // maximum version which supports java 8
@@ -46,6 +51,46 @@ tasks.test {
"java.library.path",
"${System.getProperty("java.library.path")}:$projectDir/src/test/resources/limbo/debug"
)
// For our fancy test logging
testLogging {
// set options for log level LIFECYCLE
events(
TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT
)
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
// set options for log level DEBUG and INFO
debug {
events(
TestLogEvent.STARTED,
TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_ERROR,
TestLogEvent.STANDARD_OUT
)
exceptionFormat = TestExceptionFormat.FULL
}
info.events = debug.events
info.exceptionFormat = debug.exceptionFormat
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
if (desc.parent == null) { // will match the outermost suite
val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
val startItem = "| "
val endItem = " |"
val repeatLength = startItem.length + output.length + endItem.length
println("\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(repeatLength))
}
}))
}
}
tasks.withType<JavaCompile> {

View File

@@ -1,84 +0,0 @@
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<Mutex<Rc<limbo_core::Connection>>>,
pub(crate) io: Arc<limbo_core::PlatformIO>,
}
/// 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) }
}

View File

@@ -1,240 +0,0 @@
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<Description>,
/// 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<Arc<Mutex<limbo_core::Statement>>>,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) struct Description {
_name: String,
_type_code: String,
_display_size: Option<String>,
_internal_size: Option<String>,
_precision: Option<String>,
_scale: Option<String>,
_null_ok: Option<String>,
}
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")
}

View File

@@ -1,34 +1,113 @@
use jni::errors::{Error, JniError};
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct CustomError {
pub message: String,
#[derive(Debug, Error)]
pub enum LimboError {
#[error("Custom error: `{0}`")]
CustomError(String),
#[error("Invalid database pointer")]
InvalidDatabasePointer,
#[error("Invalid connection pointer")]
InvalidConnectionPointer,
#[error("JNI Errors: `{0}`")]
JNIErrors(Error),
}
/// This struct defines error codes that correspond to the constants defined in the
/// Java package `org.github.tursodatabase.LimboErrorCode`.
///
/// 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 {
// TODO: change CONNECTION_FAILURE_STATEMENT_IS_DML to appropriate error code number
pub const STATEMENT_IS_DML: i32 = -1;
impl From<limbo_core::LimboError> for LimboError {
fn from(_value: limbo_core::LimboError) -> Self {
todo!()
}
}
impl From<jni::errors::Error> for CustomError {
fn from(value: Error) -> Self {
CustomError {
message: value.to_string(),
impl From<LimboError> for JniError {
fn from(value: LimboError) -> Self {
match value {
LimboError::CustomError(_)
| LimboError::InvalidDatabasePointer
| LimboError::InvalidConnectionPointer
| LimboError::JNIErrors(_) => {
eprintln!("Error occurred: {:?}", value);
JniError::Other(-1)
}
}
}
}
impl From<CustomError> for JniError {
fn from(value: CustomError) -> Self {
eprintln!("Error occurred: {:?}", value.message);
JniError::Other(-1)
impl From<jni::errors::Error> for LimboError {
fn from(value: jni::errors::Error) -> Self {
LimboError::JNIErrors(value)
}
}
pub type Result<T> = std::result::Result<T, LimboError>;
#[allow(dead_code)]
pub const SQLITE_OK: i32 = 0;
#[allow(dead_code)]
pub const SQLITE_ERROR: i32 = 1;
#[allow(dead_code)]
pub const SQLITE_INTERNAL: i32 = 2;
#[allow(dead_code)]
pub const SQLITE_PERM: i32 = 3;
#[allow(dead_code)]
pub const SQLITE_ABORT: i32 = 4;
#[allow(dead_code)]
pub const SQLITE_BUSY: i32 = 5;
#[allow(dead_code)]
pub const SQLITE_LOCKED: i32 = 6;
#[allow(dead_code)]
pub const SQLITE_NOMEM: i32 = 7;
#[allow(dead_code)]
pub const SQLITE_READONLY: i32 = 8;
#[allow(dead_code)]
pub const SQLITE_INTERRUPT: i32 = 9;
#[allow(dead_code)]
pub const SQLITE_IOERR: i32 = 10;
#[allow(dead_code)]
pub const SQLITE_CORRUPT: i32 = 11;
#[allow(dead_code)]
pub const SQLITE_NOTFOUND: i32 = 12;
#[allow(dead_code)]
pub const SQLITE_FULL: i32 = 13;
#[allow(dead_code)]
pub const SQLITE_CANTOPEN: i32 = 14;
#[allow(dead_code)]
pub const SQLITE_PROTOCOL: i32 = 15;
#[allow(dead_code)]
pub const SQLITE_EMPTY: i32 = 16;
#[allow(dead_code)]
pub const SQLITE_SCHEMA: i32 = 17;
#[allow(dead_code)]
pub const SQLITE_TOOBIG: i32 = 18;
#[allow(dead_code)]
pub const SQLITE_CONSTRAINT: i32 = 19;
#[allow(dead_code)]
pub const SQLITE_MISMATCH: i32 = 20;
#[allow(dead_code)]
pub const SQLITE_MISUSE: i32 = 21;
#[allow(dead_code)]
pub const SQLITE_NOLFS: i32 = 22;
#[allow(dead_code)]
pub const SQLITE_AUTH: i32 = 23;
#[allow(dead_code)]
pub const SQLITE_ROW: i32 = 100;
#[allow(dead_code)]
pub const SQLITE_DONE: i32 = 101;
#[allow(dead_code)]
pub const SQLITE_INTEGER: i32 = 1;
#[allow(dead_code)]
pub const SQLITE_FLOAT: i32 = 2;
#[allow(dead_code)]
pub const SQLITE_TEXT: i32 = 3;
#[allow(dead_code)]
pub const SQLITE_BLOB: i32 = 4;
#[allow(dead_code)]
pub const SQLITE_NULL: i32 = 5;
pub const LIMBO_FAILED_TO_PARSE_BYTE_ARRAY: i32 = 1100;
pub const LIMBO_FAILED_TO_PREPARE_STATEMENT: i32 = 1200;
pub const LIMBO_ETC: i32 = 9999;

View File

@@ -1,6 +1,5 @@
mod connection;
mod cursor;
mod errors;
mod limbo_connection;
mod limbo_db;
mod macros;
mod limbo_statement;
mod utils;

View File

@@ -0,0 +1,83 @@
use crate::errors::{
LimboError, Result, LIMBO_ETC, LIMBO_FAILED_TO_PARSE_BYTE_ARRAY,
LIMBO_FAILED_TO_PREPARE_STATEMENT,
};
use crate::limbo_statement::LimboStatement;
use crate::utils::{set_err_msg_and_throw_exception, utf8_byte_arr_to_str};
use jni::objects::{JByteArray, JObject};
use jni::sys::jlong;
use jni::JNIEnv;
use limbo_core::Connection;
use std::rc::Rc;
#[derive(Clone)]
#[allow(dead_code)]
pub struct LimboConnection {
pub(crate) conn: Rc<Connection>,
pub(crate) io: Rc<dyn limbo_core::IO>,
}
impl LimboConnection {
pub fn new(conn: Rc<Connection>, io: Rc<dyn limbo_core::IO>) -> Self {
LimboConnection { conn, io }
}
pub fn to_ptr(self) -> jlong {
Box::into_raw(Box::new(self)) as jlong
}
#[allow(dead_code)]
pub fn drop(ptr: jlong) {
let _boxed = unsafe { Box::from_raw(ptr as *mut LimboConnection) };
}
}
pub fn to_limbo_connection(ptr: jlong) -> Result<&'static mut LimboConnection> {
if ptr == 0 {
Err(LimboError::InvalidConnectionPointer)
} else {
unsafe { Ok(&mut *(ptr as *mut LimboConnection)) }
}
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_core_LimboConnection_prepareUtf8<'local>(
mut env: JNIEnv<'local>,
obj: JObject<'local>,
connection_ptr: jlong,
sql_bytes: JByteArray<'local>,
) -> jlong {
let connection = match to_limbo_connection(connection_ptr) {
Ok(conn) => conn,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return 0;
}
};
let sql = match utf8_byte_arr_to_str(&env, sql_bytes) {
Ok(sql) => sql,
Err(e) => {
set_err_msg_and_throw_exception(
&mut env,
obj,
LIMBO_FAILED_TO_PARSE_BYTE_ARRAY,
e.to_string(),
);
return 0;
}
};
match connection.conn.prepare(sql) {
Ok(stmt) => LimboStatement::new(stmt).to_ptr(),
Err(e) => {
set_err_msg_and_throw_exception(
&mut env,
obj,
LIMBO_FAILED_TO_PREPARE_STATEMENT,
e.to_string(),
);
0
}
}
}

View File

@@ -1,40 +1,69 @@
use crate::errors::{LimboError, Result, LIMBO_ETC};
use crate::limbo_connection::LimboConnection;
use crate::utils::set_err_msg_and_throw_exception;
use jni::objects::{JByteArray, JObject};
use jni::sys::{jint, jlong};
use jni::JNIEnv;
use limbo_core::Database;
use std::rc::Rc;
use std::sync::Arc;
const ERROR_CODE_ETC: i32 = 9999;
struct LimboDB {
db: Arc<Database>,
}
impl LimboDB {
pub fn new(db: Arc<Database>) -> Self {
LimboDB { db }
}
pub fn to_ptr(self) -> jlong {
Box::into_raw(Box::new(self)) as jlong
}
#[allow(dead_code)]
pub fn drop(ptr: jlong) {
let _boxed = unsafe { Box::from_raw(ptr as *mut LimboDB) };
}
}
fn to_limbo_db(ptr: jlong) -> Result<&'static mut LimboDB> {
if ptr == 0 {
Err(LimboError::InvalidDatabasePointer)
} else {
unsafe { Ok(&mut *(ptr as *mut LimboDB)) }
}
}
#[no_mangle]
#[allow(clippy::arc_with_non_send_sync)]
pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_openUtf8<'local>(
mut env: JNIEnv<'local>,
obj: JObject<'local>,
file_name_byte_arr: JByteArray<'local>,
file_path_byte_arr: JByteArray<'local>,
_open_flags: jint,
) -> jlong {
let io = match limbo_core::PlatformIO::new() {
Ok(io) => Arc::new(io),
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string());
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return -1;
}
};
let path = match env
.convert_byte_array(file_name_byte_arr)
.convert_byte_array(file_path_byte_arr)
.map_err(|e| e.to_string())
{
Ok(bytes) => match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string());
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return -1;
}
},
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string());
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return -1;
}
};
@@ -42,12 +71,65 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_openUtf8<'loca
let db = match Database::open_file(io.clone(), &path) {
Ok(db) => db,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string());
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return -1;
}
};
Box::into_raw(Box::new(db)) as jlong
LimboDB::new(db).to_ptr()
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_connect0<'local>(
mut env: JNIEnv<'local>,
obj: JObject<'local>,
file_path_byte_arr: JByteArray<'local>,
db_pointer: jlong,
) -> jlong {
let db = match to_limbo_db(db_pointer) {
Ok(db) => db,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return 0;
}
};
let path = match env
.convert_byte_array(file_path_byte_arr)
.map_err(|e| e.to_string())
{
Ok(bytes) => match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return 0;
}
},
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return 0;
}
};
let io: Rc<dyn limbo_core::IO> = match path.as_str() {
":memory:" => match limbo_core::MemoryIO::new() {
Ok(io) => Rc::new(io),
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return 0;
}
},
_ => match limbo_core::PlatformIO::new() {
Ok(io) => Rc::new(io),
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return 0;
}
},
};
let conn = LimboConnection::new(db.db.connect(), io);
conn.to_ptr()
}
#[no_mangle]
@@ -63,27 +145,3 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_throwJavaExcep
"throw java exception".to_string(),
);
}
fn set_err_msg_and_throw_exception<'local>(
env: &mut JNIEnv<'local>,
obj: JObject<'local>,
err_code: i32,
err_msg: String,
) {
let error_message_bytes = env
.byte_array_from_slice(err_msg.as_bytes())
.expect("Failed to convert to byte array");
match env.call_method(
obj,
"throwLimboException",
"(I[B)V",
&[err_code.into(), (&error_message_bytes).into()],
) {
Ok(_) => {
// do nothing because above method will always return Err
}
Err(_e) => {
// do nothing because our java app will handle Err
}
}
}

View File

@@ -0,0 +1,98 @@
use crate::errors::Result;
use crate::errors::{LimboError, LIMBO_ETC};
use crate::utils::set_err_msg_and_throw_exception;
use jni::objects::{JObject, JValue};
use jni::sys::jlong;
use jni::JNIEnv;
use limbo_core::{Statement, StepResult};
pub struct LimboStatement {
pub(crate) stmt: Statement,
}
impl LimboStatement {
pub fn new(stmt: Statement) -> Self {
LimboStatement { stmt }
}
pub fn to_ptr(self) -> jlong {
Box::into_raw(Box::new(self)) as jlong
}
#[allow(dead_code)]
pub fn drop(ptr: jlong) {
let _boxed = unsafe { Box::from_raw(ptr as *mut LimboStatement) };
}
}
pub fn to_limbo_statement(ptr: jlong) -> Result<&'static mut LimboStatement> {
if ptr == 0 {
Err(LimboError::InvalidConnectionPointer)
} else {
unsafe { Ok(&mut *(ptr as *mut LimboStatement)) }
}
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_core_LimboStatement_step<'local>(
mut env: JNIEnv<'local>,
obj: JObject<'local>,
stmt_ptr: jlong,
) -> JObject<'local> {
let stmt = match to_limbo_statement(stmt_ptr) {
Ok(stmt) => stmt,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
return JObject::null();
}
};
match stmt.stmt.step() {
Ok(StepResult::Row(row)) => match row_to_obj_array(&mut env, &row) {
Ok(row) => row,
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
JObject::null()
}
},
Ok(StepResult::IO) => match env.new_object_array(0, "java/lang/Object", JObject::null()) {
Ok(row) => row.into(),
Err(e) => {
set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string());
JObject::null()
}
},
_ => JObject::null(),
}
}
#[allow(dead_code)]
fn row_to_obj_array<'local>(
env: &mut JNIEnv<'local>,
row: &limbo_core::Row,
) -> Result<JObject<'local>> {
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())
}

View File

@@ -1,16 +0,0 @@
// 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()
}};
}

View File

@@ -1,30 +1,55 @@
use crate::errors::CustomError;
use jni::objects::{JObject, JValue};
use crate::errors::LimboError;
use jni::objects::{JByteArray, JObject};
use jni::JNIEnv;
pub(crate) fn row_to_obj_array<'local>(
env: &mut JNIEnv<'local>,
row: &limbo_core::Row,
) -> Result<JObject<'local>, CustomError> {
let obj_array =
env.new_object_array(row.values.len() as i32, "java/lang/Object", JObject::null())?;
pub(crate) fn utf8_byte_arr_to_str(
env: &JNIEnv,
bytes: JByteArray,
) -> crate::errors::Result<String> {
let bytes = env
.convert_byte_array(bytes)
.map_err(|_| LimboError::CustomError("Failed to retrieve bytes".to_string()))?;
let str = String::from_utf8(bytes).map_err(|_| {
LimboError::CustomError("Failed to convert utf8 byte array into string".to_string())
})?;
Ok(str)
}
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);
/// Sets the error message and throws a Java exception.
///
/// This function converts the provided error message to a byte array and calls the
/// `throwLimboException` method on the provided Java object to throw an exception.
///
/// # Parameters
/// - `env`: The JNI environment.
/// - `obj`: The Java object on which the exception will be thrown.
/// - `err_code`: The error code corresponding to the exception. Refer to `org.github.tursodatabase.core.Codes` for the list of error codes.
/// - `err_msg`: The error message to be included in the exception.
///
/// # Example
/// ```rust
/// set_err_msg_and_throw_exception(env, obj, Codes::SQLITE_ERROR, "An error occurred".to_string());
/// ```
pub fn set_err_msg_and_throw_exception<'local>(
env: &mut JNIEnv<'local>,
obj: JObject<'local>,
err_code: i32,
err_msg: String,
) {
let error_message_bytes = env
.byte_array_from_slice(err_msg.as_bytes())
.expect("Failed to convert to byte array");
match env.call_method(
obj,
"throwLimboException",
"(I[B)V",
&[err_code.into(), (&error_message_bytes).into()],
) {
Ok(_) => {
// do nothing because above method will always return Err
}
Err(_e) => {
// do nothing because our java app will handle Err
}
}
Ok(obj_array.into())
}

View File

@@ -2,6 +2,7 @@ package org.github.tursodatabase;
import org.github.tursodatabase.annotations.Nullable;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import org.github.tursodatabase.core.LimboConnection;
import org.github.tursodatabase.jdbc4.JDBC4Connection;
import java.sql.*;

View File

@@ -1,8 +1,47 @@
package org.github.tursodatabase;
import org.github.tursodatabase.core.SqliteCode;
/**
* Limbo error code. Superset of SQLite3 error code.
*/
public enum LimboErrorCode {
SQLITE_OK(SqliteCode.SQLITE_OK, "Successful result"),
SQLITE_ERROR(SqliteCode.SQLITE_ERROR, "SQL error or missing database"),
SQLITE_INTERNAL(SqliteCode.SQLITE_INTERNAL, "An internal logic error in SQLite"),
SQLITE_PERM(SqliteCode.SQLITE_PERM, "Access permission denied"),
SQLITE_ABORT(SqliteCode.SQLITE_ABORT, "Callback routine requested an abort"),
SQLITE_BUSY(SqliteCode.SQLITE_BUSY, "The database file is locked"),
SQLITE_LOCKED(SqliteCode.SQLITE_LOCKED, "A table in the database is locked"),
SQLITE_NOMEM(SqliteCode.SQLITE_NOMEM, "A malloc() failed"),
SQLITE_READONLY(SqliteCode.SQLITE_READONLY, "Attempt to write a readonly database"),
SQLITE_INTERRUPT(SqliteCode.SQLITE_INTERRUPT, "Operation terminated by sqlite_interrupt()"),
SQLITE_IOERR(SqliteCode.SQLITE_IOERR, "Some kind of disk I/O error occurred"),
SQLITE_CORRUPT(SqliteCode.SQLITE_CORRUPT, "The database disk image is malformed"),
SQLITE_NOTFOUND(SqliteCode.SQLITE_NOTFOUND, "(Internal Only) Table or record not found"),
SQLITE_FULL(SqliteCode.SQLITE_FULL, "Insertion failed because database is full"),
SQLITE_CANTOPEN(SqliteCode.SQLITE_CANTOPEN, "Unable to open the database file"),
SQLITE_PROTOCOL(SqliteCode.SQLITE_PROTOCOL, "Database lock protocol error"),
SQLITE_EMPTY(SqliteCode.SQLITE_EMPTY, "(Internal Only) Database table is empty"),
SQLITE_SCHEMA(SqliteCode.SQLITE_SCHEMA, "The database schema changed"),
SQLITE_TOOBIG(SqliteCode.SQLITE_TOOBIG, "Too much data for one row of a table"),
SQLITE_CONSTRAINT(SqliteCode.SQLITE_CONSTRAINT, "Abort due to constraint violation"),
SQLITE_MISMATCH(SqliteCode.SQLITE_MISMATCH, "Data type mismatch"),
SQLITE_MISUSE(SqliteCode.SQLITE_MISUSE, "Library used incorrectly"),
SQLITE_NOLFS(SqliteCode.SQLITE_NOLFS, "Uses OS features not supported on host"),
SQLITE_AUTH(SqliteCode.SQLITE_AUTH, "Authorization denied"),
SQLITE_ROW(SqliteCode.SQLITE_ROW, "sqlite_step() has another row ready"),
SQLITE_DONE(SqliteCode.SQLITE_DONE, "sqlite_step() has finished executing"),
SQLITE_INTEGER(SqliteCode.SQLITE_INTEGER, "Integer type"),
SQLITE_FLOAT(SqliteCode.SQLITE_FLOAT, "Float type"),
SQLITE_TEXT(SqliteCode.SQLITE_TEXT, "Text type"),
SQLITE_BLOB(SqliteCode.SQLITE_BLOB, "Blob type"),
SQLITE_NULL(SqliteCode.SQLITE_NULL, "Null type"),
UNKNOWN_ERROR(-1, "Unknown error"),
ETC(9999, "Unclassified error");
LIMBO_FAILED_TO_PARSE_BYTE_ARRAY(1100, "Failed to parse ut8 byte array"),
LIMBO_FAILED_TO_PREPARE_STATEMENT(1200, "Failed to prepare statement"),
LIMBO_ETC(9999, "Unclassified error");
public final int code;
public final String message;

View File

@@ -11,13 +11,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
* differences between the JDBC specification and the Limbo API.
*/
public abstract class AbstractDB {
private final String url;
private final String fileName;
protected final String url;
protected final String filePath;
private final AtomicBoolean closed = new AtomicBoolean(true);
public AbstractDB(String url, String filaName) {
public AbstractDB(String url, String filePath) {
this.url = url;
this.fileName = filaName;
this.filePath = filePath;
}
public boolean isClosed() {
@@ -29,18 +29,6 @@ public abstract class AbstractDB {
*/
public abstract void interrupt() throws SQLException;
/**
* Executes an SQL statement.
*
* @param sql SQL statement to be executed.
* @param autoCommit Whether to auto-commit the transaction.
* @throws SQLException if a database access error occurs.
*/
public final synchronized void exec(String sql, boolean autoCommit) throws SQLException {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
}
/**
* Creates an SQLite interface to a database for the given connection.
*
@@ -48,7 +36,7 @@ public abstract class AbstractDB {
* @throws SQLException if a database access error occurs.
*/
public final synchronized void open(int openFlags) throws SQLException {
open0(fileName, openFlags);
open0(filePath, openFlags);
}
protected abstract void open0(String fileName, int openFlags) throws SQLException;
@@ -65,28 +53,11 @@ public abstract class AbstractDB {
}
/**
* Compiles an SQL statement.
* Connects to a database.
*
* @param stmt The SQL statement to compile.
* @throws SQLException if a database access error occurs.
* @return Pointer to the connection.
*/
public final synchronized void prepare(CoreStatement stmt) throws SQLException {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
}
/**
* Destroys a statement.
*
* @param safePtr the pointer wrapper to remove from internal structures.
* @param ptr the raw pointer to free.
* @return <a href="https://www.sqlite.org/c3ref/c_abort.html">Result Codes</a>
* @throws SQLException if a database access error occurs.
*/
public synchronized int finalize(SafeStmtPtr safePtr, long ptr) throws SQLException {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
}
public abstract long connect() throws SQLException;
/**
* Creates an SQLite interface to a database with the provided open flags.
@@ -104,68 +75,4 @@ public abstract class AbstractDB {
* @throws SQLException if a database access error occurs.
*/
protected abstract void close0() throws SQLException;
/**
* Compiles, evaluates, executes and commits an SQL statement.
*
* @param sql An SQL statement.
* @return Result code.
* @throws SQLException if a database access error occurs.
*/
public abstract int exec(String sql) throws SQLException;
/**
* Compiles an SQL statement.
*
* @param sql An SQL statement.
* @return A SafeStmtPtr object.
* @throws SQLException if a database access error occurs.
*/
protected abstract SafeStmtPtr prepare(String sql) throws SQLException;
/**
* Destroys a prepared statement.
*
* @param stmt Pointer to the statement pointer.
* @return Result code.
* @throws SQLException if a database access error occurs.
*/
protected abstract int finalize(long stmt) throws SQLException;
/**
* Evaluates a statement.
*
* @param stmt Pointer to the statement.
* @return Result code.
* @throws SQLException if a database access error occurs.
*/
public abstract int step(long stmt) throws SQLException;
/**
* Executes a statement with the provided parameters.
*
* @param stmt Stmt object.
* @param vals Array of parameter values.
* @return True if a row of ResultSet is ready; false otherwise.
* @throws SQLException if a database access error occurs.
* @see <a href="https://www.sqlite.org/c_interface.html#sqlite_exec">SQLite Exec</a>
*/
public final synchronized boolean execute(CoreStatement stmt, Object[] vals) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
/**
* Executes an SQL INSERT, UPDATE or DELETE statement with the Stmt object and an array of
* parameter values of the SQL statement.
*
* @param stmt Stmt object.
* @param vals Array of parameter values.
* @return Number of database rows that were changed or inserted or deleted by the most recently
* completed SQL.
* @throws SQLException if a database access error occurs.
*/
public final synchronized long executeUpdate(CoreStatement stmt, Object[] vals) throws SQLException {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
}
}

View File

@@ -1,26 +0,0 @@
package org.github.tursodatabase.core;
import org.github.tursodatabase.LimboConnection;
import java.sql.SQLException;
public abstract class CoreStatement {
private final LimboConnection connection;
protected CoreStatement(LimboConnection connection) {
this.connection = connection;
}
protected void internalClose() throws SQLException {
// TODO
}
protected void clearGeneratedKeys() throws SQLException {
// TODO
}
protected void updateGeneratedKeys() throws SQLException {
// TODO
}
}

View File

@@ -1,36 +1,38 @@
package org.github.tursodatabase;
package org.github.tursodatabase.core;
import org.github.tursodatabase.core.AbstractDB;
import org.github.tursodatabase.core.LimboDB;
import org.github.tursodatabase.annotations.NativeInvocation;
import org.github.tursodatabase.utils.LimboExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
public abstract class LimboConnection implements Connection {
import static org.github.tursodatabase.utils.ByteArrayUtils.stringToUtf8ByteArray;
public abstract class LimboConnection implements Connection {
private static final Logger logger = LoggerFactory.getLogger(LimboConnection.class);
private final long connectionPtr;
private final AbstractDB database;
public LimboConnection(AbstractDB database) {
this.database = database;
}
public LimboConnection(String url, String fileName) throws SQLException {
this(url, fileName, new Properties());
public LimboConnection(String url, String filePath) throws SQLException {
this(url, filePath, new Properties());
}
/**
* Creates a connection to limbo database.
* Creates a connection to limbo database
*
* @param url e.g. "jdbc:sqlite:fileName"
* @param fileName path to file
* @param filePath path to file
*/
public LimboConnection(String url, String fileName, Properties properties) throws SQLException {
public LimboConnection(String url, String filePath, Properties properties) throws SQLException {
AbstractDB db = null;
try {
db = open(url, fileName, properties);
db = open(url, filePath, properties);
} catch (Throwable t) {
try {
if (db != null) {
@@ -44,23 +46,11 @@ public abstract class LimboConnection implements Connection {
}
this.database = db;
this.connectionPtr = db.connect();
}
private static AbstractDB open(String url, String fileName, Properties properties) throws SQLException {
if (fileName.isEmpty()) {
throw new IllegalArgumentException("fileName should not be empty");
}
final AbstractDB database;
try {
LimboDB.load();
database = LimboDB.create(url, fileName);
} catch (Exception e) {
throw new SQLException("Error opening connection", e);
}
database.open(0);
return database;
private static AbstractDB open(String url, String filePath, Properties properties) throws SQLException {
return LimboDBFactory.open(url, filePath, properties);
}
protected void checkOpen() throws SQLException {
@@ -78,7 +68,38 @@ public abstract class LimboConnection implements Connection {
return database.isClosed();
}
public AbstractDB getDatabase() {
return database;
}
/**
* Compiles an SQL statement.
*
* @param sql An SQL statement.
* @return Pointer to statement.
* @throws SQLException if a database access error occurs.
*/
public long 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);
}
private native long prepareUtf8(long connectionPtr, byte[] sqlUtf8) throws SQLException;
/**
* @return busy timeout in milliseconds.
*/
public int getBusyTimeout() {
// TODO: add support for busyTimeout
return 0;
}
// TODO: check whether this is still valid for limbo
/**
* Checks whether the type, concurrency, and holdability settings for a {@link ResultSet} are
* supported by the SQLite interface. Supported settings are:
@@ -101,4 +122,19 @@ public abstract class LimboConnection implements Connection {
if (resultSetHoldability != ResultSet.CLOSE_CURSORS_AT_COMMIT)
throw new SQLException("SQLite only supports closing cursors at commit");
}
public void setBusyTimeout(int busyTimeout) {
// TODO: add support for busy timeout
}
/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorMessageBytes Error message.
*/
@NativeInvocation
private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}
}

View File

@@ -4,23 +4,27 @@ package org.github.tursodatabase.core;
import org.github.tursodatabase.LimboErrorCode;
import org.github.tursodatabase.annotations.NativeInvocation;
import org.github.tursodatabase.annotations.VisibleForTesting;
import org.github.tursodatabase.annotations.Nullable;
import org.github.tursodatabase.exceptions.LimboException;
import org.github.tursodatabase.utils.LimboExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.locks.ReentrantLock;
import static org.github.tursodatabase.utils.ByteArrayUtils.stringToUtf8ByteArray;
/**
* This class provides a thin JNI layer over the SQLite3 C API.
*/
public final class LimboDB extends AbstractDB {
private static final Logger logger = LoggerFactory.getLogger(LimboDB.class);
// Pointer to database instance
private long dbPtr;
private long dbPointer;
private boolean isOpen;
private static boolean isLoaded;
private ReentrantLock dbLock = new ReentrantLock();
static {
if ("The Android Project".equals(System.getProperty("java.vm.vendor"))) {
@@ -46,68 +50,59 @@ public final class LimboDB extends AbstractDB {
/**
* @param url e.g. "jdbc:sqlite:fileName
* @param fileName e.g. path to file
* @param filePath e.g. path to file
*/
public static LimboDB create(String url, String fileName) throws SQLException {
return new LimboDB(url, fileName);
public static LimboDB create(String url, String filePath) throws SQLException {
return new LimboDB(url, filePath);
}
// TODO: receive config as argument
private LimboDB(String url, String fileName) {
super(url, fileName);
private LimboDB(String url, String filePath) {
super(url, filePath);
}
// WRAPPER FUNCTIONS ////////////////////////////////////////////
// TODO: add support for JNI
@Override
protected synchronized native long openUtf8(byte[] file, int openFlags) throws SQLException;
protected native long openUtf8(byte[] file, int openFlags) throws SQLException;
// TODO: add support for JNI
@Override
protected synchronized native void close0() throws SQLException;
@Override
public synchronized int exec(String sql) throws SQLException {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
}
protected native void close0() throws SQLException;
// TODO: add support for JNI
synchronized native int execUtf8(byte[] sqlUtf8) throws SQLException;
native int execUtf8(byte[] sqlUtf8) throws SQLException;
// TODO: add support for JNI
@Override
public native void interrupt();
@Override
protected void open0(String fileName, int openFlags) throws SQLException {
protected void open0(String filePath, int openFlags) throws SQLException {
if (isOpen) {
throw buildLimboException(LimboErrorCode.ETC.code, "Already opened");
throw LimboExceptionUtils.buildLimboException(LimboErrorCode.LIMBO_ETC.code, "Already opened");
}
byte[] fileNameBytes = stringToUtf8ByteArray(fileName);
if (fileNameBytes == null) {
throw buildLimboException(LimboErrorCode.ETC.code, "File name cannot be converted to byteArray. File name: " + fileName);
byte[] filePathBytes = stringToUtf8ByteArray(filePath);
if (filePathBytes == null) {
throw LimboExceptionUtils.buildLimboException(LimboErrorCode.LIMBO_ETC.code, "File path cannot be converted to byteArray. File name: " + filePath);
}
dbPtr = openUtf8(fileNameBytes, openFlags);
dbPointer = openUtf8(filePathBytes, openFlags);
isOpen = true;
}
@Override
protected synchronized SafeStmtPtr prepare(String sql) throws SQLException {
// TODO: add implementation
throw new SQLFeatureNotSupportedException();
public long connect() throws SQLException {
byte[] filePathBytes = stringToUtf8ByteArray(filePath);
if (filePathBytes == null) {
throw LimboExceptionUtils.buildLimboException(LimboErrorCode.LIMBO_ETC.code, "File path cannot be converted to byteArray. File name: " + filePath);
}
return connect0(filePathBytes, dbPointer);
}
// TODO: add support for JNI
@Override
protected synchronized native int finalize(long stmt);
// TODO: add support for JNI
@Override
public synchronized native int step(long stmt);
private native long connect0(byte[] path, long databasePtr) throws SQLException;
@VisibleForTesting
native void throwJavaException(int errorCode) throws SQLException;
@@ -120,42 +115,6 @@ public final class LimboDB extends AbstractDB {
*/
@NativeInvocation
private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
String errorMessage = utf8ByteBufferToString(errorMessageBytes);
throw buildLimboException(errorCode, errorMessage);
}
/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorMessage Error message.
*/
public LimboException buildLimboException(int errorCode, @Nullable String errorMessage) throws SQLException {
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);
}
@Nullable
private static String utf8ByteBufferToString(@Nullable byte[] buffer) {
if (buffer == null) {
return null;
}
return new String(buffer, StandardCharsets.UTF_8);
}
@Nullable
private static byte[] stringToUtf8ByteArray(@Nullable String str) {
if (str == null) {
return null;
}
return str.getBytes(StandardCharsets.UTF_8);
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}
}

View File

@@ -0,0 +1,47 @@
package org.github.tursodatabase.core;
import java.sql.SQLException;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* Factory class for managing and creating instances of {@link LimboDB}.
* This class ensures that multiple instances of {@link LimboDB} with the same URL are not created.
*/
public class LimboDBFactory {
private static final ConcurrentHashMap<String, LimboDB> databaseHolder = new ConcurrentHashMap<>();
/**
* If a database with the same URL already exists, it returns the existing instance.
* Otherwise, it creates a new instance and stores it in the database holder.
*
* @param url the URL of the database
* @param filePath the path to the database file
* @param properties additional properties for the database connection
* @return an instance of {@link LimboDB}
* @throws SQLException if there is an error opening the connection
* @throws IllegalArgumentException if the fileName is empty
*/
public static LimboDB open(String url, String filePath, Properties properties) throws SQLException {
if (databaseHolder.containsKey(url)) {
return databaseHolder.get(url);
}
if (filePath.isEmpty()) {
throw new IllegalArgumentException("filePath should not be empty");
}
final LimboDB database;
try {
LimboDB.load();
database = LimboDB.create(url, filePath);
} catch (Exception e) {
throw new SQLException("Error opening connection", e);
}
database.open(0);
databaseHolder.put(url, database);
return database;
}
}

View File

@@ -0,0 +1,42 @@
package org.github.tursodatabase.core;
import java.sql.SQLException;
/**
* JDBC ResultSet.
*/
public abstract class LimboResultSet {
protected final LimboStatement statement;
// Whether the result set does not have any rows.
protected boolean isEmptyResultSet = false;
// If the result set is open. Doesn't mean it has results.
private boolean open = false;
// Maximum number of rows as set by the statement
protected long maxRows;
// number of current row, starts at 1 (0 is used to represent loading data)
protected int row = 0;
protected LimboResultSet(LimboStatement statement) {
this.statement = statement;
}
/**
* Checks the status of the result set.
*
* @return true if it's ready to iterate over the result set; false otherwise.
*/
public boolean isOpen() {
return open;
}
/**
* @throws SQLException if not {@link #open}
*/
protected void checkOpen() throws SQLException {
if (!open) {
throw new SQLException("ResultSet closed");
}
}
}

View File

@@ -0,0 +1,68 @@
package org.github.tursodatabase.core;
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 java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public abstract class LimboStatement {
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);
}
protected void internalClose() throws SQLException {
// TODO
}
protected void clearGeneratedKeys() throws SQLException {
// TODO
}
protected void updateGeneratedKeys() throws SQLException {
// TODO
}
// TODO: associate the result with CoreResultSet
// TODO: we can make this async!!
// TODO: distinguish queries that return result or doesn't return result
protected List<Object[]> execute(long stmtPointer) throws SQLException {
List<Object[]> result = new ArrayList<>();
while (true) {
Object[] stepResult = step(stmtPointer);
if (stepResult != null) {
for (int i = 0; i < stepResult.length; i++) {
System.out.println("stepResult" + i + ": " + stepResult[i]);
}
}
if (stepResult == null) break;
result.add(stepResult);
}
return result;
}
private native Object[] step(long stmtPointer) throws SQLException;
/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorMessageBytes Error message.
*/
@NativeInvocation
private void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}
}

View File

@@ -1,5 +0,0 @@
package org.github.tursodatabase.core;
// TODO: add fields and methods
public class SafeStmtPtr {
}

View File

@@ -15,7 +15,10 @@
*/
package org.github.tursodatabase.core;
public class Codes {
/**
* Sqlite error codes.
*/
public class SqliteCode {
/** Successful result */
public static final int SQLITE_OK = 0;

View File

@@ -1,13 +0,0 @@
package org.github.tursodatabase.exceptions;
/**
* This class defines error codes that correspond to specific error conditions
* that may occur while communicating with the JNI.
* <p />
* Refer to ErrorCode in rust package.
* TODO: Deprecate
*/
public class ErrorCode {
public static int CONNECTION_FAILURE = -1;
}

View File

@@ -1,6 +1,6 @@
package org.github.tursodatabase.jdbc4;
import org.github.tursodatabase.LimboConnection;
import org.github.tursodatabase.core.LimboConnection;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import java.sql.*;
@@ -11,8 +11,12 @@ import java.util.concurrent.Executor;
public class JDBC4Connection extends LimboConnection {
public JDBC4Connection(String url, String fileName, Properties properties) throws SQLException {
super(url, fileName, properties);
public JDBC4Connection(String url, String filePath) throws SQLException {
super(url, filePath);
}
public JDBC4Connection(String url, String filePath, Properties properties) throws SQLException {
super(url, filePath, properties);
}
@Override

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,18 @@
package org.github.tursodatabase.jdbc4;
import org.github.tursodatabase.LimboConnection;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import org.github.tursodatabase.core.CoreStatement;
import org.github.tursodatabase.core.LimboConnection;
import org.github.tursodatabase.core.LimboStatement;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* Implementation of the {@link Statement} interface for JDBC 4.
*/
public class JDBC4Statement extends CoreStatement implements Statement {
public class JDBC4Statement extends LimboStatement implements Statement {
private boolean closed;
private boolean closeOnCompletion;
@@ -18,6 +21,12 @@ public class JDBC4Statement extends CoreStatement implements Statement {
private final int resultSetConcurrency;
private final int resultSetHoldability;
private int queryTimeoutSeconds;
private long updateCount;
private boolean exhaustedResults = false;
private ReentrantLock connectionLock = new ReentrantLock();
public JDBC4Statement(LimboConnection connection) {
this(connection, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT);
}
@@ -84,7 +93,10 @@ public class JDBC4Statement extends CoreStatement implements Statement {
@Override
public void setQueryTimeout(int seconds) throws SQLException {
// TODO
if (seconds < 0) {
throw new SQLException("Query timeout must be greater than 0");
}
this.queryTimeoutSeconds = seconds;
}
@Override
@@ -111,8 +123,22 @@ public class JDBC4Statement extends CoreStatement implements Statement {
@Override
public boolean execute(String sql) throws SQLException {
// TODO
return false;
internalClose();
return this.withConnectionTimeout(
() -> {
try {
connectionLock.lock();
final long stmtPointer = connection.prepare(sql);
List<Object[]> result = execute(stmtPointer);
updateGeneratedKeys();
exhaustedResults = false;
return !result.isEmpty();
} finally {
connectionLock.unlock();
}
}
);
}
@Override
@@ -287,4 +313,25 @@ public class JDBC4Statement extends CoreStatement implements Statement {
// TODO
return false;
}
private <T> T withConnectionTimeout(SQLCallable<T> callable) throws SQLException {
final int originalBusyTimeoutMillis = connection.getBusyTimeout();
if (queryTimeoutSeconds > 0) {
// TODO: set busy timeout
connection.setBusyTimeout(1000 * queryTimeoutSeconds);
}
try {
return callable.call();
} finally {
if (queryTimeoutSeconds > 0) {
connection.setBusyTimeout(originalBusyTimeoutMillis);
}
}
}
@FunctionalInterface
protected interface SQLCallable<T> {
T call() throws SQLException;
}
}

View File

@@ -0,0 +1,24 @@
package org.github.tursodatabase.utils;
import org.github.tursodatabase.annotations.Nullable;
import java.nio.charset.StandardCharsets;
public class ByteArrayUtils {
@Nullable
public static String utf8ByteBufferToString(@Nullable byte[] buffer) {
if (buffer == null) {
return null;
}
return new String(buffer, StandardCharsets.UTF_8);
}
@Nullable
public static byte[] stringToUtf8ByteArray(@Nullable String str) {
if (str == null) {
return null;
}
return str.getBytes(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,40 @@
package org.github.tursodatabase.utils;
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 errorMessageBytes Error message.
*/
public static void throwLimboException(int errorCode, byte[] errorMessageBytes) throws SQLException {
String errorMessage = utf8ByteBufferToString(errorMessageBytes);
throw buildLimboException(errorCode, errorMessage);
}
/**
* Throws formatted SQLException with error code and message.
*
* @param errorCode Error code.
* @param errorMessage Error message.
*/
public static LimboException buildLimboException(int errorCode, @Nullable String errorMessage) throws SQLException {
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);
}
}

View File

@@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -0,0 +1,38 @@
package org.github.tursodatabase;
import org.github.tursodatabase.jdbc4.JDBC4Connection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class IntegrationTest {
private JDBC4Connection connection;
@BeforeEach
void setUp() throws Exception {
String filePath = TestUtils.createTempFile();
String url = "jdbc:sqlite:" + filePath;
connection = new JDBC4Connection(url, filePath, new Properties());
}
@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);");
stmt.execute("INSERT INTO users VALUES (1, 'seonwoo');");
stmt.execute("INSERT INTO users VALUES (2, 'seonwoo');");
stmt.execute("INSERT INTO users VALUES (3, 'seonwoo');");
stmt.execute("SELECT * FROM users");
}
private Statement createDefaultStatement() throws SQLException {
return connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT);
}
}

View File

@@ -1,5 +1,6 @@
package org.github.tursodatabase;
import org.github.tursodatabase.core.LimboConnection;
import org.junit.jupiter.api.Test;
import java.sql.Connection;

View File

@@ -0,0 +1,32 @@
package org.github.tursodatabase.core;
import org.github.tursodatabase.TestUtils;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class LimboDBFactoryTest {
@Test
void single_database_should_be_created_when_urls_are_same() throws Exception {
String filePath = TestUtils.createTempFile();
String url = "jdbc:sqlite:" + filePath;
LimboDB db1 = LimboDBFactory.open(url, filePath, new Properties());
LimboDB db2 = LimboDBFactory.open(url, filePath, new Properties());
assertEquals(db1, db2);
}
@Test
void multiple_databases_should_be_created_when_urls_differ() throws Exception {
String filePath1 = TestUtils.createTempFile();
String filePath2 = TestUtils.createTempFile();
String url1 = "jdbc:sqlite:" + filePath1;
String url2 = "jdbc:sqlite:" + filePath2;
LimboDB db1 = LimboDBFactory.open(url1, filePath1, new Properties());
LimboDB db2 = LimboDBFactory.open(url2, filePath2, new Properties());
assertNotEquals(db1, db2);
}
}

View File

@@ -36,7 +36,7 @@ public class LimboDBTest {
LimboDB.load();
LimboDB db = LimboDB.create("jdbc:sqlite:" + dbPath, dbPath);
final int limboExceptionCode = LimboErrorCode.ETC.code;
final int limboExceptionCode = LimboErrorCode.LIMBO_ETC.code;
try {
db.throwJavaException(limboExceptionCode);
} catch (Exception e) {

View File

@@ -1,6 +1,7 @@
package org.github.tursodatabase.jdbc4;
import org.github.tursodatabase.TestUtils;
import org.github.tursodatabase.core.LimboConnection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -10,7 +11,6 @@ import java.sql.Statement;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
class JDBC4ConnectionTest {
@@ -18,9 +18,9 @@ class JDBC4ConnectionTest {
@BeforeEach
void setUp() throws Exception {
String fileUrl = TestUtils.createTempFile();
String url = "jdbc:sqlite:" + fileUrl;
connection = new JDBC4Connection(url, fileUrl, new Properties());
String filePath = TestUtils.createTempFile();
String url = "jdbc:sqlite:" + filePath;
connection = new JDBC4Connection(url, filePath, new Properties());
}
@Test
@@ -55,4 +55,9 @@ class JDBC4ConnectionTest {
connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, -1);
});
}
@Test
void prepare_simple_create_table() throws Exception {
connection.prepare("CREATE TABLE users (id INT PRIMARY KEY, username TEXT)");
}
}

View File

@@ -0,0 +1,14 @@
Logback LICENSE
---------------
Logback: the reliable, generic, fast and flexible logging framework.
Copyright (C) 1999-2024, QOS.ch. All rights reserved.
This program and the accompanying materials are dual-licensed under
either the terms of the Eclipse Public License v1.0 as published by
the Eclipse Foundation
or (per the licensee's choosing)
under the terms of the GNU Lesser General Public License version 2.1
as published by the Free Software Foundation.