1use crate::database::{Database, TxnHandle};
19use std::ffi::CStr;
20use std::os::raw::{c_char, c_int};
21use std::ptr;
22use std::slice;
23use std::sync::Arc;
24use serde_json::Value;
25use std::collections::HashMap;
26use std::sync::Mutex;
27use std::sync::OnceLock;
28use sochdb_index::hnsw::{DistanceMetric, HnswConfig, HnswIndex};
29
30pub struct DatabasePtr(Arc<Database>);
32
33struct CollectionIndex {
38 index: Arc<HnswIndex>,
39 dimension: usize,
40 metric: DistanceMetric,
41}
42
43static COLLECTION_INDEXES: OnceLock<Mutex<HashMap<String, Arc<CollectionIndex>>>> = OnceLock::new();
44
45fn collection_key(namespace: &str, collection: &str) -> String {
46 format!("{}/{}", namespace, collection)
47}
48
49fn vector_bin_key(namespace: &str, collection: &str, id_hash: u128) -> String {
50 format!("{}/collections/{}/vectors_bin/{:032x}", namespace, collection, id_hash)
51}
52
53fn metadata_key(namespace: &str, collection: &str, id_hash: u128) -> String {
54 format!("{}/collections/{}/meta/{:032x}", namespace, collection, id_hash)
55}
56
57fn hash_id_to_u128(id: &str) -> u128 {
58 let hash = blake3::hash(id.as_bytes());
59 let bytes = hash.as_bytes();
60 u128::from_le_bytes([
61 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
62 bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
63 ])
64}
65
66fn ensure_collection_index(
67 db: &Database,
68 namespace: &str,
69 collection: &str,
70 dimension: usize,
71 metric: DistanceMetric,
72) -> Arc<CollectionIndex> {
73 let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
74 let key = collection_key(namespace, collection);
75
76 let mut registry_guard = registry.lock().unwrap();
77 if let Some(existing) = registry_guard.get(&key) {
78 return existing.clone();
79 }
80
81 let mut config = HnswConfig::default();
82 config.metric = metric;
83 let index = Arc::new(HnswIndex::new(dimension, config));
84
85 let entry = Arc::new(CollectionIndex {
86 index,
87 dimension,
88 metric,
89 });
90 registry_guard.insert(key, entry.clone());
91
92 entry
93}
94
95fn resolve_collection_config(
96 db: &Database,
97 namespace: &str,
98 collection: &str,
99) -> Option<(usize, DistanceMetric)> {
100 let key = format!("{}/_collections/{}", namespace, collection);
101 let txn = db.begin_transaction().ok()?;
102 let value = db.get(txn, key.as_bytes()).ok().flatten();
103 let _ = db.commit(txn);
104 let value = value?;
105
106 let parsed: serde_json::Value = serde_json::from_slice(&value).ok()?;
107 let dimension = parsed.get("dimension")?.as_u64()? as usize;
108 let metric = match parsed.get("metric").and_then(|v| v.as_u64()).unwrap_or(0) {
109 1 => DistanceMetric::Euclidean,
110 2 => DistanceMetric::DotProduct,
111 _ => DistanceMetric::Cosine,
112 };
113 Some((dimension, metric))
114}
115
116fn serialize_vector_binary(vector: &[f32]) -> Vec<u8> {
117 let mut out = Vec::with_capacity(4 + vector.len() * 4);
118 let len = vector.len() as u32;
119 out.extend_from_slice(&len.to_le_bytes());
120 for value in vector {
121 out.extend_from_slice(&value.to_le_bytes());
122 }
123 out
124}
125
126fn decode_score(metric: DistanceMetric, distance: f32) -> f32 {
127 match metric {
128 DistanceMetric::Cosine => 1.0 - distance,
129 DistanceMetric::DotProduct => -distance,
130 DistanceMetric::Euclidean => -distance,
131 }
132}
133
134#[repr(C)]
136pub struct C_TxnHandle {
137 pub txn_id: u64,
138 pub snapshot_ts: u64,
139}
140
141#[repr(C)]
144pub struct C_CommitResult {
145 pub commit_ts: u64,
148 pub error_code: i32,
150}
151
152#[repr(C)]
157pub struct C_DatabaseConfig {
158 pub wal_enabled: bool,
160 pub wal_enabled_set: bool,
162 pub sync_mode: u8,
164 pub sync_mode_set: bool,
166 pub memtable_size_bytes: u64,
168 pub group_commit: bool,
170 pub group_commit_set: bool,
172 pub default_index_policy: u8,
174 pub default_index_policy_set: bool,
176}
177
178#[unsafe(no_mangle)]
183pub unsafe extern "C" fn sochdb_open_with_config(
184 path: *const c_char,
185 config: C_DatabaseConfig
186) -> *mut DatabasePtr {
187 if path.is_null() {
188 return ptr::null_mut();
189 }
190
191 let c_str = unsafe { CStr::from_ptr(path) };
192 let path_str = match c_str.to_str() {
193 Ok(s) => s,
194 Err(_) => return ptr::null_mut(),
195 };
196
197 let mut db_config = crate::database::DatabaseConfig::default();
199
200 if config.wal_enabled_set {
201 db_config.wal_enabled = config.wal_enabled;
202 }
203
204 if config.sync_mode_set {
205 db_config.sync_mode = match config.sync_mode {
206 0 => crate::database::SyncMode::Off,
207 1 => crate::database::SyncMode::Normal,
208 _ => crate::database::SyncMode::Full,
209 };
210 }
211
212 if config.memtable_size_bytes > 0 {
213 db_config.memtable_size_limit = config.memtable_size_bytes as usize;
214 }
215
216 if config.group_commit_set {
217 db_config.group_commit = config.group_commit;
218 }
219
220 if config.default_index_policy_set {
221 db_config.default_index_policy = match config.default_index_policy {
222 0 => crate::index_policy::IndexPolicy::WriteOptimized,
223 1 => crate::index_policy::IndexPolicy::Balanced,
224 2 => crate::index_policy::IndexPolicy::ScanOptimized,
225 _ => crate::index_policy::IndexPolicy::AppendOnly,
226 };
227 }
228
229 match Database::open_with_config(path_str, db_config) {
230 Ok(db) => {
231 let ptr = Box::new(DatabasePtr(db));
232 Box::into_raw(ptr)
233 }
234 Err(_) => ptr::null_mut(),
235 }
236}
237
238#[unsafe(no_mangle)]
243pub unsafe extern "C" fn sochdb_open(path: *const c_char) -> *mut DatabasePtr {
244 if path.is_null() {
245 return ptr::null_mut();
246 }
247
248 let c_str = unsafe { CStr::from_ptr(path) };
249 let path_str = match c_str.to_str() {
250 Ok(s) => s,
251 Err(_) => return ptr::null_mut(),
252 };
253
254 let config = crate::database::DatabaseConfig::default();
256
257 match Database::open_with_config(path_str, config) {
259 Ok(db) => {
260 let ptr = Box::new(DatabasePtr(db));
261 Box::into_raw(ptr)
262 }
263 Err(_) => ptr::null_mut(),
264 }
265}
266
267#[unsafe(no_mangle)]
278pub unsafe extern "C" fn sochdb_open_concurrent(path: *const c_char) -> *mut DatabasePtr {
279 if path.is_null() {
280 return ptr::null_mut();
281 }
282
283 let c_str = unsafe { CStr::from_ptr(path) };
284 let path_str = match c_str.to_str() {
285 Ok(s) => s,
286 Err(_) => return ptr::null_mut(),
287 };
288
289 match Database::open_concurrent(path_str) {
290 Ok(db) => {
291 let ptr = Box::new(DatabasePtr(db));
292 Box::into_raw(ptr)
293 }
294 Err(_) => ptr::null_mut(),
295 }
296}
297
298#[unsafe(no_mangle)]
303pub unsafe extern "C" fn sochdb_is_concurrent(ptr: *mut DatabasePtr) -> c_int {
304 if ptr.is_null() {
305 return 0;
306 }
307 let db = unsafe { &*ptr };
308 if db.0.is_concurrent() { 1 } else { 0 }
309}
310
311#[unsafe(no_mangle)]
315pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
316 if !ptr.is_null() {
317 unsafe {
318 let _ = Box::from_raw(ptr);
319 }
320 }
321}
322
323#[unsafe(no_mangle)]
328pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
329 if ptr.is_null() {
330 return C_TxnHandle {
331 txn_id: 0,
332 snapshot_ts: 0,
333 };
334 }
335 let db = unsafe { &(*ptr).0 };
336 match db.begin_transaction() {
337 Ok(txn) => C_TxnHandle {
338 txn_id: txn.txn_id,
339 snapshot_ts: txn.snapshot_ts,
340 },
341 Err(_) => C_TxnHandle {
342 txn_id: 0,
343 snapshot_ts: 0,
344 },
345 }
346}
347
348#[unsafe(no_mangle)]
355pub unsafe extern "C" fn sochdb_commit(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> C_CommitResult {
356 if ptr.is_null() {
357 return C_CommitResult {
358 commit_ts: 0,
359 error_code: -1,
360 };
361 }
362 let db = unsafe { &(*ptr).0 };
363 let txn = TxnHandle {
364 txn_id: handle.txn_id,
365 snapshot_ts: handle.snapshot_ts,
366 };
367 match db.commit(txn) {
368 Ok(commit_ts) => C_CommitResult {
369 commit_ts,
370 error_code: 0,
371 },
372 Err(_) => C_CommitResult {
373 commit_ts: 0,
374 error_code: -1,
375 },
376 }
377}
378
379#[unsafe(no_mangle)]
384pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
385 if ptr.is_null() {
386 return -1;
387 }
388 let db = unsafe { &(*ptr).0 };
389 let txn = TxnHandle {
390 txn_id: handle.txn_id,
391 snapshot_ts: handle.snapshot_ts,
392 };
393 match db.abort(txn) {
394 Ok(_) => 0,
395 Err(_) => -1,
396 }
397}
398
399#[unsafe(no_mangle)]
405pub unsafe extern "C" fn sochdb_put(
406 ptr: *mut DatabasePtr,
407 handle: C_TxnHandle,
408 key_ptr: *const u8,
409 key_len: usize,
410 val_ptr: *const u8,
411 val_len: usize,
412) -> c_int {
413 if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
414 return -1;
415 }
416 let db = unsafe { &(*ptr).0 };
417 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
418 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
419 let txn = TxnHandle {
420 txn_id: handle.txn_id,
421 snapshot_ts: handle.snapshot_ts,
422 };
423
424 match db.put(txn, key, val) {
425 Ok(_) => 0,
426 Err(_) => -1,
427 }
428}
429
430#[unsafe(no_mangle)]
437pub unsafe extern "C" fn sochdb_get(
438 ptr: *mut DatabasePtr,
439 handle: C_TxnHandle,
440 key_ptr: *const u8,
441 key_len: usize,
442 val_out: *mut *mut u8,
443 len_out: *mut usize,
444) -> c_int {
445 if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.is_null() {
446 return -1;
447 }
448 let db = unsafe { &(*ptr).0 };
449 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
450 let txn = TxnHandle {
451 txn_id: handle.txn_id,
452 snapshot_ts: handle.snapshot_ts,
453 };
454
455 match db.get(txn, key) {
456 Ok(Some(val)) => {
457 let mut buf = val.into_boxed_slice();
459 unsafe {
460 *val_out = buf.as_mut_ptr();
461 *len_out = buf.len();
462 }
463 let _ = Box::into_raw(buf); 0
465 }
466 Ok(None) => 1, Err(_) => -1,
468 }
469}
470
471#[unsafe(no_mangle)]
475pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
476 if !ptr.is_null() {
477 unsafe {
478 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
479 }
480 }
481}
482
483#[unsafe(no_mangle)]
488pub unsafe extern "C" fn sochdb_delete(
489 ptr: *mut DatabasePtr,
490 handle: C_TxnHandle,
491 key_ptr: *const u8,
492 key_len: usize,
493) -> c_int {
494 if ptr.is_null() || key_ptr.is_null() {
495 return -1;
496 }
497 let db = unsafe { &(*ptr).0 };
498 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
499 let txn = TxnHandle {
500 txn_id: handle.txn_id,
501 snapshot_ts: handle.snapshot_ts,
502 };
503
504 match db.delete(txn, key) {
505 Ok(_) => 0,
506 Err(_) => -1,
507 }
508}
509
510#[unsafe(no_mangle)]
514pub unsafe extern "C" fn sochdb_put_path(
515 ptr: *mut DatabasePtr,
516 handle: C_TxnHandle,
517 path_ptr: *const c_char,
518 val_ptr: *const u8,
519 val_len: usize,
520) -> c_int {
521 if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
522 return -1;
523 }
524 let db = unsafe { &(*ptr).0 };
525 let c_str = unsafe { CStr::from_ptr(path_ptr) };
526 let path_str = match c_str.to_str() {
527 Ok(s) => s,
528 Err(_) => return -1,
529 };
530 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
531 let txn = TxnHandle {
532 txn_id: handle.txn_id,
533 snapshot_ts: handle.snapshot_ts,
534 };
535
536 match db.put_path(txn, path_str, val) {
537 Ok(_) => 0,
538 Err(_) => -1,
539 }
540}
541
542#[unsafe(no_mangle)]
546pub unsafe extern "C" fn sochdb_get_path(
547 ptr: *mut DatabasePtr,
548 handle: C_TxnHandle,
549 path_ptr: *const c_char,
550 val_out: *mut *mut u8,
551 len_out: *mut usize,
552) -> c_int {
553 if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
554 return -1;
555 }
556 let db = unsafe { &(*ptr).0 };
557 let c_str = unsafe { CStr::from_ptr(path_ptr) };
558 let path_str = match c_str.to_str() {
559 Ok(s) => s,
560 Err(_) => return -1,
561 };
562 let txn = TxnHandle {
563 txn_id: handle.txn_id,
564 snapshot_ts: handle.snapshot_ts,
565 };
566
567 match db.get_path(txn, path_str) {
568 Ok(Some(val)) => {
569 let mut buf = val.into_boxed_slice();
570 unsafe {
571 *val_out = buf.as_mut_ptr();
572 *len_out = buf.len();
573 }
574 let _ = Box::into_raw(buf);
575 0
576 }
577 Ok(None) => 1,
578 Err(_) => -1,
579 }
580}
581
582#[allow(clippy::type_complexity)]
584pub struct ScanIteratorPtr(
585 Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
586);
587
588#[unsafe(no_mangle)]
592pub unsafe extern "C" fn sochdb_scan(
593 ptr: *mut DatabasePtr,
594 handle: C_TxnHandle,
595 start_ptr: *const u8,
596 start_len: usize,
597 end_ptr: *const u8,
598 end_len: usize,
599) -> *mut ScanIteratorPtr {
600 if ptr.is_null() {
601 return ptr::null_mut();
602 }
603 let db = unsafe { &(*ptr).0 };
604 let txn = TxnHandle {
605 txn_id: handle.txn_id,
606 snapshot_ts: handle.snapshot_ts,
607 };
608
609 let start = if !start_ptr.is_null() && start_len > 0 {
610 unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
611 } else {
612 vec![]
613 };
614
615 let end = if !end_ptr.is_null() && end_len > 0 {
616 unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
617 } else {
618 vec![] };
620
621 match db.scan_range(txn, &start, &end) {
634 Ok(rows) => {
635 let iter = Box::new(rows.into_iter().map(Ok));
638
639 let ptr = Box::new(ScanIteratorPtr(iter));
640 Box::into_raw(ptr)
641 }
642 Err(_) => ptr::null_mut(),
643 }
644}
645
646#[unsafe(no_mangle)]
651pub unsafe extern "C" fn sochdb_scan_prefix(
652 ptr: *mut DatabasePtr,
653 handle: C_TxnHandle,
654 prefix_ptr: *const u8,
655 prefix_len: usize,
656) -> *mut ScanIteratorPtr {
657 if ptr.is_null() {
658 return ptr::null_mut();
659 }
660 let db = unsafe { &(*ptr).0 };
661 let txn = TxnHandle {
662 txn_id: handle.txn_id,
663 snapshot_ts: handle.snapshot_ts,
664 };
665
666 let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
667 unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
668 } else {
669 vec![]
670 };
671
672 match db.scan(txn, &prefix) {
674 Ok(rows) => {
675 let prefix_owned = prefix.clone();
678 let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
679 .into_iter()
680 .filter(|(k, _)| k.starts_with(&prefix_owned))
681 .collect();
682
683 let iter = Box::new(filtered.into_iter().map(Ok));
684 let ptr = Box::new(ScanIteratorPtr(iter));
685 Box::into_raw(ptr)
686 }
687 Err(_) => ptr::null_mut(),
688 }
689}
690
691#[unsafe(no_mangle)]
696pub unsafe extern "C" fn sochdb_scan_next(
697 iter_ptr: *mut ScanIteratorPtr,
698 key_out: *mut *mut u8,
699 key_len_out: *mut usize,
700 val_out: *mut *mut u8,
701 val_len_out: *mut usize,
702) -> c_int {
703 if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
704 return -1;
705 }
706 let iter = unsafe { &mut (*iter_ptr).0 };
707
708 match iter.next() {
709 Some(Ok((key, val))) => {
710 let mut key_buf = key.into_boxed_slice();
711 let mut val_buf = val.into_boxed_slice();
712 unsafe {
713 *key_out = key_buf.as_mut_ptr();
714 *key_len_out = key_buf.len();
715 *val_out = val_buf.as_mut_ptr();
716 *val_len_out = val_buf.len();
717 }
718 let _ = Box::into_raw(key_buf);
719 let _ = Box::into_raw(val_buf);
720 0
721 }
722 Some(Err(_)) => -1,
723 None => 1, }
725}
726
727#[unsafe(no_mangle)]
731pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
732 if !ptr.is_null() {
733 unsafe {
734 let _ = Box::from_raw(ptr);
735 }
736 }
737}
738
739#[unsafe(no_mangle)]
743pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
744 if ptr.is_null() {
745 return -1;
746 }
747 let db = unsafe { &(*ptr).0 };
748 match db.flush() {
749 Ok(_) => 0,
750 Err(_) => -1,
751 }
752}
753
754#[repr(C)]
756pub struct CStorageStats {
757 pub memtable_size_bytes: u64,
758 pub wal_size_bytes: u64,
759 pub active_transactions: usize,
760 pub min_active_snapshot: u64,
761 pub last_checkpoint_lsn: u64,
762}
763
764#[unsafe(no_mangle)]
768pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
769 if ptr.is_null() {
770 return CStorageStats {
771 memtable_size_bytes: 0,
772 wal_size_bytes: 0,
773 active_transactions: 0,
774 min_active_snapshot: 0,
775 last_checkpoint_lsn: 0,
776 };
777 }
778 let db = unsafe { &(*ptr).0 };
779 let stats = db.storage_stats();
780
781 CStorageStats {
782 memtable_size_bytes: stats.memtable_size_bytes,
783 wal_size_bytes: stats.wal_size_bytes,
784 active_transactions: stats.active_transactions,
785 min_active_snapshot: stats.min_active_snapshot,
786 last_checkpoint_lsn: stats.last_checkpoint_lsn,
787 }
788}
789
790#[repr(C)]
808pub struct CBatchPut {
809 pub data: *const u8,
811 pub len: usize,
813}
814
815#[unsafe(no_mangle)]
856pub unsafe extern "C" fn sochdb_put_many(
857 ptr: *mut DatabasePtr,
858 handle: C_TxnHandle,
859 batch: CBatchPut,
860) -> c_int {
861 if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
862 return -1;
863 }
864
865 let db = unsafe { &(*ptr).0 };
866 let txn = TxnHandle {
867 txn_id: handle.txn_id,
868 snapshot_ts: handle.snapshot_ts,
869 };
870
871 let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
873
874 if data.len() < 4 {
876 return -1;
877 }
878 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
879
880 let mut offset = 4;
881 let mut success_count = 0;
882
883 for _ in 0..num_entries {
884 if offset + 8 > data.len() {
886 return success_count;
887 }
888 let key_len = u32::from_le_bytes([
889 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
890 ]) as usize;
891 let val_len = u32::from_le_bytes([
892 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]
893 ]) as usize;
894 offset += 8;
895
896 if offset + key_len + val_len > data.len() {
898 return success_count;
899 }
900 let key = &data[offset..offset + key_len];
901 offset += key_len;
902 let value = &data[offset..offset + val_len];
903 offset += val_len;
904
905 match db.put(txn, key, value) {
907 Ok(_) => success_count += 1,
908 Err(_) => return success_count,
909 }
910 }
911
912 success_count
913}
914
915#[unsafe(no_mangle)]
935pub unsafe extern "C" fn sochdb_delete_many(
936 ptr: *mut DatabasePtr,
937 handle: C_TxnHandle,
938 keys_data: *const u8,
939 keys_len: usize,
940) -> c_int {
941 if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
942 return -1;
943 }
944
945 let db = unsafe { &(*ptr).0 };
946 let txn = TxnHandle {
947 txn_id: handle.txn_id,
948 snapshot_ts: handle.snapshot_ts,
949 };
950
951 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
952
953 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
954
955 let mut offset = 4;
956 let mut success_count = 0;
957
958 for _ in 0..num_entries {
959 if offset + 4 > data.len() {
960 return success_count;
961 }
962 let key_len = u32::from_le_bytes([
963 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
964 ]) as usize;
965 offset += 4;
966
967 if offset + key_len > data.len() {
968 return success_count;
969 }
970 let key = &data[offset..offset + key_len];
971 offset += key_len;
972
973 match db.delete(txn, key) {
974 Ok(_) => success_count += 1,
975 Err(_) => return success_count,
976 }
977 }
978
979 success_count
980}
981
982#[unsafe(no_mangle)]
1010pub unsafe extern "C" fn sochdb_get_many(
1011 ptr: *mut DatabasePtr,
1012 handle: C_TxnHandle,
1013 keys_data: *const u8,
1014 keys_len: usize,
1015 result_out: *mut *mut u8,
1016 result_len_out: *mut usize,
1017) -> c_int {
1018 if ptr.is_null() || keys_data.is_null() || keys_len < 4
1019 || result_out.is_null() || result_len_out.is_null() {
1020 return -1;
1021 }
1022
1023 let db = unsafe { &(*ptr).0 };
1024 let txn = TxnHandle {
1025 txn_id: handle.txn_id,
1026 snapshot_ts: handle.snapshot_ts,
1027 };
1028
1029 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
1030
1031 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1032
1033 let mut result = Vec::with_capacity(4 + num_entries * 10); result.extend_from_slice(&(num_entries as u32).to_le_bytes());
1036
1037 let mut offset = 4;
1038
1039 for _ in 0..num_entries {
1040 if offset + 4 > data.len() {
1041 result.push(2); continue;
1043 }
1044 let key_len = u32::from_le_bytes([
1045 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
1046 ]) as usize;
1047 offset += 4;
1048
1049 if offset + key_len > data.len() {
1050 result.push(2); continue;
1052 }
1053 let key = &data[offset..offset + key_len];
1054 offset += key_len;
1055
1056 match db.get(txn, key) {
1057 Ok(Some(value)) => {
1058 result.push(0); result.extend_from_slice(&(value.len() as u32).to_le_bytes());
1060 result.extend_from_slice(&value);
1061 }
1062 Ok(None) => {
1063 result.push(1); }
1065 Err(_) => {
1066 result.push(2); }
1068 }
1069 }
1070
1071 let mut boxed = result.into_boxed_slice();
1073 unsafe {
1074 *result_out = boxed.as_mut_ptr();
1075 *result_len_out = boxed.len();
1076 }
1077 let _ = Box::into_raw(boxed); 0
1080}
1081
1082#[unsafe(no_mangle)]
1139pub unsafe extern "C" fn sochdb_scan_batch(
1140 iter_ptr: *mut ScanIteratorPtr,
1141 batch_size: usize,
1142 result_out: *mut *mut u8,
1143 result_len_out: *mut usize,
1144) -> c_int {
1145 if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0 {
1146 return -1;
1147 }
1148
1149 let iter = unsafe { &mut (*iter_ptr).0 };
1150
1151 let estimated_size = 5 + batch_size * 108;
1154 let mut result = Vec::with_capacity(estimated_size);
1155
1156 result.extend_from_slice(&[0u8; 5]); let mut count = 0u32;
1160 let mut is_done = false;
1161
1162 for _ in 0..batch_size {
1163 match iter.next() {
1164 Some(Ok((key, val))) => {
1165 result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1167 result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1168 result.extend_from_slice(&key);
1169 result.extend_from_slice(&val);
1170 count += 1;
1171 }
1172 Some(Err(_)) => {
1173 result[0..4].copy_from_slice(&count.to_le_bytes());
1175 result[4] = 0; let mut boxed = result.into_boxed_slice();
1178 unsafe {
1179 *result_out = boxed.as_mut_ptr();
1180 *result_len_out = boxed.len();
1181 }
1182 let _ = Box::into_raw(boxed);
1183 return -1;
1184 }
1185 None => {
1186 is_done = true;
1187 break;
1188 }
1189 }
1190 }
1191
1192 result[0..4].copy_from_slice(&count.to_le_bytes());
1194 result[4] = if is_done { 1 } else { 0 };
1195
1196 if count == 0 && is_done {
1198 let mut boxed = result.into_boxed_slice();
1200 unsafe {
1201 *result_out = boxed.as_mut_ptr();
1202 *result_len_out = boxed.len();
1203 }
1204 let _ = Box::into_raw(boxed);
1205 return 1; }
1207
1208 let mut boxed = result.into_boxed_slice();
1210 unsafe {
1211 *result_out = boxed.as_mut_ptr();
1212 *result_len_out = boxed.len();
1213 }
1214 let _ = Box::into_raw(boxed);
1215
1216 0 }
1218
1219#[unsafe(no_mangle)]
1239pub unsafe extern "C" fn sochdb_set_table_index_policy(
1240 ptr: *mut DatabasePtr,
1241 table_name: *const c_char,
1242 policy: u8,
1243) -> c_int {
1244 if ptr.is_null() || table_name.is_null() {
1245 return -1;
1246 }
1247
1248 let c_str = unsafe { CStr::from_ptr(table_name) };
1249 let table = match c_str.to_str() {
1250 Ok(s) => s,
1251 Err(_) => return -1,
1252 };
1253
1254 let index_policy = match policy {
1255 0 => crate::index_policy::IndexPolicy::WriteOptimized,
1256 1 => crate::index_policy::IndexPolicy::Balanced,
1257 2 => crate::index_policy::IndexPolicy::ScanOptimized,
1258 3 => crate::index_policy::IndexPolicy::AppendOnly,
1259 _ => return -2,
1260 };
1261
1262 let db = unsafe { &(*ptr).0 };
1263
1264 let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1266 db.index_registry().configure_table(config);
1267
1268 0
1269}
1270
1271#[unsafe(no_mangle)]
1283pub unsafe extern "C" fn sochdb_get_table_index_policy(
1284 ptr: *mut DatabasePtr,
1285 table_name: *const c_char,
1286) -> u8 {
1287 if ptr.is_null() || table_name.is_null() {
1288 return 255;
1289 }
1290
1291 let c_str = unsafe { CStr::from_ptr(table_name) };
1292 let table = match c_str.to_str() {
1293 Ok(s) => s,
1294 Err(_) => return 255,
1295 };
1296
1297 let db = unsafe { &(*ptr).0 };
1298 let config = db.index_registry().get_config(table);
1299
1300 match config.policy {
1301 crate::index_policy::IndexPolicy::WriteOptimized => 0,
1302 crate::index_policy::IndexPolicy::Balanced => 1,
1303 crate::index_policy::IndexPolicy::ScanOptimized => 2,
1304 crate::index_policy::IndexPolicy::AppendOnly => 3,
1305 }
1306}
1307
1308#[repr(C)]
1310pub struct C_TemporalEdge {
1311 pub from_id: *const c_char,
1312 pub edge_type: *const c_char,
1313 pub to_id: *const c_char,
1314 pub valid_from: u64,
1315 pub valid_until: u64,
1316 pub properties_json: *const c_char, }
1318
1319#[unsafe(no_mangle)]
1323pub unsafe extern "C" fn sochdb_add_temporal_edge(
1324 ptr: *mut DatabasePtr,
1325 namespace: *const c_char,
1326 edge: C_TemporalEdge,
1327) -> c_int {
1328 if ptr.is_null() || namespace.is_null() || edge.from_id.is_null()
1329 || edge.edge_type.is_null() || edge.to_id.is_null() {
1330 return -1;
1331 }
1332
1333 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1334 Ok(s) => s,
1335 Err(_) => return -1,
1336 };
1337 let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1338 Ok(s) => s,
1339 Err(_) => return -1,
1340 };
1341 let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1342 Ok(s) => s,
1343 Err(_) => return -1,
1344 };
1345 let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1346 Ok(s) => s,
1347 Err(_) => return -1,
1348 };
1349
1350 let db = unsafe { &(*ptr).0 };
1351
1352 let txn = match db.begin_transaction() {
1354 Ok(t) => t,
1355 Err(_) => return -1,
1356 };
1357
1358 let key = format!(
1360 "_graph/{}/temporal/{}/{}/{}/{:016x}",
1361 ns, from, etype, to, edge.valid_from
1362 );
1363
1364 let props_str = if edge.properties_json.is_null() {
1365 "{}".to_string()
1366 } else {
1367 match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1368 Ok(s) => s.to_string(),
1369 Err(_) => return -1,
1370 }
1371 };
1372
1373 let value = format!(
1374 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1375 from, etype, to, edge.valid_from, edge.valid_until, props_str
1376 );
1377
1378 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1379 let _ = db.abort(txn);
1380 return -1;
1381 }
1382
1383 match db.commit(txn) {
1384 Ok(_) => 0,
1385 Err(_) => -1,
1386 }
1387}
1388
1389#[unsafe(no_mangle)]
1396pub unsafe extern "C" fn sochdb_query_temporal_graph(
1397 ptr: *mut DatabasePtr,
1398 namespace: *const c_char,
1399 node_id: *const c_char,
1400 query_mode: u8,
1401 timestamp: u64, start_time: u64, end_time: u64, edge_type: *const c_char, out_len: *mut usize,
1406) -> *mut c_char {
1407 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1408 return ptr::null_mut();
1409 }
1410
1411 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1412 Ok(s) => s,
1413 Err(_) => return ptr::null_mut(),
1414 };
1415 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1416 Ok(s) => s,
1417 Err(_) => return ptr::null_mut(),
1418 };
1419
1420 let edge_filter = if edge_type.is_null() {
1421 None
1422 } else {
1423 match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1424 Ok(s) => Some(s),
1425 Err(_) => return ptr::null_mut(),
1426 }
1427 };
1428
1429 let db = unsafe { &(*ptr).0 };
1430
1431 let txn = match db.begin_transaction() {
1433 Ok(t) => t,
1434 Err(_) => return ptr::null_mut(),
1435 };
1436
1437 let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1439 let pairs = match db.scan(txn, prefix.as_bytes()) {
1440 Ok(p) => p,
1441 Err(_) => {
1442 let _ = db.abort(txn);
1443 return ptr::null_mut();
1444 }
1445 };
1446
1447 if let Err(_) = db.commit(txn) {
1449 return ptr::null_mut();
1450 }
1451
1452 let mut results = Vec::new();
1453 let now = std::time::SystemTime::now()
1454 .duration_since(std::time::UNIX_EPOCH)
1455 .unwrap()
1456 .as_millis() as u64;
1457
1458 for (_key, value) in pairs {
1459 let value_str = match std::str::from_utf8(&value) {
1461 Ok(s) => s,
1462 Err(_) => continue,
1463 };
1464
1465 if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1467 if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1468 let vf_start = valid_from_pos + r#""valid_from":"#.len();
1469 let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1470 let vu_start = valid_until_pos + r#""valid_until":"#.len();
1471 let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1472
1473 let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1474 let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1475
1476 if let Some(filter) = edge_filter {
1478 if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1479 continue;
1480 }
1481 }
1482
1483 let matches = match query_mode {
1485 0 => timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until),
1486 1 => {
1487 let edge_end = if valid_until == 0 { u64::MAX } else { valid_until };
1488 valid_from < end_time && edge_end > start_time
1489 }
1490 2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1491 _ => false,
1492 };
1493
1494 if matches {
1495 results.push(value_str.to_string());
1496 }
1497 }
1498 }
1499 }
1500
1501 let json = format!("[{}]", results.join(","));
1503 let c_string = match std::ffi::CString::new(json) {
1504 Ok(s) => s,
1505 Err(_) => return ptr::null_mut(),
1506 };
1507
1508 unsafe { *out_len = c_string.as_bytes().len() };
1509 c_string.into_raw()
1510}
1511
1512#[unsafe(no_mangle)]
1516pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1517 if !ptr.is_null() {
1518 unsafe {
1519 let _ = std::ffi::CString::from_raw(ptr);
1520 }
1521 }
1522}
1523
1524#[unsafe(no_mangle)]
1539pub unsafe extern "C" fn sochdb_graph_add_node(
1540 ptr: *mut DatabasePtr,
1541 namespace: *const c_char,
1542 node_id: *const c_char,
1543 node_type: *const c_char,
1544 properties_json: *const c_char,
1545) -> c_int {
1546 if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1547 return -1;
1548 }
1549
1550 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1551 Ok(s) => s,
1552 Err(_) => return -1,
1553 };
1554 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1555 Ok(s) => s,
1556 Err(_) => return -1,
1557 };
1558 let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1559 Ok(s) => s,
1560 Err(_) => return -1,
1561 };
1562 let props = if properties_json.is_null() {
1563 "{}".to_string()
1564 } else {
1565 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1566 Ok(s) => s.to_string(),
1567 Err(_) => return -1,
1568 }
1569 };
1570
1571 let db = unsafe { &(*ptr).0 };
1572
1573 let txn = match db.begin_transaction() {
1574 Ok(t) => t,
1575 Err(_) => return -1,
1576 };
1577
1578 let key = format!("_graph/{}/nodes/{}", ns, id);
1579 let value = format!(
1580 r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1581 id, ntype, props
1582 );
1583
1584 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1585 let _ = db.abort(txn);
1586 return -1;
1587 }
1588
1589 match db.commit(txn) {
1590 Ok(_) => 0,
1591 Err(_) => -1,
1592 }
1593}
1594
1595#[unsafe(no_mangle)]
1606pub unsafe extern "C" fn sochdb_graph_add_edge(
1607 ptr: *mut DatabasePtr,
1608 namespace: *const c_char,
1609 from_id: *const c_char,
1610 edge_type: *const c_char,
1611 to_id: *const c_char,
1612 properties_json: *const c_char,
1613) -> c_int {
1614 if ptr.is_null() || namespace.is_null() || from_id.is_null()
1615 || edge_type.is_null() || to_id.is_null() {
1616 return -1;
1617 }
1618
1619 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1620 Ok(s) => s,
1621 Err(_) => return -1,
1622 };
1623 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1624 Ok(s) => s,
1625 Err(_) => return -1,
1626 };
1627 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1628 Ok(s) => s,
1629 Err(_) => return -1,
1630 };
1631 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1632 Ok(s) => s,
1633 Err(_) => return -1,
1634 };
1635 let props = if properties_json.is_null() {
1636 "{}".to_string()
1637 } else {
1638 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1639 Ok(s) => s.to_string(),
1640 Err(_) => return -1,
1641 }
1642 };
1643
1644 let db = unsafe { &(*ptr).0 };
1645
1646 let txn = match db.begin_transaction() {
1647 Ok(t) => t,
1648 Err(_) => return -1,
1649 };
1650
1651 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1652 let value = format!(
1653 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1654 from, etype, to, props
1655 );
1656
1657 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1658 let _ = db.abort(txn);
1659 return -1;
1660 }
1661
1662 match db.commit(txn) {
1663 Ok(_) => 0,
1664 Err(_) => -1,
1665 }
1666}
1667
1668#[unsafe(no_mangle)]
1678pub unsafe extern "C" fn sochdb_graph_traverse(
1679 ptr: *mut DatabasePtr,
1680 namespace: *const c_char,
1681 start_node: *const c_char,
1682 max_depth: usize,
1683 order: u8, out_len: *mut usize,
1685) -> *mut c_char {
1686 if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1687 return ptr::null_mut();
1688 }
1689
1690 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1691 Ok(s) => s,
1692 Err(_) => return ptr::null_mut(),
1693 };
1694 let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1695 Ok(s) => s,
1696 Err(_) => return ptr::null_mut(),
1697 };
1698
1699 let db = unsafe { &(*ptr).0 };
1700
1701 let txn = match db.begin_transaction() {
1702 Ok(t) => t,
1703 Err(_) => return ptr::null_mut(),
1704 };
1705
1706 let mut visited_nodes = std::collections::HashSet::new();
1708 let mut nodes_json = Vec::new();
1709 let mut edges_json = Vec::new();
1710
1711 let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1713
1714 while let Some((current_node, depth)) = if order == 0 {
1715 if frontier.is_empty() { None } else { Some(frontier.remove(0)) }
1717 } else {
1718 frontier.pop()
1720 } {
1721 if depth > max_depth || visited_nodes.contains(¤t_node) {
1722 continue;
1723 }
1724 visited_nodes.insert(current_node.clone());
1725
1726 let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1728 if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1729 if let Ok(s) = std::str::from_utf8(&node_data) {
1730 nodes_json.push(s.to_string());
1731 }
1732 }
1733
1734 let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1736 if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1737 for (_key, value) in edges {
1738 if let Ok(edge_str) = std::str::from_utf8(&value) {
1739 edges_json.push(edge_str.to_string());
1740
1741 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
1743 let start_idx = to_pos + r#""to_id":""#.len();
1744 if let Some(end_idx) = edge_str[start_idx..].find('"') {
1745 let to_id = &edge_str[start_idx..start_idx + end_idx];
1746 if !visited_nodes.contains(to_id) {
1747 frontier.push((to_id.to_string(), depth + 1));
1748 }
1749 }
1750 }
1751 }
1752 }
1753 }
1754 }
1755
1756 if let Err(_) = db.commit(txn) {
1757 return ptr::null_mut();
1758 }
1759
1760 let result = format!(
1761 r#"{{"nodes":[{}],"edges":[{}]}}"#,
1762 nodes_json.join(","),
1763 edges_json.join(",")
1764 );
1765
1766 let c_string = match std::ffi::CString::new(result) {
1767 Ok(s) => s,
1768 Err(_) => return ptr::null_mut(),
1769 };
1770
1771 unsafe { *out_len = c_string.as_bytes().len() };
1772 c_string.into_raw()
1773}
1774
1775#[unsafe(no_mangle)]
1788pub unsafe extern "C" fn sochdb_cache_put(
1789 ptr: *mut DatabasePtr,
1790 cache_name: *const c_char,
1791 key: *const c_char,
1792 value: *const c_char,
1793 embedding_ptr: *const f32,
1794 embedding_len: usize,
1795 ttl_seconds: u64,
1796) -> c_int {
1797 if ptr.is_null() || cache_name.is_null() || key.is_null()
1798 || value.is_null() || embedding_ptr.is_null() {
1799 return -1;
1800 }
1801
1802 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1803 Ok(s) => s,
1804 Err(_) => return -1,
1805 };
1806 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
1807 Ok(s) => s,
1808 Err(_) => return -1,
1809 };
1810 let v = match unsafe { CStr::from_ptr(value) }.to_str() {
1811 Ok(s) => s,
1812 Err(_) => return -1,
1813 };
1814 let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
1815
1816 let db = unsafe { &(*ptr).0 };
1817
1818 let txn = match db.begin_transaction() {
1819 Ok(t) => t,
1820 Err(_) => return -1,
1821 };
1822
1823 let expires_at = if ttl_seconds > 0 {
1825 std::time::SystemTime::now()
1826 .duration_since(std::time::UNIX_EPOCH)
1827 .unwrap()
1828 .as_secs() + ttl_seconds
1829 } else {
1830 0 };
1832
1833 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
1835 let cache_key = format!("_cache/{}/{}", cache, key_hash);
1836
1837 let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
1839
1840 let cache_value = format!(
1841 r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
1842 k, v, embedding_json.join(","), expires_at
1843 );
1844
1845 if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
1846 let _ = db.abort(txn);
1847 return -1;
1848 }
1849
1850 match db.commit(txn) {
1851 Ok(_) => 0,
1852 Err(_) => -1,
1853 }
1854}
1855
1856#[unsafe(no_mangle)]
1864pub unsafe extern "C" fn sochdb_cache_get(
1865 ptr: *mut DatabasePtr,
1866 cache_name: *const c_char,
1867 query_embedding_ptr: *const f32,
1868 embedding_len: usize,
1869 threshold: f32,
1870 out_len: *mut usize,
1871) -> *mut c_char {
1872 if ptr.is_null() || cache_name.is_null() || query_embedding_ptr.is_null() || out_len.is_null() {
1873 return ptr::null_mut();
1874 }
1875
1876 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1877 Ok(s) => s,
1878 Err(_) => return ptr::null_mut(),
1879 };
1880 let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
1881
1882 let db = unsafe { &(*ptr).0 };
1883
1884 let txn = match db.begin_transaction() {
1885 Ok(t) => t,
1886 Err(_) => return ptr::null_mut(),
1887 };
1888
1889 let prefix = format!("_cache/{}/", cache);
1890 let entries = match db.scan(txn, prefix.as_bytes()) {
1891 Ok(e) => e,
1892 Err(_) => {
1893 let _ = db.abort(txn);
1894 return ptr::null_mut();
1895 }
1896 };
1897
1898 let _ = db.commit(txn);
1899
1900 let now = std::time::SystemTime::now()
1901 .duration_since(std::time::UNIX_EPOCH)
1902 .unwrap()
1903 .as_secs();
1904
1905 let mut best_match: Option<(f32, String)> = None;
1906
1907 for (_key, value) in entries {
1908 let value_str = match std::str::from_utf8(&value) {
1909 Ok(s) => s,
1910 Err(_) => continue,
1911 };
1912
1913 if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
1915 let exp_start = exp_pos + r#""expires_at":"#.len();
1916 if let Some(exp_end) = value_str[exp_start..].find('}') {
1917 let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
1918 .parse()
1919 .unwrap_or(0);
1920 if expires_at > 0 && now > expires_at {
1921 continue; }
1923 }
1924 }
1925
1926 if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
1928 let emb_start = emb_pos + r#""embedding":["#.len();
1929 if let Some(emb_end) = value_str[emb_start..].find(']') {
1930 let emb_str = &value_str[emb_start..emb_start + emb_end];
1931 let cached_embedding: Vec<f32> = emb_str
1932 .split(',')
1933 .filter_map(|s| s.trim().parse().ok())
1934 .collect();
1935
1936 if cached_embedding.len() == query.len() {
1937 let similarity = cosine_similarity(query, &cached_embedding);
1938 if similarity >= threshold {
1939 if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
1940 if let Some(val_pos) = value_str.find(r#""value":""#) {
1942 let val_start = val_pos + r#""value":""#.len();
1943 if let Some(val_end) = value_str[val_start..].find('"') {
1944 let cached_value = &value_str[val_start..val_start + val_end];
1945 best_match = Some((similarity, cached_value.to_string()));
1946 }
1947 }
1948 }
1949 }
1950 }
1951 }
1952 }
1953 }
1954
1955 match best_match {
1956 Some((_, value)) => {
1957 let c_string = match std::ffi::CString::new(value) {
1958 Ok(s) => s,
1959 Err(_) => return ptr::null_mut(),
1960 };
1961 unsafe { *out_len = c_string.as_bytes().len() };
1962 c_string.into_raw()
1963 }
1964 None => ptr::null_mut(),
1965 }
1966}
1967
1968fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
1971 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1972 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
1973 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1974 if norm_a == 0.0 || norm_b == 0.0 {
1975 0.0
1976 } else {
1977 let similarity = dot / (norm_a * norm_b);
1978 (similarity + 1.0) / 2.0
1981 }
1982}
1983
1984#[unsafe(no_mangle)]
1999pub unsafe extern "C" fn sochdb_trace_start(
2000 ptr: *mut DatabasePtr,
2001 name: *const c_char,
2002 trace_id_out: *mut *mut c_char,
2003 span_id_out: *mut *mut c_char,
2004) -> c_int {
2005 if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
2006 return -1;
2007 }
2008
2009 let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2010 Ok(s) => s,
2011 Err(_) => return -1,
2012 };
2013
2014 let db = unsafe { &(*ptr).0 };
2015
2016 let trace_id = format!("trace_{:016x}", rand_u64());
2018 let span_id = format!("span_{:016x}", rand_u64());
2019
2020 let txn = match db.begin_transaction() {
2021 Ok(t) => t,
2022 Err(_) => return -1,
2023 };
2024
2025 let now = std::time::SystemTime::now()
2026 .duration_since(std::time::UNIX_EPOCH)
2027 .unwrap()
2028 .as_micros() as u64;
2029
2030 let trace_key = format!("_traces/{}", trace_id);
2032 let trace_value = format!(
2033 r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
2034 trace_id, trace_name, now, span_id
2035 );
2036
2037 if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
2038 let _ = db.abort(txn);
2039 return -1;
2040 }
2041
2042 let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
2044 let span_value = format!(
2045 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
2046 span_id, trace_name, now
2047 );
2048
2049 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2050 let _ = db.abort(txn);
2051 return -1;
2052 }
2053
2054 if let Err(_) = db.commit(txn) {
2055 return -1;
2056 }
2057
2058 let trace_c = match std::ffi::CString::new(trace_id) {
2060 Ok(s) => s,
2061 Err(_) => return -1,
2062 };
2063 let span_c = match std::ffi::CString::new(span_id) {
2064 Ok(s) => s,
2065 Err(_) => return -1,
2066 };
2067
2068 unsafe {
2069 *trace_id_out = trace_c.into_raw();
2070 *span_id_out = span_c.into_raw();
2071 }
2072
2073 0
2074}
2075
2076#[unsafe(no_mangle)]
2083pub unsafe extern "C" fn sochdb_trace_span_start(
2084 ptr: *mut DatabasePtr,
2085 trace_id: *const c_char,
2086 parent_span_id: *const c_char,
2087 name: *const c_char,
2088 span_id_out: *mut *mut c_char,
2089) -> c_int {
2090 if ptr.is_null() || trace_id.is_null() || parent_span_id.is_null()
2091 || name.is_null() || span_id_out.is_null() {
2092 return -1;
2093 }
2094
2095 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2096 Ok(s) => s,
2097 Err(_) => return -1,
2098 };
2099 let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
2100 Ok(s) => s,
2101 Err(_) => return -1,
2102 };
2103 let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2104 Ok(s) => s,
2105 Err(_) => return -1,
2106 };
2107
2108 let db = unsafe { &(*ptr).0 };
2109 let span_id = format!("span_{:016x}", rand_u64());
2110
2111 let txn = match db.begin_transaction() {
2112 Ok(t) => t,
2113 Err(_) => return -1,
2114 };
2115
2116 let now = std::time::SystemTime::now()
2117 .duration_since(std::time::UNIX_EPOCH)
2118 .unwrap()
2119 .as_micros() as u64;
2120
2121 let span_key = format!("_traces/{}/spans/{}", tid, span_id);
2122 let span_value = format!(
2123 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
2124 span_id, span_name, now, pid
2125 );
2126
2127 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2128 let _ = db.abort(txn);
2129 return -1;
2130 }
2131
2132 if let Err(_) = db.commit(txn) {
2133 return -1;
2134 }
2135
2136 let span_c = match std::ffi::CString::new(span_id) {
2137 Ok(s) => s,
2138 Err(_) => return -1,
2139 };
2140
2141 unsafe { *span_id_out = span_c.into_raw() };
2142 0
2143}
2144
2145#[unsafe(no_mangle)]
2155pub unsafe extern "C" fn sochdb_trace_span_end(
2156 ptr: *mut DatabasePtr,
2157 trace_id: *const c_char,
2158 span_id: *const c_char,
2159 status: u8,
2160) -> i64 {
2161 if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2162 return -1;
2163 }
2164
2165 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2166 Ok(s) => s,
2167 Err(_) => return -1,
2168 };
2169 let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2170 Ok(s) => s,
2171 Err(_) => return -1,
2172 };
2173
2174 let db = unsafe { &(*ptr).0 };
2175
2176 let txn = match db.begin_transaction() {
2177 Ok(t) => t,
2178 Err(_) => return -1,
2179 };
2180
2181 let span_key = format!("_traces/{}/spans/{}", tid, sid);
2182
2183 let span_data = match db.get(txn, span_key.as_bytes()) {
2185 Ok(Some(data)) => data,
2186 _ => {
2187 let _ = db.abort(txn);
2188 return -1;
2189 }
2190 };
2191
2192 let span_str = match std::str::from_utf8(&span_data) {
2193 Ok(s) => s,
2194 Err(_) => {
2195 let _ = db.abort(txn);
2196 return -1;
2197 }
2198 };
2199
2200 let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2202 let start = pos + r#""start_us":"#.len();
2203 if let Some(end) = span_str[start..].find(',') {
2204 span_str[start..start + end].parse().unwrap_or(0u64)
2205 } else {
2206 0u64
2207 }
2208 } else {
2209 0u64
2210 };
2211
2212 let now = std::time::SystemTime::now()
2213 .duration_since(std::time::UNIX_EPOCH)
2214 .unwrap()
2215 .as_micros() as u64;
2216
2217 let duration_us = now.saturating_sub(start_us);
2218 let status_str = match status {
2219 1 => "ok",
2220 2 => "error",
2221 _ => "unset",
2222 };
2223
2224 let new_span = span_str
2226 .replace(r#""status":"active""#, &format!(r#""status":"{}","end_us":{},"duration_us":{}"#, status_str, now, duration_us));
2227
2228 if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2229 let _ = db.abort(txn);
2230 return -1;
2231 }
2232
2233 if let Err(_) = db.commit(txn) {
2234 return -1;
2235 }
2236
2237 duration_us as i64
2238}
2239
2240fn rand_u64() -> u64 {
2242 use std::sync::atomic::{AtomicU64, Ordering};
2243 static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2244
2245 let mut s = STATE.load(Ordering::Relaxed);
2246 if s == 0 {
2247 s = std::time::SystemTime::now()
2248 .duration_since(std::time::UNIX_EPOCH)
2249 .unwrap()
2250 .as_nanos() as u64;
2251 }
2252 s ^= s >> 12;
2253 s ^= s << 25;
2254 s ^= s >> 27;
2255 STATE.store(s, Ordering::Relaxed);
2256 s.wrapping_mul(0x2545F4914F6CDD1D)
2257}
2258
2259#[unsafe(no_mangle)]
2269pub unsafe extern "C" fn sochdb_collection_create(
2270 ptr: *mut DatabasePtr,
2271 namespace: *const c_char,
2272 collection: *const c_char,
2273 dimension: usize,
2274 dist_type: u8, ) -> c_int {
2276 if ptr.is_null() || namespace.is_null() || collection.is_null() {
2277 return -1;
2278 }
2279
2280 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2281 Ok(s) => s,
2282 Err(_) => return -1,
2283 };
2284 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2285 Ok(s) => s,
2286 Err(_) => return -1,
2287 };
2288
2289 let db = unsafe { &(*ptr).0 };
2290 let txn = match db.begin_transaction() {
2291 Ok(t) => t,
2292 Err(_) => return -1,
2293 };
2294
2295 let config_key = format!("{}/_collections/{}", ns, col);
2297 let config_value = format!(
2298 r#"{{"dimension":{},"metric":{}}}"#,
2299 dimension, dist_type
2300 );
2301
2302 if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2303 let _ = db.abort(txn);
2304 return -1;
2305 }
2306
2307 let result = match db.commit(txn) {
2308 Ok(_) => 0,
2309 Err(_) => -1,
2310 };
2311
2312 if result == 0 {
2313 let metric = match dist_type {
2314 1 => DistanceMetric::Euclidean,
2315 2 => DistanceMetric::DotProduct,
2316 _ => DistanceMetric::Cosine,
2317 };
2318 let _ = ensure_collection_index(db, ns, col, dimension, metric);
2319 }
2320
2321 result
2322}
2323
2324#[unsafe(no_mangle)]
2330pub unsafe extern "C" fn sochdb_collection_insert(
2331 ptr: *mut DatabasePtr,
2332 namespace: *const c_char,
2333 collection: *const c_char,
2334 id: *const c_char,
2335 vector_ptr: *const f32,
2336 vector_len: usize,
2337 metadata_json: *const c_char, ) -> c_int {
2339 if ptr.is_null() || namespace.is_null() || collection.is_null()
2340 || id.is_null() || vector_ptr.is_null() {
2341 return -1;
2342 }
2343
2344 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2345 Ok(s) => s,
2346 Err(_) => return -1,
2347 };
2348 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2349 Ok(s) => s,
2350 Err(_) => return -1,
2351 };
2352 let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2353 Ok(s) => s,
2354 Err(_) => return -1,
2355 };
2356 let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2357 let db = unsafe { &(*ptr).0 };
2358
2359 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2360 Some(config) => config,
2361 None => (vector_len, DistanceMetric::Cosine),
2362 };
2363 if vector_len != dimension {
2364 return -1;
2365 }
2366
2367 let metadata = if !metadata_json.is_null() {
2368 match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2369 Ok(s) => s.to_string(),
2370 Err(_) => "{}".to_string(),
2371 }
2372 } else {
2373 "{}".to_string()
2374 };
2375
2376 let txn = match db.begin_transaction() {
2377 Ok(t) => t,
2378 Err(_) => return -1,
2379 };
2380
2381 let id_hash = hash_id_to_u128(doc_id);
2382 let vec_key = vector_bin_key(ns, col, id_hash);
2383 let vec_value = serialize_vector_binary(vector);
2384
2385 if let Err(_) = db.put(txn, vec_key.as_bytes(), &vec_value) {
2386 let _ = db.abort(txn);
2387 return -1;
2388 }
2389
2390 let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2391 Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2392 Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2393 };
2394 let meta_key = metadata_key(ns, col, id_hash);
2395 if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2396 if let Err(_) = db.put(txn, meta_key.as_bytes(), &meta_bytes) {
2397 let _ = db.abort(txn);
2398 return -1;
2399 }
2400 }
2401
2402 if let Err(_) = db.commit(txn) {
2403 return -1;
2404 }
2405
2406 let index = ensure_collection_index(db, ns, col, dimension, metric);
2407 let _ = index.index.insert(id_hash, vector.to_vec());
2408
2409 0
2410}
2411
2412#[repr(C)]
2414pub struct CSearchResult {
2415 pub id_ptr: *mut c_char,
2416 pub score: f32,
2417 pub metadata_ptr: *mut c_char,
2418}
2419
2420#[unsafe(no_mangle)]
2426pub unsafe extern "C" fn sochdb_collection_search(
2427 ptr: *mut DatabasePtr,
2428 namespace: *const c_char,
2429 collection: *const c_char,
2430 query_ptr: *const f32,
2431 query_len: usize,
2432 k: usize,
2433 results_out: *mut CSearchResult,
2434) -> c_int {
2435 if ptr.is_null() || namespace.is_null() || collection.is_null()
2436 || query_ptr.is_null() || results_out.is_null() {
2437 return -1;
2438 }
2439 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2440 Ok(s) => s,
2441 Err(_) => return -1,
2442 };
2443 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2444 Ok(s) => s,
2445 Err(_) => return -1,
2446 };
2447 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2448 let db = unsafe { &(*ptr).0 };
2449 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2450 Some(config) => config,
2451 None => return 0,
2452 };
2453
2454 if query_len != dimension {
2455 return -1;
2456 }
2457
2458 let index = ensure_collection_index(db, ns, col, dimension, metric);
2459 let mut scored = match index.index.search(query, k) {
2460 Ok(results) => results,
2461 Err(_) => return -1,
2462 };
2463
2464 let result_count = scored.len().min(k);
2465 for (i, (id_hash, distance)) in scored.drain(..result_count).enumerate() {
2466 let meta_key = metadata_key(ns, col, id_hash);
2467 let txn = match db.begin_transaction() {
2468 Ok(t) => t,
2469 Err(_) => return -1,
2470 };
2471 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2472 let _ = db.commit(txn);
2473
2474 let mut id_value = String::new();
2475 let mut metadata_json = serde_json::json!({});
2476 if let Some(bytes) = meta_value.as_deref() {
2477 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(bytes) {
2478 id_value = parsed.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2479 metadata_json = parsed.get("metadata").cloned().unwrap_or(serde_json::json!({}));
2480 }
2481 }
2482 let metadata = serde_json::to_string(&metadata_json).unwrap_or_else(|_| "{}".to_string());
2483
2484 let c_id = match std::ffi::CString::new(id_value) {
2485 Ok(s) => s.into_raw(),
2486 Err(_) => ptr::null_mut(),
2487 };
2488 let c_meta = match std::ffi::CString::new(metadata) {
2489 Ok(s) => s.into_raw(),
2490 Err(_) => ptr::null_mut(),
2491 };
2492
2493 unsafe {
2494 (*results_out.add(i)).id_ptr = c_id;
2495 (*results_out.add(i)).score = decode_score(metric, distance);
2496 (*results_out.add(i)).metadata_ptr = c_meta;
2497 }
2498 }
2499
2500 result_count as c_int
2501}
2502
2503#[unsafe(no_mangle)]
2509pub unsafe extern "C" fn sochdb_collection_search_soa(
2510 ptr: *mut DatabasePtr,
2511 namespace: *const c_char,
2512 collection: *const c_char,
2513 query_ptr: *const f32,
2514 query_len: usize,
2515 k: usize,
2516 min_score: f32,
2517 filter_json: *const c_char,
2518 ids_hi_out: *mut *mut u64,
2519 ids_lo_out: *mut *mut u64,
2520 scores_out: *mut *mut f32,
2521 len_out: *mut usize,
2522) -> c_int {
2523 if ptr.is_null() || namespace.is_null() || collection.is_null()
2524 || query_ptr.is_null() || ids_hi_out.is_null() || ids_lo_out.is_null()
2525 || scores_out.is_null() || len_out.is_null() {
2526 return -1;
2527 }
2528
2529 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2530 Ok(s) => s,
2531 Err(_) => return -1,
2532 };
2533 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2534 Ok(s) => s,
2535 Err(_) => return -1,
2536 };
2537 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2538 let db = unsafe { &(*ptr).0 };
2539
2540 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2541 Some(config) => config,
2542 None => return 0,
2543 };
2544 if query_len != dimension {
2545 return -1;
2546 }
2547
2548 let filter = if !filter_json.is_null() {
2549 match unsafe { CStr::from_ptr(filter_json) }.to_str() {
2550 Ok(s) => serde_json::from_str::<serde_json::Value>(s).ok(),
2551 Err(_) => None,
2552 }
2553 } else {
2554 None
2555 };
2556
2557 let index = ensure_collection_index(db, ns, col, dimension, metric);
2558 let results = match index.index.search(query, k) {
2559 Ok(results) => results,
2560 Err(_) => return -1,
2561 };
2562
2563 let mut ids_hi: Vec<u64> = Vec::with_capacity(results.len());
2564 let mut ids_lo: Vec<u64> = Vec::with_capacity(results.len());
2565 let mut scores: Vec<f32> = Vec::with_capacity(results.len());
2566
2567 for (id_hash, distance) in results {
2568 let score = decode_score(metric, distance);
2569 if min_score > 0.0 && score < min_score {
2570 continue;
2571 }
2572
2573 if let Some(filter_value) = &filter {
2574 let meta_key = metadata_key(ns, col, id_hash);
2575 let txn = match db.begin_transaction() {
2576 Ok(t) => t,
2577 Err(_) => return -1,
2578 };
2579 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2580 let _ = db.commit(txn);
2581 let meta_value = match meta_value {
2582 Some(value) => value,
2583 None => continue,
2584 };
2585 let parsed = match serde_json::from_slice::<serde_json::Value>(&meta_value) {
2586 Ok(value) => value,
2587 Err(_) => continue,
2588 };
2589 let metadata = parsed.get("metadata").cloned().unwrap_or(Value::Null);
2590
2591 if !metadata_matches_filter(&metadata, filter_value) {
2592 continue;
2593 }
2594 }
2595
2596 ids_hi.push((id_hash >> 64) as u64);
2597 ids_lo.push((id_hash & u128::from(u64::MAX)) as u64);
2598 scores.push(score);
2599 if ids_hi.len() >= k {
2600 break;
2601 }
2602 }
2603
2604 let len = ids_hi.len();
2605 let mut ids_hi_box = ids_hi.into_boxed_slice();
2606 let mut ids_lo_box = ids_lo.into_boxed_slice();
2607 let mut scores_box = scores.into_boxed_slice();
2608
2609 unsafe {
2610 *len_out = len;
2611 *ids_hi_out = ids_hi_box.as_mut_ptr();
2612 *ids_lo_out = ids_lo_box.as_mut_ptr();
2613 *scores_out = scores_box.as_mut_ptr();
2614 }
2615
2616 std::mem::forget(ids_hi_box);
2617 std::mem::forget(ids_lo_box);
2618 std::mem::forget(scores_box);
2619
2620 len as c_int
2621}
2622
2623#[unsafe(no_mangle)]
2625pub unsafe extern "C" fn sochdb_collection_fetch_metadata_json(
2626 ptr: *mut DatabasePtr,
2627 namespace: *const c_char,
2628 collection: *const c_char,
2629 ids_hi_ptr: *const u64,
2630 ids_lo_ptr: *const u64,
2631 ids_len: usize,
2632) -> *mut c_char {
2633 if ptr.is_null() || namespace.is_null() || collection.is_null()
2634 || ids_hi_ptr.is_null() || ids_lo_ptr.is_null() {
2635 return ptr::null_mut();
2636 }
2637
2638 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2639 Ok(s) => s,
2640 Err(_) => return ptr::null_mut(),
2641 };
2642 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2643 Ok(s) => s,
2644 Err(_) => return ptr::null_mut(),
2645 };
2646 let ids_hi = unsafe { slice::from_raw_parts(ids_hi_ptr, ids_len) };
2647 let ids_lo = unsafe { slice::from_raw_parts(ids_lo_ptr, ids_len) };
2648 let db = unsafe { &(*ptr).0 };
2649
2650 let mut results = Vec::with_capacity(ids_len);
2651 for i in 0..ids_len {
2652 let id_hash = ((ids_hi[i] as u128) << 64) | (ids_lo[i] as u128);
2653 let meta_key = metadata_key(ns, col, id_hash);
2654 let txn = match db.begin_transaction() {
2655 Ok(t) => t,
2656 Err(_) => return ptr::null_mut(),
2657 };
2658 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2659 let _ = db.commit(txn);
2660 if let Some(bytes) = meta_value {
2661 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&bytes) {
2662 results.push(parsed);
2663 continue;
2664 }
2665 }
2666 results.push(serde_json::json!({"id": "", "metadata": {}}));
2667 }
2668
2669 match serde_json::to_string(&results) {
2670 Ok(json) => match std::ffi::CString::new(json) {
2671 Ok(cstr) => cstr.into_raw(),
2672 Err(_) => ptr::null_mut(),
2673 },
2674 Err(_) => ptr::null_mut(),
2675 }
2676}
2677
2678#[unsafe(no_mangle)]
2680pub unsafe extern "C" fn sochdb_collection_free_u64(ptr: *mut u64, len: usize) {
2681 if ptr.is_null() || len == 0 {
2682 return;
2683 }
2684 unsafe {
2685 let _ = Vec::from_raw_parts(ptr, len, len);
2686 }
2687}
2688
2689#[unsafe(no_mangle)]
2690pub unsafe extern "C" fn sochdb_collection_free_f32(ptr: *mut f32, len: usize) {
2691 if ptr.is_null() || len == 0 {
2692 return;
2693 }
2694 unsafe {
2695 let _ = Vec::from_raw_parts(ptr, len, len);
2696 }
2697}
2698
2699fn metadata_matches_filter(metadata: &Value, filter: &Value) -> bool {
2700 let filter_obj = match filter.as_object() {
2701 Some(obj) => obj,
2702 None => return true,
2703 };
2704 let metadata_obj = match metadata.as_object() {
2705 Some(obj) => obj,
2706 None => return false,
2707 };
2708
2709 for (key, expected) in filter_obj.iter() {
2710 match metadata_obj.get(key) {
2711 Some(actual) if actual == expected => {}
2712 _ => return false,
2713 }
2714 }
2715
2716 true
2717}
2718
2719#[unsafe(no_mangle)]
2725pub unsafe extern "C" fn sochdb_collection_keyword_search(
2726 ptr: *mut DatabasePtr,
2727 namespace: *const c_char,
2728 collection: *const c_char,
2729 query_ptr: *const c_char,
2730 k: usize,
2731 results_out: *mut CSearchResult,
2732) -> c_int {
2733 if ptr.is_null() || namespace.is_null() || collection.is_null()
2734 || query_ptr.is_null() || results_out.is_null() {
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 query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
2747 Ok(s) => s.to_lowercase(),
2748 Err(_) => return -1,
2749 };
2750 let terms: Vec<&str> = query_str.split_whitespace().collect();
2751 if terms.is_empty() {
2752 return 0;
2753 }
2754
2755 let db = unsafe { &(*ptr).0 };
2756 let txn = match db.begin_transaction() {
2757 Ok(t) => t,
2758 Err(_) => return -1,
2759 };
2760
2761 let prefix = format!("{}/collections/{}/vectors/", ns, col);
2763 let entries = match db.scan(txn, prefix.as_bytes()) {
2764 Ok(e) => e,
2765 Err(_) => {
2766 let _ = db.abort(txn);
2767 return -1;
2768 }
2769 };
2770 let _ = db.commit(txn);
2771
2772 let mut scored: Vec<(f32, String, String)> = Vec::new();
2774
2775 for (_key, value) in entries {
2776 let doc: Value = match serde_json::from_slice(&value) {
2778 Ok(v) => v,
2779 Err(_) => continue,
2780 };
2781
2782 let metadata_val = doc.get("metadata");
2784 let metadata_str = metadata_val.map(|v| v.to_string()).unwrap_or("{}".to_string());
2785
2786 let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
2788
2789 let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
2791
2792 let mut score = 0.0;
2793 for term in &terms {
2794 score += search_text.matches(term).count() as f32;
2795 }
2796
2797 if score > 0.0 {
2798 let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2799 if id.is_empty() { continue; }
2800
2801 scored.push((score, id, metadata_str));
2802 }
2803 }
2804
2805 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
2807
2808 let result_count = scored.len().min(k);
2810 for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
2811 let c_id = match std::ffi::CString::new(id) {
2812 Ok(s) => s.into_raw(),
2813 Err(_) => ptr::null_mut(),
2814 };
2815 let c_meta = match std::ffi::CString::new(metadata) {
2816 Ok(s) => s.into_raw(),
2817 Err(_) => ptr::null_mut(),
2818 };
2819
2820 unsafe {
2821 (*results_out.add(i)).id_ptr = c_id;
2822 (*results_out.add(i)).score = score;
2823 (*results_out.add(i)).metadata_ptr = c_meta;
2824 }
2825 }
2826
2827 result_count as c_int
2828}
2829
2830#[unsafe(no_mangle)]
2832pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
2833 if result.is_null() {
2834 return;
2835 }
2836
2837 for i in 0..count {
2838 let r = unsafe { &mut *result.add(i) };
2839 if !r.id_ptr.is_null() {
2840 let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
2841 }
2842 if !r.metadata_ptr.is_null() {
2843 let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
2844 }
2845 }
2846}
2847