use kglite_c::{
kglite_abi_version, kglite_cypher_result_columns_json, kglite_cypher_result_free,
kglite_cypher_result_row_count, kglite_cypher_result_rows_json, kglite_free_string,
kglite_load_file, kglite_session_execute_read, kglite_session_free, kglite_session_new,
KgliteCypherResult, KgliteGraph, KgliteSession, KgliteStatusCode,
};
#[cfg(feature = "fastembed")]
use kglite_c::{
kglite_embedder_fastembed_new, kglite_embedder_free, kglite_session_set_embedder,
KgliteEmbedder,
};
use std::ffi::{c_char, CStr, CString};
use std::path::PathBuf;
fn fixture_path() -> CString {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = PathBuf::from(manifest_dir)
.parent()
.unwrap()
.parent()
.unwrap()
.join("tests/fixtures/timeseries_graph.kgl");
CString::new(path.to_str().unwrap()).unwrap()
}
#[test]
fn abi_version_is_aligned_with_crate() {
let v = kglite_abi_version();
assert_eq!(v.major, 0);
assert_eq!(v.minor, 10);
assert!(v.patch >= 2);
}
#[test]
fn end_to_end_load_query_free() {
let path = fixture_path();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err_msg: *const c_char = std::ptr::null();
let rc =
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err_msg as *mut _) };
assert_eq!(rc, KgliteStatusCode::Ok, "load failed");
assert!(!graph.is_null());
assert!(err_msg.is_null());
let mut session: *mut KgliteSession = std::ptr::null_mut();
let rc = unsafe { kglite_session_new(graph, &mut session as *mut _) };
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!session.is_null());
let query = CString::new("MATCH (n) RETURN count(n) AS n").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err_msg: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read(
session,
query.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err_msg as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok, "execute_read failed");
assert!(!result.is_null());
assert!(err_msg.is_null());
let cols_ptr = unsafe { kglite_cypher_result_columns_json(result) };
assert!(!cols_ptr.is_null());
let cols_str = unsafe { CStr::from_ptr(cols_ptr).to_str().unwrap() };
assert_eq!(cols_str, r#"["n"]"#);
unsafe { kglite_free_string(cols_ptr) };
let rows_ptr = unsafe { kglite_cypher_result_rows_json(result) };
assert!(!rows_ptr.is_null());
let rows_str = unsafe { CStr::from_ptr(rows_ptr).to_str().unwrap() };
assert!(rows_str.starts_with("[{\"n\":"));
assert!(rows_str.ends_with("}]"));
unsafe { kglite_free_string(rows_ptr) };
let row_count = unsafe { kglite_cypher_result_row_count(result) };
assert_eq!(row_count, 1);
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn cypher_syntax_error_returns_error_message() {
let path = fixture_path();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err_msg: *const c_char = std::ptr::null();
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err_msg as *mut _) };
let mut session: *mut KgliteSession = std::ptr::null_mut();
unsafe { kglite_session_new(graph, &mut session as *mut _) };
let query = CString::new("MATCH (n RETURN n").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err_msg: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read(
session,
query.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err_msg as *mut _,
)
};
assert_ne!(rc, KgliteStatusCode::Ok);
assert_eq!(rc, KgliteStatusCode::CypherSyntax);
assert!(result.is_null());
assert!(!err_msg.is_null());
let msg = unsafe { CStr::from_ptr(err_msg).to_str().unwrap() };
assert!(!msg.is_empty());
unsafe { kglite_free_string(err_msg) };
unsafe { kglite_session_free(session) };
}
#[test]
fn params_json_round_trip() {
let path = fixture_path();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err_msg: *const c_char = std::ptr::null();
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err_msg as *mut _) };
let mut session: *mut KgliteSession = std::ptr::null_mut();
unsafe { kglite_session_new(graph, &mut session as *mut _) };
let query = CString::new("RETURN $x AS x, $y AS y").unwrap();
let params = CString::new(r#"{"x": 42, "y": "hello"}"#).unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err_msg: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read(
session,
query.as_ptr(),
params.as_ptr(),
&mut result as *mut _,
&mut err_msg as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!result.is_null());
let rows_ptr = unsafe { kglite_cypher_result_rows_json(result) };
let rows_str = unsafe { CStr::from_ptr(rows_ptr).to_str().unwrap() };
assert!(rows_str.contains("\"x\":") && rows_str.contains("42"));
assert!(rows_str.contains("\"y\":") && rows_str.contains("hello"));
unsafe { kglite_free_string(rows_ptr) };
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[cfg(feature = "fastembed")]
#[test]
fn fastembed_factory_rejects_unknown_model() {
let model = CString::new("definitely-not-a-real-model").unwrap();
let mut embedder: *mut KgliteEmbedder = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_embedder_fastembed_new(model.as_ptr(), &mut embedder as *mut _, &mut err as *mut _)
};
assert_eq!(rc, KgliteStatusCode::InvalidArgument);
assert!(embedder.is_null());
assert!(!err.is_null());
unsafe { kglite_free_string(err) };
}
#[cfg(feature = "fastembed")]
#[test]
fn set_embedder_with_null_args_returns_null_pointer() {
let rc = unsafe { kglite_session_set_embedder(std::ptr::null_mut(), std::ptr::null()) };
assert_eq!(rc, KgliteStatusCode::NullPointer);
}
#[cfg(feature = "fastembed")]
#[test]
fn embedder_free_is_null_safe() {
unsafe { kglite_embedder_free(std::ptr::null_mut()) };
}
#[cfg(feature = "sodir")]
#[test]
fn sodir_fetch_with_bad_json_returns_invalid_argument() {
use kglite_c::kglite_datasets_sodir_fetch_all;
let workdir = CString::new("/tmp/kglite_c_sodir_integration_bad").unwrap();
let bad = CString::new("not-a-json-array").unwrap();
let mut out_report: *const c_char = std::ptr::null();
let mut out_err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_datasets_sodir_fetch_all(
workdir.as_ptr(),
bad.as_ptr(),
7,
30,
10,
&mut out_report as *mut _,
&mut out_err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::InvalidArgument);
assert!(out_report.is_null());
}
#[cfg(feature = "sodir")]
#[test]
fn sodir_fetch_empty_datasets_succeeds_with_empty_report() {
use kglite_c::kglite_datasets_sodir_fetch_all;
use std::fs;
let workdir_path = std::env::temp_dir().join("kglite_c_sodir_empty");
let _ = fs::remove_dir_all(&workdir_path); let workdir = CString::new(workdir_path.to_str().unwrap()).unwrap();
let datasets = CString::new("[]").unwrap();
let mut out_report: *const c_char = std::ptr::null();
let mut out_err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_datasets_sodir_fetch_all(
workdir.as_ptr(),
datasets.as_ptr(),
7,
30,
10,
&mut out_report as *mut _,
&mut out_err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!out_report.is_null());
let report_str = unsafe { CStr::from_ptr(out_report).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(report_str).unwrap();
assert!(parsed["refresh"].is_object());
assert!(parsed["preprocess"].is_object());
unsafe { kglite_free_string(out_report) };
let _ = fs::remove_dir_all(&workdir_path);
}