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;
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: Option<Arc<dyn Embedder>>,
}
impl SessionState {
fn into_handle(session: Session) -> *mut KgliteSession {
let boxed = Box::new(SessionState {
inner: session,
embedder: 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) unsafe fn from_handle_mut<'a>(handle: *mut KgliteSession) -> &'a mut SessionState {
unsafe { &mut *handle.cast::<SessionState>() }
}
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 {
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 {
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 {
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 {
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 opts = session_state.make_opts(¶ms);
let mut tx = session_state.inner.begin();
let exec_result = {
let working = match tx.working_mut() {
Ok(w) => w,
Err(err) => {
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());
}
}
unsafe {
*out_result = std::ptr::null_mut();
}
return code;
}
};
execute_mut(working, query_str, &opts)
};
match exec_result {
Ok(outcome) => {
let _ = session_state.inner.commit(tx, false);
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 {
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 {
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 mut tx = session_state.inner.begin();
let mut results = Vec::with_capacity(queries.len());
for (query, params) in &queries {
let opts = session_state.make_opts(params);
let exec = {
let working = match tx.working_mut() {
Ok(w) => w,
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;
}
};
execute_mut(working, query, &opts)
};
match exec {
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 _ = session_state.inner.commit(tx, false);
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 {
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 mut tx = session_state.inner.begin();
let working = match tx.working_mut() {
Ok(w) => w,
Err(err) => {
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());
}
}
unsafe {
*out_report_json = std::ptr::null();
}
return code;
}
};
match add_edges_from_specs(working, specs) {
Ok(report) => {
let _ = session_state.inner.commit(tx, false);
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) {
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.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);
}
}