use crate::embedded::c_api::database::LinkrsHandle;
use crate::embedded::c_api::error::{
error_code_from_core_error, extended_error_code_from_core_error, linkrs_error_code_t,
set_last_error_message,
};
use crate::embedded::c_api::types::{
linkrs_commit_hook_callback, linkrs_extended_error_code_t, linkrs_rollback_hook_callback,
linkrs_session_t, linkrs_t, linkrs_trace_callback, linkrs_update_hook_callback,
};
use crate::embedded::Session;
use linkrs_storage::GraphStorage;
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::ptr;
pub struct LinkrsSessionHandle {
pub(crate) inner: Session<GraphStorage>,
pub(crate) last_error: Option<CString>,
pub(crate) busy_timeout_ms: u32,
pub(crate) last_error_offset: Option<usize>,
pub(crate) last_extended_error: Option<linkrs_extended_error_code_t>,
pub(crate) trace_callback: linkrs_trace_callback,
pub(crate) trace_user_data: *mut c_void,
pub(crate) commit_hook_user_data: *mut c_void,
pub(crate) commit_veto_id: Option<linkrs_core::event_dispatch::SubscriptionId>,
pub(crate) rollback_hook: linkrs_rollback_hook_callback,
pub(crate) rollback_hook_user_data: *mut c_void,
pub(crate) update_hook: linkrs_update_hook_callback,
pub(crate) update_hook_user_data: *mut c_void,
}
unsafe impl Send for LinkrsSessionHandle {}
unsafe impl Sync for LinkrsSessionHandle {}
impl LinkrsSessionHandle {
pub(crate) fn new(inner: Session<GraphStorage>) -> Self {
Self {
inner,
last_error: None,
busy_timeout_ms: 5000, last_error_offset: None,
last_extended_error: None,
trace_callback: None,
trace_user_data: ptr::null_mut(),
commit_hook_user_data: ptr::null_mut(),
commit_veto_id: None,
rollback_hook: None,
rollback_hook_user_data: ptr::null_mut(),
update_hook: None,
update_hook_user_data: ptr::null_mut(),
}
}
pub(crate) fn has_update_hook(&self) -> bool {
self.update_hook.is_some()
}
pub(crate) fn invoke_update_hook(&self, operation: i32, space_name: &str, rowid: i64) {
if let Some(callback) = self.update_hook {
if let Ok(c_space) = CString::new(space_name) {
if let Ok(empty_table) = CString::new("") {
let user_data = self.update_hook_user_data;
let space_ptr = c_space.as_ptr();
let table_ptr = empty_table.as_ptr();
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
callback(user_data, operation, space_ptr, table_ptr, rowid);
}))
.is_err()
{
log::error!("update hook panicked; continuing query");
}
}
}
}
}
pub(crate) fn trace(&self, sql: &str) {
if let Some(callback) = self.trace_callback {
if let Ok(c_sql) = CString::new(sql) {
let user_data = self.trace_user_data;
let sql_ptr = c_sql.as_ptr();
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
callback(sql_ptr, user_data);
}))
.is_err()
{
log::error!("trace callback panicked; continuing query");
}
}
}
}
pub(crate) fn set_error(
&mut self,
message: String,
offset: Option<usize>,
extended_code: Option<linkrs_extended_error_code_t>,
) {
self.last_error = CString::new(message.clone()).ok();
self.last_error_offset = offset;
self.last_extended_error = extended_code;
set_last_error_message(message);
}
pub(crate) fn clear_error(&mut self) {
self.last_error = None;
self.last_error_offset = None;
self.last_extended_error = None;
}
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_session_create(
db: *mut linkrs_t,
session: *mut *mut linkrs_session_t,
) -> c_int {
if db.is_null() || session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let db_handle = &*(db as *mut LinkrsHandle);
match db_handle.inner.session() {
Ok(sess) => {
let mut handle = Box::new(LinkrsSessionHandle::new(sess));
let timeout_ms = db_handle.inner.config().default_timeout.as_millis();
handle.busy_timeout_ms = timeout_ms.min(u32::MAX as u128) as u32;
*session = Box::into_raw(handle) as *mut linkrs_session_t;
linkrs_error_code_t::LINKRS_OK as c_int
}
Err(e) => {
let (error_code, _) = error_code_from_core_error(&e);
let error_msg = format!("{}", e);
set_last_error_message(error_msg);
*session = ptr::null_mut();
error_code
}
}
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_session_close(session: *mut linkrs_session_t) -> c_int {
if session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let handle = Box::from_raw(session as *mut LinkrsSessionHandle);
if let Some(id) = handle.commit_veto_id {
handle.inner.txn_manager().unregister_commit_veto(id);
}
drop(handle);
linkrs_error_code_t::LINKRS_OK as c_int
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_session_use_space(
session: *mut linkrs_session_t,
space_name: *const c_char,
) -> c_int {
if session.is_null() || space_name.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let name_str = match CStr::from_ptr(space_name).to_str() {
Ok(s) => s,
Err(_) => return linkrs_error_code_t::LINKRS_MISUSE as c_int,
};
let handle = &mut *(session as *mut LinkrsSessionHandle);
match handle.inner.use_space(name_str) {
Ok(_) => {
handle.clear_error();
linkrs_error_code_t::LINKRS_OK as c_int
}
Err(e) => {
let (error_code, _) = error_code_from_core_error(&e);
let error_msg = format!("{}", e);
let offset = e.error_offset();
let extended_code = Some(extended_error_code_from_core_error(&e));
handle.set_error(error_msg, offset, extended_code);
error_code
}
}
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_session_current_space(
session: *mut linkrs_session_t,
) -> *mut c_char {
if session.is_null() {
return ptr::null_mut();
}
let handle = &*(session as *mut LinkrsSessionHandle);
match handle.inner.current_space() {
Some(name) => {
match CString::new(name) {
Ok(c_name) => {
c_name.into_raw()
}
Err(_) => ptr::null_mut(),
}
}
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_session_set_autocommit(
session: *mut linkrs_session_t,
autocommit: bool,
) -> c_int {
if session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let handle = &mut *(session as *mut LinkrsSessionHandle);
handle.inner.set_auto_commit(autocommit);
linkrs_error_code_t::LINKRS_OK as c_int
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_session_get_autocommit(session: *mut linkrs_session_t) -> bool {
if session.is_null() {
return true; }
let handle = &*(session as *mut LinkrsSessionHandle);
handle.inner.auto_commit()
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_busy_timeout(
session: *mut linkrs_session_t,
timeout_ms: c_int,
) -> c_int {
if session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let handle = &mut *(session as *mut LinkrsSessionHandle);
handle.busy_timeout_ms = timeout_ms.max(0) as u32;
linkrs_error_code_t::LINKRS_OK as c_int
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_busy_timeout_get(session: *mut linkrs_session_t) -> c_int {
if session.is_null() {
return 0;
}
let handle = &*(session as *mut LinkrsSessionHandle);
handle.busy_timeout_ms as c_int
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_trace(
session: *mut linkrs_session_t,
callback: linkrs_trace_callback,
user_data: *mut c_void,
) -> c_int {
if session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let handle = &mut *(session as *mut LinkrsSessionHandle);
handle.trace_callback = callback;
handle.trace_user_data = user_data;
linkrs_error_code_t::LINKRS_OK as c_int
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_commit_hook(
session: *mut linkrs_session_t,
callback: linkrs_commit_hook_callback,
user_data: *mut c_void,
) -> *mut c_void {
if session.is_null() {
return ptr::null_mut();
}
let handle = &mut *(session as *mut LinkrsSessionHandle);
let old_user_data = handle.commit_hook_user_data;
if let Some(id) = handle.commit_veto_id.take() {
handle.inner.txn_manager().unregister_commit_veto(id);
}
handle.commit_hook_user_data = user_data;
if let Some(func) = callback {
let user_data_addr = user_data as usize;
let bridge: std::sync::Arc<
dyn Fn(&linkrs_transaction::CommitVetoContext) -> linkrs_transaction::VetoDecision
+ Send
+ Sync,
> = std::sync::Arc::new(move |_| {
let restored = user_data_addr as *mut c_void;
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| func(restored)));
match outcome {
Ok(0) => linkrs_transaction::VetoDecision::allow(),
Ok(_) => linkrs_transaction::VetoDecision::veto("c commit hook vetoed"),
Err(_) => {
log::error!("commit hook panicked; treating as allow");
linkrs_transaction::VetoDecision::allow()
}
}
});
let id = handle.inner.txn_manager().register_commit_veto(bridge);
handle.commit_veto_id = Some(id);
}
old_user_data
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_rollback_hook(
session: *mut linkrs_session_t,
callback: linkrs_rollback_hook_callback,
user_data: *mut c_void,
) -> *mut c_void {
if session.is_null() {
return ptr::null_mut();
}
let handle = &mut *(session as *mut LinkrsSessionHandle);
let old_user_data = handle.rollback_hook_user_data;
handle.rollback_hook = callback;
handle.rollback_hook_user_data = user_data;
old_user_data
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_update_hook(
session: *mut linkrs_session_t,
callback: linkrs_update_hook_callback,
user_data: *mut c_void,
) -> *mut c_void {
if session.is_null() {
return ptr::null_mut();
}
let handle = &mut *(session as *mut LinkrsSessionHandle);
let old_user_data = handle.update_hook_user_data;
handle.update_hook = callback;
handle.update_hook_user_data = user_data;
old_user_data
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_connection_interrupt(session: *mut linkrs_session_t) -> c_int {
if session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let handle = &*(session as *const LinkrsSessionHandle);
handle.inner.interrupt();
linkrs_error_code_t::LINKRS_OK as c_int
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_connection_clear_interrupt(
session: *mut linkrs_session_t,
) -> c_int {
if session.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let handle = &*(session as *const LinkrsSessionHandle);
handle.inner.clear_interrupt();
linkrs_error_code_t::LINKRS_OK as c_int
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded::c_api::database::{linkrs_close, linkrs_open};
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn create_test_db() -> *mut linkrs_t {
let counter = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let temp_dir = std::env::temp_dir().join("linkrs_c_api_test");
std::fs::create_dir_all(&temp_dir).ok();
let db_path = temp_dir.join(format!(
"test_session_{}_{}.db",
std::process::id(),
counter
));
if db_path.exists() {
std::fs::remove_file(&db_path).ok();
std::thread::sleep(std::time::Duration::from_millis(50));
}
let path_cstring = CString::new(db_path.to_str().expect("Invalid path"))
.expect("Failed to create CString");
let mut db: *mut linkrs_t = ptr::null_mut();
let rc = unsafe { linkrs_open(path_cstring.as_ptr(), &mut db) };
if rc != linkrs_error_code_t::LINKRS_OK as c_int {
panic!(
"Failed to open database, error code: {}, path: {:?}",
rc, db_path
);
}
assert!(!db.is_null());
db
}
#[test]
fn test_session_create_close() {
let db = create_test_db();
let mut session: *mut linkrs_session_t = ptr::null_mut();
let rc = unsafe { linkrs_session_create(db, &mut session) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
assert!(!session.is_null());
let rc = unsafe { linkrs_session_close(session) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
let rc = unsafe { linkrs_close(db) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
}
#[test]
fn test_session_autocommit() {
let db = create_test_db();
let mut session: *mut linkrs_session_t = ptr::null_mut();
let rc = unsafe { linkrs_session_create(db, &mut session) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
let autocommit = unsafe { linkrs_session_get_autocommit(session) };
assert!(autocommit);
let rc = unsafe { linkrs_session_set_autocommit(session, false) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
let autocommit = unsafe { linkrs_session_get_autocommit(session) };
assert!(!autocommit);
unsafe { linkrs_session_close(session) };
unsafe { linkrs_close(db) };
}
}