mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-27 21:14:21 +01:00
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:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod connection;
|
||||
mod cursor;
|
||||
mod errors;
|
||||
mod limbo_connection;
|
||||
mod limbo_db;
|
||||
mod macros;
|
||||
mod limbo_statement;
|
||||
mod utils;
|
||||
|
||||
83
bindings/java/rs_src/limbo_connection.rs
Normal file
83
bindings/java/rs_src/limbo_connection.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
bindings/java/rs_src/limbo_statement.rs
Normal file
98
bindings/java/rs_src/limbo_statement.rs
Normal 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())
|
||||
}
|
||||
@@ -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()
|
||||
}};
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.github.tursodatabase.core;
|
||||
|
||||
// TODO: add fields and methods
|
||||
public class SafeStmtPtr {
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
11
bindings/java/src/main/resources/logback.xml
Normal file
11
bindings/java/src/main/resources/logback.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.github.tursodatabase;
|
||||
|
||||
import org.github.tursodatabase.core.LimboConnection;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
14
licenses/bindings/java/logback-license.md
Normal file
14
licenses/bindings/java/logback-license.md
Normal 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.
|
||||
Reference in New Issue
Block a user