diff --git a/bindings/java/rs_src/connection.rs b/bindings/java/rs_src/connection.rs index ef3a565bd..d4a492d40 100644 --- a/bindings/java/rs_src/connection.rs +++ b/bindings/java/rs_src/connection.rs @@ -1,14 +1,19 @@ -use crate::cursor::Cursor; -use jni::objects::JClass; +use crate::errors::{ + LimboError, Result, LIMBO_ETC, LIMBO_FAILED_TO_PARSE_BYTE_ARRAY, + LIMBO_FAILED_TO_PREPARE_STATEMENT, +}; +use crate::utils::utf8_byte_arr_to_str; +use jni::objects::{JByteArray, JClass, JObject}; use jni::sys::jlong; use jni::JNIEnv; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +#[allow(dead_code)] #[derive(Clone)] pub struct Connection { - pub(crate) conn: Arc>>, - pub(crate) io: Arc, + pub(crate) conn: Rc, + pub(crate) io: Arc, } /// Returns a pointer to a `Cursor` object. @@ -26,20 +31,45 @@ pub struct Connection { /// /// 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>, +pub extern "system" fn Java_org_github_tursodatabase_limbo_LimboConnection_prepareUtf8<'local>( + mut env: JNIEnv<'local>, + obj: JObject<'local>, connection_ptr: jlong, + sql_bytes: JByteArray<'local>, ) -> jlong { - let connection = to_connection(connection_ptr); - let cursor = Cursor { - array_size: 1, - conn: connection.clone(), - description: None, - rowcount: -1, - smt: None, + let connection = match to_connection(connection_ptr) { + Ok(conn) => conn, + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, LIMBO_ETC, e.to_string()); + return 0; + } }; - Box::into_raw(Box::new(cursor)) as jlong + + 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) => Box::into_raw(Box::new(stmt)) as jlong, + Err(e) => { + set_err_msg_and_throw_exception( + &mut env, + obj, + LIMBO_FAILED_TO_PREPARE_STATEMENT, + e.to_string(), + ); + 0 + } + } } /// Closes the connection and releases the associated resources. @@ -61,24 +91,34 @@ pub unsafe extern "system" fn Java_org_github_tursodatabase_limbo_Connection_clo 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!() +fn to_connection(connection_ptr: jlong) -> Result<&'static mut Rc> { + if connection_ptr == 0 { + Err(LimboError::InvalidConnectionPointer) + } else { + unsafe { Ok(&mut *(connection_ptr as *mut Rc)) } + } } -#[no_mangle] -pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_rollback<'local>( - _env: &mut JNIEnv<'local>, - _class: JClass<'local>, - _connection_id: jlong, +fn set_err_msg_and_throw_exception<'local>( + env: &mut JNIEnv<'local>, + obj: JObject<'local>, + err_code: i32, + err_msg: String, ) { - unimplemented!() -} - -fn to_connection(connection_ptr: jlong) -> &'static mut Connection { - unsafe { &mut *(connection_ptr as *mut Connection) } + 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 + } + } } diff --git a/bindings/java/rs_src/cursor.rs b/bindings/java/rs_src/cursor.rs deleted file mode 100644 index da67292d4..000000000 --- a/bindings/java/rs_src/cursor.rs +++ /dev/null @@ -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, - - /// 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 index b11f46e3c..0fa2e0276 100644 --- a/bindings/java/rs_src/errors.rs +++ b/bindings/java/rs_src/errors.rs @@ -13,11 +13,11 @@ pub enum LimboError { InvalidConnectionPointer, #[error("JNI Errors: `{0}`")] - JNIErrors(Error) + JNIErrors(Error), } impl From for LimboError { - fn from(value: limbo_core::LimboError) -> Self { + fn from(_value: limbo_core::LimboError) -> Self { todo!() } } @@ -44,53 +44,70 @@ impl From for LimboError { pub type Result = std::result::Result; -/// 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; -} - +#[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; -// types returned by sqlite3_column_type() +#[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; -// Limbo custom error codes -pub const LIMBO_DATABASE_ALREADY_CLOSED: i32 = 1000; +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; diff --git a/bindings/java/rs_src/lib.rs b/bindings/java/rs_src/lib.rs index 4dfadd743..9796ff19a 100644 --- a/bindings/java/rs_src/lib.rs +++ b/bindings/java/rs_src/lib.rs @@ -1,6 +1,4 @@ mod connection; -mod cursor; mod errors; mod limbo_db; -mod macros; mod utils; diff --git a/bindings/java/rs_src/limbo_db.rs b/bindings/java/rs_src/limbo_db.rs index e71a849ec..bd1ad5082 100644 --- a/bindings/java/rs_src/limbo_db.rs +++ b/bindings/java/rs_src/limbo_db.rs @@ -1,3 +1,4 @@ +use crate::connection::Connection; use crate::errors::{LimboError, Result}; use jni::objects::{JByteArray, JObject}; use jni::sys::{jint, jlong}; @@ -12,7 +13,7 @@ const ERROR_CODE_ETC: i32 = 9999; 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() { @@ -24,7 +25,7 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_openUtf8<'loca }; 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) { @@ -52,9 +53,11 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_openUtf8<'loca } #[no_mangle] +#[allow(clippy::arc_with_non_send_sync)] 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_db(db_pointer) { @@ -65,7 +68,45 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_connect0<'loca } }; - Box::into_raw(Box::new(db.connect())) as jlong + 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, ERROR_CODE_ETC, e.to_string()); + return 0; + } + }, + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string()); + return 0; + } + }; + + let io: Arc = match path.as_str() { + ":memory:" => match limbo_core::MemoryIO::new() { + Ok(io) => Arc::new(io), + Err(e) => { + set_err_msg_and_throw_exception(&mut env, obj, ERROR_CODE_ETC, e.to_string()); + return 0; + } + }, + _ => 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()); + return 0; + } + }, + }; + let conn = Connection { + conn: db.connect(), + io, + }; + + Box::into_raw(Box::new(conn)) as jlong } fn to_db(db_pointer: jlong) -> Result<&'static mut Arc> { @@ -90,16 +131,6 @@ pub extern "system" fn Java_org_github_tursodatabase_core_LimboDB_throwJavaExcep ); } -fn utf8_byte_arr_to_str(env: &JNIEnv, bytes: JByteArray) -> Result { - let bytes = env - .convert_byte_array(bytes) - .map_err(|e| LimboError::CustomError("Failed to retrieve bytes".to_string()))?; - let str = String::from_utf8(bytes).map_err(|e| { - LimboError::CustomError("Failed to convert utf8 byte array into string".to_string()) - })?; - Ok(str) -} - /// Sets the error message and throws a Java exception. /// /// This function converts the provided error message to a byte array and calls the diff --git a/bindings/java/rs_src/macros.rs b/bindings/java/rs_src/macros.rs deleted file mode 100644 index 967834f9f..000000000 --- a/bindings/java/rs_src/macros.rs +++ /dev/null @@ -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() - }}; -} diff --git a/bindings/java/rs_src/utils.rs b/bindings/java/rs_src/utils.rs index 8f50be554..4f640be0f 100644 --- a/bindings/java/rs_src/utils.rs +++ b/bindings/java/rs_src/utils.rs @@ -1,7 +1,8 @@ use crate::errors::LimboError; -use jni::objects::{JObject, JValue}; +use jni::objects::{JByteArray, JObject, JValue}; use jni::JNIEnv; +#[allow(dead_code)] pub(crate) fn row_to_obj_array<'local>( env: &mut JNIEnv<'local>, row: &limbo_core::Row, @@ -28,3 +29,16 @@ pub(crate) fn row_to_obj_array<'local>( Ok(obj_array.into()) } + +pub(crate) fn utf8_byte_arr_to_str( + env: &JNIEnv, + bytes: JByteArray, +) -> crate::errors::Result { + 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) +}