use crate::graph::{GraphState, KgliteGraph};
use crate::result::{result_to_json_object, KgliteCypherResult, ResultState};
use crate::status::KgliteStatusCode;
use crate::strings::alloc_c_string;
use kglite::api::mutation::{add_edges_from_specs, EdgeSpec};
use kglite::api::param::{json_object_to_value_map, json_value_to_kglite_value};
use kglite::api::session::{execute_mut, execute_read, ExecuteOptions, Session};
use kglite::api::{Embedder, Value};
use std::collections::HashMap;
use std::ffi::{c_char, CStr};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
#[repr(C)]
pub struct KgliteSession {
_opaque: [u8; 0],
_marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
pub(crate) struct SessionState {
pub(crate) inner: Session,
pub(crate) embedder: Mutex<Option<Arc<dyn Embedder>>>,
}
impl SessionState {
fn into_handle(session: Session) -> *mut KgliteSession {
let boxed = Box::new(SessionState {
inner: session,
embedder: Mutex::new(None),
});
Box::into_raw(boxed).cast::<KgliteSession>()
}
pub(crate) unsafe fn from_handle<'a>(handle: *const KgliteSession) -> &'a SessionState {
unsafe { &*handle.cast::<SessionState>() }
}
pub(crate) fn set_embedder(&self, embedder: Arc<dyn Embedder>) {
*self.embedder.lock().unwrap_or_else(PoisonError::into_inner) = Some(embedder);
}
unsafe fn free_handle(handle: *mut KgliteSession) {
if handle.is_null() {
return;
}
let _ = unsafe { Box::from_raw(handle.cast::<SessionState>()) };
}
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_new(
graph: *mut KgliteGraph,
out_session: *mut *mut KgliteSession,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
std::ptr::null_mut(),
|| crate::ffi::init_out(out_session, std::ptr::null_mut()),
|| {
if graph.is_null() || out_session.is_null() {
return KgliteStatusCode::NullPointer;
}
let graph_state = unsafe { Box::from_raw(graph.cast::<GraphState>()) };
let session = Session::from_arc(graph_state.inner);
unsafe { *out_session = SessionState::into_handle(session) };
KgliteStatusCode::Ok
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_execute_read(
session: *const KgliteSession,
query: *const c_char,
params_json: *const c_char,
out_result: *mut *mut KgliteCypherResult,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_result, std::ptr::null_mut()),
|| {
if session.is_null() || query.is_null() || out_result.is_null() {
return KgliteStatusCode::NullPointer;
}
let query_str = match unsafe { CStr::from_ptr(query) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let params = match parse_params_json(params_json) {
Ok(p) => p,
Err(rc) => return rc,
};
let session_state = unsafe { SessionState::from_handle(session) };
let snapshot = session_state.inner.snapshot();
let opts = session_state.make_opts(¶ms);
match execute_read(&snapshot, query_str, &opts) {
Ok(outcome) => {
unsafe {
*out_result = ResultState::into_handle(outcome.result);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(err) => {
unsafe {
*out_result = std::ptr::null_mut();
}
let code = KgliteStatusCode::from_kg_error_code(err.code());
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&err.to_string());
}
}
code
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_execute_read_opts(
session: *const KgliteSession,
query: *const c_char,
params_json: *const c_char,
timeout_ms: u64,
max_rows: u64,
out_result: *mut *mut KgliteCypherResult,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_result, std::ptr::null_mut()),
|| {
if session.is_null() || query.is_null() || out_result.is_null() {
return KgliteStatusCode::NullPointer;
}
let query_str = match unsafe { CStr::from_ptr(query) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let params = match parse_params_json(params_json) {
Ok(p) => p,
Err(rc) => return rc,
};
let session_state = unsafe { SessionState::from_handle(session) };
let snapshot = session_state.inner.snapshot();
let mut opts = session_state.make_opts(¶ms);
if timeout_ms > 0 {
opts.deadline = Some(Instant::now() + Duration::from_millis(timeout_ms));
}
if max_rows > 0 {
opts.max_rows = Some(max_rows as usize);
}
match execute_read(&snapshot, query_str, &opts) {
Ok(outcome) => {
unsafe {
*out_result = ResultState::into_handle(outcome.result);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(err) => {
unsafe {
*out_result = std::ptr::null_mut();
}
let code = KgliteStatusCode::from_kg_error_code(err.code());
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&err.to_string());
}
}
code
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_execute_mut(
session: *mut KgliteSession,
query: *const c_char,
params_json: *const c_char,
out_result: *mut *mut KgliteCypherResult,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
unsafe { execute_mut_impl(session, query, params_json, 0, 0, out_result, out_error_msg) }
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_execute_mut_opts(
session: *mut KgliteSession,
query: *const c_char,
params_json: *const c_char,
timeout_ms: u64,
max_rows: u64,
out_result: *mut *mut KgliteCypherResult,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
unsafe {
execute_mut_impl(
session,
query,
params_json,
timeout_ms,
max_rows,
out_result,
out_error_msg,
)
}
}
#[allow(clippy::too_many_arguments)]
unsafe fn execute_mut_impl(
session: *mut KgliteSession,
query: *const c_char,
params_json: *const c_char,
timeout_ms: u64,
max_rows: u64,
out_result: *mut *mut KgliteCypherResult,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_result, std::ptr::null_mut()),
|| {
if session.is_null() || query.is_null() || out_result.is_null() {
return KgliteStatusCode::NullPointer;
}
let query_str = match unsafe { CStr::from_ptr(query) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let params = match parse_params_json(params_json) {
Ok(p) => p,
Err(rc) => return rc,
};
let session_state = unsafe { SessionState::from_handle(session) };
let mut opts = session_state.make_opts(¶ms);
if timeout_ms > 0 {
opts.deadline = Some(Instant::now() + Duration::from_millis(timeout_ms));
}
if max_rows > 0 {
opts.max_rows = Some(max_rows as usize);
}
let mut working = session_state.inner.write();
let exec_result = execute_mut(&mut working, query_str, &opts);
match exec_result {
Ok(outcome) => {
unsafe {
*out_result = ResultState::into_handle(outcome.result);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(err) => {
unsafe {
*out_result = std::ptr::null_mut();
}
let code = KgliteStatusCode::from_kg_error_code(err.code());
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&err.to_string());
}
}
code
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_execute_read_batch(
session: *const KgliteSession,
queries_json: *const c_char,
out_results_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_results_json, std::ptr::null()),
|| {
if session.is_null() || queries_json.is_null() || out_results_json.is_null() {
return KgliteStatusCode::NullPointer;
}
let queries = match parse_batch_queries(queries_json) {
Ok(q) => q,
Err(rc) => return rc,
};
let session_state = unsafe { SessionState::from_handle(session) };
let snapshot = session_state.inner.snapshot();
let mut results = Vec::with_capacity(queries.len());
for (query, params) in &queries {
let opts = session_state.make_opts(params);
match execute_read(&snapshot, query, &opts) {
Ok(outcome) => results.push(result_to_json_object(&outcome.result)),
Err(err) => {
unsafe {
*out_results_json = std::ptr::null();
}
let code = KgliteStatusCode::from_kg_error_code(err.code());
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&err.to_string());
}
}
return code;
}
}
}
let json = serde_json::Value::Array(results).to_string();
unsafe {
*out_results_json = alloc_c_string(&json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_execute_mut_batch(
session: *mut KgliteSession,
queries_json: *const c_char,
out_results_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_results_json, std::ptr::null()),
|| {
if session.is_null() || queries_json.is_null() || out_results_json.is_null() {
return KgliteStatusCode::NullPointer;
}
let queries = match parse_batch_queries(queries_json) {
Ok(q) => q,
Err(rc) => return rc,
};
let session_state = unsafe { SessionState::from_handle(session) };
let transaction: Result<Vec<serde_json::Value>, Box<kglite::api::KgError>> =
session_state.inner.transact(|working| {
let mut results = Vec::with_capacity(queries.len());
for (query, params) in &queries {
let opts = session_state.make_opts(params);
let outcome = execute_mut(working, query, &opts).map_err(Box::new)?;
results.push(result_to_json_object(&outcome.result));
}
Ok(results)
});
let results = match transaction {
Ok(results) => results,
Err(err) => {
unsafe {
*out_results_json = std::ptr::null();
}
let code = KgliteStatusCode::from_kg_error_code(err.code());
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&err.to_string());
}
}
return code;
}
};
let json = serde_json::Value::Array(results).to_string();
unsafe {
*out_results_json = alloc_c_string(&json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_create_edges_batch(
session: *mut KgliteSession,
edges_json: *const c_char,
out_report_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_report_json, std::ptr::null()),
|| {
if session.is_null() || edges_json.is_null() || out_report_json.is_null() {
return KgliteStatusCode::NullPointer;
}
let specs = match parse_edge_specs(edges_json) {
Ok(s) => s,
Err(rc) => return rc,
};
let session_state = unsafe { SessionState::from_handle(session) };
let transaction: Result<_, String> = session_state
.inner
.transact(|working| add_edges_from_specs(working, specs));
match transaction {
Ok(report) => {
let json = serde_json::json!({
"connections_created": report.connections_created,
"skipped_missing_endpoint": report.skipped_missing_endpoint,
})
.to_string();
unsafe {
*out_report_json = alloc_c_string(&json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(msg) => {
unsafe {
*out_report_json = std::ptr::null();
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&msg);
}
}
KgliteStatusCode::Internal
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_session_free(session: *mut KgliteSession) {
crate::ffi::void_boundary(|| unsafe { SessionState::free_handle(session) });
}
impl SessionState {
fn make_opts<'a>(&self, params: &'a HashMap<String, Value>) -> ExecuteOptions<'a> {
let mut opts = ExecuteOptions::eager(params);
opts.embedder = self
.embedder
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
opts
}
}
fn parse_params_json(
params_json: *const c_char,
) -> Result<HashMap<String, Value>, KgliteStatusCode> {
if params_json.is_null() {
return Ok(HashMap::new());
}
let s = match unsafe { CStr::from_ptr(params_json) }.to_str() {
Ok(s) => s,
Err(_) => return Err(KgliteStatusCode::InvalidUtf8),
};
if s.is_empty() {
return Ok(HashMap::new());
}
let parsed: serde_json::Value = match serde_json::from_str(s) {
Ok(v) => v,
Err(_) => return Err(KgliteStatusCode::InvalidArgument),
};
match parsed {
serde_json::Value::Object(obj) => Ok(json_object_to_value_map(&obj)),
serde_json::Value::Null => Ok(HashMap::new()),
_ => Err(KgliteStatusCode::InvalidArgument),
}
}
fn optional_object_map(
obj: &serde_json::Map<String, serde_json::Value>,
key: &str,
) -> Result<HashMap<String, Value>, KgliteStatusCode> {
match obj.get(key) {
None | Some(serde_json::Value::Null) => Ok(HashMap::new()),
Some(serde_json::Value::Object(o)) => Ok(json_object_to_value_map(o)),
Some(_) => Err(KgliteStatusCode::InvalidArgument),
}
}
type BatchQuery = (String, HashMap<String, Value>);
fn parse_batch_queries(queries_json: *const c_char) -> Result<Vec<BatchQuery>, KgliteStatusCode> {
let s = match unsafe { CStr::from_ptr(queries_json) }.to_str() {
Ok(s) => s,
Err(_) => return Err(KgliteStatusCode::InvalidUtf8),
};
let parsed: serde_json::Value = match serde_json::from_str(s) {
Ok(v) => v,
Err(_) => return Err(KgliteStatusCode::InvalidArgument),
};
let arr = match parsed.as_array() {
Some(a) => a,
None => return Err(KgliteStatusCode::InvalidArgument),
};
let mut out = Vec::with_capacity(arr.len());
for item in arr {
let obj = match item.as_object() {
Some(o) => o,
None => return Err(KgliteStatusCode::InvalidArgument),
};
let query = match obj.get("query").and_then(|v| v.as_str()) {
Some(q) => q.to_string(),
None => return Err(KgliteStatusCode::InvalidArgument),
};
let params = optional_object_map(obj, "params")?;
out.push((query, params));
}
Ok(out)
}
fn parse_edge_specs(edges_json: *const c_char) -> Result<Vec<EdgeSpec>, KgliteStatusCode> {
let s = match unsafe { CStr::from_ptr(edges_json) }.to_str() {
Ok(s) => s,
Err(_) => return Err(KgliteStatusCode::InvalidUtf8),
};
let parsed: serde_json::Value = match serde_json::from_str(s) {
Ok(v) => v,
Err(_) => return Err(KgliteStatusCode::InvalidArgument),
};
let arr = match parsed.as_array() {
Some(a) => a,
None => return Err(KgliteStatusCode::InvalidArgument),
};
let mut out = Vec::with_capacity(arr.len());
for item in arr {
let obj = match item.as_object() {
Some(o) => o,
None => return Err(KgliteStatusCode::InvalidArgument),
};
let req_str = |key: &str| -> Result<String, KgliteStatusCode> {
obj.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or(KgliteStatusCode::InvalidArgument)
};
let req_id = |key: &str| -> Result<Value, KgliteStatusCode> {
obj.get(key)
.map(json_value_to_kglite_value)
.ok_or(KgliteStatusCode::InvalidArgument)
};
let properties = optional_object_map(obj, "props")?;
out.push(EdgeSpec {
source_type: req_str("src_type")?,
source_id: req_id("src_id")?,
target_type: req_str("dst_type")?,
target_id: req_id("dst_id")?,
edge_type: req_str("type")?,
properties,
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn parse_params_empty_string_is_empty_map() {
let s = CString::new("").unwrap();
let m = parse_params_json(s.as_ptr()).unwrap();
assert!(m.is_empty());
}
#[test]
fn parse_params_object_round_trips() {
let s = CString::new(r#"{"x": 42, "y": "hello"}"#).unwrap();
let m = parse_params_json(s.as_ptr()).unwrap();
assert_eq!(m.get("x"), Some(&Value::Int64(42)));
assert_eq!(m.get("y"), Some(&Value::String("hello".to_string())));
}
#[test]
fn parse_params_null_pointer_is_empty_map() {
let m = parse_params_json(std::ptr::null()).unwrap();
assert!(m.is_empty());
}
#[test]
fn parse_params_array_is_invalid_argument() {
let s = CString::new("[1, 2, 3]").unwrap();
let err = parse_params_json(s.as_ptr()).unwrap_err();
assert_eq!(err, KgliteStatusCode::InvalidArgument);
}
fn new_test_session() -> *mut KgliteSession {
use kglite::api::storage::{new_dir_graph_in_mode, StorageMode};
let graph = new_dir_graph_in_mode(StorageMode::Memory, None).expect("memory graph");
SessionState::into_handle(Session::new(graph))
}
fn exec_mut(session: *mut KgliteSession, query: &str) {
let q = CString::new(query).unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let status = unsafe {
kglite_session_execute_mut(session, q.as_ptr(), std::ptr::null(), &mut result, &mut err)
};
assert_eq!(status, KgliteStatusCode::Ok, "query failed: {query}");
unsafe { crate::kglite_cypher_result_free(result) };
}
fn count(session: *const KgliteSession, query: &str) -> i64 {
let state = unsafe { SessionState::from_handle(session) };
let snapshot = state.inner.snapshot();
let params = HashMap::new();
let opts = state.make_opts(¶ms);
let outcome = execute_read(&snapshot, query, &opts).expect("count query");
match outcome.result.rows.first().and_then(|r| r.first()) {
Some(Value::Int64(n)) => *n,
other => panic!("expected Int64 count, got {other:?}"),
}
}
fn edges_batch(
session: *mut KgliteSession,
edges_json: &str,
) -> (KgliteStatusCode, Option<serde_json::Value>, Option<String>) {
let edges_c = CString::new(edges_json).unwrap();
let mut report: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let status =
unsafe { kglite_create_edges_batch(session, edges_c.as_ptr(), &mut report, &mut err) };
let report_json = (!report.is_null()).then(|| {
let s = unsafe { CStr::from_ptr(report) }.to_str().unwrap();
let v = serde_json::from_str(s).unwrap();
unsafe { crate::kglite_free_string(report) };
v
});
let err_msg = (!err.is_null()).then(|| {
let s = unsafe { CStr::from_ptr(err) }.to_str().unwrap().to_string();
unsafe { crate::kglite_free_string(err) };
s
});
(status, report_json, err_msg)
}
#[test]
fn create_edges_batch_lands_atomically_and_reports_errors() {
let session = new_test_session();
exec_mut(session, "CREATE (:Src {id: 1})");
exec_mut(session, "CREATE (:Dst {id: 2})");
let (status, report, err) = edges_batch(
session,
r#"[
{"src_id": 1, "src_type": "Src", "dst_id": 2, "dst_type": "Dst", "type": "REL"},
{"src_id": 99, "src_type": "Src", "dst_id": 2, "dst_type": "Dst", "type": "REL"}
]"#,
);
assert_eq!(status, KgliteStatusCode::Ok, "err: {err:?}");
let report = report.expect("report json");
assert_eq!(report["connections_created"], 1);
assert_eq!(report["skipped_missing_endpoint"], 1);
assert_eq!(
count(session, "MATCH (:Src)-[r:REL]->(:Dst) RETURN count(r) AS c"),
1
);
let (status, report, err) = edges_batch(
session,
r#"[
{"src_id": 1, "src_type": "Src", "dst_id": 2, "dst_type": "Dst", "type": "REL2"},
{"src_id": 1, "src_type": "", "dst_id": 2, "dst_type": "Dst", "type": "REL2"}
]"#,
);
assert_eq!(status, KgliteStatusCode::Internal);
assert!(report.is_none(), "failed batch must not produce a report");
assert!(
err.is_some_and(|m| !m.is_empty()),
"failed batch must report its error message"
);
assert_eq!(
count(session, "MATCH ()-[r:REL2]->() RETURN count(r) AS c"),
0,
"failed batch must be atomic — no partial edges"
);
unsafe { kglite_session_free(session) };
}
#[test]
fn create_edges_batch_serializes_with_concurrent_execute_mut() {
const N: usize = 30;
let session = new_test_session();
exec_mut(session, "CREATE (:Src {id: 0})");
for i in 0..N {
exec_mut(session, &format!("CREATE (:Dst {{id: {i}}})"));
}
let addr = session as usize;
let writer = std::thread::spawn(move || {
let session = addr as *mut KgliteSession;
for i in 0..N {
exec_mut(session, &format!("CREATE (:P {{id: {i}}})"));
}
});
for i in 0..N {
let edges = format!(
r#"[{{"src_id": 0, "src_type": "Src", "dst_id": {i}, "dst_type": "Dst", "type": "REL"}}]"#
);
let (status, report, err) = edges_batch(session, &edges);
assert_eq!(status, KgliteStatusCode::Ok, "err: {err:?}");
assert_eq!(report.expect("report")["connections_created"], 1);
}
writer.join().expect("writer thread panicked");
assert_eq!(
count(session, "MATCH (n:P) RETURN count(n) AS c"),
N as i64,
"no execute_mut commit may be lost to a concurrent edge batch"
);
assert_eq!(
count(session, "MATCH ()-[r:REL]->() RETURN count(r) AS c"),
N as i64,
"no edge batch may be lost to a concurrent execute_mut"
);
unsafe { kglite_session_free(session) };
}
}