use kglite_c::{
kglite_abi_version, kglite_blueprint_build, kglite_compute_schema_json,
kglite_create_edges_batch, kglite_cypher_result_columns_json, kglite_cypher_result_free,
kglite_cypher_result_row_count, kglite_cypher_result_rows_json, kglite_free_bytes,
kglite_free_string, kglite_graph_free, kglite_graph_from_bytes, kglite_graph_new,
kglite_graph_to_bytes, kglite_load_file, kglite_save_graph_durable, kglite_session_execute_mut,
kglite_session_execute_mut_batch, kglite_session_execute_read,
kglite_session_execute_read_batch, kglite_session_execute_read_opts, 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!(
format!("{}.{}.{}", v.major, v.minor, v.patch),
format!(
"{}.{}.{}",
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR"),
env!("CARGO_PKG_VERSION_PATCH"),
)
);
}
#[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_eq!(rows_str, r#"[{"x":42,"y":"hello"}]"#);
unsafe { kglite_free_string(rows_ptr) };
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn create_empty_graph_then_mutate_and_read() {
let graph = kglite_graph_new();
assert!(!graph.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);
let create = CString::new("CREATE (:T {id: 1}), (:T {id: 2})").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_mut(
session,
create.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok, "execute_mut failed");
assert!(err.is_null());
unsafe { kglite_cypher_result_free(result) };
let q = CString::new("MATCH (n:T) RETURN n.id AS id ORDER BY id").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read(
session,
q.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
let row_count = unsafe { kglite_cypher_result_row_count(result) };
assert_eq!(row_count, 2, "both created nodes should be returned");
let rows_ptr = unsafe { kglite_cypher_result_rows_json(result) };
let rows = unsafe { CStr::from_ptr(rows_ptr).to_str().unwrap() };
assert_eq!(rows, r#"[{"id":1},{"id":2}]"#);
unsafe { kglite_free_string(rows_ptr) };
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn execute_batch_read_and_mut() {
let graph = kglite_graph_new();
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);
let muts = CString::new(r#"[{"query":"CREATE (:T {id: 1})"},{"query":"CREATE (:T {id: 2})"}]"#)
.unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_mut_batch(
session,
muts.as_ptr(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok, "mut batch failed");
assert!(!out.is_null());
let parsed: serde_json::Value =
serde_json::from_str(unsafe { CStr::from_ptr(out).to_str().unwrap() }).unwrap();
assert_eq!(parsed.as_array().unwrap().len(), 2, "one result per query");
unsafe { kglite_free_string(out) };
let reads = CString::new(
r#"[{"query":"MATCH (n:T) RETURN count(n) AS c"},{"query":"MATCH (n:T) RETURN n.id AS id ORDER BY id"}]"#,
)
.unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read_batch(
session,
reads.as_ptr(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
let parsed: serde_json::Value =
serde_json::from_str(unsafe { CStr::from_ptr(out).to_str().unwrap() }).unwrap();
let arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["rows"][0]["c"], serde_json::json!(2));
assert_eq!(arr[1]["rows"].as_array().unwrap().len(), 2);
unsafe { kglite_free_string(out) };
unsafe { kglite_session_free(session) };
}
#[test]
fn execute_mut_batch_is_atomic_on_failure() {
let graph = kglite_graph_new();
let mut session: *mut KgliteSession = std::ptr::null_mut();
unsafe { kglite_session_new(graph, &mut session as *mut _) };
let muts =
CString::new(r#"[{"query":"CREATE (:Z {id: 1})"},{"query":"MATCH (n RETURN n"}]"#).unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_mut_batch(
session,
muts.as_ptr(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_ne!(rc, KgliteStatusCode::Ok);
assert!(out.is_null());
assert!(!err.is_null());
unsafe { kglite_free_string(err) };
let q = CString::new("MATCH (n:Z) RETURN count(n) AS c").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe {
kglite_session_execute_read(
session,
q.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
let rows_ptr = unsafe { kglite_cypher_result_rows_json(result) };
let rows = unsafe { CStr::from_ptr(rows_ptr).to_str().unwrap() };
assert_eq!(rows, r#"[{"c":0}]"#, "first create should have rolled back");
unsafe { kglite_free_string(rows_ptr) };
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn create_edges_batch_by_id() {
let graph = kglite_graph_new();
let mut session: *mut KgliteSession = std::ptr::null_mut();
unsafe { kglite_session_new(graph, &mut session as *mut _) };
let create =
CString::new("CREATE (:Person {id: 1}), (:Person {id: 2}), (:Company {id: 10})").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_mut(
session,
create.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
unsafe { kglite_cypher_result_free(result) };
let edges = CString::new(
r#"[
{"src_id":1,"src_type":"Person","dst_id":2,"dst_type":"Person","type":"KNOWS"},
{"src_id":1,"src_type":"Person","dst_id":10,"dst_type":"Company","type":"WORKS_AT","props":{"since":2020}},
{"src_id":99,"src_type":"Person","dst_id":2,"dst_type":"Person","type":"KNOWS"}
]"#,
)
.unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_create_edges_batch(
session,
edges.as_ptr(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok, "create_edges_batch failed");
assert!(!out.is_null());
let report: serde_json::Value =
serde_json::from_str(unsafe { CStr::from_ptr(out).to_str().unwrap() }).unwrap();
assert_eq!(report["connections_created"], serde_json::json!(2));
assert_eq!(report["skipped_missing_endpoint"], serde_json::json!(1));
unsafe { kglite_free_string(out) };
let q = CString::new("MATCH ()-[r]->() RETURN count(r) AS c").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe {
kglite_session_execute_read(
session,
q.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
let rows_ptr = unsafe { kglite_cypher_result_rows_json(result) };
let rows = unsafe { CStr::from_ptr(rows_ptr).to_str().unwrap() };
assert_eq!(rows, r#"[{"c":2}]"#);
unsafe { kglite_free_string(rows_ptr) };
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn execute_read_opts_caps_rows() {
let graph = kglite_graph_new();
let mut session: *mut KgliteSession = std::ptr::null_mut();
unsafe { kglite_session_new(graph, &mut session as *mut _) };
let create = CString::new("CREATE (:T {id: 1}), (:T {id: 2}), (:T {id: 3})").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe {
kglite_session_execute_mut(
session,
create.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
unsafe { kglite_cypher_result_free(result) };
let q = CString::new("MATCH (n:T) RETURN n.id AS id").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read_opts(
session,
q.as_ptr(),
std::ptr::null(),
0,
2,
&mut result as *mut _,
&mut err as *mut _,
)
};
assert_ne!(rc, KgliteStatusCode::Ok, "exceeding max_rows must error");
assert!(result.is_null());
assert!(!err.is_null());
unsafe { kglite_free_string(err) };
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_session_execute_read_opts(
session,
q.as_ptr(),
std::ptr::null(),
0,
5,
&mut result as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
assert_eq!(unsafe { kglite_cypher_result_row_count(result) }, 3);
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn graph_bytes_round_trip() {
let path = fixture_path();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err as *mut _) };
let mut buf: *mut u8 = std::ptr::null_mut();
let mut len: usize = 0;
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_graph_to_bytes(
graph,
&mut buf as *mut _,
&mut len as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!buf.is_null() && len > 0);
unsafe { kglite_graph_free(graph) };
let mut graph2: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc =
unsafe { kglite_graph_from_bytes(buf, len, &mut graph2 as *mut _, &mut err as *mut _) };
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!graph2.is_null());
unsafe { kglite_free_bytes(buf, len) };
let mut session: *mut KgliteSession = std::ptr::null_mut();
unsafe { kglite_session_new(graph2, &mut session as *mut _) };
let q = CString::new("MATCH (n) RETURN count(n) AS n").unwrap();
let mut result: *mut KgliteCypherResult = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe {
kglite_session_execute_read(
session,
q.as_ptr(),
std::ptr::null(),
&mut result as *mut _,
&mut err as *mut _,
)
};
let rows_ptr = unsafe { kglite_cypher_result_rows_json(result) };
let rows = unsafe { CStr::from_ptr(rows_ptr).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(rows).unwrap();
assert!(
parsed[0]["n"].as_u64().unwrap() > 0,
"round-tripped graph has nodes"
);
unsafe { kglite_free_string(rows_ptr) };
unsafe { kglite_cypher_result_free(result) };
unsafe { kglite_session_free(session) };
}
#[test]
fn save_graph_durable_round_trips() {
let path = fixture_path();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err as *mut _) };
let tmp = std::env::temp_dir().join("kglite_c_durable.kgl");
let _ = std::fs::remove_file(&tmp);
let tmp_c = CString::new(tmp.to_str().unwrap()).unwrap();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe { kglite_save_graph_durable(graph, tmp_c.as_ptr(), 1, &mut err as *mut _) };
assert_eq!(rc, KgliteStatusCode::Ok);
unsafe { kglite_graph_free(graph) };
let mut graph2: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe { kglite_load_file(tmp_c.as_ptr(), &mut graph2 as *mut _, &mut err as *mut _) };
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!graph2.is_null());
unsafe { kglite_graph_free(graph2) };
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn compute_schema_json_describes_graph() {
let path = fixture_path();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err as *mut _) };
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe { kglite_compute_schema_json(graph, &mut out as *mut _, &mut err as *mut _) };
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!out.is_null());
let parsed: serde_json::Value =
serde_json::from_str(unsafe { CStr::from_ptr(out).to_str().unwrap() }).unwrap();
assert!(parsed["node_count"].as_u64().unwrap() > 0);
assert!(!parsed["node_types"].as_array().unwrap().is_empty());
unsafe { kglite_free_string(out) };
unsafe { kglite_graph_free(graph) };
}
#[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);
}
#[test]
fn blueprint_build_error_clears_out_report_json() {
let bad_path = CString::new("/nonexistent/does-not-exist.yaml").unwrap();
let dir = CString::new("/tmp").unwrap();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut report: *const c_char = std::ptr::NonNull::<c_char>::dangling().as_ptr();
let mut err_msg: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_blueprint_build(
bad_path.as_ptr(),
dir.as_ptr(),
&mut graph as *mut _,
&mut report as *mut _,
&mut err_msg as *mut _,
)
};
assert_ne!(rc, KgliteStatusCode::Ok, "bad blueprint path must fail");
assert!(graph.is_null(), "out_graph must be null on error");
assert!(
report.is_null(),
"out_report_json must be nulled on error (else freeing it is UB)"
);
if !err_msg.is_null() {
unsafe { kglite_free_string(err_msg) };
}
}