use crate::embedded::c_api::error::{
error_code_from_core_error, extended_error_code_from_core_error, linkrs_error_code_t,
};
use crate::embedded::c_api::result::LinkrsResultHandle;
use crate::embedded::c_api::session::LinkrsSessionHandle;
use crate::embedded::c_api::types::{linkrs_result_t, linkrs_session_t, linkrs_value_t};
use linkrs_core::Value;
use std::collections::HashMap;
use std::ffi::{c_char, c_int, CStr};
use std::ptr;
#[no_mangle]
pub unsafe extern "C" fn linkrs_execute(
session: *mut linkrs_session_t,
query: *const c_char,
result: *mut *mut linkrs_result_t,
) -> c_int {
if session.is_null() || query.is_null() || result.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let query_str = unsafe {
match CStr::from_ptr(query).to_str() {
Ok(s) => s,
Err(_) => return linkrs_error_code_t::LINKRS_MISUSE as c_int,
}
};
unsafe {
let handle = &mut *(session as *mut LinkrsSessionHandle);
handle.trace(query_str);
match handle.inner.execute(query_str) {
Ok(query_result) => {
handle.clear_error();
if handle.has_update_hook() || handle.inner.has_dml_observers() {
let rows = query_result.metadata().rows_returned as u64;
if let Some(op) = handle.inner.notify_dml(query_str, rows) {
if handle.has_update_hook() {
let operation = match op {
linkrs_query::DmlOp::Insert => 1,
linkrs_query::DmlOp::Update => 2,
linkrs_query::DmlOp::Delete => 3,
};
let space_name_owned = handle.inner.current_space();
let space_name = space_name_owned.as_deref().unwrap_or("default");
handle.invoke_update_hook(operation, space_name, rows as i64);
}
}
}
let result_handle = Box::new(LinkrsResultHandle {
inner: query_result,
});
*result = Box::into_raw(result_handle) as *mut linkrs_result_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);
let offset = e.error_offset();
let extended_code = Some(extended_error_code_from_core_error(&e));
handle.set_error(error_msg, offset, extended_code);
*result = ptr::null_mut();
error_code
}
}
}
}
#[no_mangle]
pub unsafe extern "C" fn linkrs_execute_params(
session: *mut linkrs_session_t,
query: *const c_char,
params: *const linkrs_value_t,
param_count: usize,
result: *mut *mut linkrs_result_t,
) -> c_int {
if session.is_null() || query.is_null() || result.is_null() {
return linkrs_error_code_t::LINKRS_MISUSE as c_int;
}
let query_str = unsafe {
match CStr::from_ptr(query).to_str() {
Ok(s) => s,
Err(_) => return linkrs_error_code_t::LINKRS_MISUSE as c_int,
}
};
let mut params_map = HashMap::new();
if !params.is_null() && param_count > 0 {
for i in 0..param_count {
unsafe {
let param = &*params.add(i);
let param_name = format!("param_{}", i);
let value = convert_c_value_to_rust(param);
params_map.insert(param_name, value);
}
}
}
unsafe {
let handle = &mut *(session as *mut LinkrsSessionHandle);
handle.trace(query_str);
match handle.inner.execute_with_params(query_str, params_map) {
Ok(query_result) => {
handle.clear_error();
if handle.has_update_hook() || handle.inner.has_dml_observers() {
let rows = query_result.metadata().rows_returned as u64;
if let Some(op) = handle.inner.notify_dml(query_str, rows) {
if handle.has_update_hook() {
let operation = match op {
linkrs_query::DmlOp::Insert => 1,
linkrs_query::DmlOp::Update => 2,
linkrs_query::DmlOp::Delete => 3,
};
let space_name_owned = handle.inner.current_space();
let space_name = space_name_owned.as_deref().unwrap_or("default");
handle.invoke_update_hook(operation, space_name, rows as i64);
}
}
}
let result_handle = Box::new(LinkrsResultHandle {
inner: query_result,
});
*result = Box::into_raw(result_handle) as *mut linkrs_result_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);
let offset = e.error_offset();
let extended_code = Some(extended_error_code_from_core_error(&e));
handle.set_error(error_msg, offset, extended_code);
*result = ptr::null_mut();
error_code
}
}
}
}
pub unsafe fn convert_c_value_to_rust(c_value: &linkrs_value_t) -> Value {
use crate::embedded::c_api::types::linkrs_value_type_t;
match c_value.type_ {
linkrs_value_type_t::LINKRS_NULL => Value::Null(linkrs_core::value::NullType::Null),
linkrs_value_type_t::LINKRS_BOOL => Value::Bool(c_value.data.boolean),
linkrs_value_type_t::LINKRS_INT => Value::BigInt(c_value.data.integer),
linkrs_value_type_t::LINKRS_FLOAT => Value::Double(c_value.data.floating),
linkrs_value_type_t::LINKRS_STRING => {
if c_value.data.string.data.is_null() || c_value.data.string.len == 0 {
Value::string("")
} else {
let slice = std::slice::from_raw_parts(
c_value.data.string.data as *const u8,
c_value.data.string.len,
);
let s = String::from_utf8_unchecked(slice.to_vec());
Value::string(s)
}
}
linkrs_value_type_t::LINKRS_BLOB => {
if c_value.data.blob.data.is_null() || c_value.data.blob.len == 0 {
Value::Blob(Vec::new())
} else {
let slice =
std::slice::from_raw_parts(c_value.data.blob.data, c_value.data.blob.len);
Value::Blob(slice.to_vec())
}
}
_ => Value::Null(linkrs_core::value::NullType::Null),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded::c_api::database::{linkrs_close, linkrs_open};
use crate::embedded::c_api::result::linkrs_result_free;
use crate::embedded::c_api::session::{linkrs_session_close, linkrs_session_create};
use crate::embedded::c_api::types::linkrs_t;
use std::ffi::CString;
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_{}_{}.db", std::process::id(), counter));
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) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
assert!(!db.is_null());
db
}
#[test]
fn test_dml_classification_skips_comments() {
use linkrs_query::{classify_dml, DmlOp};
assert_eq!(
classify_dml(" -- comment\nCREATE (n)"),
Some(DmlOp::Insert)
);
assert_eq!(classify_dml("/* block */ MERGE (n)"), Some(DmlOp::Insert));
assert_eq!(
classify_dml("# hash\n// slash\nSET n.x = 1"),
Some(DmlOp::Update)
);
assert_eq!(classify_dml(" match (n) return n"), None);
assert_eq!(classify_dml("OPTIONAL MATCH (n) RETURN n"), None);
assert_eq!(classify_dml("DETACH DELETE n"), Some(DmlOp::Delete));
}
#[test]
fn test_execute_null_params() {
let rc = unsafe { linkrs_execute(ptr::null_mut(), ptr::null(), ptr::null_mut()) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_MISUSE as c_int);
let mut result: *mut linkrs_result_t = ptr::null_mut();
let rc = unsafe { linkrs_execute(ptr::null_mut(), ptr::null(), &mut result) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_MISUSE as c_int);
}
#[test]
fn test_execute_params_null_params() {
let rc = unsafe {
linkrs_execute_params(
ptr::null_mut(),
ptr::null(),
ptr::null(),
0,
ptr::null_mut(),
)
};
assert_eq!(rc, linkrs_error_code_t::LINKRS_MISUSE as c_int);
let mut result: *mut linkrs_result_t = ptr::null_mut();
let rc = unsafe {
linkrs_execute_params(ptr::null_mut(), ptr::null(), ptr::null(), 0, &mut result)
};
assert_eq!(rc, linkrs_error_code_t::LINKRS_MISUSE as c_int);
}
#[test]
fn test_execute_simple_query() {
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 query = CString::new("RETURN 1").expect("Failed to create query CString");
let mut result: *mut linkrs_result_t = ptr::null_mut();
let rc = unsafe { linkrs_execute(session, query.as_ptr(), &mut result) };
assert_eq!(rc, linkrs_error_code_t::LINKRS_OK as c_int);
assert!(!result.is_null());
unsafe { linkrs_result_free(result) };
unsafe { linkrs_session_close(session) };
unsafe { linkrs_close(db) };
}
#[test]
fn test_literal_int_roundtrip_through_wide_getter() {
use crate::embedded::c_api::result::{
linkrs_column_type, linkrs_get_int_by_index, linkrs_result_execution_time_ms,
linkrs_result_rows_scanned,
};
use crate::embedded::c_api::types::linkrs_value_type_t;
let db = create_test_db();
let mut session: *mut linkrs_session_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_session_create(db, &mut session) },
linkrs_error_code_t::LINKRS_OK as c_int
);
let query = CString::new("RETURN 1").unwrap();
let mut result: *mut linkrs_result_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_execute(session, query.as_ptr(), &mut result) },
linkrs_error_code_t::LINKRS_OK as c_int
);
assert_eq!(
unsafe { linkrs_column_type(result, 0) },
linkrs_value_type_t::LINKRS_INT
);
let mut value: i64 = 0;
assert_eq!(
unsafe { linkrs_get_int_by_index(result, 0, 0, &mut value) },
linkrs_error_code_t::LINKRS_OK as c_int
);
assert_eq!(value, 1);
let mut ms: u64 = 0;
assert_eq!(
unsafe { linkrs_result_execution_time_ms(result, &mut ms) },
linkrs_error_code_t::LINKRS_OK as c_int
);
let mut scanned: u64 = 0;
assert_eq!(
unsafe { linkrs_result_rows_scanned(result, &mut scanned) },
linkrs_error_code_t::LINKRS_OK as c_int
);
assert_eq!(
unsafe { linkrs_result_execution_time_ms(ptr::null_mut(), &mut ms) },
linkrs_error_code_t::LINKRS_MISUSE as c_int
);
unsafe { linkrs_result_free(result) };
unsafe { linkrs_session_close(session) };
unsafe { linkrs_close(db) };
}
#[test]
fn test_execute_params_positional_binding() {
use crate::embedded::c_api::result::{linkrs_get_int_by_index, linkrs_result_free};
use crate::embedded::c_api::types::{
linkrs_value_data_t, linkrs_value_t, linkrs_value_type_t,
};
let db = create_test_db();
let mut session: *mut linkrs_session_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_session_create(db, &mut session) },
linkrs_error_code_t::LINKRS_OK as c_int
);
let query = CString::new("RETURN @param_0").unwrap();
let param = linkrs_value_t {
type_: linkrs_value_type_t::LINKRS_INT,
data: linkrs_value_data_t { integer: 41 },
};
let mut result: *mut linkrs_result_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_execute_params(session, query.as_ptr(), ¶m, 1, &mut result) },
linkrs_error_code_t::LINKRS_OK as c_int
);
let mut value: i64 = 0;
assert_eq!(
unsafe { linkrs_get_int_by_index(result, 0, 0, &mut value) },
linkrs_error_code_t::LINKRS_OK as c_int
);
assert_eq!(value, 41);
unsafe { linkrs_result_free(result) };
unsafe { linkrs_session_close(session) };
unsafe { linkrs_close(db) };
}
#[test]
fn test_read_only_write_maps_to_readonly_code() {
use crate::embedded::c_api::config::{
linkrs_config_file, linkrs_config_free, linkrs_config_set_read_only,
};
use crate::embedded::c_api::database::linkrs_open_with_config;
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_ro_{}_{}.db", std::process::id(), counter));
let path_cstring = CString::new(db_path.to_str().expect("Invalid path")).unwrap();
let config = unsafe { linkrs_config_file(path_cstring.as_ptr()) };
assert!(!config.is_null());
assert_eq!(
unsafe { linkrs_config_set_read_only(config, 1) },
linkrs_error_code_t::LINKRS_OK as c_int
);
let mut db: *mut linkrs_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_open_with_config(config, &mut db) },
linkrs_error_code_t::LINKRS_OK as c_int
);
assert!(!db.is_null());
let mut session: *mut linkrs_session_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_session_create(db, &mut session) },
linkrs_error_code_t::LINKRS_OK as c_int
);
let query = CString::new("DROP SPACE nosuch").unwrap();
let mut result: *mut linkrs_result_t = ptr::null_mut();
assert_eq!(
unsafe { linkrs_execute(session, query.as_ptr(), &mut result) },
linkrs_error_code_t::LINKRS_READONLY as c_int
);
unsafe { linkrs_session_close(session) };
unsafe { linkrs_close(db) };
unsafe { linkrs_config_free(config) };
}
}