1use crate::database::{Database, TxnHandle};
16use std::ffi::CStr;
17use std::os::raw::{c_char, c_int};
18use std::ptr;
19use std::slice;
20use std::sync::Arc;
21use serde_json::Value;
22
23pub struct DatabasePtr(Arc<Database>);
25
26#[repr(C)]
28pub struct C_TxnHandle {
29 pub txn_id: u64,
30 pub snapshot_ts: u64,
31}
32
33#[repr(C)]
36pub struct C_CommitResult {
37 pub commit_ts: u64,
40 pub error_code: i32,
42}
43
44#[repr(C)]
49pub struct C_DatabaseConfig {
50 pub wal_enabled: bool,
52 pub wal_enabled_set: bool,
54 pub sync_mode: u8,
56 pub sync_mode_set: bool,
58 pub memtable_size_bytes: u64,
60 pub group_commit: bool,
62 pub group_commit_set: bool,
64 pub default_index_policy: u8,
66 pub default_index_policy_set: bool,
68}
69
70#[unsafe(no_mangle)]
75pub unsafe extern "C" fn sochdb_open_with_config(
76 path: *const c_char,
77 config: C_DatabaseConfig
78) -> *mut DatabasePtr {
79 if path.is_null() {
80 return ptr::null_mut();
81 }
82
83 let c_str = unsafe { CStr::from_ptr(path) };
84 let path_str = match c_str.to_str() {
85 Ok(s) => s,
86 Err(_) => return ptr::null_mut(),
87 };
88
89 let mut db_config = crate::database::DatabaseConfig::default();
91
92 if config.wal_enabled_set {
93 db_config.wal_enabled = config.wal_enabled;
94 }
95
96 if config.sync_mode_set {
97 db_config.sync_mode = match config.sync_mode {
98 0 => crate::database::SyncMode::Off,
99 1 => crate::database::SyncMode::Normal,
100 _ => crate::database::SyncMode::Full,
101 };
102 }
103
104 if config.memtable_size_bytes > 0 {
105 db_config.memtable_size_limit = config.memtable_size_bytes as usize;
106 }
107
108 if config.group_commit_set {
109 db_config.group_commit = config.group_commit;
110 }
111
112 if config.default_index_policy_set {
113 db_config.default_index_policy = match config.default_index_policy {
114 0 => crate::index_policy::IndexPolicy::WriteOptimized,
115 1 => crate::index_policy::IndexPolicy::Balanced,
116 2 => crate::index_policy::IndexPolicy::ScanOptimized,
117 _ => crate::index_policy::IndexPolicy::AppendOnly,
118 };
119 }
120
121 match Database::open_with_config(path_str, db_config) {
122 Ok(db) => {
123 let ptr = Box::new(DatabasePtr(db));
124 Box::into_raw(ptr)
125 }
126 Err(_) => ptr::null_mut(),
127 }
128}
129
130#[unsafe(no_mangle)]
135pub unsafe extern "C" fn sochdb_open(path: *const c_char) -> *mut DatabasePtr {
136 if path.is_null() {
137 return ptr::null_mut();
138 }
139
140 let c_str = unsafe { CStr::from_ptr(path) };
141 let path_str = match c_str.to_str() {
142 Ok(s) => s,
143 Err(_) => return ptr::null_mut(),
144 };
145
146 let config = crate::database::DatabaseConfig::default();
148
149 match Database::open_with_config(path_str, config) {
151 Ok(db) => {
152 let ptr = Box::new(DatabasePtr(db));
153 Box::into_raw(ptr)
154 }
155 Err(_) => ptr::null_mut(),
156 }
157}
158
159#[unsafe(no_mangle)]
163pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
164 if !ptr.is_null() {
165 unsafe {
166 let _ = Box::from_raw(ptr);
167 }
168 }
169}
170
171#[unsafe(no_mangle)]
176pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
177 if ptr.is_null() {
178 return C_TxnHandle {
179 txn_id: 0,
180 snapshot_ts: 0,
181 };
182 }
183 let db = unsafe { &(*ptr).0 };
184 match db.begin_transaction() {
185 Ok(txn) => C_TxnHandle {
186 txn_id: txn.txn_id,
187 snapshot_ts: txn.snapshot_ts,
188 },
189 Err(_) => C_TxnHandle {
190 txn_id: 0,
191 snapshot_ts: 0,
192 },
193 }
194}
195
196#[unsafe(no_mangle)]
203pub unsafe extern "C" fn sochdb_commit(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> C_CommitResult {
204 if ptr.is_null() {
205 return C_CommitResult {
206 commit_ts: 0,
207 error_code: -1,
208 };
209 }
210 let db = unsafe { &(*ptr).0 };
211 let txn = TxnHandle {
212 txn_id: handle.txn_id,
213 snapshot_ts: handle.snapshot_ts,
214 };
215 match db.commit(txn) {
216 Ok(commit_ts) => C_CommitResult {
217 commit_ts,
218 error_code: 0,
219 },
220 Err(_) => C_CommitResult {
221 commit_ts: 0,
222 error_code: -1,
223 },
224 }
225}
226
227#[unsafe(no_mangle)]
232pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
233 if ptr.is_null() {
234 return -1;
235 }
236 let db = unsafe { &(*ptr).0 };
237 let txn = TxnHandle {
238 txn_id: handle.txn_id,
239 snapshot_ts: handle.snapshot_ts,
240 };
241 match db.abort(txn) {
242 Ok(_) => 0,
243 Err(_) => -1,
244 }
245}
246
247#[unsafe(no_mangle)]
253pub unsafe extern "C" fn sochdb_put(
254 ptr: *mut DatabasePtr,
255 handle: C_TxnHandle,
256 key_ptr: *const u8,
257 key_len: usize,
258 val_ptr: *const u8,
259 val_len: usize,
260) -> c_int {
261 if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
262 return -1;
263 }
264 let db = unsafe { &(*ptr).0 };
265 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
266 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
267 let txn = TxnHandle {
268 txn_id: handle.txn_id,
269 snapshot_ts: handle.snapshot_ts,
270 };
271
272 match db.put(txn, key, val) {
273 Ok(_) => 0,
274 Err(_) => -1,
275 }
276}
277
278#[unsafe(no_mangle)]
285pub unsafe extern "C" fn sochdb_get(
286 ptr: *mut DatabasePtr,
287 handle: C_TxnHandle,
288 key_ptr: *const u8,
289 key_len: usize,
290 val_out: *mut *mut u8,
291 len_out: *mut usize,
292) -> c_int {
293 if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.is_null() {
294 return -1;
295 }
296 let db = unsafe { &(*ptr).0 };
297 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
298 let txn = TxnHandle {
299 txn_id: handle.txn_id,
300 snapshot_ts: handle.snapshot_ts,
301 };
302
303 match db.get(txn, key) {
304 Ok(Some(val)) => {
305 let mut buf = val.into_boxed_slice();
307 unsafe {
308 *val_out = buf.as_mut_ptr();
309 *len_out = buf.len();
310 }
311 let _ = Box::into_raw(buf); 0
313 }
314 Ok(None) => 1, Err(_) => -1,
316 }
317}
318
319#[unsafe(no_mangle)]
323pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
324 if !ptr.is_null() {
325 unsafe {
326 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
327 }
328 }
329}
330
331#[unsafe(no_mangle)]
336pub unsafe extern "C" fn sochdb_delete(
337 ptr: *mut DatabasePtr,
338 handle: C_TxnHandle,
339 key_ptr: *const u8,
340 key_len: usize,
341) -> c_int {
342 if ptr.is_null() || key_ptr.is_null() {
343 return -1;
344 }
345 let db = unsafe { &(*ptr).0 };
346 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
347 let txn = TxnHandle {
348 txn_id: handle.txn_id,
349 snapshot_ts: handle.snapshot_ts,
350 };
351
352 match db.delete(txn, key) {
353 Ok(_) => 0,
354 Err(_) => -1,
355 }
356}
357
358#[unsafe(no_mangle)]
362pub unsafe extern "C" fn sochdb_put_path(
363 ptr: *mut DatabasePtr,
364 handle: C_TxnHandle,
365 path_ptr: *const c_char,
366 val_ptr: *const u8,
367 val_len: usize,
368) -> c_int {
369 if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
370 return -1;
371 }
372 let db = unsafe { &(*ptr).0 };
373 let c_str = unsafe { CStr::from_ptr(path_ptr) };
374 let path_str = match c_str.to_str() {
375 Ok(s) => s,
376 Err(_) => return -1,
377 };
378 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
379 let txn = TxnHandle {
380 txn_id: handle.txn_id,
381 snapshot_ts: handle.snapshot_ts,
382 };
383
384 match db.put_path(txn, path_str, val) {
385 Ok(_) => 0,
386 Err(_) => -1,
387 }
388}
389
390#[unsafe(no_mangle)]
394pub unsafe extern "C" fn sochdb_get_path(
395 ptr: *mut DatabasePtr,
396 handle: C_TxnHandle,
397 path_ptr: *const c_char,
398 val_out: *mut *mut u8,
399 len_out: *mut usize,
400) -> c_int {
401 if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
402 return -1;
403 }
404 let db = unsafe { &(*ptr).0 };
405 let c_str = unsafe { CStr::from_ptr(path_ptr) };
406 let path_str = match c_str.to_str() {
407 Ok(s) => s,
408 Err(_) => return -1,
409 };
410 let txn = TxnHandle {
411 txn_id: handle.txn_id,
412 snapshot_ts: handle.snapshot_ts,
413 };
414
415 match db.get_path(txn, path_str) {
416 Ok(Some(val)) => {
417 let mut buf = val.into_boxed_slice();
418 unsafe {
419 *val_out = buf.as_mut_ptr();
420 *len_out = buf.len();
421 }
422 let _ = Box::into_raw(buf);
423 0
424 }
425 Ok(None) => 1,
426 Err(_) => -1,
427 }
428}
429
430#[allow(clippy::type_complexity)]
432pub struct ScanIteratorPtr(
433 Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
434);
435
436#[unsafe(no_mangle)]
440pub unsafe extern "C" fn sochdb_scan(
441 ptr: *mut DatabasePtr,
442 handle: C_TxnHandle,
443 start_ptr: *const u8,
444 start_len: usize,
445 end_ptr: *const u8,
446 end_len: usize,
447) -> *mut ScanIteratorPtr {
448 if ptr.is_null() {
449 return ptr::null_mut();
450 }
451 let db = unsafe { &(*ptr).0 };
452 let txn = TxnHandle {
453 txn_id: handle.txn_id,
454 snapshot_ts: handle.snapshot_ts,
455 };
456
457 let start = if !start_ptr.is_null() && start_len > 0 {
458 unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
459 } else {
460 vec![]
461 };
462
463 let end = if !end_ptr.is_null() && end_len > 0 {
464 unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
465 } else {
466 vec![] };
468
469 match db.scan_range(txn, &start, &end) {
482 Ok(rows) => {
483 let iter = Box::new(rows.into_iter().map(Ok));
486
487 let ptr = Box::new(ScanIteratorPtr(iter));
488 Box::into_raw(ptr)
489 }
490 Err(_) => ptr::null_mut(),
491 }
492}
493
494#[unsafe(no_mangle)]
499pub unsafe extern "C" fn sochdb_scan_prefix(
500 ptr: *mut DatabasePtr,
501 handle: C_TxnHandle,
502 prefix_ptr: *const u8,
503 prefix_len: usize,
504) -> *mut ScanIteratorPtr {
505 if ptr.is_null() {
506 return ptr::null_mut();
507 }
508 let db = unsafe { &(*ptr).0 };
509 let txn = TxnHandle {
510 txn_id: handle.txn_id,
511 snapshot_ts: handle.snapshot_ts,
512 };
513
514 let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
515 unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
516 } else {
517 vec![]
518 };
519
520 match db.scan(txn, &prefix) {
522 Ok(rows) => {
523 let prefix_owned = prefix.clone();
526 let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
527 .into_iter()
528 .filter(|(k, _)| k.starts_with(&prefix_owned))
529 .collect();
530
531 let iter = Box::new(filtered.into_iter().map(Ok));
532 let ptr = Box::new(ScanIteratorPtr(iter));
533 Box::into_raw(ptr)
534 }
535 Err(_) => ptr::null_mut(),
536 }
537}
538
539#[unsafe(no_mangle)]
544pub unsafe extern "C" fn sochdb_scan_next(
545 iter_ptr: *mut ScanIteratorPtr,
546 key_out: *mut *mut u8,
547 key_len_out: *mut usize,
548 val_out: *mut *mut u8,
549 val_len_out: *mut usize,
550) -> c_int {
551 if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
552 return -1;
553 }
554 let iter = unsafe { &mut (*iter_ptr).0 };
555
556 match iter.next() {
557 Some(Ok((key, val))) => {
558 let mut key_buf = key.into_boxed_slice();
559 let mut val_buf = val.into_boxed_slice();
560 unsafe {
561 *key_out = key_buf.as_mut_ptr();
562 *key_len_out = key_buf.len();
563 *val_out = val_buf.as_mut_ptr();
564 *val_len_out = val_buf.len();
565 }
566 let _ = Box::into_raw(key_buf);
567 let _ = Box::into_raw(val_buf);
568 0
569 }
570 Some(Err(_)) => -1,
571 None => 1, }
573}
574
575#[unsafe(no_mangle)]
579pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
580 if !ptr.is_null() {
581 unsafe {
582 let _ = Box::from_raw(ptr);
583 }
584 }
585}
586
587#[unsafe(no_mangle)]
591pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
592 if ptr.is_null() {
593 return -1;
594 }
595 let db = unsafe { &(*ptr).0 };
596 match db.flush() {
597 Ok(_) => 0,
598 Err(_) => -1,
599 }
600}
601
602#[repr(C)]
604pub struct CStorageStats {
605 pub memtable_size_bytes: u64,
606 pub wal_size_bytes: u64,
607 pub active_transactions: usize,
608 pub min_active_snapshot: u64,
609 pub last_checkpoint_lsn: u64,
610}
611
612#[unsafe(no_mangle)]
616pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
617 if ptr.is_null() {
618 return CStorageStats {
619 memtable_size_bytes: 0,
620 wal_size_bytes: 0,
621 active_transactions: 0,
622 min_active_snapshot: 0,
623 last_checkpoint_lsn: 0,
624 };
625 }
626 let db = unsafe { &(*ptr).0 };
627 let stats = db.storage_stats();
628
629 CStorageStats {
630 memtable_size_bytes: stats.memtable_size_bytes,
631 wal_size_bytes: stats.wal_size_bytes,
632 active_transactions: stats.active_transactions,
633 min_active_snapshot: stats.min_active_snapshot,
634 last_checkpoint_lsn: stats.last_checkpoint_lsn,
635 }
636}
637
638#[repr(C)]
656pub struct CBatchPut {
657 pub data: *const u8,
659 pub len: usize,
661}
662
663#[unsafe(no_mangle)]
704pub unsafe extern "C" fn sochdb_put_many(
705 ptr: *mut DatabasePtr,
706 handle: C_TxnHandle,
707 batch: CBatchPut,
708) -> c_int {
709 if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
710 return -1;
711 }
712
713 let db = unsafe { &(*ptr).0 };
714 let txn = TxnHandle {
715 txn_id: handle.txn_id,
716 snapshot_ts: handle.snapshot_ts,
717 };
718
719 let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
721
722 if data.len() < 4 {
724 return -1;
725 }
726 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
727
728 let mut offset = 4;
729 let mut success_count = 0;
730
731 for _ in 0..num_entries {
732 if offset + 8 > data.len() {
734 return success_count;
735 }
736 let key_len = u32::from_le_bytes([
737 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
738 ]) as usize;
739 let val_len = u32::from_le_bytes([
740 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]
741 ]) as usize;
742 offset += 8;
743
744 if offset + key_len + val_len > data.len() {
746 return success_count;
747 }
748 let key = &data[offset..offset + key_len];
749 offset += key_len;
750 let value = &data[offset..offset + val_len];
751 offset += val_len;
752
753 match db.put(txn, key, value) {
755 Ok(_) => success_count += 1,
756 Err(_) => return success_count,
757 }
758 }
759
760 success_count
761}
762
763#[unsafe(no_mangle)]
783pub unsafe extern "C" fn sochdb_delete_many(
784 ptr: *mut DatabasePtr,
785 handle: C_TxnHandle,
786 keys_data: *const u8,
787 keys_len: usize,
788) -> c_int {
789 if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
790 return -1;
791 }
792
793 let db = unsafe { &(*ptr).0 };
794 let txn = TxnHandle {
795 txn_id: handle.txn_id,
796 snapshot_ts: handle.snapshot_ts,
797 };
798
799 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
800
801 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
802
803 let mut offset = 4;
804 let mut success_count = 0;
805
806 for _ in 0..num_entries {
807 if offset + 4 > data.len() {
808 return success_count;
809 }
810 let key_len = u32::from_le_bytes([
811 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
812 ]) as usize;
813 offset += 4;
814
815 if offset + key_len > data.len() {
816 return success_count;
817 }
818 let key = &data[offset..offset + key_len];
819 offset += key_len;
820
821 match db.delete(txn, key) {
822 Ok(_) => success_count += 1,
823 Err(_) => return success_count,
824 }
825 }
826
827 success_count
828}
829
830#[unsafe(no_mangle)]
858pub unsafe extern "C" fn sochdb_get_many(
859 ptr: *mut DatabasePtr,
860 handle: C_TxnHandle,
861 keys_data: *const u8,
862 keys_len: usize,
863 result_out: *mut *mut u8,
864 result_len_out: *mut usize,
865) -> c_int {
866 if ptr.is_null() || keys_data.is_null() || keys_len < 4
867 || result_out.is_null() || result_len_out.is_null() {
868 return -1;
869 }
870
871 let db = unsafe { &(*ptr).0 };
872 let txn = TxnHandle {
873 txn_id: handle.txn_id,
874 snapshot_ts: handle.snapshot_ts,
875 };
876
877 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
878
879 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
880
881 let mut result = Vec::with_capacity(4 + num_entries * 10); result.extend_from_slice(&(num_entries as u32).to_le_bytes());
884
885 let mut offset = 4;
886
887 for _ in 0..num_entries {
888 if offset + 4 > data.len() {
889 result.push(2); continue;
891 }
892 let key_len = u32::from_le_bytes([
893 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
894 ]) as usize;
895 offset += 4;
896
897 if offset + key_len > data.len() {
898 result.push(2); continue;
900 }
901 let key = &data[offset..offset + key_len];
902 offset += key_len;
903
904 match db.get(txn, key) {
905 Ok(Some(value)) => {
906 result.push(0); result.extend_from_slice(&(value.len() as u32).to_le_bytes());
908 result.extend_from_slice(&value);
909 }
910 Ok(None) => {
911 result.push(1); }
913 Err(_) => {
914 result.push(2); }
916 }
917 }
918
919 let mut boxed = result.into_boxed_slice();
921 unsafe {
922 *result_out = boxed.as_mut_ptr();
923 *result_len_out = boxed.len();
924 }
925 let _ = Box::into_raw(boxed); 0
928}
929
930#[unsafe(no_mangle)]
987pub unsafe extern "C" fn sochdb_scan_batch(
988 iter_ptr: *mut ScanIteratorPtr,
989 batch_size: usize,
990 result_out: *mut *mut u8,
991 result_len_out: *mut usize,
992) -> c_int {
993 if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0 {
994 return -1;
995 }
996
997 let iter = unsafe { &mut (*iter_ptr).0 };
998
999 let estimated_size = 5 + batch_size * 108;
1002 let mut result = Vec::with_capacity(estimated_size);
1003
1004 result.extend_from_slice(&[0u8; 5]); let mut count = 0u32;
1008 let mut is_done = false;
1009
1010 for _ in 0..batch_size {
1011 match iter.next() {
1012 Some(Ok((key, val))) => {
1013 result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1015 result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1016 result.extend_from_slice(&key);
1017 result.extend_from_slice(&val);
1018 count += 1;
1019 }
1020 Some(Err(_)) => {
1021 result[0..4].copy_from_slice(&count.to_le_bytes());
1023 result[4] = 0; let mut boxed = result.into_boxed_slice();
1026 unsafe {
1027 *result_out = boxed.as_mut_ptr();
1028 *result_len_out = boxed.len();
1029 }
1030 let _ = Box::into_raw(boxed);
1031 return -1;
1032 }
1033 None => {
1034 is_done = true;
1035 break;
1036 }
1037 }
1038 }
1039
1040 result[0..4].copy_from_slice(&count.to_le_bytes());
1042 result[4] = if is_done { 1 } else { 0 };
1043
1044 if count == 0 && is_done {
1046 let mut boxed = result.into_boxed_slice();
1048 unsafe {
1049 *result_out = boxed.as_mut_ptr();
1050 *result_len_out = boxed.len();
1051 }
1052 let _ = Box::into_raw(boxed);
1053 return 1; }
1055
1056 let mut boxed = result.into_boxed_slice();
1058 unsafe {
1059 *result_out = boxed.as_mut_ptr();
1060 *result_len_out = boxed.len();
1061 }
1062 let _ = Box::into_raw(boxed);
1063
1064 0 }
1066
1067#[unsafe(no_mangle)]
1087pub unsafe extern "C" fn sochdb_set_table_index_policy(
1088 ptr: *mut DatabasePtr,
1089 table_name: *const c_char,
1090 policy: u8,
1091) -> c_int {
1092 if ptr.is_null() || table_name.is_null() {
1093 return -1;
1094 }
1095
1096 let c_str = unsafe { CStr::from_ptr(table_name) };
1097 let table = match c_str.to_str() {
1098 Ok(s) => s,
1099 Err(_) => return -1,
1100 };
1101
1102 let index_policy = match policy {
1103 0 => crate::index_policy::IndexPolicy::WriteOptimized,
1104 1 => crate::index_policy::IndexPolicy::Balanced,
1105 2 => crate::index_policy::IndexPolicy::ScanOptimized,
1106 3 => crate::index_policy::IndexPolicy::AppendOnly,
1107 _ => return -2,
1108 };
1109
1110 let db = unsafe { &(*ptr).0 };
1111
1112 let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1114 db.index_registry().configure_table(config);
1115
1116 0
1117}
1118
1119#[unsafe(no_mangle)]
1131pub unsafe extern "C" fn sochdb_get_table_index_policy(
1132 ptr: *mut DatabasePtr,
1133 table_name: *const c_char,
1134) -> u8 {
1135 if ptr.is_null() || table_name.is_null() {
1136 return 255;
1137 }
1138
1139 let c_str = unsafe { CStr::from_ptr(table_name) };
1140 let table = match c_str.to_str() {
1141 Ok(s) => s,
1142 Err(_) => return 255,
1143 };
1144
1145 let db = unsafe { &(*ptr).0 };
1146 let config = db.index_registry().get_config(table);
1147
1148 match config.policy {
1149 crate::index_policy::IndexPolicy::WriteOptimized => 0,
1150 crate::index_policy::IndexPolicy::Balanced => 1,
1151 crate::index_policy::IndexPolicy::ScanOptimized => 2,
1152 crate::index_policy::IndexPolicy::AppendOnly => 3,
1153 }
1154}
1155
1156#[repr(C)]
1158pub struct C_TemporalEdge {
1159 pub from_id: *const c_char,
1160 pub edge_type: *const c_char,
1161 pub to_id: *const c_char,
1162 pub valid_from: u64,
1163 pub valid_until: u64,
1164 pub properties_json: *const c_char, }
1166
1167#[unsafe(no_mangle)]
1171pub unsafe extern "C" fn sochdb_add_temporal_edge(
1172 ptr: *mut DatabasePtr,
1173 namespace: *const c_char,
1174 edge: C_TemporalEdge,
1175) -> c_int {
1176 if ptr.is_null() || namespace.is_null() || edge.from_id.is_null()
1177 || edge.edge_type.is_null() || edge.to_id.is_null() {
1178 return -1;
1179 }
1180
1181 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1182 Ok(s) => s,
1183 Err(_) => return -1,
1184 };
1185 let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1186 Ok(s) => s,
1187 Err(_) => return -1,
1188 };
1189 let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1190 Ok(s) => s,
1191 Err(_) => return -1,
1192 };
1193 let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1194 Ok(s) => s,
1195 Err(_) => return -1,
1196 };
1197
1198 let db = unsafe { &(*ptr).0 };
1199
1200 let txn = match db.begin_transaction() {
1202 Ok(t) => t,
1203 Err(_) => return -1,
1204 };
1205
1206 let key = format!(
1208 "_graph/{}/temporal/{}/{}/{}/{:016x}",
1209 ns, from, etype, to, edge.valid_from
1210 );
1211
1212 let props_str = if edge.properties_json.is_null() {
1213 "{}".to_string()
1214 } else {
1215 match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1216 Ok(s) => s.to_string(),
1217 Err(_) => return -1,
1218 }
1219 };
1220
1221 let value = format!(
1222 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1223 from, etype, to, edge.valid_from, edge.valid_until, props_str
1224 );
1225
1226 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1227 let _ = db.abort(txn);
1228 return -1;
1229 }
1230
1231 match db.commit(txn) {
1232 Ok(_) => 0,
1233 Err(_) => -1,
1234 }
1235}
1236
1237#[unsafe(no_mangle)]
1244pub unsafe extern "C" fn sochdb_query_temporal_graph(
1245 ptr: *mut DatabasePtr,
1246 namespace: *const c_char,
1247 node_id: *const c_char,
1248 query_mode: u8,
1249 timestamp: u64, start_time: u64, end_time: u64, edge_type: *const c_char, out_len: *mut usize,
1254) -> *mut c_char {
1255 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1256 return ptr::null_mut();
1257 }
1258
1259 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1260 Ok(s) => s,
1261 Err(_) => return ptr::null_mut(),
1262 };
1263 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1264 Ok(s) => s,
1265 Err(_) => return ptr::null_mut(),
1266 };
1267
1268 let edge_filter = if edge_type.is_null() {
1269 None
1270 } else {
1271 match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1272 Ok(s) => Some(s),
1273 Err(_) => return ptr::null_mut(),
1274 }
1275 };
1276
1277 let db = unsafe { &(*ptr).0 };
1278
1279 let txn = match db.begin_transaction() {
1281 Ok(t) => t,
1282 Err(_) => return ptr::null_mut(),
1283 };
1284
1285 let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1287 let pairs = match db.scan(txn, prefix.as_bytes()) {
1288 Ok(p) => p,
1289 Err(_) => {
1290 let _ = db.abort(txn);
1291 return ptr::null_mut();
1292 }
1293 };
1294
1295 if let Err(_) = db.commit(txn) {
1297 return ptr::null_mut();
1298 }
1299
1300 let mut results = Vec::new();
1301 let now = std::time::SystemTime::now()
1302 .duration_since(std::time::UNIX_EPOCH)
1303 .unwrap()
1304 .as_millis() as u64;
1305
1306 for (_key, value) in pairs {
1307 let value_str = match std::str::from_utf8(&value) {
1309 Ok(s) => s,
1310 Err(_) => continue,
1311 };
1312
1313 if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1315 if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1316 let vf_start = valid_from_pos + r#""valid_from":"#.len();
1317 let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1318 let vu_start = valid_until_pos + r#""valid_until":"#.len();
1319 let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1320
1321 let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1322 let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1323
1324 if let Some(filter) = edge_filter {
1326 if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1327 continue;
1328 }
1329 }
1330
1331 let matches = match query_mode {
1333 0 => timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until),
1334 1 => {
1335 let edge_end = if valid_until == 0 { u64::MAX } else { valid_until };
1336 valid_from < end_time && edge_end > start_time
1337 }
1338 2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1339 _ => false,
1340 };
1341
1342 if matches {
1343 results.push(value_str.to_string());
1344 }
1345 }
1346 }
1347 }
1348
1349 let json = format!("[{}]", results.join(","));
1351 let c_string = match std::ffi::CString::new(json) {
1352 Ok(s) => s,
1353 Err(_) => return ptr::null_mut(),
1354 };
1355
1356 unsafe { *out_len = c_string.as_bytes().len() };
1357 c_string.into_raw()
1358}
1359
1360#[unsafe(no_mangle)]
1364pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1365 if !ptr.is_null() {
1366 unsafe {
1367 let _ = std::ffi::CString::from_raw(ptr);
1368 }
1369 }
1370}
1371
1372#[unsafe(no_mangle)]
1387pub unsafe extern "C" fn sochdb_graph_add_node(
1388 ptr: *mut DatabasePtr,
1389 namespace: *const c_char,
1390 node_id: *const c_char,
1391 node_type: *const c_char,
1392 properties_json: *const c_char,
1393) -> c_int {
1394 if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1395 return -1;
1396 }
1397
1398 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1399 Ok(s) => s,
1400 Err(_) => return -1,
1401 };
1402 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1403 Ok(s) => s,
1404 Err(_) => return -1,
1405 };
1406 let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1407 Ok(s) => s,
1408 Err(_) => return -1,
1409 };
1410 let props = if properties_json.is_null() {
1411 "{}".to_string()
1412 } else {
1413 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1414 Ok(s) => s.to_string(),
1415 Err(_) => return -1,
1416 }
1417 };
1418
1419 let db = unsafe { &(*ptr).0 };
1420
1421 let txn = match db.begin_transaction() {
1422 Ok(t) => t,
1423 Err(_) => return -1,
1424 };
1425
1426 let key = format!("_graph/{}/nodes/{}", ns, id);
1427 let value = format!(
1428 r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1429 id, ntype, props
1430 );
1431
1432 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1433 let _ = db.abort(txn);
1434 return -1;
1435 }
1436
1437 match db.commit(txn) {
1438 Ok(_) => 0,
1439 Err(_) => -1,
1440 }
1441}
1442
1443#[unsafe(no_mangle)]
1454pub unsafe extern "C" fn sochdb_graph_add_edge(
1455 ptr: *mut DatabasePtr,
1456 namespace: *const c_char,
1457 from_id: *const c_char,
1458 edge_type: *const c_char,
1459 to_id: *const c_char,
1460 properties_json: *const c_char,
1461) -> c_int {
1462 if ptr.is_null() || namespace.is_null() || from_id.is_null()
1463 || edge_type.is_null() || to_id.is_null() {
1464 return -1;
1465 }
1466
1467 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1468 Ok(s) => s,
1469 Err(_) => return -1,
1470 };
1471 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1472 Ok(s) => s,
1473 Err(_) => return -1,
1474 };
1475 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1476 Ok(s) => s,
1477 Err(_) => return -1,
1478 };
1479 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1480 Ok(s) => s,
1481 Err(_) => return -1,
1482 };
1483 let props = if properties_json.is_null() {
1484 "{}".to_string()
1485 } else {
1486 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1487 Ok(s) => s.to_string(),
1488 Err(_) => return -1,
1489 }
1490 };
1491
1492 let db = unsafe { &(*ptr).0 };
1493
1494 let txn = match db.begin_transaction() {
1495 Ok(t) => t,
1496 Err(_) => return -1,
1497 };
1498
1499 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1500 let value = format!(
1501 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1502 from, etype, to, props
1503 );
1504
1505 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1506 let _ = db.abort(txn);
1507 return -1;
1508 }
1509
1510 match db.commit(txn) {
1511 Ok(_) => 0,
1512 Err(_) => -1,
1513 }
1514}
1515
1516#[unsafe(no_mangle)]
1526pub unsafe extern "C" fn sochdb_graph_traverse(
1527 ptr: *mut DatabasePtr,
1528 namespace: *const c_char,
1529 start_node: *const c_char,
1530 max_depth: usize,
1531 order: u8, out_len: *mut usize,
1533) -> *mut c_char {
1534 if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1535 return ptr::null_mut();
1536 }
1537
1538 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1539 Ok(s) => s,
1540 Err(_) => return ptr::null_mut(),
1541 };
1542 let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1543 Ok(s) => s,
1544 Err(_) => return ptr::null_mut(),
1545 };
1546
1547 let db = unsafe { &(*ptr).0 };
1548
1549 let txn = match db.begin_transaction() {
1550 Ok(t) => t,
1551 Err(_) => return ptr::null_mut(),
1552 };
1553
1554 let mut visited_nodes = std::collections::HashSet::new();
1556 let mut nodes_json = Vec::new();
1557 let mut edges_json = Vec::new();
1558
1559 let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1561
1562 while let Some((current_node, depth)) = if order == 0 {
1563 if frontier.is_empty() { None } else { Some(frontier.remove(0)) }
1565 } else {
1566 frontier.pop()
1568 } {
1569 if depth > max_depth || visited_nodes.contains(¤t_node) {
1570 continue;
1571 }
1572 visited_nodes.insert(current_node.clone());
1573
1574 let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1576 if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1577 if let Ok(s) = std::str::from_utf8(&node_data) {
1578 nodes_json.push(s.to_string());
1579 }
1580 }
1581
1582 let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1584 if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1585 for (_key, value) in edges {
1586 if let Ok(edge_str) = std::str::from_utf8(&value) {
1587 edges_json.push(edge_str.to_string());
1588
1589 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
1591 let start_idx = to_pos + r#""to_id":""#.len();
1592 if let Some(end_idx) = edge_str[start_idx..].find('"') {
1593 let to_id = &edge_str[start_idx..start_idx + end_idx];
1594 if !visited_nodes.contains(to_id) {
1595 frontier.push((to_id.to_string(), depth + 1));
1596 }
1597 }
1598 }
1599 }
1600 }
1601 }
1602 }
1603
1604 if let Err(_) = db.commit(txn) {
1605 return ptr::null_mut();
1606 }
1607
1608 let result = format!(
1609 r#"{{"nodes":[{}],"edges":[{}]}}"#,
1610 nodes_json.join(","),
1611 edges_json.join(",")
1612 );
1613
1614 let c_string = match std::ffi::CString::new(result) {
1615 Ok(s) => s,
1616 Err(_) => return ptr::null_mut(),
1617 };
1618
1619 unsafe { *out_len = c_string.as_bytes().len() };
1620 c_string.into_raw()
1621}
1622
1623#[unsafe(no_mangle)]
1636pub unsafe extern "C" fn sochdb_cache_put(
1637 ptr: *mut DatabasePtr,
1638 cache_name: *const c_char,
1639 key: *const c_char,
1640 value: *const c_char,
1641 embedding_ptr: *const f32,
1642 embedding_len: usize,
1643 ttl_seconds: u64,
1644) -> c_int {
1645 if ptr.is_null() || cache_name.is_null() || key.is_null()
1646 || value.is_null() || embedding_ptr.is_null() {
1647 return -1;
1648 }
1649
1650 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1651 Ok(s) => s,
1652 Err(_) => return -1,
1653 };
1654 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
1655 Ok(s) => s,
1656 Err(_) => return -1,
1657 };
1658 let v = match unsafe { CStr::from_ptr(value) }.to_str() {
1659 Ok(s) => s,
1660 Err(_) => return -1,
1661 };
1662 let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
1663
1664 let db = unsafe { &(*ptr).0 };
1665
1666 let txn = match db.begin_transaction() {
1667 Ok(t) => t,
1668 Err(_) => return -1,
1669 };
1670
1671 let expires_at = if ttl_seconds > 0 {
1673 std::time::SystemTime::now()
1674 .duration_since(std::time::UNIX_EPOCH)
1675 .unwrap()
1676 .as_secs() + ttl_seconds
1677 } else {
1678 0 };
1680
1681 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
1683 let cache_key = format!("_cache/{}/{}", cache, key_hash);
1684
1685 let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
1687
1688 let cache_value = format!(
1689 r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
1690 k, v, embedding_json.join(","), expires_at
1691 );
1692
1693 if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
1694 let _ = db.abort(txn);
1695 return -1;
1696 }
1697
1698 match db.commit(txn) {
1699 Ok(_) => 0,
1700 Err(_) => -1,
1701 }
1702}
1703
1704#[unsafe(no_mangle)]
1712pub unsafe extern "C" fn sochdb_cache_get(
1713 ptr: *mut DatabasePtr,
1714 cache_name: *const c_char,
1715 query_embedding_ptr: *const f32,
1716 embedding_len: usize,
1717 threshold: f32,
1718 out_len: *mut usize,
1719) -> *mut c_char {
1720 if ptr.is_null() || cache_name.is_null() || query_embedding_ptr.is_null() || out_len.is_null() {
1721 return ptr::null_mut();
1722 }
1723
1724 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1725 Ok(s) => s,
1726 Err(_) => return ptr::null_mut(),
1727 };
1728 let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
1729
1730 let db = unsafe { &(*ptr).0 };
1731
1732 let txn = match db.begin_transaction() {
1733 Ok(t) => t,
1734 Err(_) => return ptr::null_mut(),
1735 };
1736
1737 let prefix = format!("_cache/{}/", cache);
1738 let entries = match db.scan(txn, prefix.as_bytes()) {
1739 Ok(e) => e,
1740 Err(_) => {
1741 let _ = db.abort(txn);
1742 return ptr::null_mut();
1743 }
1744 };
1745
1746 let _ = db.commit(txn);
1747
1748 let now = std::time::SystemTime::now()
1749 .duration_since(std::time::UNIX_EPOCH)
1750 .unwrap()
1751 .as_secs();
1752
1753 let mut best_match: Option<(f32, String)> = None;
1754
1755 for (_key, value) in entries {
1756 let value_str = match std::str::from_utf8(&value) {
1757 Ok(s) => s,
1758 Err(_) => continue,
1759 };
1760
1761 if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
1763 let exp_start = exp_pos + r#""expires_at":"#.len();
1764 if let Some(exp_end) = value_str[exp_start..].find('}') {
1765 let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
1766 .parse()
1767 .unwrap_or(0);
1768 if expires_at > 0 && now > expires_at {
1769 continue; }
1771 }
1772 }
1773
1774 if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
1776 let emb_start = emb_pos + r#""embedding":["#.len();
1777 if let Some(emb_end) = value_str[emb_start..].find(']') {
1778 let emb_str = &value_str[emb_start..emb_start + emb_end];
1779 let cached_embedding: Vec<f32> = emb_str
1780 .split(',')
1781 .filter_map(|s| s.trim().parse().ok())
1782 .collect();
1783
1784 if cached_embedding.len() == query.len() {
1785 let similarity = cosine_similarity(query, &cached_embedding);
1786 if similarity >= threshold {
1787 if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
1788 if let Some(val_pos) = value_str.find(r#""value":""#) {
1790 let val_start = val_pos + r#""value":""#.len();
1791 if let Some(val_end) = value_str[val_start..].find('"') {
1792 let cached_value = &value_str[val_start..val_start + val_end];
1793 best_match = Some((similarity, cached_value.to_string()));
1794 }
1795 }
1796 }
1797 }
1798 }
1799 }
1800 }
1801 }
1802
1803 match best_match {
1804 Some((_, value)) => {
1805 let c_string = match std::ffi::CString::new(value) {
1806 Ok(s) => s,
1807 Err(_) => return ptr::null_mut(),
1808 };
1809 unsafe { *out_len = c_string.as_bytes().len() };
1810 c_string.into_raw()
1811 }
1812 None => ptr::null_mut(),
1813 }
1814}
1815
1816fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
1819 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1820 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
1821 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1822 if norm_a == 0.0 || norm_b == 0.0 {
1823 0.0
1824 } else {
1825 let similarity = dot / (norm_a * norm_b);
1826 (similarity + 1.0) / 2.0
1829 }
1830}
1831
1832#[unsafe(no_mangle)]
1847pub unsafe extern "C" fn sochdb_trace_start(
1848 ptr: *mut DatabasePtr,
1849 name: *const c_char,
1850 trace_id_out: *mut *mut c_char,
1851 span_id_out: *mut *mut c_char,
1852) -> c_int {
1853 if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
1854 return -1;
1855 }
1856
1857 let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
1858 Ok(s) => s,
1859 Err(_) => return -1,
1860 };
1861
1862 let db = unsafe { &(*ptr).0 };
1863
1864 let trace_id = format!("trace_{:016x}", rand_u64());
1866 let span_id = format!("span_{:016x}", rand_u64());
1867
1868 let txn = match db.begin_transaction() {
1869 Ok(t) => t,
1870 Err(_) => return -1,
1871 };
1872
1873 let now = std::time::SystemTime::now()
1874 .duration_since(std::time::UNIX_EPOCH)
1875 .unwrap()
1876 .as_micros() as u64;
1877
1878 let trace_key = format!("_traces/{}", trace_id);
1880 let trace_value = format!(
1881 r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
1882 trace_id, trace_name, now, span_id
1883 );
1884
1885 if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
1886 let _ = db.abort(txn);
1887 return -1;
1888 }
1889
1890 let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
1892 let span_value = format!(
1893 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
1894 span_id, trace_name, now
1895 );
1896
1897 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
1898 let _ = db.abort(txn);
1899 return -1;
1900 }
1901
1902 if let Err(_) = db.commit(txn) {
1903 return -1;
1904 }
1905
1906 let trace_c = match std::ffi::CString::new(trace_id) {
1908 Ok(s) => s,
1909 Err(_) => return -1,
1910 };
1911 let span_c = match std::ffi::CString::new(span_id) {
1912 Ok(s) => s,
1913 Err(_) => return -1,
1914 };
1915
1916 unsafe {
1917 *trace_id_out = trace_c.into_raw();
1918 *span_id_out = span_c.into_raw();
1919 }
1920
1921 0
1922}
1923
1924#[unsafe(no_mangle)]
1931pub unsafe extern "C" fn sochdb_trace_span_start(
1932 ptr: *mut DatabasePtr,
1933 trace_id: *const c_char,
1934 parent_span_id: *const c_char,
1935 name: *const c_char,
1936 span_id_out: *mut *mut c_char,
1937) -> c_int {
1938 if ptr.is_null() || trace_id.is_null() || parent_span_id.is_null()
1939 || name.is_null() || span_id_out.is_null() {
1940 return -1;
1941 }
1942
1943 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
1944 Ok(s) => s,
1945 Err(_) => return -1,
1946 };
1947 let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
1948 Ok(s) => s,
1949 Err(_) => return -1,
1950 };
1951 let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
1952 Ok(s) => s,
1953 Err(_) => return -1,
1954 };
1955
1956 let db = unsafe { &(*ptr).0 };
1957 let span_id = format!("span_{:016x}", rand_u64());
1958
1959 let txn = match db.begin_transaction() {
1960 Ok(t) => t,
1961 Err(_) => return -1,
1962 };
1963
1964 let now = std::time::SystemTime::now()
1965 .duration_since(std::time::UNIX_EPOCH)
1966 .unwrap()
1967 .as_micros() as u64;
1968
1969 let span_key = format!("_traces/{}/spans/{}", tid, span_id);
1970 let span_value = format!(
1971 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
1972 span_id, span_name, now, pid
1973 );
1974
1975 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
1976 let _ = db.abort(txn);
1977 return -1;
1978 }
1979
1980 if let Err(_) = db.commit(txn) {
1981 return -1;
1982 }
1983
1984 let span_c = match std::ffi::CString::new(span_id) {
1985 Ok(s) => s,
1986 Err(_) => return -1,
1987 };
1988
1989 unsafe { *span_id_out = span_c.into_raw() };
1990 0
1991}
1992
1993#[unsafe(no_mangle)]
2003pub unsafe extern "C" fn sochdb_trace_span_end(
2004 ptr: *mut DatabasePtr,
2005 trace_id: *const c_char,
2006 span_id: *const c_char,
2007 status: u8,
2008) -> i64 {
2009 if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2010 return -1;
2011 }
2012
2013 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2014 Ok(s) => s,
2015 Err(_) => return -1,
2016 };
2017 let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2018 Ok(s) => s,
2019 Err(_) => return -1,
2020 };
2021
2022 let db = unsafe { &(*ptr).0 };
2023
2024 let txn = match db.begin_transaction() {
2025 Ok(t) => t,
2026 Err(_) => return -1,
2027 };
2028
2029 let span_key = format!("_traces/{}/spans/{}", tid, sid);
2030
2031 let span_data = match db.get(txn, span_key.as_bytes()) {
2033 Ok(Some(data)) => data,
2034 _ => {
2035 let _ = db.abort(txn);
2036 return -1;
2037 }
2038 };
2039
2040 let span_str = match std::str::from_utf8(&span_data) {
2041 Ok(s) => s,
2042 Err(_) => {
2043 let _ = db.abort(txn);
2044 return -1;
2045 }
2046 };
2047
2048 let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2050 let start = pos + r#""start_us":"#.len();
2051 if let Some(end) = span_str[start..].find(',') {
2052 span_str[start..start + end].parse().unwrap_or(0u64)
2053 } else {
2054 0u64
2055 }
2056 } else {
2057 0u64
2058 };
2059
2060 let now = std::time::SystemTime::now()
2061 .duration_since(std::time::UNIX_EPOCH)
2062 .unwrap()
2063 .as_micros() as u64;
2064
2065 let duration_us = now.saturating_sub(start_us);
2066 let status_str = match status {
2067 1 => "ok",
2068 2 => "error",
2069 _ => "unset",
2070 };
2071
2072 let new_span = span_str
2074 .replace(r#""status":"active""#, &format!(r#""status":"{}","end_us":{},"duration_us":{}"#, status_str, now, duration_us));
2075
2076 if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2077 let _ = db.abort(txn);
2078 return -1;
2079 }
2080
2081 if let Err(_) = db.commit(txn) {
2082 return -1;
2083 }
2084
2085 duration_us as i64
2086}
2087
2088fn rand_u64() -> u64 {
2090 use std::sync::atomic::{AtomicU64, Ordering};
2091 static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2092
2093 let mut s = STATE.load(Ordering::Relaxed);
2094 if s == 0 {
2095 s = std::time::SystemTime::now()
2096 .duration_since(std::time::UNIX_EPOCH)
2097 .unwrap()
2098 .as_nanos() as u64;
2099 }
2100 s ^= s >> 12;
2101 s ^= s << 25;
2102 s ^= s >> 27;
2103 STATE.store(s, Ordering::Relaxed);
2104 s.wrapping_mul(0x2545F4914F6CDD1D)
2105}
2106
2107#[unsafe(no_mangle)]
2117pub unsafe extern "C" fn sochdb_collection_create(
2118 ptr: *mut DatabasePtr,
2119 namespace: *const c_char,
2120 collection: *const c_char,
2121 dimension: usize,
2122 dist_type: u8, ) -> c_int {
2124 if ptr.is_null() || namespace.is_null() || collection.is_null() {
2125 return -1;
2126 }
2127
2128 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2129 Ok(s) => s,
2130 Err(_) => return -1,
2131 };
2132 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2133 Ok(s) => s,
2134 Err(_) => return -1,
2135 };
2136
2137 let db = unsafe { &(*ptr).0 };
2138 let txn = match db.begin_transaction() {
2139 Ok(t) => t,
2140 Err(_) => return -1,
2141 };
2142
2143 let config_key = format!("{}/_collections/{}", ns, col);
2145 let config_value = format!(
2146 r#"{{"dimension":{},"metric":{}}}"#,
2147 dimension, dist_type
2148 );
2149
2150 if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2151 let _ = db.abort(txn);
2152 return -1;
2153 }
2154
2155 match db.commit(txn) {
2156 Ok(_) => 0,
2157 Err(_) => -1,
2158 }
2159}
2160
2161#[unsafe(no_mangle)]
2167pub unsafe extern "C" fn sochdb_collection_insert(
2168 ptr: *mut DatabasePtr,
2169 namespace: *const c_char,
2170 collection: *const c_char,
2171 id: *const c_char,
2172 vector_ptr: *const f32,
2173 vector_len: usize,
2174 metadata_json: *const c_char, ) -> c_int {
2176 if ptr.is_null() || namespace.is_null() || collection.is_null()
2177 || id.is_null() || vector_ptr.is_null() {
2178 return -1;
2179 }
2180
2181 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2182 Ok(s) => s,
2183 Err(_) => return -1,
2184 };
2185 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2186 Ok(s) => s,
2187 Err(_) => return -1,
2188 };
2189 let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2190 Ok(s) => s,
2191 Err(_) => return -1,
2192 };
2193 let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2194
2195 let metadata = if !metadata_json.is_null() {
2196 match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2197 Ok(s) => s.to_string(),
2198 Err(_) => "{}".to_string(),
2199 }
2200 } else {
2201 "{}".to_string()
2202 };
2203
2204 let db = unsafe { &(*ptr).0 };
2205 let txn = match db.begin_transaction() {
2206 Ok(t) => t,
2207 Err(_) => return -1,
2208 };
2209
2210 let vec_key = format!("{}/collections/{}/vectors/{}", ns, col, doc_id);
2212 let vec_json: Vec<String> = vector.iter().map(|f| f.to_string()).collect();
2213 let vec_value = format!(
2214 r#"{{"id":"{}","vector":[{}],"metadata":{}}}"#,
2215 doc_id, vec_json.join(","), metadata
2216 );
2217
2218 if let Err(_) = db.put(txn, vec_key.as_bytes(), vec_value.as_bytes()) {
2219 let _ = db.abort(txn);
2220 return -1;
2221 }
2222
2223 match db.commit(txn) {
2224 Ok(_) => 0,
2225 Err(_) => -1,
2226 }
2227}
2228
2229#[repr(C)]
2231pub struct CSearchResult {
2232 pub id_ptr: *mut c_char,
2233 pub score: f32,
2234 pub metadata_ptr: *mut c_char,
2235}
2236
2237#[unsafe(no_mangle)]
2243pub unsafe extern "C" fn sochdb_collection_search(
2244 ptr: *mut DatabasePtr,
2245 namespace: *const c_char,
2246 collection: *const c_char,
2247 query_ptr: *const f32,
2248 query_len: usize,
2249 k: usize,
2250 results_out: *mut CSearchResult,
2251) -> c_int {
2252 if ptr.is_null() || namespace.is_null() || collection.is_null()
2253 || query_ptr.is_null() || results_out.is_null() {
2254 return -1;
2255 }
2256
2257 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2258 Ok(s) => s,
2259 Err(_) => return -1,
2260 };
2261 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2262 Ok(s) => s,
2263 Err(_) => return -1,
2264 };
2265 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2266
2267 let db = unsafe { &(*ptr).0 };
2268 let txn = match db.begin_transaction() {
2269 Ok(t) => t,
2270 Err(_) => return -1,
2271 };
2272
2273 let prefix = format!("{}/collections/{}/vectors/", ns, col);
2275 let entries = match db.scan(txn, prefix.as_bytes()) {
2276 Ok(e) => e,
2277 Err(_) => {
2278 let _ = db.abort(txn);
2279 return -1;
2280 }
2281 };
2282 let _ = db.commit(txn);
2283
2284 let mut scored: Vec<(f32, String, String)> = Vec::new();
2286
2287 for (_key, value) in entries {
2288 let value_str = match std::str::from_utf8(&value) {
2289 Ok(s) => s,
2290 Err(_) => continue,
2291 };
2292
2293 if let Some(vec_start) = value_str.find(r#""vector":["#) {
2295 let start = vec_start + r#""vector":["#.len();
2296 if let Some(end) = value_str[start..].find(']') {
2297 let vec_str = &value_str[start..start + end];
2298 let doc_vec: Vec<f32> = vec_str
2299 .split(',')
2300 .filter_map(|s| s.trim().parse().ok())
2301 .collect();
2302
2303 if doc_vec.len() == query.len() {
2304 let score = cosine_similarity(query, &doc_vec);
2305
2306 let id = if let Some(id_pos) = value_str.find(r#""id":""#) {
2308 let id_start = id_pos + r#""id":""#.len();
2309 if let Some(id_end) = value_str[id_start..].find('"') {
2310 value_str[id_start..id_start + id_end].to_string()
2311 } else {
2312 continue;
2313 }
2314 } else {
2315 continue;
2316 };
2317
2318 let metadata = if let Some(meta_pos) = value_str.find(r#""metadata":"#) {
2320 let meta_start = meta_pos + r#""metadata":"#.len();
2321 &value_str[meta_start..value_str.len()-1]
2322 } else {
2323 "{}"
2324 };
2325
2326 scored.push((score, id, metadata.to_string()));
2327 }
2328 }
2329 }
2330 }
2331
2332 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
2334
2335 let result_count = scored.len().min(k);
2337 for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
2338 let c_id = match std::ffi::CString::new(id) {
2339 Ok(s) => s.into_raw(),
2340 Err(_) => ptr::null_mut(),
2341 };
2342 let c_meta = match std::ffi::CString::new(metadata) {
2343 Ok(s) => s.into_raw(),
2344 Err(_) => ptr::null_mut(),
2345 };
2346
2347 unsafe {
2348 (*results_out.add(i)).id_ptr = c_id;
2349 (*results_out.add(i)).score = score;
2350 (*results_out.add(i)).metadata_ptr = c_meta;
2351 }
2352 }
2353
2354 result_count as c_int
2355}
2356
2357#[unsafe(no_mangle)]
2363pub unsafe extern "C" fn sochdb_collection_keyword_search(
2364 ptr: *mut DatabasePtr,
2365 namespace: *const c_char,
2366 collection: *const c_char,
2367 query_ptr: *const c_char,
2368 k: usize,
2369 results_out: *mut CSearchResult,
2370) -> c_int {
2371 if ptr.is_null() || namespace.is_null() || collection.is_null()
2372 || query_ptr.is_null() || results_out.is_null() {
2373 return -1;
2374 }
2375
2376 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2377 Ok(s) => s,
2378 Err(_) => return -1,
2379 };
2380 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2381 Ok(s) => s,
2382 Err(_) => return -1,
2383 };
2384 let query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
2385 Ok(s) => s.to_lowercase(),
2386 Err(_) => return -1,
2387 };
2388 let terms: Vec<&str> = query_str.split_whitespace().collect();
2389 if terms.is_empty() {
2390 return 0;
2391 }
2392
2393 let db = unsafe { &(*ptr).0 };
2394 let txn = match db.begin_transaction() {
2395 Ok(t) => t,
2396 Err(_) => return -1,
2397 };
2398
2399 let prefix = format!("{}/collections/{}/vectors/", ns, col);
2401 let entries = match db.scan(txn, prefix.as_bytes()) {
2402 Ok(e) => e,
2403 Err(_) => {
2404 let _ = db.abort(txn);
2405 return -1;
2406 }
2407 };
2408 let _ = db.commit(txn);
2409
2410 let mut scored: Vec<(f32, String, String)> = Vec::new();
2412
2413 for (_key, value) in entries {
2414 let doc: Value = match serde_json::from_slice(&value) {
2416 Ok(v) => v,
2417 Err(_) => continue,
2418 };
2419
2420 let metadata_val = doc.get("metadata");
2422 let metadata_str = metadata_val.map(|v| v.to_string()).unwrap_or("{}".to_string());
2423
2424 let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
2426
2427 let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
2429
2430 let mut score = 0.0;
2431 for term in &terms {
2432 score += search_text.matches(term).count() as f32;
2433 }
2434
2435 if score > 0.0 {
2436 let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2437 if id.is_empty() { continue; }
2438
2439 scored.push((score, id, metadata_str));
2440 }
2441 }
2442
2443 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
2445
2446 let result_count = scored.len().min(k);
2448 for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
2449 let c_id = match std::ffi::CString::new(id) {
2450 Ok(s) => s.into_raw(),
2451 Err(_) => ptr::null_mut(),
2452 };
2453 let c_meta = match std::ffi::CString::new(metadata) {
2454 Ok(s) => s.into_raw(),
2455 Err(_) => ptr::null_mut(),
2456 };
2457
2458 unsafe {
2459 (*results_out.add(i)).id_ptr = c_id;
2460 (*results_out.add(i)).score = score;
2461 (*results_out.add(i)).metadata_ptr = c_meta;
2462 }
2463 }
2464
2465 result_count as c_int
2466}
2467
2468#[unsafe(no_mangle)]
2470pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
2471 if result.is_null() {
2472 return;
2473 }
2474
2475 for i in 0..count {
2476 let r = unsafe { &mut *result.add(i) };
2477 if !r.id_ptr.is_null() {
2478 let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
2479 }
2480 if !r.metadata_ptr.is_null() {
2481 let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
2482 }
2483 }
2484}
2485