1use std::cell::RefCell;
36use std::collections::HashMap;
37use std::ffi::{CStr, CString};
38use std::os::raw::c_char;
39use std::path::Path;
40use std::ptr;
41
42use omendb::vector::{MetadataFilter, Vector, VectorStore, VectorStoreOptions};
43use serde_json::{json, Value as JsonValue};
44
45thread_local! {
46 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
47}
48
49fn set_last_error(err: String) {
50 LAST_ERROR.with(|e| {
51 *e.borrow_mut() = CString::new(err).ok();
52 });
53}
54
55fn clear_last_error() {
56 LAST_ERROR.with(|e| *e.borrow_mut() = None);
57}
58
59pub struct OmenDB {
61 store: VectorStore,
62 dimensions: usize,
63 index_to_id: HashMap<usize, String>,
65}
66
67impl OmenDB {
68 fn rebuild_cache(&mut self) {
70 self.index_to_id = self
71 .store
72 .id_to_index
73 .iter()
74 .map(|(id, &idx)| (idx, id.clone()))
75 .collect();
76 }
77
78 fn get_id(&self, idx: usize) -> String {
80 self.index_to_id
81 .get(&idx)
82 .cloned()
83 .unwrap_or_else(|| idx.to_string())
84 }
85}
86
87#[no_mangle]
110pub unsafe extern "C" fn omendb_open(
111 path: *const c_char,
112 dimensions: usize,
113 config_json: *const c_char,
114) -> *mut OmenDB {
115 clear_last_error();
116
117 if path.is_null() {
118 set_last_error("Null path pointer".to_string());
119 return ptr::null_mut();
120 }
121
122 let path = match CStr::from_ptr(path).to_str() {
123 Ok(s) => s,
124 Err(e) => {
125 set_last_error(format!("Invalid path: {e}"));
126 return ptr::null_mut();
127 }
128 };
129
130 let config: Option<JsonValue> = if config_json.is_null() {
132 None
133 } else {
134 let config_str = match CStr::from_ptr(config_json).to_str() {
135 Ok(s) => s,
136 Err(e) => {
137 set_last_error(format!("Invalid config string: {e}"));
138 return ptr::null_mut();
139 }
140 };
141 match serde_json::from_str(config_str) {
142 Ok(v) => Some(v),
143 Err(e) => {
144 set_last_error(format!("Invalid config JSON: {e}"));
145 return ptr::null_mut();
146 }
147 }
148 };
149
150 let result = if let Some(cfg) = config {
152 let mut options = VectorStoreOptions::new().dimensions(dimensions);
153
154 if let Some(m) = cfg.get("m").and_then(JsonValue::as_u64) {
155 options = options.m(m as usize);
156 }
157 if let Some(ef_c) = cfg.get("ef_construction").and_then(JsonValue::as_u64) {
158 options = options.ef_construction(ef_c as usize);
159 }
160 if let Some(ef_s) = cfg.get("ef_search").and_then(JsonValue::as_u64) {
161 options = options.ef_search(ef_s as usize);
162 }
163
164 options.open(Path::new(path))
165 } else {
166 VectorStore::open_with_dimensions(Path::new(path), dimensions)
167 };
168
169 match result {
170 Ok(store) => {
171 let index_to_id: HashMap<usize, String> = store
173 .id_to_index
174 .iter()
175 .map(|(id, &idx)| (idx, id.clone()))
176 .collect();
177 Box::into_raw(Box::new(OmenDB {
178 store,
179 dimensions,
180 index_to_id,
181 }))
182 }
183 Err(e) => {
184 set_last_error(format!("Failed to open database: {e}"));
185 ptr::null_mut()
186 }
187 }
188}
189
190#[no_mangle]
196pub unsafe extern "C" fn omendb_close(db: *mut OmenDB) {
197 if !db.is_null() {
198 drop(Box::from_raw(db));
199 }
200}
201
202#[no_mangle]
215pub unsafe extern "C" fn omendb_set(db: *mut OmenDB, items_json: *const c_char) -> i64 {
216 clear_last_error();
217
218 let Some(db) = db.as_mut() else {
219 set_last_error("Null database handle".to_string());
220 return -1;
221 };
222
223 if items_json.is_null() {
224 set_last_error("Null items_json pointer".to_string());
225 return -1;
226 }
227
228 let items_str = match CStr::from_ptr(items_json).to_str() {
229 Ok(s) => s,
230 Err(e) => {
231 set_last_error(format!("Invalid JSON string: {e}"));
232 return -1;
233 }
234 };
235
236 let items: Vec<JsonValue> = match serde_json::from_str(items_str) {
237 Ok(v) => v,
238 Err(e) => {
239 set_last_error(format!("JSON parse error: {e}"));
240 return -1;
241 }
242 };
243
244 let mut count = 0i64;
245 for item in items {
246 let id = if let Some(s) = item.get("id").and_then(|v| v.as_str()) {
247 s.to_string()
248 } else {
249 set_last_error("Item missing 'id' field".to_string());
250 return -1;
251 };
252
253 let vector_data: Vec<f32> = if let Some(arr) = item.get("vector").and_then(|v| v.as_array())
254 {
255 arr.iter()
256 .filter_map(|v| v.as_f64().map(|f| f as f32))
257 .collect()
258 } else {
259 set_last_error("Item missing 'vector' field".to_string());
260 return -1;
261 };
262
263 let metadata = item.get("metadata").cloned().unwrap_or(json!({}));
264
265 let vector = Vector::new(vector_data);
266 if let Err(e) = db.store.set(id, vector, metadata) {
267 db.rebuild_cache();
269 set_last_error(format!("Set failed after {count} items: {e}"));
270 return -1;
271 }
272 count += 1;
273 }
274
275 db.rebuild_cache();
277 count
278}
279
280#[no_mangle]
295pub unsafe extern "C" fn omendb_get(
296 db: *mut OmenDB,
297 ids_json: *const c_char,
298 result: *mut *mut c_char,
299) -> i32 {
300 clear_last_error();
301
302 let Some(db) = db.as_ref() else {
303 set_last_error("Null database handle".to_string());
304 return -1;
305 };
306
307 if ids_json.is_null() {
308 set_last_error("Null ids_json pointer".to_string());
309 return -1;
310 }
311
312 let ids_str = match CStr::from_ptr(ids_json).to_str() {
313 Ok(s) => s,
314 Err(e) => {
315 set_last_error(format!("Invalid JSON string: {e}"));
316 return -1;
317 }
318 };
319
320 let ids: Vec<String> = match serde_json::from_str(ids_str) {
321 Ok(v) => v,
322 Err(e) => {
323 set_last_error(format!("JSON parse error: {e}"));
324 return -1;
325 }
326 };
327
328 let mut results = Vec::new();
329 for id in ids {
330 if let Some((vector, metadata)) = db.store.get(&id) {
331 results.push(json!({
332 "id": id,
333 "vector": vector.data,
334 "metadata": metadata
335 }));
336 }
337 }
338
339 let json_str = match serde_json::to_string(&results) {
340 Ok(s) => s,
341 Err(e) => {
342 set_last_error(format!("JSON serialize error: {e}"));
343 return -1;
344 }
345 };
346
347 if result.is_null() {
348 set_last_error("Output pointer is NULL".to_string());
349 return -1;
350 }
351
352 match CString::new(json_str) {
353 Ok(cstr) => {
354 *result = cstr.into_raw();
355 0
356 }
357 Err(e) => {
358 set_last_error(format!("CString error: {e}"));
359 -1
360 }
361 }
362}
363
364#[no_mangle]
373pub unsafe extern "C" fn omendb_delete(db: *mut OmenDB, ids_json: *const c_char) -> i64 {
374 clear_last_error();
375
376 let Some(db) = db.as_mut() else {
377 set_last_error("Null database handle".to_string());
378 return -1;
379 };
380
381 if ids_json.is_null() {
382 set_last_error("Null ids_json pointer".to_string());
383 return -1;
384 }
385
386 let ids_str = match CStr::from_ptr(ids_json).to_str() {
387 Ok(s) => s,
388 Err(e) => {
389 set_last_error(format!("Invalid JSON string: {e}"));
390 return -1;
391 }
392 };
393
394 let ids: Vec<String> = match serde_json::from_str(ids_str) {
395 Ok(v) => v,
396 Err(e) => {
397 set_last_error(format!("JSON parse error: {e}"));
398 return -1;
399 }
400 };
401
402 match db.store.delete_batch(&ids) {
403 Ok(count) => {
404 db.rebuild_cache();
405 i64::try_from(count).unwrap_or(i64::MAX)
406 }
407 Err(e) => {
408 set_last_error(format!("Delete failed: {e}"));
409 -1
410 }
411 }
412}
413
414#[no_mangle]
433pub unsafe extern "C" fn omendb_search(
434 db: *mut OmenDB,
435 query: *const f32,
436 query_len: usize,
437 k: usize,
438 filter_json: *const c_char,
439 result: *mut *mut c_char,
440) -> i32 {
441 clear_last_error();
442
443 let Some(db) = db.as_mut() else {
444 set_last_error("Null database handle".to_string());
445 return -1;
446 };
447
448 if query.is_null() {
449 set_last_error("Null query pointer".to_string());
450 return -1;
451 }
452
453 if query_len != db.dimensions {
454 set_last_error(format!(
455 "Query dimension mismatch: expected {}, got {query_len}",
456 db.dimensions
457 ));
458 return -1;
459 }
460
461 let query_vec: Vec<f32> = std::slice::from_raw_parts(query, query_len).to_vec();
462 let query = Vector::new(query_vec);
463
464 let filter: Option<MetadataFilter> = if filter_json.is_null() {
466 None
467 } else {
468 let filter_str = match CStr::from_ptr(filter_json).to_str() {
469 Ok(s) => s,
470 Err(e) => {
471 set_last_error(format!("Invalid filter string: {e}"));
472 return -1;
473 }
474 };
475 match serde_json::from_str::<JsonValue>(filter_str) {
476 Ok(v) => match MetadataFilter::from_json(&v) {
477 Ok(f) => Some(f),
478 Err(e) => {
479 set_last_error(format!("Invalid filter format: {e}"));
480 return -1;
481 }
482 },
483 Err(e) => {
484 set_last_error(format!("Invalid filter JSON: {e}"));
485 return -1;
486 }
487 }
488 };
489
490 let results = match db.store.search(&query, k, filter.as_ref()) {
492 Ok(r) => r,
493 Err(e) => {
494 set_last_error(format!("Search failed: {e}"));
495 return -1;
496 }
497 };
498
499 let json_results: Vec<JsonValue> = results
501 .into_iter()
502 .map(|(idx, distance, metadata)| {
503 json!({
504 "id": db.get_id(idx),
505 "distance": distance,
506 "metadata": metadata
507 })
508 })
509 .collect();
510
511 let json_str = match serde_json::to_string(&json_results) {
512 Ok(s) => s,
513 Err(e) => {
514 set_last_error(format!("JSON serialize error: {e}"));
515 return -1;
516 }
517 };
518
519 if result.is_null() {
520 set_last_error("Output pointer is NULL".to_string());
521 return -1;
522 }
523
524 match CString::new(json_str) {
525 Ok(cstr) => {
526 *result = cstr.into_raw();
527 0
528 }
529 Err(e) => {
530 set_last_error(format!("CString error: {e}"));
531 -1
532 }
533 }
534}
535
536#[no_mangle]
541pub unsafe extern "C" fn omendb_count(db: *const OmenDB) -> i64 {
542 clear_last_error();
543 match db.as_ref() {
544 Some(db) => i64::try_from(db.store.len()).unwrap_or(i64::MAX),
545 None => {
546 set_last_error("Null database handle".to_string());
547 -1
548 }
549 }
550}
551
552#[no_mangle]
557pub unsafe extern "C" fn omendb_save(db: *mut OmenDB) -> i32 {
558 clear_last_error();
559
560 let Some(db) = db.as_mut() else {
561 set_last_error("Null database handle".to_string());
562 return -1;
563 };
564
565 match db.store.flush() {
566 Ok(()) => 0,
567 Err(e) => {
568 set_last_error(format!("Save failed: {e}"));
569 -1
570 }
571 }
572}
573
574#[no_mangle]
579pub extern "C" fn omendb_last_error() -> *const c_char {
580 LAST_ERROR.with(|e| match &*e.borrow() {
581 Some(cstr) => cstr.as_ptr(),
582 None => ptr::null(),
583 })
584}
585
586#[no_mangle]
592pub unsafe extern "C" fn omendb_free_string(s: *mut c_char) {
593 if !s.is_null() {
594 drop(CString::from_raw(s));
595 }
596}
597
598#[no_mangle]
600pub extern "C" fn omendb_version() -> *const c_char {
601 concat!(env!("CARGO_PKG_VERSION"), "\0")
603 .as_ptr()
604 .cast::<c_char>()
605}
606
607#[no_mangle]
619pub unsafe extern "C" fn omendb_enable_text_search(db: *mut OmenDB) -> i32 {
620 clear_last_error();
621
622 let Some(db) = db.as_mut() else {
623 set_last_error("Null database handle".to_string());
624 return -1;
625 };
626
627 match db.store.enable_text_search() {
628 Ok(()) => 0,
629 Err(e) => {
630 set_last_error(format!("Failed to enable text search: {e}"));
631 -1
632 }
633 }
634}
635
636#[no_mangle]
644pub unsafe extern "C" fn omendb_has_text_search(db: *const OmenDB) -> i32 {
645 clear_last_error();
646 let Some(db) = db.as_ref() else {
647 set_last_error("Null database handle".to_string());
648 return -1;
649 };
650 i32::from(db.store.has_text_search())
651}
652
653#[no_mangle]
666pub unsafe extern "C" fn omendb_set_with_text(db: *mut OmenDB, items_json: *const c_char) -> i64 {
667 clear_last_error();
668
669 let Some(db) = db.as_mut() else {
670 set_last_error("Null database handle".to_string());
671 return -1;
672 };
673
674 if !db.store.has_text_search() {
675 set_last_error(
676 "Text search not enabled. Call omendb_enable_text_search first.".to_string(),
677 );
678 return -1;
679 }
680
681 if items_json.is_null() {
682 set_last_error("Null items_json pointer".to_string());
683 return -1;
684 }
685
686 let items_str = match CStr::from_ptr(items_json).to_str() {
687 Ok(s) => s,
688 Err(e) => {
689 set_last_error(format!("Invalid JSON string: {e}"));
690 return -1;
691 }
692 };
693
694 let items: Vec<JsonValue> = match serde_json::from_str(items_str) {
695 Ok(v) => v,
696 Err(e) => {
697 set_last_error(format!("JSON parse error: {e}"));
698 return -1;
699 }
700 };
701
702 let mut count = 0i64;
703 for item in items {
704 let id = if let Some(s) = item.get("id").and_then(|v| v.as_str()) {
705 s.to_string()
706 } else {
707 set_last_error("Item missing 'id' field".to_string());
708 return -1;
709 };
710
711 let vector_data: Vec<f32> = if let Some(arr) = item.get("vector").and_then(|v| v.as_array())
712 {
713 arr.iter()
714 .filter_map(|v| v.as_f64().map(|f| f as f32))
715 .collect()
716 } else {
717 set_last_error("Item missing 'vector' field".to_string());
718 return -1;
719 };
720
721 let text = if let Some(s) = item.get("text").and_then(|v| v.as_str()) {
722 s
723 } else {
724 set_last_error("Item missing 'text' field".to_string());
725 return -1;
726 };
727
728 let metadata = item.get("metadata").cloned().unwrap_or(json!({}));
729
730 let vector = Vector::new(vector_data);
731 if let Err(e) = db.store.set_with_text(id, vector, text, metadata) {
732 db.rebuild_cache();
734 set_last_error(format!("Set with text failed after {count} items: {e}"));
735 return -1;
736 }
737 count += 1;
738 }
739
740 db.rebuild_cache();
742 count
743}
744
745#[no_mangle]
759pub unsafe extern "C" fn omendb_text_search(
760 db: *mut OmenDB,
761 query: *const c_char,
762 k: usize,
763 result: *mut *mut c_char,
764) -> i32 {
765 clear_last_error();
766
767 let Some(db) = db.as_ref() else {
768 set_last_error("Null database handle".to_string());
769 return -1;
770 };
771
772 if query.is_null() {
773 set_last_error("Null query pointer".to_string());
774 return -1;
775 }
776
777 let query_str = match CStr::from_ptr(query).to_str() {
778 Ok(s) => s,
779 Err(e) => {
780 set_last_error(format!("Invalid query string: {e}"));
781 return -1;
782 }
783 };
784
785 let search_results = match db.store.text_search(query_str, k) {
786 Ok(r) => r,
787 Err(e) => {
788 set_last_error(format!("Text search failed: {e}"));
789 return -1;
790 }
791 };
792
793 let json_results: Vec<JsonValue> = search_results
794 .into_iter()
795 .map(|(id, score)| json!({"id": id, "score": score}))
796 .collect();
797
798 let json_str = match serde_json::to_string(&json_results) {
799 Ok(s) => s,
800 Err(e) => {
801 set_last_error(format!("JSON serialize error: {e}"));
802 return -1;
803 }
804 };
805
806 if result.is_null() {
807 set_last_error("Output pointer is NULL".to_string());
808 return -1;
809 }
810
811 match CString::new(json_str) {
812 Ok(cstr) => {
813 *result = cstr.into_raw();
814 0
815 }
816 Err(e) => {
817 set_last_error(format!("CString error: {e}"));
818 -1
819 }
820 }
821}
822
823#[no_mangle]
844pub unsafe extern "C" fn omendb_hybrid_search(
845 db: *mut OmenDB,
846 query_vector: *const f32,
847 query_len: usize,
848 query_text: *const c_char,
849 k: usize,
850 alpha: f32,
851 rrf_k: usize,
852 filter_json: *const c_char,
853 result: *mut *mut c_char,
854) -> i32 {
855 clear_last_error();
856
857 let Some(db) = db.as_mut() else {
858 set_last_error("Null database handle".to_string());
859 return -1;
860 };
861
862 if query_vector.is_null() {
863 set_last_error("Null query_vector pointer".to_string());
864 return -1;
865 }
866
867 if query_text.is_null() {
868 set_last_error("Null query_text pointer".to_string());
869 return -1;
870 }
871
872 if query_len != db.dimensions {
873 set_last_error(format!(
874 "Query dimension mismatch: expected {}, got {query_len}",
875 db.dimensions
876 ));
877 return -1;
878 }
879
880 let query_vec: Vec<f32> = std::slice::from_raw_parts(query_vector, query_len).to_vec();
881 let vector = Vector::new(query_vec);
882
883 let text_str = match CStr::from_ptr(query_text).to_str() {
884 Ok(s) => s,
885 Err(e) => {
886 set_last_error(format!("Invalid text query: {e}"));
887 return -1;
888 }
889 };
890
891 let alpha_opt = if alpha < 0.0 { None } else { Some(alpha) };
893 let rrf_k_opt = if rrf_k == 0 { None } else { Some(rrf_k) };
894
895 let filter = if filter_json.is_null() {
897 None
898 } else {
899 let filter_str = match CStr::from_ptr(filter_json).to_str() {
900 Ok(s) => s,
901 Err(e) => {
902 set_last_error(format!("Invalid filter string: {e}"));
903 return -1;
904 }
905 };
906 match serde_json::from_str::<JsonValue>(filter_str) {
907 Ok(v) => match MetadataFilter::from_json(&v) {
908 Ok(f) => Some(f),
909 Err(e) => {
910 set_last_error(format!("Invalid filter format: {e}"));
911 return -1;
912 }
913 },
914 Err(e) => {
915 set_last_error(format!("Invalid filter JSON: {e}"));
916 return -1;
917 }
918 }
919 };
920
921 let search_results = if let Some(f) = filter {
922 match db
923 .store
924 .hybrid_search_with_filter_rrf_k(&vector, text_str, k, &f, alpha_opt, rrf_k_opt)
925 {
926 Ok(r) => r,
927 Err(e) => {
928 set_last_error(format!("Hybrid search failed: {e}"));
929 return -1;
930 }
931 }
932 } else {
933 match db
934 .store
935 .hybrid_search_with_rrf_k(&vector, text_str, k, alpha_opt, rrf_k_opt)
936 {
937 Ok(r) => r,
938 Err(e) => {
939 set_last_error(format!("Hybrid search failed: {e}"));
940 return -1;
941 }
942 }
943 };
944
945 let json_results: Vec<JsonValue> = search_results
946 .into_iter()
947 .map(|(id, score, metadata)| json!({"id": id, "score": score, "metadata": metadata}))
948 .collect();
949
950 let json_str = match serde_json::to_string(&json_results) {
951 Ok(s) => s,
952 Err(e) => {
953 set_last_error(format!("JSON serialize error: {e}"));
954 return -1;
955 }
956 };
957
958 if result.is_null() {
959 set_last_error("Output pointer is NULL".to_string());
960 return -1;
961 }
962
963 match CString::new(json_str) {
964 Ok(cstr) => {
965 *result = cstr.into_raw();
966 0
967 }
968 Err(e) => {
969 set_last_error(format!("CString error: {e}"));
970 -1
971 }
972 }
973}
974
975#[no_mangle]
980pub unsafe extern "C" fn omendb_flush(db: *mut OmenDB) -> i32 {
981 clear_last_error();
982
983 let Some(db) = db.as_mut() else {
984 set_last_error("Null database handle".to_string());
985 return -1;
986 };
987
988 match db.store.flush() {
989 Ok(()) => 0,
990 Err(e) => {
991 set_last_error(format!("Flush failed: {e}"));
992 -1
993 }
994 }
995}