1use crate::database::{Database, TxnHandle};
19use parking_lot::Mutex;
20use serde_json::Value;
21use sochdb_index::hnsw::{DistanceMetric, HnswConfig, HnswIndex};
22use std::collections::HashMap;
23use std::ffi::CStr;
24use std::os::raw::{c_char, c_int};
25use std::ptr;
26use std::slice;
27use std::sync::Arc;
28use std::sync::OnceLock;
29
30pub trait FfiDefault {
51 fn ffi_default() -> Self;
53}
54
55impl FfiDefault for () {
56 #[inline]
57 fn ffi_default() -> Self {}
58}
59
60impl<T> FfiDefault for *mut T {
61 #[inline]
62 fn ffi_default() -> Self {
63 std::ptr::null_mut()
64 }
65}
66
67impl<T> FfiDefault for *const T {
68 #[inline]
69 fn ffi_default() -> Self {
70 std::ptr::null()
71 }
72}
73
74impl FfiDefault for c_int {
75 #[inline]
76 fn ffi_default() -> Self {
77 -1
78 }
79}
80
81impl FfiDefault for i64 {
82 #[inline]
83 fn ffi_default() -> Self {
84 -1
85 }
86}
87
88impl FfiDefault for u8 {
89 #[inline]
90 fn ffi_default() -> Self {
91 0
92 }
93}
94
95impl FfiDefault for u64 {
96 #[inline]
97 fn ffi_default() -> Self {
98 0
99 }
100}
101
102impl FfiDefault for usize {
103 #[inline]
104 fn ffi_default() -> Self {
105 0
106 }
107}
108
109impl FfiDefault for bool {
110 #[inline]
111 fn ffi_default() -> Self {
112 false
113 }
114}
115
116impl FfiDefault for f32 {
117 #[inline]
118 fn ffi_default() -> Self {
119 0.0
120 }
121}
122
123impl FfiDefault for f64 {
124 #[inline]
125 fn ffi_default() -> Self {
126 0.0
127 }
128}
129
130impl FfiDefault for C_TxnHandle {
131 #[inline]
132 fn ffi_default() -> Self {
133 C_TxnHandle {
134 txn_id: 0,
135 snapshot_ts: 0,
136 }
137 }
138}
139
140impl FfiDefault for C_CommitResult {
141 #[inline]
142 fn ffi_default() -> Self {
143 C_CommitResult {
144 commit_ts: 0,
145 error_code: -1,
146 }
147 }
148}
149
150impl FfiDefault for CStorageStats {
151 #[inline]
152 fn ffi_default() -> Self {
153 CStorageStats {
154 memtable_size_bytes: 0,
155 wal_size_bytes: 0,
156 active_transactions: 0,
157 min_active_snapshot: 0,
158 last_checkpoint_lsn: 0,
159 }
160 }
161}
162
163macro_rules! ffi_guard {
171 ($body:block) => {{
172 match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(move || $body)) {
173 ::std::result::Result::Ok(__ffi_ok) => __ffi_ok,
174 ::std::result::Result::Err(_) => FfiDefault::ffi_default(),
175 }
176 }};
177}
178
179pub struct DatabasePtr(Arc<Database>);
181
182struct CollectionIndex {
187 index: Arc<HnswIndex>,
188 dimension: usize,
189 metric: DistanceMetric,
190}
191
192static COLLECTION_INDEXES: OnceLock<Mutex<HashMap<String, Arc<CollectionIndex>>>> = OnceLock::new();
193
194fn collection_key(namespace: &str, collection: &str) -> String {
195 format!("{}/{}", namespace, collection)
196}
197
198fn vector_bin_key(namespace: &str, collection: &str, id_hash: u128) -> String {
199 format!(
200 "{}/collections/{}/vectors_bin/{:032x}",
201 namespace, collection, id_hash
202 )
203}
204
205fn metadata_key(namespace: &str, collection: &str, id_hash: u128) -> String {
206 format!(
207 "{}/collections/{}/meta/{:032x}",
208 namespace, collection, id_hash
209 )
210}
211
212fn hash_id_to_u128(id: &str) -> u128 {
213 let hash = blake3::hash(id.as_bytes());
214 let bytes = hash.as_bytes();
215 u128::from_le_bytes([
216 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
217 bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
218 ])
219}
220
221fn ensure_collection_index(
222 _db: &Database,
223 namespace: &str,
224 collection: &str,
225 dimension: usize,
226 metric: DistanceMetric,
227) -> Arc<CollectionIndex> {
228 let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
229 let key = collection_key(namespace, collection);
230
231 let mut registry_guard = registry.lock();
232 if let Some(existing) = registry_guard.get(&key) {
233 return existing.clone();
234 }
235
236 let mut config = HnswConfig::default();
237 config.metric = metric;
238 let index = Arc::new(HnswIndex::new(dimension, config));
239
240 let entry = Arc::new(CollectionIndex {
241 index,
242 dimension,
243 metric,
244 });
245 registry_guard.insert(key, entry.clone());
246
247 entry
248}
249
250fn resolve_collection_config(
251 db: &Database,
252 namespace: &str,
253 collection: &str,
254) -> Option<(usize, DistanceMetric)> {
255 let key = format!("{}/_collections/{}", namespace, collection);
256 let txn = db.begin_transaction().ok()?;
257 let value = db.get(txn, key.as_bytes()).ok().flatten();
258 let _ = db.commit(txn);
259 let value = value?;
260
261 let parsed: serde_json::Value = serde_json::from_slice(&value).ok()?;
262 let dimension = parsed.get("dimension")?.as_u64()? as usize;
263 let metric = match parsed.get("metric").and_then(|v| v.as_u64()).unwrap_or(0) {
264 1 => DistanceMetric::Euclidean,
265 2 => DistanceMetric::DotProduct,
266 _ => DistanceMetric::Cosine,
267 };
268 Some((dimension, metric))
269}
270
271fn serialize_vector_binary(vector: &[f32]) -> Vec<u8> {
272 let mut out = Vec::with_capacity(4 + vector.len() * 4);
273 let len = vector.len() as u32;
274 out.extend_from_slice(&len.to_le_bytes());
275 for value in vector {
276 out.extend_from_slice(&value.to_le_bytes());
277 }
278 out
279}
280
281fn decode_score(metric: DistanceMetric, distance: f32) -> f32 {
282 match metric {
283 DistanceMetric::Cosine => 1.0 - distance,
284 DistanceMetric::DotProduct => -distance,
285 DistanceMetric::Euclidean => -distance,
286 }
287}
288
289#[repr(C)]
291pub struct C_TxnHandle {
292 pub txn_id: u64,
293 pub snapshot_ts: u64,
294}
295
296#[repr(C)]
299pub struct C_CommitResult {
300 pub commit_ts: u64,
303 pub error_code: i32,
305}
306
307#[repr(C)]
312pub struct C_DatabaseConfig {
313 pub wal_enabled: bool,
315 pub wal_enabled_set: bool,
317 pub sync_mode: u8,
319 pub sync_mode_set: bool,
321 pub memtable_size_bytes: u64,
323 pub group_commit: bool,
325 pub group_commit_set: bool,
327 pub default_index_policy: u8,
329 pub default_index_policy_set: bool,
331}
332
333#[unsafe(no_mangle)]
338pub unsafe extern "C" fn sochdb_open_with_config(
339 path: *const c_char,
340 config: C_DatabaseConfig,
341) -> *mut DatabasePtr {
342 ffi_guard!({
343 if path.is_null() {
344 return ptr::null_mut();
345 }
346
347 let c_str = unsafe { CStr::from_ptr(path) };
348 let path_str = match c_str.to_str() {
349 Ok(s) => s,
350 Err(_) => return ptr::null_mut(),
351 };
352
353 let mut db_config = crate::database::DatabaseConfig::default();
355
356 if config.wal_enabled_set {
357 db_config.wal_enabled = config.wal_enabled;
358 }
359
360 if config.sync_mode_set {
361 db_config.sync_mode = match config.sync_mode {
362 0 => crate::database::SyncMode::Off,
363 1 => crate::database::SyncMode::Normal,
364 _ => crate::database::SyncMode::Full,
365 };
366 }
367
368 if config.memtable_size_bytes > 0 {
369 db_config.memtable_size_limit = config.memtable_size_bytes as usize;
370 }
371
372 if config.group_commit_set {
373 db_config.group_commit = config.group_commit;
374 }
375
376 if config.default_index_policy_set {
377 db_config.default_index_policy = match config.default_index_policy {
378 0 => crate::index_policy::IndexPolicy::WriteOptimized,
379 1 => crate::index_policy::IndexPolicy::Balanced,
380 2 => crate::index_policy::IndexPolicy::ScanOptimized,
381 _ => crate::index_policy::IndexPolicy::AppendOnly,
382 };
383 }
384
385 match Database::open_with_config(path_str, db_config) {
386 Ok(db) => {
387 let ptr = Box::new(DatabasePtr(db));
388 Box::into_raw(ptr)
389 }
390 Err(_) => ptr::null_mut(),
391 }
392 })
393}
394
395#[unsafe(no_mangle)]
400pub unsafe extern "C" fn sochdb_open(path: *const c_char) -> *mut DatabasePtr {
401 ffi_guard!({
402 if path.is_null() {
403 return ptr::null_mut();
404 }
405
406 let c_str = unsafe { CStr::from_ptr(path) };
407 let path_str = match c_str.to_str() {
408 Ok(s) => s,
409 Err(_) => return ptr::null_mut(),
410 };
411
412 let config = crate::database::DatabaseConfig::default();
414
415 match Database::open_with_config(path_str, config) {
417 Ok(db) => {
418 let ptr = Box::new(DatabasePtr(db));
419 Box::into_raw(ptr)
420 }
421 Err(_) => ptr::null_mut(),
422 }
423 })
424}
425
426#[unsafe(no_mangle)]
437pub unsafe extern "C" fn sochdb_open_concurrent(path: *const c_char) -> *mut DatabasePtr {
438 ffi_guard!({
439 if path.is_null() {
440 return ptr::null_mut();
441 }
442
443 let c_str = unsafe { CStr::from_ptr(path) };
444 let path_str = match c_str.to_str() {
445 Ok(s) => s,
446 Err(_) => return ptr::null_mut(),
447 };
448
449 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
450 Database::open_concurrent(path_str)
451 })) {
452 Ok(Ok(db)) => {
453 let ptr = Box::new(DatabasePtr(db));
454 Box::into_raw(ptr)
455 }
456 Ok(Err(_)) | Err(_) => ptr::null_mut(),
457 }
458 })
459}
460
461#[unsafe(no_mangle)]
466pub unsafe extern "C" fn sochdb_is_concurrent(ptr: *mut DatabasePtr) -> c_int {
467 ffi_guard!({
468 if ptr.is_null() {
469 return 0;
470 }
471 let db = unsafe { &*ptr };
472 if db.0.is_concurrent() { 1 } else { 0 }
473 })
474}
475
476#[unsafe(no_mangle)]
480pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
481 ffi_guard!({
482 if !ptr.is_null() {
483 unsafe {
484 let _ = Box::from_raw(ptr);
485 }
486 }
487 })
488}
489
490#[unsafe(no_mangle)]
495pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
496 ffi_guard!({
497 if ptr.is_null() {
498 return C_TxnHandle {
499 txn_id: 0,
500 snapshot_ts: 0,
501 };
502 }
503 let db = unsafe { &(*ptr).0 };
504 match db.begin_transaction() {
505 Ok(txn) => C_TxnHandle {
506 txn_id: txn.txn_id,
507 snapshot_ts: txn.snapshot_ts,
508 },
509 Err(_) => C_TxnHandle {
510 txn_id: 0,
511 snapshot_ts: 0,
512 },
513 }
514 })
515}
516
517#[unsafe(no_mangle)]
524pub unsafe extern "C" fn sochdb_commit(
525 ptr: *mut DatabasePtr,
526 handle: C_TxnHandle,
527) -> C_CommitResult {
528 ffi_guard!({
529 if ptr.is_null() {
530 return C_CommitResult {
531 commit_ts: 0,
532 error_code: -1,
533 };
534 }
535 let db = unsafe { &(*ptr).0 };
536 let txn = TxnHandle {
537 txn_id: handle.txn_id,
538 snapshot_ts: handle.snapshot_ts,
539 };
540 match db.commit(txn) {
541 Ok(commit_ts) => C_CommitResult {
542 commit_ts,
543 error_code: 0,
544 },
545 Err(_) => C_CommitResult {
546 commit_ts: 0,
547 error_code: -1,
548 },
549 }
550 })
551}
552
553#[unsafe(no_mangle)]
558pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
559 ffi_guard!({
560 if ptr.is_null() {
561 return -1;
562 }
563 let db = unsafe { &(*ptr).0 };
564 let txn = TxnHandle {
565 txn_id: handle.txn_id,
566 snapshot_ts: handle.snapshot_ts,
567 };
568 match db.abort(txn) {
569 Ok(_) => 0,
570 Err(_) => -1,
571 }
572 })
573}
574
575#[unsafe(no_mangle)]
581pub unsafe extern "C" fn sochdb_put(
582 ptr: *mut DatabasePtr,
583 handle: C_TxnHandle,
584 key_ptr: *const u8,
585 key_len: usize,
586 val_ptr: *const u8,
587 val_len: usize,
588) -> c_int {
589 ffi_guard!({
590 if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
591 return -1;
592 }
593 let db = unsafe { &(*ptr).0 };
594 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
595 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
596 let txn = TxnHandle {
597 txn_id: handle.txn_id,
598 snapshot_ts: handle.snapshot_ts,
599 };
600
601 match db.put(txn, key, val) {
602 Ok(_) => 0,
603 Err(_) => -1,
604 }
605 })
606}
607
608#[unsafe(no_mangle)]
615pub unsafe extern "C" fn sochdb_get(
616 ptr: *mut DatabasePtr,
617 handle: C_TxnHandle,
618 key_ptr: *const u8,
619 key_len: usize,
620 val_out: *mut *mut u8,
621 len_out: *mut usize,
622) -> c_int {
623 ffi_guard!({
624 if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.is_null() {
625 return -1;
626 }
627 let db = unsafe { &(*ptr).0 };
628 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
629 let txn = TxnHandle {
630 txn_id: handle.txn_id,
631 snapshot_ts: handle.snapshot_ts,
632 };
633
634 match db.get(txn, key) {
635 Ok(Some(val)) => {
636 let mut buf = val.into_boxed_slice();
638 unsafe {
639 *val_out = buf.as_mut_ptr();
640 *len_out = buf.len();
641 }
642 let _ = Box::into_raw(buf); 0
644 }
645 Ok(None) => 1, Err(_) => -1,
647 }
648 })
649}
650
651#[unsafe(no_mangle)]
655pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
656 ffi_guard!({
657 if !ptr.is_null() {
658 unsafe {
659 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
660 }
661 }
662 })
663}
664
665#[unsafe(no_mangle)]
670pub unsafe extern "C" fn sochdb_delete(
671 ptr: *mut DatabasePtr,
672 handle: C_TxnHandle,
673 key_ptr: *const u8,
674 key_len: usize,
675) -> c_int {
676 ffi_guard!({
677 if ptr.is_null() || key_ptr.is_null() {
678 return -1;
679 }
680 let db = unsafe { &(*ptr).0 };
681 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
682 let txn = TxnHandle {
683 txn_id: handle.txn_id,
684 snapshot_ts: handle.snapshot_ts,
685 };
686
687 match db.delete(txn, key) {
688 Ok(_) => 0,
689 Err(_) => -1,
690 }
691 })
692}
693
694#[unsafe(no_mangle)]
698pub unsafe extern "C" fn sochdb_put_path(
699 ptr: *mut DatabasePtr,
700 handle: C_TxnHandle,
701 path_ptr: *const c_char,
702 val_ptr: *const u8,
703 val_len: usize,
704) -> c_int {
705 ffi_guard!({
706 if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
707 return -1;
708 }
709 let db = unsafe { &(*ptr).0 };
710 let c_str = unsafe { CStr::from_ptr(path_ptr) };
711 let path_str = match c_str.to_str() {
712 Ok(s) => s,
713 Err(_) => return -1,
714 };
715 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
716 let txn = TxnHandle {
717 txn_id: handle.txn_id,
718 snapshot_ts: handle.snapshot_ts,
719 };
720
721 match db.put_path(txn, path_str, val) {
722 Ok(_) => 0,
723 Err(_) => -1,
724 }
725 })
726}
727
728#[unsafe(no_mangle)]
732pub unsafe extern "C" fn sochdb_get_path(
733 ptr: *mut DatabasePtr,
734 handle: C_TxnHandle,
735 path_ptr: *const c_char,
736 val_out: *mut *mut u8,
737 len_out: *mut usize,
738) -> c_int {
739 ffi_guard!({
740 if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
741 return -1;
742 }
743 let db = unsafe { &(*ptr).0 };
744 let c_str = unsafe { CStr::from_ptr(path_ptr) };
745 let path_str = match c_str.to_str() {
746 Ok(s) => s,
747 Err(_) => return -1,
748 };
749 let txn = TxnHandle {
750 txn_id: handle.txn_id,
751 snapshot_ts: handle.snapshot_ts,
752 };
753
754 match db.get_path(txn, path_str) {
755 Ok(Some(val)) => {
756 let mut buf = val.into_boxed_slice();
757 unsafe {
758 *val_out = buf.as_mut_ptr();
759 *len_out = buf.len();
760 }
761 let _ = Box::into_raw(buf);
762 0
763 }
764 Ok(None) => 1,
765 Err(_) => -1,
766 }
767 })
768}
769
770#[allow(clippy::type_complexity)]
772pub struct ScanIteratorPtr(
773 Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
774);
775
776#[unsafe(no_mangle)]
780pub unsafe extern "C" fn sochdb_scan(
781 ptr: *mut DatabasePtr,
782 handle: C_TxnHandle,
783 start_ptr: *const u8,
784 start_len: usize,
785 end_ptr: *const u8,
786 end_len: usize,
787) -> *mut ScanIteratorPtr {
788 ffi_guard!({
789 if ptr.is_null() {
790 return ptr::null_mut();
791 }
792 let db = unsafe { &(*ptr).0 };
793 let txn = TxnHandle {
794 txn_id: handle.txn_id,
795 snapshot_ts: handle.snapshot_ts,
796 };
797
798 let start = if !start_ptr.is_null() && start_len > 0 {
799 unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
800 } else {
801 vec![]
802 };
803
804 let end = if !end_ptr.is_null() && end_len > 0 {
805 unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
806 } else {
807 vec![] };
809
810 match db.scan_range(txn, &start, &end) {
823 Ok(rows) => {
824 let iter = Box::new(rows.into_iter().map(Ok));
827
828 let ptr = Box::new(ScanIteratorPtr(iter));
829 Box::into_raw(ptr)
830 }
831 Err(_) => ptr::null_mut(),
832 }
833 })
834}
835
836#[unsafe(no_mangle)]
841pub unsafe extern "C" fn sochdb_scan_prefix(
842 ptr: *mut DatabasePtr,
843 handle: C_TxnHandle,
844 prefix_ptr: *const u8,
845 prefix_len: usize,
846) -> *mut ScanIteratorPtr {
847 ffi_guard!({
848 if ptr.is_null() {
849 return ptr::null_mut();
850 }
851 let db = unsafe { &(*ptr).0 };
852 let txn = TxnHandle {
853 txn_id: handle.txn_id,
854 snapshot_ts: handle.snapshot_ts,
855 };
856
857 let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
858 unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
859 } else {
860 vec![]
861 };
862
863 match db.scan(txn, &prefix) {
865 Ok(rows) => {
866 let prefix_owned = prefix.clone();
869 let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
870 .into_iter()
871 .filter(|(k, _)| k.starts_with(&prefix_owned))
872 .collect();
873
874 let iter = Box::new(filtered.into_iter().map(Ok));
875 let ptr = Box::new(ScanIteratorPtr(iter));
876 Box::into_raw(ptr)
877 }
878 Err(_) => ptr::null_mut(),
879 }
880 })
881}
882
883#[unsafe(no_mangle)]
888pub unsafe extern "C" fn sochdb_scan_next(
889 iter_ptr: *mut ScanIteratorPtr,
890 key_out: *mut *mut u8,
891 key_len_out: *mut usize,
892 val_out: *mut *mut u8,
893 val_len_out: *mut usize,
894) -> c_int {
895 ffi_guard!({
896 if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
897 return -1;
898 }
899 let iter = unsafe { &mut (*iter_ptr).0 };
900
901 match iter.next() {
902 Some(Ok((key, val))) => {
903 let mut key_buf = key.into_boxed_slice();
904 let mut val_buf = val.into_boxed_slice();
905 unsafe {
906 *key_out = key_buf.as_mut_ptr();
907 *key_len_out = key_buf.len();
908 *val_out = val_buf.as_mut_ptr();
909 *val_len_out = val_buf.len();
910 }
911 let _ = Box::into_raw(key_buf);
912 let _ = Box::into_raw(val_buf);
913 0
914 }
915 Some(Err(_)) => -1,
916 None => 1, }
918 })
919}
920
921#[unsafe(no_mangle)]
925pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
926 ffi_guard!({
927 if !ptr.is_null() {
928 unsafe {
929 let _ = Box::from_raw(ptr);
930 }
931 }
932 })
933}
934
935#[unsafe(no_mangle)]
939pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
940 ffi_guard!({
941 if ptr.is_null() {
942 return -1;
943 }
944 let db = unsafe { &(*ptr).0 };
945 match db.flush() {
946 Ok(_) => 0,
947 Err(_) => -1,
948 }
949 })
950}
951
952#[repr(C)]
954pub struct CStorageStats {
955 pub memtable_size_bytes: u64,
956 pub wal_size_bytes: u64,
957 pub active_transactions: usize,
958 pub min_active_snapshot: u64,
959 pub last_checkpoint_lsn: u64,
960}
961
962#[unsafe(no_mangle)]
966pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
967 ffi_guard!({
968 if ptr.is_null() {
969 return CStorageStats {
970 memtable_size_bytes: 0,
971 wal_size_bytes: 0,
972 active_transactions: 0,
973 min_active_snapshot: 0,
974 last_checkpoint_lsn: 0,
975 };
976 }
977 let db = unsafe { &(*ptr).0 };
978 let stats = db.storage_stats();
979
980 CStorageStats {
981 memtable_size_bytes: stats.memtable_size_bytes,
982 wal_size_bytes: stats.wal_size_bytes,
983 active_transactions: stats.active_transactions,
984 min_active_snapshot: stats.min_active_snapshot,
985 last_checkpoint_lsn: stats.last_checkpoint_lsn,
986 }
987 })
988}
989
990#[repr(C)]
1008pub struct CBatchPut {
1009 pub data: *const u8,
1011 pub len: usize,
1013}
1014
1015#[unsafe(no_mangle)]
1056pub unsafe extern "C" fn sochdb_put_many(
1057 ptr: *mut DatabasePtr,
1058 handle: C_TxnHandle,
1059 batch: CBatchPut,
1060) -> c_int {
1061 ffi_guard!({
1062 if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
1063 return -1;
1064 }
1065
1066 let db = unsafe { &(*ptr).0 };
1067 let txn = TxnHandle {
1068 txn_id: handle.txn_id,
1069 snapshot_ts: handle.snapshot_ts,
1070 };
1071
1072 let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
1074
1075 if data.len() < 4 {
1077 return -1;
1078 }
1079 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1080
1081 let mut offset = 4;
1082 let mut success_count = 0;
1083
1084 for _ in 0..num_entries {
1085 if offset + 8 > data.len() {
1087 return success_count;
1088 }
1089 let key_len = u32::from_le_bytes([
1090 data[offset],
1091 data[offset + 1],
1092 data[offset + 2],
1093 data[offset + 3],
1094 ]) as usize;
1095 let val_len = u32::from_le_bytes([
1096 data[offset + 4],
1097 data[offset + 5],
1098 data[offset + 6],
1099 data[offset + 7],
1100 ]) as usize;
1101 offset += 8;
1102
1103 if offset + key_len + val_len > data.len() {
1105 return success_count;
1106 }
1107 let key = &data[offset..offset + key_len];
1108 offset += key_len;
1109 let value = &data[offset..offset + val_len];
1110 offset += val_len;
1111
1112 match db.put(txn, key, value) {
1114 Ok(_) => success_count += 1,
1115 Err(_) => return success_count,
1116 }
1117 }
1118
1119 success_count
1120 })
1121}
1122
1123#[unsafe(no_mangle)]
1143pub unsafe extern "C" fn sochdb_delete_many(
1144 ptr: *mut DatabasePtr,
1145 handle: C_TxnHandle,
1146 keys_data: *const u8,
1147 keys_len: usize,
1148) -> c_int {
1149 ffi_guard!({
1150 if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
1151 return -1;
1152 }
1153
1154 let db = unsafe { &(*ptr).0 };
1155 let txn = TxnHandle {
1156 txn_id: handle.txn_id,
1157 snapshot_ts: handle.snapshot_ts,
1158 };
1159
1160 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
1161
1162 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1163
1164 let mut offset = 4;
1165 let mut success_count = 0;
1166
1167 for _ in 0..num_entries {
1168 if offset + 4 > data.len() {
1169 return success_count;
1170 }
1171 let key_len = u32::from_le_bytes([
1172 data[offset],
1173 data[offset + 1],
1174 data[offset + 2],
1175 data[offset + 3],
1176 ]) as usize;
1177 offset += 4;
1178
1179 if offset + key_len > data.len() {
1180 return success_count;
1181 }
1182 let key = &data[offset..offset + key_len];
1183 offset += key_len;
1184
1185 match db.delete(txn, key) {
1186 Ok(_) => success_count += 1,
1187 Err(_) => return success_count,
1188 }
1189 }
1190
1191 success_count
1192 })
1193}
1194
1195#[unsafe(no_mangle)]
1223pub unsafe extern "C" fn sochdb_get_many(
1224 ptr: *mut DatabasePtr,
1225 handle: C_TxnHandle,
1226 keys_data: *const u8,
1227 keys_len: usize,
1228 result_out: *mut *mut u8,
1229 result_len_out: *mut usize,
1230) -> c_int {
1231 ffi_guard!({
1232 if ptr.is_null()
1233 || keys_data.is_null()
1234 || keys_len < 4
1235 || result_out.is_null()
1236 || result_len_out.is_null()
1237 {
1238 return -1;
1239 }
1240
1241 let db = unsafe { &(*ptr).0 };
1242 let txn = TxnHandle {
1243 txn_id: handle.txn_id,
1244 snapshot_ts: handle.snapshot_ts,
1245 };
1246
1247 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
1248
1249 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1250
1251 let mut result = Vec::with_capacity(4 + num_entries * 10); result.extend_from_slice(&(num_entries as u32).to_le_bytes());
1254
1255 let mut offset = 4;
1256
1257 for _ in 0..num_entries {
1258 if offset + 4 > data.len() {
1259 result.push(2); continue;
1261 }
1262 let key_len = u32::from_le_bytes([
1263 data[offset],
1264 data[offset + 1],
1265 data[offset + 2],
1266 data[offset + 3],
1267 ]) as usize;
1268 offset += 4;
1269
1270 if offset + key_len > data.len() {
1271 result.push(2); continue;
1273 }
1274 let key = &data[offset..offset + key_len];
1275 offset += key_len;
1276
1277 match db.get(txn, key) {
1278 Ok(Some(value)) => {
1279 result.push(0); result.extend_from_slice(&(value.len() as u32).to_le_bytes());
1281 result.extend_from_slice(&value);
1282 }
1283 Ok(None) => {
1284 result.push(1); }
1286 Err(_) => {
1287 result.push(2); }
1289 }
1290 }
1291
1292 let mut boxed = result.into_boxed_slice();
1294 unsafe {
1295 *result_out = boxed.as_mut_ptr();
1296 *result_len_out = boxed.len();
1297 }
1298 let _ = Box::into_raw(boxed); 0
1301 })
1302}
1303
1304#[unsafe(no_mangle)]
1361pub unsafe extern "C" fn sochdb_scan_batch(
1362 iter_ptr: *mut ScanIteratorPtr,
1363 batch_size: usize,
1364 result_out: *mut *mut u8,
1365 result_len_out: *mut usize,
1366) -> c_int {
1367 ffi_guard!({
1368 if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0
1369 {
1370 return -1;
1371 }
1372
1373 let iter = unsafe { &mut (*iter_ptr).0 };
1374
1375 let estimated_size = 5 + batch_size * 108;
1378 let mut result = Vec::with_capacity(estimated_size);
1379
1380 result.extend_from_slice(&[0u8; 5]); let mut count = 0u32;
1384 let mut is_done = false;
1385
1386 for _ in 0..batch_size {
1387 match iter.next() {
1388 Some(Ok((key, val))) => {
1389 result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1391 result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1392 result.extend_from_slice(&key);
1393 result.extend_from_slice(&val);
1394 count += 1;
1395 }
1396 Some(Err(_)) => {
1397 result[0..4].copy_from_slice(&count.to_le_bytes());
1399 result[4] = 0; let mut boxed = result.into_boxed_slice();
1402 unsafe {
1403 *result_out = boxed.as_mut_ptr();
1404 *result_len_out = boxed.len();
1405 }
1406 let _ = Box::into_raw(boxed);
1407 return -1;
1408 }
1409 None => {
1410 is_done = true;
1411 break;
1412 }
1413 }
1414 }
1415
1416 result[0..4].copy_from_slice(&count.to_le_bytes());
1418 result[4] = if is_done { 1 } else { 0 };
1419
1420 if count == 0 && is_done {
1422 let mut boxed = result.into_boxed_slice();
1424 unsafe {
1425 *result_out = boxed.as_mut_ptr();
1426 *result_len_out = boxed.len();
1427 }
1428 let _ = Box::into_raw(boxed);
1429 return 1; }
1431
1432 let mut boxed = result.into_boxed_slice();
1434 unsafe {
1435 *result_out = boxed.as_mut_ptr();
1436 *result_len_out = boxed.len();
1437 }
1438 let _ = Box::into_raw(boxed);
1439
1440 0 })
1442}
1443
1444#[unsafe(no_mangle)]
1464pub unsafe extern "C" fn sochdb_set_table_index_policy(
1465 ptr: *mut DatabasePtr,
1466 table_name: *const c_char,
1467 policy: u8,
1468) -> c_int {
1469 ffi_guard!({
1470 if ptr.is_null() || table_name.is_null() {
1471 return -1;
1472 }
1473
1474 let c_str = unsafe { CStr::from_ptr(table_name) };
1475 let table = match c_str.to_str() {
1476 Ok(s) => s,
1477 Err(_) => return -1,
1478 };
1479
1480 let index_policy = match policy {
1481 0 => crate::index_policy::IndexPolicy::WriteOptimized,
1482 1 => crate::index_policy::IndexPolicy::Balanced,
1483 2 => crate::index_policy::IndexPolicy::ScanOptimized,
1484 3 => crate::index_policy::IndexPolicy::AppendOnly,
1485 _ => return -2,
1486 };
1487
1488 let db = unsafe { &(*ptr).0 };
1489
1490 let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1492 db.index_registry().configure_table(config);
1493
1494 0
1495 })
1496}
1497
1498#[unsafe(no_mangle)]
1510pub unsafe extern "C" fn sochdb_get_table_index_policy(
1511 ptr: *mut DatabasePtr,
1512 table_name: *const c_char,
1513) -> u8 {
1514 ffi_guard!({
1515 if ptr.is_null() || table_name.is_null() {
1516 return 255;
1517 }
1518
1519 let c_str = unsafe { CStr::from_ptr(table_name) };
1520 let table = match c_str.to_str() {
1521 Ok(s) => s,
1522 Err(_) => return 255,
1523 };
1524
1525 let db = unsafe { &(*ptr).0 };
1526 let config = db.index_registry().get_config(table);
1527
1528 match config.policy {
1529 crate::index_policy::IndexPolicy::WriteOptimized => 0,
1530 crate::index_policy::IndexPolicy::Balanced => 1,
1531 crate::index_policy::IndexPolicy::ScanOptimized => 2,
1532 crate::index_policy::IndexPolicy::AppendOnly => 3,
1533 }
1534 })
1535}
1536
1537#[repr(C)]
1539pub struct C_TemporalEdge {
1540 pub from_id: *const c_char,
1541 pub edge_type: *const c_char,
1542 pub to_id: *const c_char,
1543 pub valid_from: u64,
1544 pub valid_until: u64,
1545 pub properties_json: *const c_char, }
1547
1548#[unsafe(no_mangle)]
1552pub unsafe extern "C" fn sochdb_add_temporal_edge(
1553 ptr: *mut DatabasePtr,
1554 namespace: *const c_char,
1555 edge: C_TemporalEdge,
1556) -> c_int {
1557 ffi_guard!({
1558 if ptr.is_null()
1559 || namespace.is_null()
1560 || edge.from_id.is_null()
1561 || edge.edge_type.is_null()
1562 || edge.to_id.is_null()
1563 {
1564 return -1;
1565 }
1566
1567 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1568 Ok(s) => s,
1569 Err(_) => return -1,
1570 };
1571 let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1572 Ok(s) => s,
1573 Err(_) => return -1,
1574 };
1575 let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1576 Ok(s) => s,
1577 Err(_) => return -1,
1578 };
1579 let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1580 Ok(s) => s,
1581 Err(_) => return -1,
1582 };
1583
1584 let db = unsafe { &(*ptr).0 };
1585
1586 let txn = match db.begin_transaction() {
1588 Ok(t) => t,
1589 Err(_) => return -1,
1590 };
1591
1592 let key = format!(
1594 "_graph/{}/temporal/{}/{}/{}/{:016x}",
1595 ns, from, etype, to, edge.valid_from
1596 );
1597
1598 let props_str = if edge.properties_json.is_null() {
1599 "{}".to_string()
1600 } else {
1601 match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1602 Ok(s) => s.to_string(),
1603 Err(_) => return -1,
1604 }
1605 };
1606
1607 let value = format!(
1608 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1609 from, etype, to, edge.valid_from, edge.valid_until, props_str
1610 );
1611
1612 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1613 let _ = db.abort(txn);
1614 return -1;
1615 }
1616
1617 match db.commit(txn) {
1618 Ok(_) => 0,
1619 Err(_) => -1,
1620 }
1621 })
1622}
1623
1624#[unsafe(no_mangle)]
1631pub unsafe extern "C" fn sochdb_query_temporal_graph(
1632 ptr: *mut DatabasePtr,
1633 namespace: *const c_char,
1634 node_id: *const c_char,
1635 query_mode: u8,
1636 timestamp: u64, start_time: u64, end_time: u64, edge_type: *const c_char, out_len: *mut usize,
1641) -> *mut c_char {
1642 ffi_guard!({
1643 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1644 return ptr::null_mut();
1645 }
1646
1647 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1648 Ok(s) => s,
1649 Err(_) => return ptr::null_mut(),
1650 };
1651 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1652 Ok(s) => s,
1653 Err(_) => return ptr::null_mut(),
1654 };
1655
1656 let edge_filter = if edge_type.is_null() {
1657 None
1658 } else {
1659 match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1660 Ok(s) => Some(s),
1661 Err(_) => return ptr::null_mut(),
1662 }
1663 };
1664
1665 let db = unsafe { &(*ptr).0 };
1666
1667 let txn = match db.begin_transaction() {
1669 Ok(t) => t,
1670 Err(_) => return ptr::null_mut(),
1671 };
1672
1673 let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1675 let pairs = match db.scan(txn, prefix.as_bytes()) {
1676 Ok(p) => p,
1677 Err(_) => {
1678 let _ = db.abort(txn);
1679 return ptr::null_mut();
1680 }
1681 };
1682
1683 if let Err(_) = db.commit(txn) {
1685 return ptr::null_mut();
1686 }
1687
1688 let mut results = Vec::new();
1689 let now = std::time::SystemTime::now()
1690 .duration_since(std::time::UNIX_EPOCH)
1691 .unwrap()
1692 .as_millis() as u64;
1693
1694 for (_key, value) in pairs {
1695 let value_str = match std::str::from_utf8(&value) {
1697 Ok(s) => s,
1698 Err(_) => continue,
1699 };
1700
1701 if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1703 if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1704 let vf_start = valid_from_pos + r#""valid_from":"#.len();
1705 let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1706 let vu_start = valid_until_pos + r#""valid_until":"#.len();
1707 let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1708
1709 let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1710 let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1711
1712 if let Some(filter) = edge_filter {
1714 if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1715 continue;
1716 }
1717 }
1718
1719 let matches = match query_mode {
1721 0 => {
1722 timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until)
1723 }
1724 1 => {
1725 let edge_end = if valid_until == 0 {
1726 u64::MAX
1727 } else {
1728 valid_until
1729 };
1730 valid_from < end_time && edge_end > start_time
1731 }
1732 2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1733 _ => false,
1734 };
1735
1736 if matches {
1737 results.push(value_str.to_string());
1738 }
1739 }
1740 }
1741 }
1742
1743 let json = format!("[{}]", results.join(","));
1745 let c_string = match std::ffi::CString::new(json) {
1746 Ok(s) => s,
1747 Err(_) => return ptr::null_mut(),
1748 };
1749
1750 unsafe { *out_len = c_string.as_bytes().len() };
1751 c_string.into_raw()
1752 })
1753}
1754
1755#[unsafe(no_mangle)]
1759pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1760 ffi_guard!({
1761 if !ptr.is_null() {
1762 unsafe {
1763 let _ = std::ffi::CString::from_raw(ptr);
1764 }
1765 }
1766 })
1767}
1768
1769#[unsafe(no_mangle)]
1784pub unsafe extern "C" fn sochdb_graph_add_node(
1785 ptr: *mut DatabasePtr,
1786 namespace: *const c_char,
1787 node_id: *const c_char,
1788 node_type: *const c_char,
1789 properties_json: *const c_char,
1790) -> c_int {
1791 ffi_guard!({
1792 if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1793 return -1;
1794 }
1795
1796 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1797 Ok(s) => s,
1798 Err(_) => return -1,
1799 };
1800 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1801 Ok(s) => s,
1802 Err(_) => return -1,
1803 };
1804 let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1805 Ok(s) => s,
1806 Err(_) => return -1,
1807 };
1808 let props = if properties_json.is_null() {
1809 "{}".to_string()
1810 } else {
1811 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1812 Ok(s) => s.to_string(),
1813 Err(_) => return -1,
1814 }
1815 };
1816
1817 let db = unsafe { &(*ptr).0 };
1818
1819 let txn = match db.begin_transaction() {
1820 Ok(t) => t,
1821 Err(_) => return -1,
1822 };
1823
1824 let key = format!("_graph/{}/nodes/{}", ns, id);
1825 let value = format!(
1826 r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1827 id, ntype, props
1828 );
1829
1830 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1831 let _ = db.abort(txn);
1832 return -1;
1833 }
1834
1835 match db.commit(txn) {
1836 Ok(_) => 0,
1837 Err(_) => -1,
1838 }
1839 })
1840}
1841
1842#[unsafe(no_mangle)]
1853pub unsafe extern "C" fn sochdb_graph_add_edge(
1854 ptr: *mut DatabasePtr,
1855 namespace: *const c_char,
1856 from_id: *const c_char,
1857 edge_type: *const c_char,
1858 to_id: *const c_char,
1859 properties_json: *const c_char,
1860) -> c_int {
1861 ffi_guard!({
1862 if ptr.is_null()
1863 || namespace.is_null()
1864 || from_id.is_null()
1865 || edge_type.is_null()
1866 || to_id.is_null()
1867 {
1868 return -1;
1869 }
1870
1871 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1872 Ok(s) => s,
1873 Err(_) => return -1,
1874 };
1875 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1876 Ok(s) => s,
1877 Err(_) => return -1,
1878 };
1879 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1880 Ok(s) => s,
1881 Err(_) => return -1,
1882 };
1883 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1884 Ok(s) => s,
1885 Err(_) => return -1,
1886 };
1887 let props = if properties_json.is_null() {
1888 "{}".to_string()
1889 } else {
1890 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1891 Ok(s) => s.to_string(),
1892 Err(_) => return -1,
1893 }
1894 };
1895
1896 let db = unsafe { &(*ptr).0 };
1897
1898 let txn = match db.begin_transaction() {
1899 Ok(t) => t,
1900 Err(_) => return -1,
1901 };
1902
1903 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1904 let value = format!(
1905 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1906 from, etype, to, props
1907 );
1908
1909 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1910 let _ = db.abort(txn);
1911 return -1;
1912 }
1913
1914 match db.commit(txn) {
1915 Ok(_) => 0,
1916 Err(_) => -1,
1917 }
1918 })
1919}
1920
1921#[unsafe(no_mangle)]
1931pub unsafe extern "C" fn sochdb_graph_traverse(
1932 ptr: *mut DatabasePtr,
1933 namespace: *const c_char,
1934 start_node: *const c_char,
1935 max_depth: usize,
1936 order: u8, out_len: *mut usize,
1938) -> *mut c_char {
1939 ffi_guard!({
1940 if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1941 return ptr::null_mut();
1942 }
1943
1944 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1945 Ok(s) => s,
1946 Err(_) => return ptr::null_mut(),
1947 };
1948 let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1949 Ok(s) => s,
1950 Err(_) => return ptr::null_mut(),
1951 };
1952
1953 let db = unsafe { &(*ptr).0 };
1954
1955 let txn = match db.begin_transaction() {
1956 Ok(t) => t,
1957 Err(_) => return ptr::null_mut(),
1958 };
1959
1960 let mut visited_nodes = std::collections::HashSet::new();
1962 let mut nodes_json = Vec::new();
1963 let mut edges_json = Vec::new();
1964
1965 let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1967
1968 while let Some((current_node, depth)) = if order == 0 {
1969 if frontier.is_empty() {
1971 None
1972 } else {
1973 Some(frontier.remove(0))
1974 }
1975 } else {
1976 frontier.pop()
1978 } {
1979 if depth > max_depth || visited_nodes.contains(¤t_node) {
1980 continue;
1981 }
1982 visited_nodes.insert(current_node.clone());
1983
1984 let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1986 if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1987 if let Ok(s) = std::str::from_utf8(&node_data) {
1988 nodes_json.push(s.to_string());
1989 }
1990 }
1991
1992 let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1994 if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1995 for (_key, value) in edges {
1996 if let Ok(edge_str) = std::str::from_utf8(&value) {
1997 edges_json.push(edge_str.to_string());
1998
1999 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
2001 let start_idx = to_pos + r#""to_id":""#.len();
2002 if let Some(end_idx) = edge_str[start_idx..].find('"') {
2003 let to_id = &edge_str[start_idx..start_idx + end_idx];
2004 if !visited_nodes.contains(to_id) {
2005 frontier.push((to_id.to_string(), depth + 1));
2006 }
2007 }
2008 }
2009 }
2010 }
2011 }
2012 }
2013
2014 if let Err(_) = db.commit(txn) {
2015 return ptr::null_mut();
2016 }
2017
2018 let result = format!(
2019 r#"{{"nodes":[{}],"edges":[{}]}}"#,
2020 nodes_json.join(","),
2021 edges_json.join(",")
2022 );
2023
2024 let c_string = match std::ffi::CString::new(result) {
2025 Ok(s) => s,
2026 Err(_) => return ptr::null_mut(),
2027 };
2028
2029 unsafe { *out_len = c_string.as_bytes().len() };
2030 c_string.into_raw()
2031 })
2032}
2033
2034#[unsafe(no_mangle)]
2047pub unsafe extern "C" fn sochdb_cache_put(
2048 ptr: *mut DatabasePtr,
2049 cache_name: *const c_char,
2050 key: *const c_char,
2051 value: *const c_char,
2052 embedding_ptr: *const f32,
2053 embedding_len: usize,
2054 ttl_seconds: u64,
2055) -> c_int {
2056 ffi_guard!({
2057 if ptr.is_null()
2058 || cache_name.is_null()
2059 || key.is_null()
2060 || value.is_null()
2061 || embedding_ptr.is_null()
2062 {
2063 return -1;
2064 }
2065
2066 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
2067 Ok(s) => s,
2068 Err(_) => return -1,
2069 };
2070 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
2071 Ok(s) => s,
2072 Err(_) => return -1,
2073 };
2074 let v = match unsafe { CStr::from_ptr(value) }.to_str() {
2075 Ok(s) => s,
2076 Err(_) => return -1,
2077 };
2078 let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
2079
2080 let db = unsafe { &(*ptr).0 };
2081
2082 let txn = match db.begin_transaction() {
2083 Ok(t) => t,
2084 Err(_) => return -1,
2085 };
2086
2087 let expires_at = if ttl_seconds > 0 {
2089 std::time::SystemTime::now()
2090 .duration_since(std::time::UNIX_EPOCH)
2091 .unwrap()
2092 .as_secs()
2093 + ttl_seconds
2094 } else {
2095 0 };
2097
2098 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
2100 let cache_key = format!("_cache/{}/{}", cache, key_hash);
2101
2102 let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
2104
2105 let cache_value = format!(
2106 r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
2107 k,
2108 v,
2109 embedding_json.join(","),
2110 expires_at
2111 );
2112
2113 if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
2114 let _ = db.abort(txn);
2115 return -1;
2116 }
2117
2118 match db.commit(txn) {
2119 Ok(_) => 0,
2120 Err(_) => -1,
2121 }
2122 })
2123}
2124
2125#[unsafe(no_mangle)]
2133pub unsafe extern "C" fn sochdb_cache_get(
2134 ptr: *mut DatabasePtr,
2135 cache_name: *const c_char,
2136 query_embedding_ptr: *const f32,
2137 embedding_len: usize,
2138 threshold: f32,
2139 out_len: *mut usize,
2140) -> *mut c_char {
2141 ffi_guard!({
2142 if ptr.is_null()
2143 || cache_name.is_null()
2144 || query_embedding_ptr.is_null()
2145 || out_len.is_null()
2146 {
2147 return ptr::null_mut();
2148 }
2149
2150 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
2151 Ok(s) => s,
2152 Err(_) => return ptr::null_mut(),
2153 };
2154 let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
2155
2156 let db = unsafe { &(*ptr).0 };
2157
2158 let txn = match db.begin_transaction() {
2159 Ok(t) => t,
2160 Err(_) => return ptr::null_mut(),
2161 };
2162
2163 let prefix = format!("_cache/{}/", cache);
2164 let entries = match db.scan(txn, prefix.as_bytes()) {
2165 Ok(e) => e,
2166 Err(_) => {
2167 let _ = db.abort(txn);
2168 return ptr::null_mut();
2169 }
2170 };
2171
2172 let _ = db.commit(txn);
2173
2174 let now = std::time::SystemTime::now()
2175 .duration_since(std::time::UNIX_EPOCH)
2176 .unwrap()
2177 .as_secs();
2178
2179 let mut best_match: Option<(f32, String)> = None;
2180
2181 for (_key, value) in entries {
2182 let value_str = match std::str::from_utf8(&value) {
2183 Ok(s) => s,
2184 Err(_) => continue,
2185 };
2186
2187 if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
2189 let exp_start = exp_pos + r#""expires_at":"#.len();
2190 if let Some(exp_end) = value_str[exp_start..].find('}') {
2191 let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
2192 .parse()
2193 .unwrap_or(0);
2194 if expires_at > 0 && now > expires_at {
2195 continue; }
2197 }
2198 }
2199
2200 if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
2202 let emb_start = emb_pos + r#""embedding":["#.len();
2203 if let Some(emb_end) = value_str[emb_start..].find(']') {
2204 let emb_str = &value_str[emb_start..emb_start + emb_end];
2205 let cached_embedding: Vec<f32> = emb_str
2206 .split(',')
2207 .filter_map(|s| s.trim().parse().ok())
2208 .collect();
2209
2210 if cached_embedding.len() == query.len() {
2211 let similarity = cosine_similarity(query, &cached_embedding);
2212 if similarity >= threshold {
2213 if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
2214 if let Some(val_pos) = value_str.find(r#""value":""#) {
2216 let val_start = val_pos + r#""value":""#.len();
2217 if let Some(val_end) = value_str[val_start..].find('"') {
2218 let cached_value =
2219 &value_str[val_start..val_start + val_end];
2220 best_match = Some((similarity, cached_value.to_string()));
2221 }
2222 }
2223 }
2224 }
2225 }
2226 }
2227 }
2228 }
2229
2230 match best_match {
2231 Some((_, value)) => {
2232 let c_string = match std::ffi::CString::new(value) {
2233 Ok(s) => s,
2234 Err(_) => return ptr::null_mut(),
2235 };
2236 unsafe { *out_len = c_string.as_bytes().len() };
2237 c_string.into_raw()
2238 }
2239 None => ptr::null_mut(),
2240 }
2241 })
2242}
2243
2244fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
2247 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
2248 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
2249 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
2250 if norm_a == 0.0 || norm_b == 0.0 {
2251 0.0
2252 } else {
2253 let similarity = dot / (norm_a * norm_b);
2254 (similarity + 1.0) / 2.0
2257 }
2258}
2259
2260#[unsafe(no_mangle)]
2275pub unsafe extern "C" fn sochdb_trace_start(
2276 ptr: *mut DatabasePtr,
2277 name: *const c_char,
2278 trace_id_out: *mut *mut c_char,
2279 span_id_out: *mut *mut c_char,
2280) -> c_int {
2281 ffi_guard!({
2282 if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
2283 return -1;
2284 }
2285
2286 let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2287 Ok(s) => s,
2288 Err(_) => return -1,
2289 };
2290
2291 let db = unsafe { &(*ptr).0 };
2292
2293 let trace_id = format!("trace_{:016x}", rand_u64());
2295 let span_id = format!("span_{:016x}", rand_u64());
2296
2297 let txn = match db.begin_transaction() {
2298 Ok(t) => t,
2299 Err(_) => return -1,
2300 };
2301
2302 let now = std::time::SystemTime::now()
2303 .duration_since(std::time::UNIX_EPOCH)
2304 .unwrap()
2305 .as_micros() as u64;
2306
2307 let trace_key = format!("_traces/{}", trace_id);
2309 let trace_value = format!(
2310 r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
2311 trace_id, trace_name, now, span_id
2312 );
2313
2314 if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
2315 let _ = db.abort(txn);
2316 return -1;
2317 }
2318
2319 let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
2321 let span_value = format!(
2322 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
2323 span_id, trace_name, now
2324 );
2325
2326 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2327 let _ = db.abort(txn);
2328 return -1;
2329 }
2330
2331 if let Err(_) = db.commit(txn) {
2332 return -1;
2333 }
2334
2335 let trace_c = match std::ffi::CString::new(trace_id) {
2337 Ok(s) => s,
2338 Err(_) => return -1,
2339 };
2340 let span_c = match std::ffi::CString::new(span_id) {
2341 Ok(s) => s,
2342 Err(_) => return -1,
2343 };
2344
2345 unsafe {
2346 *trace_id_out = trace_c.into_raw();
2347 *span_id_out = span_c.into_raw();
2348 }
2349
2350 0
2351 })
2352}
2353
2354#[unsafe(no_mangle)]
2361pub unsafe extern "C" fn sochdb_trace_span_start(
2362 ptr: *mut DatabasePtr,
2363 trace_id: *const c_char,
2364 parent_span_id: *const c_char,
2365 name: *const c_char,
2366 span_id_out: *mut *mut c_char,
2367) -> c_int {
2368 ffi_guard!({
2369 if ptr.is_null()
2370 || trace_id.is_null()
2371 || parent_span_id.is_null()
2372 || name.is_null()
2373 || span_id_out.is_null()
2374 {
2375 return -1;
2376 }
2377
2378 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2379 Ok(s) => s,
2380 Err(_) => return -1,
2381 };
2382 let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
2383 Ok(s) => s,
2384 Err(_) => return -1,
2385 };
2386 let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2387 Ok(s) => s,
2388 Err(_) => return -1,
2389 };
2390
2391 let db = unsafe { &(*ptr).0 };
2392 let span_id = format!("span_{:016x}", rand_u64());
2393
2394 let txn = match db.begin_transaction() {
2395 Ok(t) => t,
2396 Err(_) => return -1,
2397 };
2398
2399 let now = std::time::SystemTime::now()
2400 .duration_since(std::time::UNIX_EPOCH)
2401 .unwrap()
2402 .as_micros() as u64;
2403
2404 let span_key = format!("_traces/{}/spans/{}", tid, span_id);
2405 let span_value = format!(
2406 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
2407 span_id, span_name, now, pid
2408 );
2409
2410 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2411 let _ = db.abort(txn);
2412 return -1;
2413 }
2414
2415 if let Err(_) = db.commit(txn) {
2416 return -1;
2417 }
2418
2419 let span_c = match std::ffi::CString::new(span_id) {
2420 Ok(s) => s,
2421 Err(_) => return -1,
2422 };
2423
2424 unsafe { *span_id_out = span_c.into_raw() };
2425 0
2426 })
2427}
2428
2429#[unsafe(no_mangle)]
2439pub unsafe extern "C" fn sochdb_trace_span_end(
2440 ptr: *mut DatabasePtr,
2441 trace_id: *const c_char,
2442 span_id: *const c_char,
2443 status: u8,
2444) -> i64 {
2445 ffi_guard!({
2446 if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2447 return -1;
2448 }
2449
2450 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2451 Ok(s) => s,
2452 Err(_) => return -1,
2453 };
2454 let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2455 Ok(s) => s,
2456 Err(_) => return -1,
2457 };
2458
2459 let db = unsafe { &(*ptr).0 };
2460
2461 let txn = match db.begin_transaction() {
2462 Ok(t) => t,
2463 Err(_) => return -1,
2464 };
2465
2466 let span_key = format!("_traces/{}/spans/{}", tid, sid);
2467
2468 let span_data = match db.get(txn, span_key.as_bytes()) {
2470 Ok(Some(data)) => data,
2471 _ => {
2472 let _ = db.abort(txn);
2473 return -1;
2474 }
2475 };
2476
2477 let span_str = match std::str::from_utf8(&span_data) {
2478 Ok(s) => s,
2479 Err(_) => {
2480 let _ = db.abort(txn);
2481 return -1;
2482 }
2483 };
2484
2485 let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2487 let start = pos + r#""start_us":"#.len();
2488 if let Some(end) = span_str[start..].find(',') {
2489 span_str[start..start + end].parse().unwrap_or(0u64)
2490 } else {
2491 0u64
2492 }
2493 } else {
2494 0u64
2495 };
2496
2497 let now = std::time::SystemTime::now()
2498 .duration_since(std::time::UNIX_EPOCH)
2499 .unwrap()
2500 .as_micros() as u64;
2501
2502 let duration_us = now.saturating_sub(start_us);
2503 let status_str = match status {
2504 1 => "ok",
2505 2 => "error",
2506 _ => "unset",
2507 };
2508
2509 let new_span = span_str.replace(
2511 r#""status":"active""#,
2512 &format!(
2513 r#""status":"{}","end_us":{},"duration_us":{}"#,
2514 status_str, now, duration_us
2515 ),
2516 );
2517
2518 if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2519 let _ = db.abort(txn);
2520 return -1;
2521 }
2522
2523 if let Err(_) = db.commit(txn) {
2524 return -1;
2525 }
2526
2527 duration_us as i64
2528 })
2529}
2530
2531fn rand_u64() -> u64 {
2533 use std::sync::atomic::{AtomicU64, Ordering};
2534 static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2535
2536 let mut s = STATE.load(Ordering::Relaxed);
2537 if s == 0 {
2538 s = std::time::SystemTime::now()
2539 .duration_since(std::time::UNIX_EPOCH)
2540 .unwrap()
2541 .as_nanos() as u64;
2542 }
2543 s ^= s >> 12;
2544 s ^= s << 25;
2545 s ^= s >> 27;
2546 STATE.store(s, Ordering::Relaxed);
2547 s.wrapping_mul(0x2545F4914F6CDD1D)
2548}
2549
2550#[unsafe(no_mangle)]
2560pub unsafe extern "C" fn sochdb_collection_create(
2561 ptr: *mut DatabasePtr,
2562 namespace: *const c_char,
2563 collection: *const c_char,
2564 dimension: usize,
2565 dist_type: u8, ) -> c_int {
2567 ffi_guard!({
2568 if ptr.is_null() || namespace.is_null() || collection.is_null() {
2569 return -1;
2570 }
2571
2572 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2573 Ok(s) => s,
2574 Err(_) => return -1,
2575 };
2576 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2577 Ok(s) => s,
2578 Err(_) => return -1,
2579 };
2580
2581 let db = unsafe { &(*ptr).0 };
2582 let txn = match db.begin_transaction() {
2583 Ok(t) => t,
2584 Err(_) => return -1,
2585 };
2586
2587 let config_key = format!("{}/_collections/{}", ns, col);
2589 let config_value = format!(r#"{{"dimension":{},"metric":{}}}"#, dimension, dist_type);
2590
2591 if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2592 let _ = db.abort(txn);
2593 return -1;
2594 }
2595
2596 let result = match db.commit(txn) {
2597 Ok(_) => 0,
2598 Err(_) => -1,
2599 };
2600
2601 if result == 0 {
2602 let metric = match dist_type {
2603 1 => DistanceMetric::Euclidean,
2604 2 => DistanceMetric::DotProduct,
2605 _ => DistanceMetric::Cosine,
2606 };
2607 let _ = ensure_collection_index(db, ns, col, dimension, metric);
2608 }
2609
2610 result
2611 })
2612}
2613
2614#[unsafe(no_mangle)]
2620pub unsafe extern "C" fn sochdb_collection_insert(
2621 ptr: *mut DatabasePtr,
2622 namespace: *const c_char,
2623 collection: *const c_char,
2624 id: *const c_char,
2625 vector_ptr: *const f32,
2626 vector_len: usize,
2627 metadata_json: *const c_char, ) -> c_int {
2629 ffi_guard!({
2630 if ptr.is_null()
2631 || namespace.is_null()
2632 || collection.is_null()
2633 || id.is_null()
2634 || vector_ptr.is_null()
2635 {
2636 return -1;
2637 }
2638
2639 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2640 Ok(s) => s,
2641 Err(_) => return -1,
2642 };
2643 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2644 Ok(s) => s,
2645 Err(_) => return -1,
2646 };
2647 let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2648 Ok(s) => s,
2649 Err(_) => return -1,
2650 };
2651 let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2652 let db = unsafe { &(*ptr).0 };
2653
2654 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2655 Some(config) => config,
2656 None => (vector_len, DistanceMetric::Cosine),
2657 };
2658 if vector_len != dimension {
2659 return -1;
2660 }
2661
2662 let metadata = if !metadata_json.is_null() {
2663 match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2664 Ok(s) => s.to_string(),
2665 Err(_) => "{}".to_string(),
2666 }
2667 } else {
2668 "{}".to_string()
2669 };
2670
2671 let txn = match db.begin_transaction() {
2672 Ok(t) => t,
2673 Err(_) => return -1,
2674 };
2675
2676 let id_hash = hash_id_to_u128(doc_id);
2677 let vec_key = vector_bin_key(ns, col, id_hash);
2678 let vec_value = serialize_vector_binary(vector);
2679
2680 if let Err(_) = db.put(txn, vec_key.as_bytes(), &vec_value) {
2681 let _ = db.abort(txn);
2682 return -1;
2683 }
2684
2685 let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2686 Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2687 Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2688 };
2689 let meta_key = metadata_key(ns, col, id_hash);
2690 if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2691 if let Err(_) = db.put(txn, meta_key.as_bytes(), &meta_bytes) {
2692 let _ = db.abort(txn);
2693 return -1;
2694 }
2695 }
2696
2697 if let Err(_) = db.commit(txn) {
2698 return -1;
2699 }
2700
2701 let index = ensure_collection_index(db, ns, col, dimension, metric);
2702 let _ = index.index.insert(id_hash, vector.to_vec());
2703
2704 0
2705 })
2706}
2707
2708#[unsafe(no_mangle)]
2717pub unsafe extern "C" fn sochdb_collection_insert_batch(
2718 ptr: *mut DatabasePtr,
2719 namespace: *const c_char,
2720 collection: *const c_char,
2721 ids: *const *const c_char, vectors: *const f32, dimension: usize,
2724 metadata_jsons: *const *const c_char, count: usize,
2726) -> c_int {
2727 ffi_guard!({
2728 if ptr.is_null()
2729 || namespace.is_null()
2730 || collection.is_null()
2731 || ids.is_null()
2732 || vectors.is_null()
2733 || count == 0
2734 {
2735 return -1;
2736 }
2737
2738 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2739 Ok(s) => s,
2740 Err(_) => return -1,
2741 };
2742 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2743 Ok(s) => s,
2744 Err(_) => return -1,
2745 };
2746 let db = unsafe { &(*ptr).0 };
2747
2748 let (expected_dim, metric) = match resolve_collection_config(db, ns, col) {
2749 Some(config) => config,
2750 None => (dimension, DistanceMetric::Cosine),
2751 };
2752 if dimension != expected_dim {
2753 return -1;
2754 }
2755
2756 let txn = match db.begin_transaction() {
2758 Ok(t) => t,
2759 Err(_) => return -1,
2760 };
2761
2762 let ids_slice = unsafe { slice::from_raw_parts(ids, count) };
2763 let vectors_flat = unsafe { slice::from_raw_parts(vectors, count * dimension) };
2764
2765 let mut inserted = 0i32;
2766 let mut id_hashes = Vec::with_capacity(count);
2767 let mut vector_copies = Vec::with_capacity(count);
2768
2769 for i in 0..count {
2770 let doc_id = match unsafe { CStr::from_ptr(ids_slice[i]) }.to_str() {
2771 Ok(s) => s,
2772 Err(_) => continue,
2773 };
2774 let vec_start = i * dimension;
2775 let vector = &vectors_flat[vec_start..vec_start + dimension];
2776
2777 let id_hash = hash_id_to_u128(doc_id);
2778 let vec_key = vector_bin_key(ns, col, id_hash);
2779 let vec_value = serialize_vector_binary(vector);
2780
2781 if db.put(txn, vec_key.as_bytes(), &vec_value).is_err() {
2782 continue;
2783 }
2784
2785 let metadata = if !metadata_jsons.is_null() {
2787 let meta_ptr = unsafe { *metadata_jsons.add(i) };
2788 if !meta_ptr.is_null() {
2789 match unsafe { CStr::from_ptr(meta_ptr) }.to_str() {
2790 Ok(s) => s.to_string(),
2791 Err(_) => "{}".to_string(),
2792 }
2793 } else {
2794 "{}".to_string()
2795 }
2796 } else {
2797 "{}".to_string()
2798 };
2799
2800 let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2801 Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2802 Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2803 };
2804 let meta_key = metadata_key(ns, col, id_hash);
2805 if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2806 let _ = db.put(txn, meta_key.as_bytes(), &meta_bytes);
2807 }
2808
2809 id_hashes.push(id_hash);
2810 vector_copies.push(vector.to_vec());
2811 inserted += 1;
2812 }
2813
2814 if db.commit(txn).is_err() {
2816 return -1;
2817 }
2818
2819 let index = ensure_collection_index(db, ns, col, dimension, metric);
2821 for (id_hash, vector) in id_hashes.into_iter().zip(vector_copies.into_iter()) {
2822 let _ = index.index.insert(id_hash, vector);
2823 }
2824
2825 inserted
2826 })
2827}
2828
2829#[repr(C)]
2831pub struct CSearchResult {
2832 pub id_ptr: *mut c_char,
2833 pub score: f32,
2834 pub metadata_ptr: *mut c_char,
2835}
2836
2837#[unsafe(no_mangle)]
2843pub unsafe extern "C" fn sochdb_collection_search(
2844 ptr: *mut DatabasePtr,
2845 namespace: *const c_char,
2846 collection: *const c_char,
2847 query_ptr: *const f32,
2848 query_len: usize,
2849 k: usize,
2850 results_out: *mut CSearchResult,
2851) -> c_int {
2852 ffi_guard!({
2853 if ptr.is_null()
2854 || namespace.is_null()
2855 || collection.is_null()
2856 || query_ptr.is_null()
2857 || results_out.is_null()
2858 {
2859 return -1;
2860 }
2861 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2862 Ok(s) => s,
2863 Err(_) => return -1,
2864 };
2865 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2866 Ok(s) => s,
2867 Err(_) => return -1,
2868 };
2869 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2870 let db = unsafe { &(*ptr).0 };
2871 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2872 Some(config) => config,
2873 None => return 0,
2874 };
2875
2876 if query_len != dimension {
2877 return -1;
2878 }
2879
2880 let index = ensure_collection_index(db, ns, col, dimension, metric);
2881 let mut scored = match index.index.search(query, k) {
2882 Ok(results) => results,
2883 Err(_) => return -1,
2884 };
2885
2886 let result_count = scored.len().min(k);
2887 for (i, (id_hash, distance)) in scored.drain(..result_count).enumerate() {
2888 let meta_key = metadata_key(ns, col, id_hash);
2889 let txn = match db.begin_transaction() {
2890 Ok(t) => t,
2891 Err(_) => return -1,
2892 };
2893 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2894 let _ = db.commit(txn);
2895
2896 let mut id_value = String::new();
2897 let mut metadata_json = serde_json::json!({});
2898 if let Some(bytes) = meta_value.as_deref() {
2899 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(bytes) {
2900 id_value = parsed
2901 .get("id")
2902 .and_then(|v| v.as_str())
2903 .unwrap_or("")
2904 .to_string();
2905 metadata_json = parsed
2906 .get("metadata")
2907 .cloned()
2908 .unwrap_or(serde_json::json!({}));
2909 }
2910 }
2911 let metadata =
2912 serde_json::to_string(&metadata_json).unwrap_or_else(|_| "{}".to_string());
2913
2914 let c_id = match std::ffi::CString::new(id_value) {
2915 Ok(s) => s.into_raw(),
2916 Err(_) => ptr::null_mut(),
2917 };
2918 let c_meta = match std::ffi::CString::new(metadata) {
2919 Ok(s) => s.into_raw(),
2920 Err(_) => ptr::null_mut(),
2921 };
2922
2923 unsafe {
2924 (*results_out.add(i)).id_ptr = c_id;
2925 (*results_out.add(i)).score = decode_score(metric, distance);
2926 (*results_out.add(i)).metadata_ptr = c_meta;
2927 }
2928 }
2929
2930 result_count as c_int
2931 })
2932}
2933
2934#[unsafe(no_mangle)]
2940pub unsafe extern "C" fn sochdb_collection_search_soa(
2941 ptr: *mut DatabasePtr,
2942 namespace: *const c_char,
2943 collection: *const c_char,
2944 query_ptr: *const f32,
2945 query_len: usize,
2946 k: usize,
2947 min_score: f32,
2948 filter_json: *const c_char,
2949 ids_hi_out: *mut *mut u64,
2950 ids_lo_out: *mut *mut u64,
2951 scores_out: *mut *mut f32,
2952 len_out: *mut usize,
2953) -> c_int {
2954 ffi_guard!({
2955 if ptr.is_null()
2956 || namespace.is_null()
2957 || collection.is_null()
2958 || query_ptr.is_null()
2959 || ids_hi_out.is_null()
2960 || ids_lo_out.is_null()
2961 || scores_out.is_null()
2962 || len_out.is_null()
2963 {
2964 return -1;
2965 }
2966
2967 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2968 Ok(s) => s,
2969 Err(_) => return -1,
2970 };
2971 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2972 Ok(s) => s,
2973 Err(_) => return -1,
2974 };
2975 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2976 let db = unsafe { &(*ptr).0 };
2977
2978 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2979 Some(config) => config,
2980 None => return 0,
2981 };
2982 if query_len != dimension {
2983 return -1;
2984 }
2985
2986 let filter = if !filter_json.is_null() {
2987 match unsafe { CStr::from_ptr(filter_json) }.to_str() {
2988 Ok(s) => serde_json::from_str::<serde_json::Value>(s).ok(),
2989 Err(_) => None,
2990 }
2991 } else {
2992 None
2993 };
2994
2995 let index = ensure_collection_index(db, ns, col, dimension, metric);
2996 let results = match index.index.search(query, k) {
2997 Ok(results) => results,
2998 Err(_) => return -1,
2999 };
3000
3001 let mut ids_hi: Vec<u64> = Vec::with_capacity(results.len());
3002 let mut ids_lo: Vec<u64> = Vec::with_capacity(results.len());
3003 let mut scores: Vec<f32> = Vec::with_capacity(results.len());
3004
3005 for (id_hash, distance) in results {
3006 let score = decode_score(metric, distance);
3007 if min_score > 0.0 && score < min_score {
3008 continue;
3009 }
3010
3011 if let Some(filter_value) = &filter {
3012 let meta_key = metadata_key(ns, col, id_hash);
3013 let txn = match db.begin_transaction() {
3014 Ok(t) => t,
3015 Err(_) => return -1,
3016 };
3017 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
3018 let _ = db.commit(txn);
3019 let meta_value = match meta_value {
3020 Some(value) => value,
3021 None => continue,
3022 };
3023 let parsed = match serde_json::from_slice::<serde_json::Value>(&meta_value) {
3024 Ok(value) => value,
3025 Err(_) => continue,
3026 };
3027 let metadata = parsed.get("metadata").cloned().unwrap_or(Value::Null);
3028
3029 if !metadata_matches_filter(&metadata, filter_value) {
3030 continue;
3031 }
3032 }
3033
3034 ids_hi.push((id_hash >> 64) as u64);
3035 ids_lo.push((id_hash & u128::from(u64::MAX)) as u64);
3036 scores.push(score);
3037 if ids_hi.len() >= k {
3038 break;
3039 }
3040 }
3041
3042 let len = ids_hi.len();
3043 let mut ids_hi_box = ids_hi.into_boxed_slice();
3044 let mut ids_lo_box = ids_lo.into_boxed_slice();
3045 let mut scores_box = scores.into_boxed_slice();
3046
3047 unsafe {
3048 *len_out = len;
3049 *ids_hi_out = ids_hi_box.as_mut_ptr();
3050 *ids_lo_out = ids_lo_box.as_mut_ptr();
3051 *scores_out = scores_box.as_mut_ptr();
3052 }
3053
3054 std::mem::forget(ids_hi_box);
3055 std::mem::forget(ids_lo_box);
3056 std::mem::forget(scores_box);
3057
3058 len as c_int
3059 })
3060}
3061
3062#[unsafe(no_mangle)]
3064pub unsafe extern "C" fn sochdb_collection_fetch_metadata_json(
3065 ptr: *mut DatabasePtr,
3066 namespace: *const c_char,
3067 collection: *const c_char,
3068 ids_hi_ptr: *const u64,
3069 ids_lo_ptr: *const u64,
3070 ids_len: usize,
3071) -> *mut c_char {
3072 ffi_guard!({
3073 if ptr.is_null()
3074 || namespace.is_null()
3075 || collection.is_null()
3076 || ids_hi_ptr.is_null()
3077 || ids_lo_ptr.is_null()
3078 {
3079 return ptr::null_mut();
3080 }
3081
3082 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3083 Ok(s) => s,
3084 Err(_) => return ptr::null_mut(),
3085 };
3086 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
3087 Ok(s) => s,
3088 Err(_) => return ptr::null_mut(),
3089 };
3090 let ids_hi = unsafe { slice::from_raw_parts(ids_hi_ptr, ids_len) };
3091 let ids_lo = unsafe { slice::from_raw_parts(ids_lo_ptr, ids_len) };
3092 let db = unsafe { &(*ptr).0 };
3093
3094 let mut results = Vec::with_capacity(ids_len);
3095 for i in 0..ids_len {
3096 let id_hash = ((ids_hi[i] as u128) << 64) | (ids_lo[i] as u128);
3097 let meta_key = metadata_key(ns, col, id_hash);
3098 let txn = match db.begin_transaction() {
3099 Ok(t) => t,
3100 Err(_) => return ptr::null_mut(),
3101 };
3102 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
3103 let _ = db.commit(txn);
3104 if let Some(bytes) = meta_value {
3105 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&bytes) {
3106 results.push(parsed);
3107 continue;
3108 }
3109 }
3110 results.push(serde_json::json!({"id": "", "metadata": {}}));
3111 }
3112
3113 match serde_json::to_string(&results) {
3114 Ok(json) => match std::ffi::CString::new(json) {
3115 Ok(cstr) => cstr.into_raw(),
3116 Err(_) => ptr::null_mut(),
3117 },
3118 Err(_) => ptr::null_mut(),
3119 }
3120 })
3121}
3122
3123#[unsafe(no_mangle)]
3125pub unsafe extern "C" fn sochdb_collection_free_u64(ptr: *mut u64, len: usize) {
3126 ffi_guard!({
3127 if ptr.is_null() || len == 0 {
3128 return;
3129 }
3130 unsafe {
3131 let _ = Vec::from_raw_parts(ptr, len, len);
3132 }
3133 })
3134}
3135
3136#[unsafe(no_mangle)]
3137pub unsafe extern "C" fn sochdb_collection_free_f32(ptr: *mut f32, len: usize) {
3138 ffi_guard!({
3139 if ptr.is_null() || len == 0 {
3140 return;
3141 }
3142 unsafe {
3143 let _ = Vec::from_raw_parts(ptr, len, len);
3144 }
3145 })
3146}
3147
3148fn metadata_matches_filter(metadata: &Value, filter: &Value) -> bool {
3149 let filter_obj = match filter.as_object() {
3150 Some(obj) => obj,
3151 None => return true,
3152 };
3153 let metadata_obj = match metadata.as_object() {
3154 Some(obj) => obj,
3155 None => return false,
3156 };
3157
3158 for (key, expected) in filter_obj.iter() {
3159 match metadata_obj.get(key) {
3160 Some(actual) if actual == expected => {}
3161 _ => return false,
3162 }
3163 }
3164
3165 true
3166}
3167
3168#[unsafe(no_mangle)]
3174pub unsafe extern "C" fn sochdb_collection_keyword_search(
3175 ptr: *mut DatabasePtr,
3176 namespace: *const c_char,
3177 collection: *const c_char,
3178 query_ptr: *const c_char,
3179 k: usize,
3180 results_out: *mut CSearchResult,
3181) -> c_int {
3182 ffi_guard!({
3183 if ptr.is_null()
3184 || namespace.is_null()
3185 || collection.is_null()
3186 || query_ptr.is_null()
3187 || results_out.is_null()
3188 {
3189 return -1;
3190 }
3191
3192 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3193 Ok(s) => s,
3194 Err(_) => return -1,
3195 };
3196 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
3197 Ok(s) => s,
3198 Err(_) => return -1,
3199 };
3200 let query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
3201 Ok(s) => s.to_lowercase(),
3202 Err(_) => return -1,
3203 };
3204 let terms: Vec<&str> = query_str.split_whitespace().collect();
3205 if terms.is_empty() {
3206 return 0;
3207 }
3208
3209 let db = unsafe { &(*ptr).0 };
3210 let txn = match db.begin_transaction() {
3211 Ok(t) => t,
3212 Err(_) => return -1,
3213 };
3214
3215 let prefix = format!("{}/collections/{}/vectors/", ns, col);
3217 let entries = match db.scan(txn, prefix.as_bytes()) {
3218 Ok(e) => e,
3219 Err(_) => {
3220 let _ = db.abort(txn);
3221 return -1;
3222 }
3223 };
3224 let _ = db.commit(txn);
3225
3226 let mut scored: Vec<(f32, String, String)> = Vec::new();
3228
3229 for (_key, value) in entries {
3230 let doc: Value = match serde_json::from_slice(&value) {
3232 Ok(v) => v,
3233 Err(_) => continue,
3234 };
3235
3236 let metadata_val = doc.get("metadata");
3238 let metadata_str = metadata_val
3239 .map(|v| v.to_string())
3240 .unwrap_or("{}".to_string());
3241
3242 let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
3244
3245 let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
3247
3248 let mut score = 0.0;
3249 for term in &terms {
3250 score += search_text.matches(term).count() as f32;
3251 }
3252
3253 if score > 0.0 {
3254 let id = doc
3255 .get("id")
3256 .and_then(|v| v.as_str())
3257 .unwrap_or("")
3258 .to_string();
3259 if id.is_empty() {
3260 continue;
3261 }
3262
3263 scored.push((score, id, metadata_str));
3264 }
3265 }
3266
3267 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
3269
3270 let result_count = scored.len().min(k);
3272 for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
3273 let c_id = match std::ffi::CString::new(id) {
3274 Ok(s) => s.into_raw(),
3275 Err(_) => ptr::null_mut(),
3276 };
3277 let c_meta = match std::ffi::CString::new(metadata) {
3278 Ok(s) => s.into_raw(),
3279 Err(_) => ptr::null_mut(),
3280 };
3281
3282 unsafe {
3283 (*results_out.add(i)).id_ptr = c_id;
3284 (*results_out.add(i)).score = score;
3285 (*results_out.add(i)).metadata_ptr = c_meta;
3286 }
3287 }
3288
3289 result_count as c_int
3290 })
3291}
3292
3293#[unsafe(no_mangle)]
3295pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
3296 ffi_guard!({
3297 if result.is_null() {
3298 return;
3299 }
3300
3301 for i in 0..count {
3302 let r = unsafe { &mut *result.add(i) };
3303 if !r.id_ptr.is_null() {
3304 let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
3305 }
3306 if !r.metadata_ptr.is_null() {
3307 let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
3308 }
3309 }
3310 })
3311}
3312
3313#[unsafe(no_mangle)]
3327pub unsafe extern "C" fn sochdb_exists(
3328 ptr: *mut DatabasePtr,
3329 handle: C_TxnHandle,
3330 key_ptr: *const u8,
3331 key_len: usize,
3332) -> c_int {
3333 ffi_guard!({
3334 if ptr.is_null() || key_ptr.is_null() {
3335 return -1;
3336 }
3337 let db = unsafe { &(*ptr).0 };
3338 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
3339 let txn = TxnHandle {
3340 txn_id: handle.txn_id,
3341 snapshot_ts: handle.snapshot_ts,
3342 };
3343
3344 match db.get(txn, key) {
3345 Ok(Some(_)) => 1,
3346 Ok(None) => 0,
3347 Err(_) => -1,
3348 }
3349 })
3350}
3351
3352#[unsafe(no_mangle)]
3361pub unsafe extern "C" fn sochdb_delete_path(
3362 ptr: *mut DatabasePtr,
3363 handle: C_TxnHandle,
3364 path_ptr: *const c_char,
3365) -> c_int {
3366 ffi_guard!({
3367 if ptr.is_null() || path_ptr.is_null() {
3368 return -1;
3369 }
3370 let db = unsafe { &(*ptr).0 };
3371 let c_str = unsafe { CStr::from_ptr(path_ptr) };
3372 let path_str = match c_str.to_str() {
3373 Ok(s) => s,
3374 Err(_) => return -1,
3375 };
3376 let txn = TxnHandle {
3377 txn_id: handle.txn_id,
3378 snapshot_ts: handle.snapshot_ts,
3379 };
3380
3381 match db.delete_path(txn, path_str) {
3382 Ok(_) => 0,
3383 Err(_) => -1,
3384 }
3385 })
3386}
3387
3388#[unsafe(no_mangle)]
3394pub unsafe extern "C" fn sochdb_scan_path(
3395 ptr: *mut DatabasePtr,
3396 handle: C_TxnHandle,
3397 prefix_ptr: *const c_char,
3398 out_len: *mut usize,
3399) -> *mut c_char {
3400 ffi_guard!({
3401 if ptr.is_null() || prefix_ptr.is_null() || out_len.is_null() {
3402 return ptr::null_mut();
3403 }
3404 let db = unsafe { &(*ptr).0 };
3405 let c_str = unsafe { CStr::from_ptr(prefix_ptr) };
3406 let prefix_str = match c_str.to_str() {
3407 Ok(s) => s,
3408 Err(_) => return ptr::null_mut(),
3409 };
3410 let txn = TxnHandle {
3411 txn_id: handle.txn_id,
3412 snapshot_ts: handle.snapshot_ts,
3413 };
3414
3415 match db.scan_path(txn, prefix_str) {
3416 Ok(pairs) => {
3417 let entries: Vec<String> = pairs
3418 .iter()
3419 .map(|(path, value)| {
3420 let val_str = String::from_utf8_lossy(value);
3421 format!(r#"{{"path":"{}","value":"{}"}}"#, path, val_str)
3422 })
3423 .collect();
3424 let json = format!("[{}]", entries.join(","));
3425 let c_string = match std::ffi::CString::new(json) {
3426 Ok(s) => s,
3427 Err(_) => return ptr::null_mut(),
3428 };
3429 unsafe { *out_len = c_string.as_bytes().len() };
3430 c_string.into_raw()
3431 }
3432 Err(_) => ptr::null_mut(),
3433 }
3434 })
3435}
3436
3437#[unsafe(no_mangle)]
3446pub unsafe extern "C" fn sochdb_begin_read_only(ptr: *mut DatabasePtr) -> C_TxnHandle {
3447 ffi_guard!({
3448 if ptr.is_null() {
3449 return C_TxnHandle {
3450 txn_id: 0,
3451 snapshot_ts: 0,
3452 };
3453 }
3454 let db = unsafe { &(*ptr).0 };
3455 match db.begin_read_only() {
3456 Ok(txn) => C_TxnHandle {
3457 txn_id: txn.txn_id,
3458 snapshot_ts: txn.snapshot_ts,
3459 },
3460 Err(_) => C_TxnHandle {
3461 txn_id: 0,
3462 snapshot_ts: 0,
3463 },
3464 }
3465 })
3466}
3467
3468#[unsafe(no_mangle)]
3473pub unsafe extern "C" fn sochdb_begin_write_only(ptr: *mut DatabasePtr) -> C_TxnHandle {
3474 ffi_guard!({
3475 if ptr.is_null() {
3476 return C_TxnHandle {
3477 txn_id: 0,
3478 snapshot_ts: 0,
3479 };
3480 }
3481 let db = unsafe { &(*ptr).0 };
3482 match db.begin_write_only() {
3483 Ok(txn) => C_TxnHandle {
3484 txn_id: txn.txn_id,
3485 snapshot_ts: txn.snapshot_ts,
3486 },
3487 Err(_) => C_TxnHandle {
3488 txn_id: 0,
3489 snapshot_ts: 0,
3490 },
3491 }
3492 })
3493}
3494
3495#[unsafe(no_mangle)]
3505pub unsafe extern "C" fn sochdb_shutdown(ptr: *mut DatabasePtr) -> c_int {
3506 ffi_guard!({
3507 if ptr.is_null() {
3508 return -1;
3509 }
3510 let db = unsafe { &(*ptr).0 };
3511 match db.shutdown() {
3512 Ok(_) => 0,
3513 Err(_) => -1,
3514 }
3515 })
3516}
3517
3518#[unsafe(no_mangle)]
3523pub unsafe extern "C" fn sochdb_fsync(ptr: *mut DatabasePtr) -> c_int {
3524 ffi_guard!({
3525 if ptr.is_null() {
3526 return -1;
3527 }
3528 let db = unsafe { &(*ptr).0 };
3529 match db.fsync() {
3530 Ok(_) => 0,
3531 Err(_) => -1,
3532 }
3533 })
3534}
3535
3536#[unsafe(no_mangle)]
3541pub unsafe extern "C" fn sochdb_truncate_wal(ptr: *mut DatabasePtr) -> c_int {
3542 ffi_guard!({
3543 if ptr.is_null() {
3544 return -1;
3545 }
3546 let db = unsafe { &(*ptr).0 };
3547 match db.truncate_wal() {
3548 Ok(_) => 0,
3549 Err(_) => -1,
3550 }
3551 })
3552}
3553
3554#[unsafe(no_mangle)]
3559pub unsafe extern "C" fn sochdb_gc(ptr: *mut DatabasePtr) -> i64 {
3560 ffi_guard!({
3561 if ptr.is_null() {
3562 return -1;
3563 }
3564 let db = unsafe { &(*ptr).0 };
3565 db.gc() as i64
3566 })
3567}
3568
3569#[unsafe(no_mangle)]
3574pub unsafe extern "C" fn sochdb_checkpoint_full(ptr: *mut DatabasePtr) -> u64 {
3575 ffi_guard!({
3576 if ptr.is_null() {
3577 return 0;
3578 }
3579 let db = unsafe { &(*ptr).0 };
3580 match db.checkpoint() {
3581 Ok(lsn) => lsn,
3582 Err(_) => 0,
3583 }
3584 })
3585}
3586
3587#[unsafe(no_mangle)]
3592pub unsafe extern "C" fn sochdb_stats_json(
3593 ptr: *mut DatabasePtr,
3594 out_len: *mut usize,
3595) -> *mut c_char {
3596 ffi_guard!({
3597 if ptr.is_null() || out_len.is_null() {
3598 return ptr::null_mut();
3599 }
3600 let db = unsafe { &(*ptr).0 };
3601 let storage_stats = db.storage_stats();
3602 let db_stats = db.stats();
3603
3604 let json = format!(
3605 r#"{{"memtable_size_bytes":{},"wal_size_bytes":{},"active_transactions":{},"min_active_snapshot":{},"last_checkpoint_lsn":{},"transactions_started":{},"transactions_committed":{},"transactions_aborted":{},"queries_executed":{},"bytes_written":{},"bytes_read":{}}}"#,
3606 storage_stats.memtable_size_bytes,
3607 storage_stats.wal_size_bytes,
3608 storage_stats.active_transactions,
3609 storage_stats.min_active_snapshot,
3610 storage_stats.last_checkpoint_lsn,
3611 db_stats.transactions_started,
3612 db_stats.transactions_committed,
3613 db_stats.transactions_aborted,
3614 db_stats.queries_executed,
3615 db_stats.bytes_written,
3616 db_stats.bytes_read,
3617 );
3618
3619 let c_string = match std::ffi::CString::new(json) {
3620 Ok(s) => s,
3621 Err(_) => return ptr::null_mut(),
3622 };
3623 unsafe { *out_len = c_string.as_bytes().len() };
3624 c_string.into_raw()
3625 })
3626}
3627
3628#[unsafe(no_mangle)]
3632pub unsafe extern "C" fn sochdb_path(ptr: *mut DatabasePtr, out_len: *mut usize) -> *mut c_char {
3633 ffi_guard!({
3634 if ptr.is_null() || out_len.is_null() {
3635 return ptr::null_mut();
3636 }
3637 let db = unsafe { &(*ptr).0 };
3638 let path_str = db.path().to_string_lossy().to_string();
3639 let c_string = match std::ffi::CString::new(path_str) {
3640 Ok(s) => s,
3641 Err(_) => return ptr::null_mut(),
3642 };
3643 unsafe { *out_len = c_string.as_bytes().len() };
3644 c_string.into_raw()
3645 })
3646}
3647
3648#[unsafe(no_mangle)]
3657pub unsafe extern "C" fn sochdb_backup_create(
3658 ptr: *mut DatabasePtr,
3659 destination: *const c_char,
3660) -> c_int {
3661 ffi_guard!({
3662 if ptr.is_null() || destination.is_null() {
3663 return -1;
3664 }
3665 let db = unsafe { &(*ptr).0 };
3666 let dest = match unsafe { CStr::from_ptr(destination) }.to_str() {
3667 Ok(s) => s,
3668 Err(_) => return -1,
3669 };
3670
3671 let _ = db.flush();
3673
3674 let manager = crate::backup::BackupManager::new(db.path());
3675 match manager.create_backup(dest) {
3676 Ok(_) => 0,
3677 Err(_) => -1,
3678 }
3679 })
3680}
3681
3682#[unsafe(no_mangle)]
3687pub unsafe extern "C" fn sochdb_backup_restore(
3688 ptr: *mut DatabasePtr,
3689 backup_path: *const c_char,
3690) -> c_int {
3691 ffi_guard!({
3692 if ptr.is_null() || backup_path.is_null() {
3693 return -1;
3694 }
3695 let db = unsafe { &(*ptr).0 };
3696 let path = match unsafe { CStr::from_ptr(backup_path) }.to_str() {
3697 Ok(s) => s,
3698 Err(_) => return -1,
3699 };
3700
3701 let manager = crate::backup::BackupManager::new(db.path());
3702 match manager.restore_backup(path) {
3703 Ok(_) => 0,
3704 Err(_) => -1,
3705 }
3706 })
3707}
3708
3709#[unsafe(no_mangle)]
3714pub unsafe extern "C" fn sochdb_backup_list(
3715 backup_dir: *const c_char,
3716 out_len: *mut usize,
3717) -> *mut c_char {
3718 ffi_guard!({
3719 if backup_dir.is_null() || out_len.is_null() {
3720 return ptr::null_mut();
3721 }
3722 let dir = match unsafe { CStr::from_ptr(backup_dir) }.to_str() {
3723 Ok(s) => s,
3724 Err(_) => return ptr::null_mut(),
3725 };
3726
3727 match crate::backup::BackupManager::list_backups(dir) {
3728 Ok(backups) => {
3729 let entries: Vec<String> = backups
3730 .iter()
3731 .map(|b| {
3732 format!(
3733 r#"{{"name":"{}","timestamp":"{}","size_bytes":{}}}"#,
3734 b.generate_name(),
3735 b.generate_name(),
3736 0 )
3738 })
3739 .collect();
3740 let json = format!("[{}]", entries.join(","));
3741 let c_string = match std::ffi::CString::new(json) {
3742 Ok(s) => s,
3743 Err(_) => return ptr::null_mut(),
3744 };
3745 unsafe { *out_len = c_string.as_bytes().len() };
3746 c_string.into_raw()
3747 }
3748 Err(_) => ptr::null_mut(),
3749 }
3750 })
3751}
3752
3753#[unsafe(no_mangle)]
3758pub unsafe extern "C" fn sochdb_backup_verify(backup_path: *const c_char) -> c_int {
3759 ffi_guard!({
3760 if backup_path.is_null() {
3761 return -1;
3762 }
3763 let path = match unsafe { CStr::from_ptr(backup_path) }.to_str() {
3764 Ok(s) => s,
3765 Err(_) => return -1,
3766 };
3767
3768 match crate::backup::BackupManager::verify_backup(path) {
3769 Ok(true) => 1,
3770 Ok(false) => 0,
3771 Err(_) => -1,
3772 }
3773 })
3774}
3775
3776#[unsafe(no_mangle)]
3786pub unsafe extern "C" fn sochdb_graph_delete_node(
3787 ptr: *mut DatabasePtr,
3788 namespace: *const c_char,
3789 node_id: *const c_char,
3790) -> c_int {
3791 ffi_guard!({
3792 if ptr.is_null() || namespace.is_null() || node_id.is_null() {
3793 return -1;
3794 }
3795 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3796 Ok(s) => s,
3797 Err(_) => return -1,
3798 };
3799 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
3800 Ok(s) => s,
3801 Err(_) => return -1,
3802 };
3803 let db = unsafe { &(*ptr).0 };
3804
3805 let txn = match db.begin_transaction() {
3806 Ok(t) => t,
3807 Err(_) => return -1,
3808 };
3809
3810 let node_key = format!("_graph/{}/nodes/{}", ns, id);
3812 let _ = db.delete(txn, node_key.as_bytes());
3813
3814 let edge_prefix_out = format!("_graph/{}/edges/{}/", ns, id);
3816 if let Ok(edges) = db.scan(txn, edge_prefix_out.as_bytes()) {
3817 for (key, _) in edges {
3818 let _ = db.delete(txn, &key);
3819 }
3820 }
3821
3822 let temporal_prefix = format!("_graph/{}/temporal/{}/", ns, id);
3824 if let Ok(edges) = db.scan(txn, temporal_prefix.as_bytes()) {
3825 for (key, _) in edges {
3826 let _ = db.delete(txn, &key);
3827 }
3828 }
3829
3830 match db.commit(txn) {
3831 Ok(_) => 0,
3832 Err(_) => -1,
3833 }
3834 })
3835}
3836
3837#[unsafe(no_mangle)]
3842pub unsafe extern "C" fn sochdb_graph_delete_edge(
3843 ptr: *mut DatabasePtr,
3844 namespace: *const c_char,
3845 from_id: *const c_char,
3846 edge_type: *const c_char,
3847 to_id: *const c_char,
3848) -> c_int {
3849 ffi_guard!({
3850 if ptr.is_null()
3851 || namespace.is_null()
3852 || from_id.is_null()
3853 || edge_type.is_null()
3854 || to_id.is_null()
3855 {
3856 return -1;
3857 }
3858
3859 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3860 Ok(s) => s,
3861 Err(_) => return -1,
3862 };
3863 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
3864 Ok(s) => s,
3865 Err(_) => return -1,
3866 };
3867 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
3868 Ok(s) => s,
3869 Err(_) => return -1,
3870 };
3871 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
3872 Ok(s) => s,
3873 Err(_) => return -1,
3874 };
3875 let db = unsafe { &(*ptr).0 };
3876
3877 let txn = match db.begin_transaction() {
3878 Ok(t) => t,
3879 Err(_) => return -1,
3880 };
3881
3882 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
3883 match db.delete(txn, key.as_bytes()) {
3884 Ok(_) => match db.commit(txn) {
3885 Ok(_) => 0,
3886 Err(_) => -1,
3887 },
3888 Err(_) => {
3889 let _ = db.abort(txn);
3890 -1
3891 }
3892 }
3893 })
3894}
3895
3896#[unsafe(no_mangle)]
3903pub unsafe extern "C" fn sochdb_graph_get_neighbors(
3904 ptr: *mut DatabasePtr,
3905 namespace: *const c_char,
3906 node_id: *const c_char,
3907 direction: u8,
3908 edge_type_filter: *const c_char,
3909 out_len: *mut usize,
3910) -> *mut c_char {
3911 ffi_guard!({
3912 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
3913 return ptr::null_mut();
3914 }
3915 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3916 Ok(s) => s,
3917 Err(_) => return ptr::null_mut(),
3918 };
3919 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
3920 Ok(s) => s,
3921 Err(_) => return ptr::null_mut(),
3922 };
3923 let et_filter = if edge_type_filter.is_null() {
3924 None
3925 } else {
3926 match unsafe { CStr::from_ptr(edge_type_filter) }.to_str() {
3927 Ok(s) => Some(s),
3928 Err(_) => None,
3929 }
3930 };
3931 let db = unsafe { &(*ptr).0 };
3932
3933 let txn = match db.begin_transaction() {
3934 Ok(t) => t,
3935 Err(_) => return ptr::null_mut(),
3936 };
3937
3938 let mut neighbors = Vec::new();
3939
3940 if direction == 0 || direction == 2 {
3942 let prefix = format!("_graph/{}/edges/{}/", ns, node);
3943 if let Ok(edges) = db.scan(txn, prefix.as_bytes()) {
3944 for (_key, value) in edges {
3945 if let Ok(edge_str) = std::str::from_utf8(&value) {
3946 if let Some(filter) = et_filter {
3947 if !edge_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
3948 continue;
3949 }
3950 }
3951 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
3953 let start = to_pos + r#""to_id":""#.len();
3954 if let Some(end) = edge_str[start..].find('"') {
3955 let to_id = &edge_str[start..start + end];
3956 neighbors.push(format!(
3957 r#"{{"node_id":"{}","direction":"outgoing","edge":{}}}"#,
3958 to_id, edge_str
3959 ));
3960 }
3961 }
3962 }
3963 }
3964 }
3965 }
3966
3967 if direction == 1 || direction == 2 {
3969 let all_edges_prefix = format!("_graph/{}/edges/", ns);
3970 if let Ok(edges) = db.scan(txn, all_edges_prefix.as_bytes()) {
3971 for (_key, value) in edges {
3972 if let Ok(edge_str) = std::str::from_utf8(&value) {
3973 let has_to = edge_str.contains(&format!(r#""to_id":"{}""#, node));
3974 if !has_to {
3975 continue;
3976 }
3977 if let Some(filter) = et_filter {
3978 if !edge_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
3979 continue;
3980 }
3981 }
3982 if let Some(from_pos) = edge_str.find(r#""from_id":""#) {
3983 let start = from_pos + r#""from_id":""#.len();
3984 if let Some(end) = edge_str[start..].find('"') {
3985 let from_id = &edge_str[start..start + end];
3986 neighbors.push(format!(
3987 r#"{{"node_id":"{}","direction":"incoming","edge":{}}}"#,
3988 from_id, edge_str
3989 ));
3990 }
3991 }
3992 }
3993 }
3994 }
3995 }
3996
3997 let _ = db.commit(txn);
3998
3999 let json = format!(r#"{{"neighbors":[{}]}}"#, neighbors.join(","));
4000 let c_string = match std::ffi::CString::new(json) {
4001 Ok(s) => s,
4002 Err(_) => return ptr::null_mut(),
4003 };
4004 unsafe { *out_len = c_string.as_bytes().len() };
4005 c_string.into_raw()
4006 })
4007}
4008
4009#[unsafe(no_mangle)]
4016pub unsafe extern "C" fn sochdb_graph_find_path(
4017 ptr: *mut DatabasePtr,
4018 namespace: *const c_char,
4019 from_node: *const c_char,
4020 to_node: *const c_char,
4021 max_depth: usize,
4022 out_len: *mut usize,
4023) -> *mut c_char {
4024 ffi_guard!({
4025 if ptr.is_null()
4026 || namespace.is_null()
4027 || from_node.is_null()
4028 || to_node.is_null()
4029 || out_len.is_null()
4030 {
4031 return ptr::null_mut();
4032 }
4033 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4034 Ok(s) => s,
4035 Err(_) => return ptr::null_mut(),
4036 };
4037 let from = match unsafe { CStr::from_ptr(from_node) }.to_str() {
4038 Ok(s) => s,
4039 Err(_) => return ptr::null_mut(),
4040 };
4041 let to = match unsafe { CStr::from_ptr(to_node) }.to_str() {
4042 Ok(s) => s,
4043 Err(_) => return ptr::null_mut(),
4044 };
4045 let db = unsafe { &(*ptr).0 };
4046
4047 let txn = match db.begin_transaction() {
4048 Ok(t) => t,
4049 Err(_) => return ptr::null_mut(),
4050 };
4051
4052 let mut visited: std::collections::HashMap<String, (String, String)> =
4054 std::collections::HashMap::new(); let mut queue: std::collections::VecDeque<(String, usize)> =
4056 std::collections::VecDeque::new();
4057 queue.push_back((from.to_string(), 0));
4058 visited.insert(from.to_string(), ("".to_string(), "".to_string()));
4059 let mut found = false;
4060
4061 while let Some((current, depth)) = queue.pop_front() {
4062 if current == to {
4063 found = true;
4064 break;
4065 }
4066 if depth >= max_depth {
4067 continue;
4068 }
4069
4070 let prefix = format!("_graph/{}/edges/{}/", ns, current);
4071 if let Ok(edges) = db.scan(txn, prefix.as_bytes()) {
4072 for (_key, value) in edges {
4073 if let Ok(edge_str) = std::str::from_utf8(&value) {
4074 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
4075 let start = to_pos + r#""to_id":""#.len();
4076 if let Some(end) = edge_str[start..].find('"') {
4077 let next = &edge_str[start..start + end];
4078 if !visited.contains_key(next) {
4079 visited.insert(
4080 next.to_string(),
4081 (current.clone(), edge_str.to_string()),
4082 );
4083 queue.push_back((next.to_string(), depth + 1));
4084 }
4085 }
4086 }
4087 }
4088 }
4089 }
4090 }
4091
4092 let _ = db.commit(txn);
4093
4094 if !found {
4095 return ptr::null_mut();
4096 }
4097
4098 let mut path = Vec::new();
4100 let mut edges = Vec::new();
4101 let mut current = to.to_string();
4102 while !current.is_empty() {
4103 path.push(format!(r#""{}""#, current));
4104 if let Some((parent, edge)) = visited.get(¤t) {
4105 if !edge.is_empty() {
4106 edges.push(edge.clone());
4107 }
4108 current = parent.clone();
4109 } else {
4110 break;
4111 }
4112 }
4113 path.reverse();
4114 edges.reverse();
4115
4116 let json = format!(
4117 r#"{{"path":[{}],"edges":[{}]}}"#,
4118 path.join(","),
4119 edges.join(",")
4120 );
4121 let c_string = match std::ffi::CString::new(json) {
4122 Ok(s) => s,
4123 Err(_) => return ptr::null_mut(),
4124 };
4125 unsafe { *out_len = c_string.as_bytes().len() };
4126 c_string.into_raw()
4127 })
4128}
4129
4130#[unsafe(no_mangle)]
4139pub unsafe extern "C" fn sochdb_end_temporal_edge(
4140 ptr: *mut DatabasePtr,
4141 namespace: *const c_char,
4142 from_id: *const c_char,
4143 edge_type: *const c_char,
4144 to_id: *const c_char,
4145) -> c_int {
4146 ffi_guard!({
4147 if ptr.is_null()
4148 || namespace.is_null()
4149 || from_id.is_null()
4150 || edge_type.is_null()
4151 || to_id.is_null()
4152 {
4153 return -1;
4154 }
4155 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4156 Ok(s) => s,
4157 Err(_) => return -1,
4158 };
4159 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
4160 Ok(s) => s,
4161 Err(_) => return -1,
4162 };
4163 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
4164 Ok(s) => s,
4165 Err(_) => return -1,
4166 };
4167 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
4168 Ok(s) => s,
4169 Err(_) => return -1,
4170 };
4171 let db = unsafe { &(*ptr).0 };
4172
4173 let txn = match db.begin_transaction() {
4174 Ok(t) => t,
4175 Err(_) => return -1,
4176 };
4177
4178 let prefix = format!("_graph/{}/temporal/{}/{}/{}/", ns, from, etype, to);
4180 let edges = match db.scan(txn, prefix.as_bytes()) {
4181 Ok(e) => e,
4182 Err(_) => {
4183 let _ = db.abort(txn);
4184 return -1;
4185 }
4186 };
4187
4188 let now = std::time::SystemTime::now()
4189 .duration_since(std::time::UNIX_EPOCH)
4190 .unwrap()
4191 .as_millis() as u64;
4192
4193 let mut found = false;
4194 for (key, value) in edges {
4195 if let Ok(val_str) = std::str::from_utf8(&value) {
4196 if val_str.contains(r#""valid_until":0"#) {
4198 let new_val =
4199 val_str.replace(r#""valid_until":0"#, &format!(r#""valid_until":{}"#, now));
4200 if db.put(txn, &key, new_val.as_bytes()).is_ok() {
4201 found = true;
4202 }
4203 }
4204 }
4205 }
4206
4207 match db.commit(txn) {
4208 Ok(_) => {
4209 if found {
4210 0
4211 } else {
4212 1
4213 }
4214 }
4215 Err(_) => -1,
4216 }
4217 })
4218}
4219
4220#[unsafe(no_mangle)]
4229pub unsafe extern "C" fn sochdb_cache_delete(
4230 ptr: *mut DatabasePtr,
4231 cache_name: *const c_char,
4232 key: *const c_char,
4233) -> c_int {
4234 ffi_guard!({
4235 if ptr.is_null() || cache_name.is_null() || key.is_null() {
4236 return -1;
4237 }
4238 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
4239 Ok(s) => s,
4240 Err(_) => return -1,
4241 };
4242 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
4243 Ok(s) => s,
4244 Err(_) => return -1,
4245 };
4246 let db = unsafe { &(*ptr).0 };
4247
4248 let txn = match db.begin_transaction() {
4249 Ok(t) => t,
4250 Err(_) => return -1,
4251 };
4252
4253 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
4254 let cache_key = format!("_cache/{}/{}", cache, key_hash);
4255
4256 match db.get(txn, cache_key.as_bytes()) {
4257 Ok(Some(_)) => match db.delete(txn, cache_key.as_bytes()) {
4258 Ok(_) => match db.commit(txn) {
4259 Ok(_) => 0,
4260 Err(_) => -1,
4261 },
4262 Err(_) => {
4263 let _ = db.abort(txn);
4264 -1
4265 }
4266 },
4267 Ok(None) => {
4268 let _ = db.commit(txn);
4269 1
4270 }
4271 Err(_) => {
4272 let _ = db.abort(txn);
4273 -1
4274 }
4275 }
4276 })
4277}
4278
4279#[unsafe(no_mangle)]
4284pub unsafe extern "C" fn sochdb_cache_clear(
4285 ptr: *mut DatabasePtr,
4286 cache_name: *const c_char,
4287) -> i64 {
4288 ffi_guard!({
4289 if ptr.is_null() || cache_name.is_null() {
4290 return -1;
4291 }
4292 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
4293 Ok(s) => s,
4294 Err(_) => return -1,
4295 };
4296 let db = unsafe { &(*ptr).0 };
4297
4298 let txn = match db.begin_transaction() {
4299 Ok(t) => t,
4300 Err(_) => return -1,
4301 };
4302
4303 let prefix = format!("_cache/{}/", cache);
4304 let entries = match db.scan(txn, prefix.as_bytes()) {
4305 Ok(e) => e,
4306 Err(_) => {
4307 let _ = db.abort(txn);
4308 return -1;
4309 }
4310 };
4311
4312 let mut count = 0i64;
4313 for (key, _) in &entries {
4314 if db.delete(txn, key).is_ok() {
4315 count += 1;
4316 }
4317 }
4318
4319 match db.commit(txn) {
4320 Ok(_) => count,
4321 Err(_) => -1,
4322 }
4323 })
4324}
4325
4326#[unsafe(no_mangle)]
4331pub unsafe extern "C" fn sochdb_cache_stats(
4332 ptr: *mut DatabasePtr,
4333 cache_name: *const c_char,
4334 out_len: *mut usize,
4335) -> *mut c_char {
4336 ffi_guard!({
4337 if ptr.is_null() || cache_name.is_null() || out_len.is_null() {
4338 return ptr::null_mut();
4339 }
4340 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
4341 Ok(s) => s,
4342 Err(_) => return ptr::null_mut(),
4343 };
4344 let db = unsafe { &(*ptr).0 };
4345
4346 let txn = match db.begin_transaction() {
4347 Ok(t) => t,
4348 Err(_) => return ptr::null_mut(),
4349 };
4350
4351 let prefix = format!("_cache/{}/", cache);
4352 let entries = match db.scan(txn, prefix.as_bytes()) {
4353 Ok(e) => e,
4354 Err(_) => {
4355 let _ = db.abort(txn);
4356 return ptr::null_mut();
4357 }
4358 };
4359 let _ = db.commit(txn);
4360
4361 let now = std::time::SystemTime::now()
4362 .duration_since(std::time::UNIX_EPOCH)
4363 .unwrap()
4364 .as_secs();
4365
4366 let total = entries.len();
4367 let mut expired = 0usize;
4368 let mut total_bytes = 0usize;
4369
4370 for (_key, value) in &entries {
4371 total_bytes += value.len();
4372 if let Ok(val_str) = std::str::from_utf8(value) {
4373 if let Some(exp_pos) = val_str.find(r#""expires_at":"#) {
4374 let exp_start = exp_pos + r#""expires_at":"#.len();
4375 if let Some(exp_end) = val_str[exp_start..].find('}') {
4376 let expires_at: u64 =
4377 val_str[exp_start..exp_start + exp_end].parse().unwrap_or(0);
4378 if expires_at > 0 && now > expires_at {
4379 expired += 1;
4380 }
4381 }
4382 }
4383 }
4384 }
4385
4386 let json = format!(
4387 r#"{{"cache_name":"{}","total_entries":{},"expired_entries":{},"active_entries":{},"total_bytes":{}}}"#,
4388 cache,
4389 total,
4390 expired,
4391 total - expired,
4392 total_bytes
4393 );
4394 let c_string = match std::ffi::CString::new(json) {
4395 Ok(s) => s,
4396 Err(_) => return ptr::null_mut(),
4397 };
4398 unsafe { *out_len = c_string.as_bytes().len() };
4399 c_string.into_raw()
4400 })
4401}
4402
4403#[unsafe(no_mangle)]
4412pub unsafe extern "C" fn sochdb_collection_delete(
4413 ptr: *mut DatabasePtr,
4414 namespace: *const c_char,
4415 collection: *const c_char,
4416) -> c_int {
4417 ffi_guard!({
4418 if ptr.is_null() || namespace.is_null() || collection.is_null() {
4419 return -1;
4420 }
4421 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4422 Ok(s) => s,
4423 Err(_) => return -1,
4424 };
4425 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
4426 Ok(s) => s,
4427 Err(_) => return -1,
4428 };
4429 let db = unsafe { &(*ptr).0 };
4430
4431 let txn = match db.begin_transaction() {
4432 Ok(t) => t,
4433 Err(_) => return -1,
4434 };
4435
4436 let config_key = format!("{}/_collections/{}", ns, col);
4438 let _ = db.delete(txn, config_key.as_bytes());
4439
4440 let vec_prefix = format!("{}/collections/{}/vectors_bin/", ns, col);
4442 if let Ok(entries) = db.scan(txn, vec_prefix.as_bytes()) {
4443 for (key, _) in entries {
4444 let _ = db.delete(txn, &key);
4445 }
4446 }
4447
4448 let meta_prefix = format!("{}/collections/{}/meta/", ns, col);
4450 if let Ok(entries) = db.scan(txn, meta_prefix.as_bytes()) {
4451 for (key, _) in entries {
4452 let _ = db.delete(txn, &key);
4453 }
4454 }
4455
4456 let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
4458 let key = collection_key(ns, col);
4459 registry.lock().remove(&key);
4460
4461 match db.commit(txn) {
4462 Ok(_) => 0,
4463 Err(_) => -1,
4464 }
4465 })
4466}
4467
4468#[unsafe(no_mangle)]
4473pub unsafe extern "C" fn sochdb_collection_count(
4474 ptr: *mut DatabasePtr,
4475 namespace: *const c_char,
4476 collection: *const c_char,
4477) -> i64 {
4478 ffi_guard!({
4479 if ptr.is_null() || namespace.is_null() || collection.is_null() {
4480 return -1;
4481 }
4482 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4483 Ok(s) => s,
4484 Err(_) => return -1,
4485 };
4486 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
4487 Ok(s) => s,
4488 Err(_) => return -1,
4489 };
4490 let db = unsafe { &(*ptr).0 };
4491
4492 let txn = match db.begin_transaction() {
4493 Ok(t) => t,
4494 Err(_) => return -1,
4495 };
4496
4497 let meta_prefix = format!("{}/collections/{}/meta/", ns, col);
4498 match db.scan(txn, meta_prefix.as_bytes()) {
4499 Ok(entries) => {
4500 let count = entries.len() as i64;
4501 let _ = db.commit(txn);
4502 count
4503 }
4504 Err(_) => {
4505 let _ = db.abort(txn);
4506 -1
4507 }
4508 }
4509 })
4510}
4511
4512#[unsafe(no_mangle)]
4517pub unsafe extern "C" fn sochdb_collection_list(
4518 ptr: *mut DatabasePtr,
4519 namespace: *const c_char,
4520 out_len: *mut usize,
4521) -> *mut c_char {
4522 ffi_guard!({
4523 if ptr.is_null() || namespace.is_null() || out_len.is_null() {
4524 return ptr::null_mut();
4525 }
4526 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4527 Ok(s) => s,
4528 Err(_) => return ptr::null_mut(),
4529 };
4530 let db = unsafe { &(*ptr).0 };
4531
4532 let txn = match db.begin_transaction() {
4533 Ok(t) => t,
4534 Err(_) => return ptr::null_mut(),
4535 };
4536
4537 let prefix = format!("{}/_collections/", ns);
4538 let entries = match db.scan(txn, prefix.as_bytes()) {
4539 Ok(e) => e,
4540 Err(_) => {
4541 let _ = db.abort(txn);
4542 return ptr::null_mut();
4543 }
4544 };
4545 let _ = db.commit(txn);
4546
4547 let names: Vec<String> = entries
4548 .iter()
4549 .filter_map(|(key, _)| {
4550 let key_str = std::str::from_utf8(key).ok()?;
4551 let name = key_str.strip_prefix(&prefix)?;
4552 Some(format!(r#""{}""#, name))
4553 })
4554 .collect();
4555
4556 let json = format!("[{}]", names.join(","));
4557 let c_string = match std::ffi::CString::new(json) {
4558 Ok(s) => s,
4559 Err(_) => return ptr::null_mut(),
4560 };
4561 unsafe { *out_len = c_string.as_bytes().len() };
4562 c_string.into_raw()
4563 })
4564}
4565
4566#[unsafe(no_mangle)]
4575pub unsafe extern "C" fn sochdb_list_tables(
4576 ptr: *mut DatabasePtr,
4577 out_len: *mut usize,
4578) -> *mut c_char {
4579 ffi_guard!({
4580 if ptr.is_null() || out_len.is_null() {
4581 return ptr::null_mut();
4582 }
4583 let db = unsafe { &(*ptr).0 };
4584 let tables = db.list_tables();
4585
4586 let names: Vec<String> = tables.iter().map(|t| format!(r#""{}""#, t)).collect();
4587 let json = format!("[{}]", names.join(","));
4588 let c_string = match std::ffi::CString::new(json) {
4589 Ok(s) => s,
4590 Err(_) => return ptr::null_mut(),
4591 };
4592 unsafe { *out_len = c_string.as_bytes().len() };
4593 c_string.into_raw()
4594 })
4595}
4596
4597#[unsafe(no_mangle)]
4602pub unsafe extern "C" fn sochdb_get_table_schema(
4603 ptr: *mut DatabasePtr,
4604 table_name: *const c_char,
4605 out_len: *mut usize,
4606) -> *mut c_char {
4607 ffi_guard!({
4608 if ptr.is_null() || table_name.is_null() || out_len.is_null() {
4609 return ptr::null_mut();
4610 }
4611 let db = unsafe { &(*ptr).0 };
4612 let name = match unsafe { CStr::from_ptr(table_name) }.to_str() {
4613 Ok(s) => s,
4614 Err(_) => return ptr::null_mut(),
4615 };
4616
4617 match db.get_table_schema(name) {
4618 Some(schema) => {
4619 let columns: Vec<String> = schema
4621 .columns
4622 .iter()
4623 .map(|col| {
4624 format!(
4625 r#"{{"name":"{}","type":"{}","nullable":{}}}"#,
4626 col.name,
4627 format!("{:?}", col.col_type),
4628 col.nullable
4629 )
4630 })
4631 .collect();
4632 let json = format!(
4633 r#"{{"table":"{}","columns":[{}]}}"#,
4634 schema.name,
4635 columns.join(",")
4636 );
4637 let c_string = match std::ffi::CString::new(json) {
4638 Ok(s) => s,
4639 Err(_) => return ptr::null_mut(),
4640 };
4641 unsafe { *out_len = c_string.as_bytes().len() };
4642 c_string.into_raw()
4643 }
4644 None => ptr::null_mut(),
4645 }
4646 })
4647}
4648
4649#[unsafe(no_mangle)]
4659pub unsafe extern "C" fn sochdb_set_compression(ptr: *mut DatabasePtr, compression: u8) -> c_int {
4660 ffi_guard!({
4661 if ptr.is_null() {
4662 return -1;
4663 }
4664 let db = unsafe { &(*ptr).0 };
4665 let _comp_type = crate::compression::CompressionType::from_u8(compression);
4666
4667 let txn = match db.begin_transaction() {
4669 Ok(t) => t,
4670 Err(_) => return -1,
4671 };
4672 let key = b"_config/compression";
4673 let val = format!("{}", compression);
4674 if db.put(txn, key, val.as_bytes()).is_err() {
4675 let _ = db.abort(txn);
4676 return -1;
4677 }
4678 match db.commit(txn) {
4679 Ok(_) => 0,
4680 Err(_) => -1,
4681 }
4682 })
4683}
4684
4685#[unsafe(no_mangle)]
4690pub unsafe extern "C" fn sochdb_get_compression(ptr: *mut DatabasePtr) -> u8 {
4691 ffi_guard!({
4692 if ptr.is_null() {
4693 return 255;
4694 }
4695 let db = unsafe { &(*ptr).0 };
4696
4697 let txn = match db.begin_transaction() {
4698 Ok(t) => t,
4699 Err(_) => return 255,
4700 };
4701 let key = b"_config/compression";
4702 match db.get(txn, key) {
4703 Ok(Some(val)) => {
4704 let _ = db.commit(txn);
4705 std::str::from_utf8(&val)
4706 .ok()
4707 .and_then(|s| s.parse::<u8>().ok())
4708 .unwrap_or(0)
4709 }
4710 _ => {
4711 let _ = db.commit(txn);
4712 0
4713 }
4714 }
4715 })
4716}
4717
4718#[unsafe(no_mangle)]
4728pub unsafe extern "C" fn sochdb_execute_sql(
4729 ptr: *mut DatabasePtr,
4730 handle: C_TxnHandle,
4731 sql_ptr: *const c_char,
4732 out_len: *mut usize,
4733) -> *mut c_char {
4734 ffi_guard!({
4735 if ptr.is_null() || sql_ptr.is_null() || out_len.is_null() {
4736 return ptr::null_mut();
4737 }
4738 let db = unsafe { &(*ptr).0 };
4739 let _sql = match unsafe { CStr::from_ptr(sql_ptr) }.to_str() {
4740 Ok(s) => s,
4741 Err(_) => return ptr::null_mut(),
4742 };
4743 let txn = TxnHandle {
4744 txn_id: handle.txn_id,
4745 snapshot_ts: handle.snapshot_ts,
4746 };
4747
4748 let result = db.query(txn, "").execute();
4751
4752 match result {
4756 Ok(qr) => {
4757 let toon = qr.to_toon();
4758 let c_string = match std::ffi::CString::new(toon) {
4759 Ok(s) => s,
4760 Err(_) => return ptr::null_mut(),
4761 };
4762 unsafe { *out_len = c_string.as_bytes().len() };
4763 c_string.into_raw()
4764 }
4765 Err(_) => ptr::null_mut(),
4766 }
4767 })
4768}
4769
4770#[unsafe(no_mangle)]
4778pub unsafe extern "C" fn sochdb_namespace_create(
4779 ptr: *mut DatabasePtr,
4780 name: *const c_char,
4781) -> c_int {
4782 ffi_guard!({
4783 if ptr.is_null() || name.is_null() {
4784 return -1;
4785 }
4786 let ns = match unsafe { CStr::from_ptr(name) }.to_str() {
4787 Ok(s) => s,
4788 Err(_) => return -1,
4789 };
4790 let db = unsafe { &(*ptr).0 };
4791
4792 let txn = match db.begin_transaction() {
4793 Ok(t) => t,
4794 Err(_) => return -1,
4795 };
4796
4797 let key = format!("_namespaces/{}", ns);
4798 let now = std::time::SystemTime::now()
4799 .duration_since(std::time::UNIX_EPOCH)
4800 .unwrap()
4801 .as_secs();
4802 let value = format!(r#"{{"name":"{}","created_at":{}}}"#, ns, now);
4803
4804 if db.put(txn, key.as_bytes(), value.as_bytes()).is_err() {
4805 let _ = db.abort(txn);
4806 return -1;
4807 }
4808 match db.commit(txn) {
4809 Ok(_) => 0,
4810 Err(_) => -1,
4811 }
4812 })
4813}
4814
4815#[unsafe(no_mangle)]
4819pub unsafe extern "C" fn sochdb_namespace_delete(
4820 ptr: *mut DatabasePtr,
4821 name: *const c_char,
4822) -> c_int {
4823 ffi_guard!({
4824 if ptr.is_null() || name.is_null() {
4825 return -1;
4826 }
4827 let ns = match unsafe { CStr::from_ptr(name) }.to_str() {
4828 Ok(s) => s,
4829 Err(_) => return -1,
4830 };
4831 let db = unsafe { &(*ptr).0 };
4832
4833 let txn = match db.begin_transaction() {
4834 Ok(t) => t,
4835 Err(_) => return -1,
4836 };
4837
4838 let ns_key = format!("_namespaces/{}", ns);
4840 let _ = db.delete(txn, ns_key.as_bytes());
4841
4842 let ns_prefix = format!("{}/", ns);
4844 if let Ok(entries) = db.scan(txn, ns_prefix.as_bytes()) {
4845 for (key, _) in entries {
4846 let _ = db.delete(txn, &key);
4847 }
4848 }
4849
4850 match db.commit(txn) {
4851 Ok(_) => 0,
4852 Err(_) => -1,
4853 }
4854 })
4855}
4856
4857#[unsafe(no_mangle)]
4862pub unsafe extern "C" fn sochdb_namespace_list(
4863 ptr: *mut DatabasePtr,
4864 out_len: *mut usize,
4865) -> *mut c_char {
4866 ffi_guard!({
4867 if ptr.is_null() || out_len.is_null() {
4868 return ptr::null_mut();
4869 }
4870 let db = unsafe { &(*ptr).0 };
4871
4872 let txn = match db.begin_transaction() {
4873 Ok(t) => t,
4874 Err(_) => return ptr::null_mut(),
4875 };
4876
4877 let prefix = b"_namespaces/";
4878 let entries = match db.scan(txn, prefix) {
4879 Ok(e) => e,
4880 Err(_) => {
4881 let _ = db.abort(txn);
4882 return ptr::null_mut();
4883 }
4884 };
4885 let _ = db.commit(txn);
4886
4887 let names: Vec<String> = entries
4888 .iter()
4889 .filter_map(|(key, _)| {
4890 let key_str = std::str::from_utf8(key).ok()?;
4891 let name = key_str.strip_prefix("_namespaces/")?;
4892 Some(format!(r#""{}""#, name))
4893 })
4894 .collect();
4895
4896 let json = format!("[{}]", names.join(","));
4897 let c_string = match std::ffi::CString::new(json) {
4898 Ok(s) => s,
4899 Err(_) => return ptr::null_mut(),
4900 };
4901 unsafe { *out_len = c_string.as_bytes().len() };
4902 c_string.into_raw()
4903 })
4904}