Skip to main content

kglite_c/
session.rs

1//! `KgliteSession` opaque handle — session creation +
2//! execute_read / execute_mut.
3//!
4//! The Session owns the graph after [`kglite_session_new`] — the
5//! Arc moves in and the caller should NOT free the graph handle
6//! afterwards.
7
8use crate::graph::{GraphState, KgliteGraph};
9use crate::result::{KgliteCypherResult, ResultState};
10use crate::status::KgliteStatusCode;
11use crate::strings::alloc_c_string;
12use kglite::api::param::json_value_to_kglite_value;
13use kglite::api::session::{execute_mut, execute_read, ExecuteOptions, Session};
14use kglite::api::{Embedder, Value};
15use std::collections::HashMap;
16use std::ffi::{c_char, CStr};
17use std::sync::Arc;
18
19/// Opaque handle for a session. See [`KgliteGraph`](crate::KgliteGraph)
20/// for the rationale on the empty `#[repr(C)]` facade pattern.
21#[repr(C)]
22pub struct KgliteSession {
23    _opaque: [u8; 0],
24    _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
25}
26
27/// Private state backing a [`KgliteSession`] handle.
28pub(crate) struct SessionState {
29    pub(crate) inner: Session,
30    /// Optional embedder attached to this session. When set, every
31    /// execute_read / execute_mut call passes the embedder into
32    /// `ExecuteOptions` so `text_score()` and friends work.
33    /// Attached via
34    /// [`kglite_session_set_embedder`](crate::kglite_session_set_embedder).
35    pub(crate) embedder: Option<Arc<dyn Embedder>>,
36}
37
38impl SessionState {
39    fn into_handle(session: Session) -> *mut KgliteSession {
40        let boxed = Box::new(SessionState {
41            inner: session,
42            embedder: None,
43        });
44        Box::into_raw(boxed).cast::<KgliteSession>()
45    }
46
47    pub(crate) unsafe fn from_handle<'a>(handle: *const KgliteSession) -> &'a SessionState {
48        unsafe { &*handle.cast::<SessionState>() }
49    }
50
51    pub(crate) unsafe fn from_handle_mut<'a>(handle: *mut KgliteSession) -> &'a mut SessionState {
52        unsafe { &mut *handle.cast::<SessionState>() }
53    }
54
55    unsafe fn free_handle(handle: *mut KgliteSession) {
56        if handle.is_null() {
57            return;
58        }
59        let _ = unsafe { Box::from_raw(handle.cast::<SessionState>()) };
60    }
61}
62
63/// Create a new session from a graph handle. The session takes
64/// ownership of the graph — the caller MUST NOT call
65/// [`kglite_graph_free`](crate::kglite_graph_free) on the handle
66/// after this call. Free the session via
67/// [`kglite_session_free`] when done.
68///
69/// # Arguments
70///
71/// - `graph` (in, MOVED): graph handle. After this call, the
72///   pointer is no longer valid for any other use.
73/// - `out_session` (out, owned): set to the session handle on
74///   success; caller must free via [`kglite_session_free`].
75///
76/// # Errors
77///
78/// - `KGLITE_ERR_NULL_POINTER` — `graph` or `out_session` is null
79///
80/// # Safety
81///
82/// `graph` must be a valid `*mut KgliteGraph` previously returned
83/// by [`kglite_load_file`](crate::kglite_load_file) and not yet
84/// freed or moved into another session. `out_session` must be a
85/// valid writable pointer to a `*mut KgliteSession` slot.
86#[no_mangle]
87pub unsafe extern "C" fn kglite_session_new(
88    graph: *mut KgliteGraph,
89    out_session: *mut *mut KgliteSession,
90) -> KgliteStatusCode {
91    if graph.is_null() || out_session.is_null() {
92        return KgliteStatusCode::NullPointer;
93    }
94    // Safety: caller's contract — graph is a valid handle, not
95    // yet freed. We MOVE the Arc out by reconstructing the Box
96    // behind the opaque facade.
97    let graph_state = unsafe { Box::from_raw(graph.cast::<GraphState>()) };
98    let session = Session::from_arc(graph_state.inner);
99    unsafe {
100        *out_session = SessionState::into_handle(session);
101    }
102    KgliteStatusCode::Ok
103}
104
105/// Run a read-only Cypher query.
106///
107/// # Arguments
108///
109/// - `session` (in, borrowed): the session.
110/// - `query` (in, borrowed): UTF-8 Cypher query, null-terminated.
111/// - `params_json` (in, borrowed, may be null): JSON object of
112///   parameter bindings. Pass null or `"{}"` for no params.
113/// - `out_result` (out, owned): on success, set to the result
114///   handle; caller must free via [`kglite_cypher_result_free`].
115/// - `out_error_msg` (out, owned, may be null): on failure, set
116///   to the error message; caller must free via
117///   [`kglite_free_string`](crate::kglite_free_string).
118///
119/// # Errors
120///
121/// Any `KgErrorCode` variant — Cypher syntax / type mismatch /
122/// timeout / execution error / node-not-found / argument
123/// validation. The error message describes the specific failure.
124///
125/// # Safety
126///
127/// `session` must be valid. `query` and (if non-null) `params_json`
128/// must be null-terminated UTF-8 strings.
129#[no_mangle]
130pub unsafe extern "C" fn kglite_session_execute_read(
131    session: *const KgliteSession,
132    query: *const c_char,
133    params_json: *const c_char,
134    out_result: *mut *mut KgliteCypherResult,
135    out_error_msg: *mut *const c_char,
136) -> KgliteStatusCode {
137    if session.is_null() || query.is_null() || out_result.is_null() {
138        return KgliteStatusCode::NullPointer;
139    }
140    let query_str = match unsafe { CStr::from_ptr(query) }.to_str() {
141        Ok(s) => s,
142        Err(_) => return KgliteStatusCode::InvalidUtf8,
143    };
144    let params = match parse_params_json(params_json) {
145        Ok(p) => p,
146        Err(rc) => return rc,
147    };
148
149    let session_state = unsafe { SessionState::from_handle(session) };
150    let snapshot = session_state.inner.snapshot();
151    let mut opts = ExecuteOptions::eager(&params);
152    opts.embedder = session_state.embedder.clone();
153
154    match execute_read(&snapshot, query_str, &opts) {
155        Ok(outcome) => {
156            unsafe {
157                *out_result = ResultState::into_handle(outcome.result);
158            }
159            if !out_error_msg.is_null() {
160                unsafe {
161                    *out_error_msg = std::ptr::null();
162                }
163            }
164            KgliteStatusCode::Ok
165        }
166        Err(err) => {
167            unsafe {
168                *out_result = std::ptr::null_mut();
169            }
170            let code = KgliteStatusCode::from_kg_error_code(err.code());
171            if !out_error_msg.is_null() {
172                unsafe {
173                    *out_error_msg = alloc_c_string(&err.to_string());
174                }
175            }
176            code
177        }
178    }
179}
180
181/// Run a mutating Cypher query. Same shape as
182/// [`kglite_session_execute_read`] but accepts CREATE / SET /
183/// DELETE / REMOVE / MERGE statements. The session's underlying
184/// graph is auto-committed after a successful execute (no
185/// explicit begin/commit in v1 — explicit transactions land in
186/// a future ABI version once a binding needs them).
187///
188/// # Safety
189///
190/// Same as [`kglite_session_execute_read`] except `session` is
191/// declared as `*mut` (the call mutates the session's interior
192/// graph via commit-swap).
193#[no_mangle]
194pub unsafe extern "C" fn kglite_session_execute_mut(
195    session: *mut KgliteSession,
196    query: *const c_char,
197    params_json: *const c_char,
198    out_result: *mut *mut KgliteCypherResult,
199    out_error_msg: *mut *const c_char,
200) -> KgliteStatusCode {
201    if session.is_null() || query.is_null() || out_result.is_null() {
202        return KgliteStatusCode::NullPointer;
203    }
204    let query_str = match unsafe { CStr::from_ptr(query) }.to_str() {
205        Ok(s) => s,
206        Err(_) => return KgliteStatusCode::InvalidUtf8,
207    };
208    let params = match parse_params_json(params_json) {
209        Ok(p) => p,
210        Err(rc) => return rc,
211    };
212
213    // `execute_mut` takes `*mut` for the C ABI but the SessionState
214    // mutex makes the actual interior mutation thread-safe — we
215    // borrow `&SessionState` here and rely on Session's internal
216    // Mutex for the commit-swap.
217    let session_state = unsafe { SessionState::from_handle(session) };
218    let mut opts = ExecuteOptions::eager(&params);
219    opts.embedder = session_state.embedder.clone();
220
221    // Mirror the bolt-server execute_in_tx pattern: begin →
222    // working_mut → execute_mut → commit. The Transaction's
223    // working_mut lazily clones the snapshot's DirGraph for
224    // mutation; commit atomically swaps it back via the Session
225    // mutex.
226    let mut tx = session_state.inner.begin();
227    let exec_result = {
228        let working = match tx.working_mut() {
229            Ok(w) => w,
230            Err(err) => {
231                let code = KgliteStatusCode::from_kg_error_code(err.code());
232                if !out_error_msg.is_null() {
233                    unsafe {
234                        *out_error_msg = alloc_c_string(&err.to_string());
235                    }
236                }
237                unsafe {
238                    *out_result = std::ptr::null_mut();
239                }
240                return code;
241            }
242        };
243        execute_mut(working, query_str, &opts)
244    };
245
246    match exec_result {
247        Ok(outcome) => {
248            // Auto-commit. `check_occ = false` matches bolt-server's
249            // current default — no inter-session OCC checking at
250            // the C ABI surface in v1. Explicit OCC lands when a
251            // binding actually needs it.
252            let _ = session_state.inner.commit(tx, /*check_occ=*/ false);
253            unsafe {
254                *out_result = ResultState::into_handle(outcome.result);
255            }
256            if !out_error_msg.is_null() {
257                unsafe {
258                    *out_error_msg = std::ptr::null();
259                }
260            }
261            KgliteStatusCode::Ok
262        }
263        Err(err) => {
264            // tx drops without commit — no mutation reaches the
265            // session's stored Arc.
266            unsafe {
267                *out_result = std::ptr::null_mut();
268            }
269            let code = KgliteStatusCode::from_kg_error_code(err.code());
270            if !out_error_msg.is_null() {
271                unsafe {
272                    *out_error_msg = alloc_c_string(&err.to_string());
273                }
274            }
275            code
276        }
277    }
278}
279
280/// Free a session handle. Idempotent on null (no-op).
281///
282/// # Safety
283///
284/// `session` must be either null or a valid pointer previously
285/// returned by [`kglite_session_new`] and not yet freed.
286#[no_mangle]
287pub unsafe extern "C" fn kglite_session_free(session: *mut KgliteSession) {
288    unsafe { SessionState::free_handle(session) };
289}
290
291/// Parse a JSON-string params argument into a HashMap. Null /
292/// empty / "{}" → empty map. Any other shape (array, scalar,
293/// nested object value) maps via
294/// [`json_value_to_kglite_value`](kglite::api::param::json_value_to_kglite_value).
295fn parse_params_json(
296    params_json: *const c_char,
297) -> Result<HashMap<String, Value>, KgliteStatusCode> {
298    if params_json.is_null() {
299        return Ok(HashMap::new());
300    }
301    let s = match unsafe { CStr::from_ptr(params_json) }.to_str() {
302        Ok(s) => s,
303        Err(_) => return Err(KgliteStatusCode::InvalidUtf8),
304    };
305    if s.is_empty() {
306        return Ok(HashMap::new());
307    }
308    let parsed: serde_json::Value = match serde_json::from_str(s) {
309        Ok(v) => v,
310        Err(_) => return Err(KgliteStatusCode::InvalidArgument),
311    };
312    match parsed {
313        serde_json::Value::Object(obj) => Ok(obj
314            .into_iter()
315            .map(|(k, v)| (k, json_value_to_kglite_value(&v)))
316            .collect()),
317        serde_json::Value::Null => Ok(HashMap::new()),
318        _ => Err(KgliteStatusCode::InvalidArgument),
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use std::ffi::CString;
326
327    #[test]
328    fn parse_params_empty_string_is_empty_map() {
329        let s = CString::new("").unwrap();
330        let m = parse_params_json(s.as_ptr()).unwrap();
331        assert!(m.is_empty());
332    }
333
334    #[test]
335    fn parse_params_object_round_trips() {
336        let s = CString::new(r#"{"x": 42, "y": "hello"}"#).unwrap();
337        let m = parse_params_json(s.as_ptr()).unwrap();
338        assert_eq!(m.get("x"), Some(&Value::Int64(42)));
339        assert_eq!(m.get("y"), Some(&Value::String("hello".to_string())));
340    }
341
342    #[test]
343    fn parse_params_null_pointer_is_empty_map() {
344        let m = parse_params_json(std::ptr::null()).unwrap();
345        assert!(m.is_empty());
346    }
347
348    #[test]
349    fn parse_params_array_is_invalid_argument() {
350        let s = CString::new("[1, 2, 3]").unwrap();
351        let err = parse_params_json(s.as_ptr()).unwrap_err();
352        assert_eq!(err, KgliteStatusCode::InvalidArgument);
353    }
354}