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;
22use std::collections::HashMap;
23use std::sync::Mutex;
24use std::sync::OnceLock;
25use sochdb_index::hnsw::{DistanceMetric, HnswConfig, HnswIndex};
26
27pub struct DatabasePtr(Arc<Database>);
29
30struct CollectionIndex {
35 index: Arc<HnswIndex>,
36 dimension: usize,
37 metric: DistanceMetric,
38}
39
40static COLLECTION_INDEXES: OnceLock<Mutex<HashMap<String, Arc<CollectionIndex>>>> = OnceLock::new();
41
42fn collection_key(namespace: &str, collection: &str) -> String {
43 format!("{}/{}", namespace, collection)
44}
45
46fn vector_bin_key(namespace: &str, collection: &str, id_hash: u128) -> String {
47 format!("{}/collections/{}/vectors_bin/{:032x}", namespace, collection, id_hash)
48}
49
50fn metadata_key(namespace: &str, collection: &str, id_hash: u128) -> String {
51 format!("{}/collections/{}/meta/{:032x}", namespace, collection, id_hash)
52}
53
54fn hash_id_to_u128(id: &str) -> u128 {
55 let hash = blake3::hash(id.as_bytes());
56 let bytes = hash.as_bytes();
57 u128::from_le_bytes([
58 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
59 bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
60 ])
61}
62
63fn ensure_collection_index(
64 db: &Database,
65 namespace: &str,
66 collection: &str,
67 dimension: usize,
68 metric: DistanceMetric,
69) -> Arc<CollectionIndex> {
70 let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
71 let key = collection_key(namespace, collection);
72
73 let mut registry_guard = registry.lock().unwrap();
74 if let Some(existing) = registry_guard.get(&key) {
75 return existing.clone();
76 }
77
78 let mut config = HnswConfig::default();
79 config.metric = metric;
80 let index = Arc::new(HnswIndex::new(dimension, config));
81
82 let entry = Arc::new(CollectionIndex {
83 index,
84 dimension,
85 metric,
86 });
87 registry_guard.insert(key, entry.clone());
88
89 entry
90}
91
92fn resolve_collection_config(
93 db: &Database,
94 namespace: &str,
95 collection: &str,
96) -> Option<(usize, DistanceMetric)> {
97 let key = format!("{}/_collections/{}", namespace, collection);
98 let txn = db.begin_transaction().ok()?;
99 let value = db.get(txn, key.as_bytes()).ok().flatten();
100 let _ = db.commit(txn);
101 let value = value?;
102
103 let parsed: serde_json::Value = serde_json::from_slice(&value).ok()?;
104 let dimension = parsed.get("dimension")?.as_u64()? as usize;
105 let metric = match parsed.get("metric").and_then(|v| v.as_u64()).unwrap_or(0) {
106 1 => DistanceMetric::Euclidean,
107 2 => DistanceMetric::DotProduct,
108 _ => DistanceMetric::Cosine,
109 };
110 Some((dimension, metric))
111}
112
113fn serialize_vector_binary(vector: &[f32]) -> Vec<u8> {
114 let mut out = Vec::with_capacity(4 + vector.len() * 4);
115 let len = vector.len() as u32;
116 out.extend_from_slice(&len.to_le_bytes());
117 for value in vector {
118 out.extend_from_slice(&value.to_le_bytes());
119 }
120 out
121}
122
123fn decode_score(metric: DistanceMetric, distance: f32) -> f32 {
124 match metric {
125 DistanceMetric::Cosine => 1.0 - distance,
126 DistanceMetric::DotProduct => -distance,
127 DistanceMetric::Euclidean => -distance,
128 }
129}
130
131#[repr(C)]
133pub struct C_TxnHandle {
134 pub txn_id: u64,
135 pub snapshot_ts: u64,
136}
137
138#[repr(C)]
141pub struct C_CommitResult {
142 pub commit_ts: u64,
145 pub error_code: i32,
147}
148
149#[repr(C)]
154pub struct C_DatabaseConfig {
155 pub wal_enabled: bool,
157 pub wal_enabled_set: bool,
159 pub sync_mode: u8,
161 pub sync_mode_set: bool,
163 pub memtable_size_bytes: u64,
165 pub group_commit: bool,
167 pub group_commit_set: bool,
169 pub default_index_policy: u8,
171 pub default_index_policy_set: bool,
173}
174
175#[unsafe(no_mangle)]
180pub unsafe extern "C" fn sochdb_open_with_config(
181 path: *const c_char,
182 config: C_DatabaseConfig
183) -> *mut DatabasePtr {
184 if path.is_null() {
185 return ptr::null_mut();
186 }
187
188 let c_str = unsafe { CStr::from_ptr(path) };
189 let path_str = match c_str.to_str() {
190 Ok(s) => s,
191 Err(_) => return ptr::null_mut(),
192 };
193
194 let mut db_config = crate::database::DatabaseConfig::default();
196
197 if config.wal_enabled_set {
198 db_config.wal_enabled = config.wal_enabled;
199 }
200
201 if config.sync_mode_set {
202 db_config.sync_mode = match config.sync_mode {
203 0 => crate::database::SyncMode::Off,
204 1 => crate::database::SyncMode::Normal,
205 _ => crate::database::SyncMode::Full,
206 };
207 }
208
209 if config.memtable_size_bytes > 0 {
210 db_config.memtable_size_limit = config.memtable_size_bytes as usize;
211 }
212
213 if config.group_commit_set {
214 db_config.group_commit = config.group_commit;
215 }
216
217 if config.default_index_policy_set {
218 db_config.default_index_policy = match config.default_index_policy {
219 0 => crate::index_policy::IndexPolicy::WriteOptimized,
220 1 => crate::index_policy::IndexPolicy::Balanced,
221 2 => crate::index_policy::IndexPolicy::ScanOptimized,
222 _ => crate::index_policy::IndexPolicy::AppendOnly,
223 };
224 }
225
226 match Database::open_with_config(path_str, db_config) {
227 Ok(db) => {
228 let ptr = Box::new(DatabasePtr(db));
229 Box::into_raw(ptr)
230 }
231 Err(_) => ptr::null_mut(),
232 }
233}
234
235#[unsafe(no_mangle)]
240pub unsafe extern "C" fn sochdb_open(path: *const c_char) -> *mut DatabasePtr {
241 if path.is_null() {
242 return ptr::null_mut();
243 }
244
245 let c_str = unsafe { CStr::from_ptr(path) };
246 let path_str = match c_str.to_str() {
247 Ok(s) => s,
248 Err(_) => return ptr::null_mut(),
249 };
250
251 let config = crate::database::DatabaseConfig::default();
253
254 match Database::open_with_config(path_str, config) {
256 Ok(db) => {
257 let ptr = Box::new(DatabasePtr(db));
258 Box::into_raw(ptr)
259 }
260 Err(_) => ptr::null_mut(),
261 }
262}
263
264#[unsafe(no_mangle)]
268pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
269 if !ptr.is_null() {
270 unsafe {
271 let _ = Box::from_raw(ptr);
272 }
273 }
274}
275
276#[unsafe(no_mangle)]
281pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
282 if ptr.is_null() {
283 return C_TxnHandle {
284 txn_id: 0,
285 snapshot_ts: 0,
286 };
287 }
288 let db = unsafe { &(*ptr).0 };
289 match db.begin_transaction() {
290 Ok(txn) => C_TxnHandle {
291 txn_id: txn.txn_id,
292 snapshot_ts: txn.snapshot_ts,
293 },
294 Err(_) => C_TxnHandle {
295 txn_id: 0,
296 snapshot_ts: 0,
297 },
298 }
299}
300
301#[unsafe(no_mangle)]
308pub unsafe extern "C" fn sochdb_commit(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> C_CommitResult {
309 if ptr.is_null() {
310 return C_CommitResult {
311 commit_ts: 0,
312 error_code: -1,
313 };
314 }
315 let db = unsafe { &(*ptr).0 };
316 let txn = TxnHandle {
317 txn_id: handle.txn_id,
318 snapshot_ts: handle.snapshot_ts,
319 };
320 match db.commit(txn) {
321 Ok(commit_ts) => C_CommitResult {
322 commit_ts,
323 error_code: 0,
324 },
325 Err(_) => C_CommitResult {
326 commit_ts: 0,
327 error_code: -1,
328 },
329 }
330}
331
332#[unsafe(no_mangle)]
337pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
338 if ptr.is_null() {
339 return -1;
340 }
341 let db = unsafe { &(*ptr).0 };
342 let txn = TxnHandle {
343 txn_id: handle.txn_id,
344 snapshot_ts: handle.snapshot_ts,
345 };
346 match db.abort(txn) {
347 Ok(_) => 0,
348 Err(_) => -1,
349 }
350}
351
352#[unsafe(no_mangle)]
358pub unsafe extern "C" fn sochdb_put(
359 ptr: *mut DatabasePtr,
360 handle: C_TxnHandle,
361 key_ptr: *const u8,
362 key_len: usize,
363 val_ptr: *const u8,
364 val_len: usize,
365) -> c_int {
366 if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
367 return -1;
368 }
369 let db = unsafe { &(*ptr).0 };
370 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
371 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
372 let txn = TxnHandle {
373 txn_id: handle.txn_id,
374 snapshot_ts: handle.snapshot_ts,
375 };
376
377 match db.put(txn, key, val) {
378 Ok(_) => 0,
379 Err(_) => -1,
380 }
381}
382
383#[unsafe(no_mangle)]
390pub unsafe extern "C" fn sochdb_get(
391 ptr: *mut DatabasePtr,
392 handle: C_TxnHandle,
393 key_ptr: *const u8,
394 key_len: usize,
395 val_out: *mut *mut u8,
396 len_out: *mut usize,
397) -> c_int {
398 if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.is_null() {
399 return -1;
400 }
401 let db = unsafe { &(*ptr).0 };
402 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
403 let txn = TxnHandle {
404 txn_id: handle.txn_id,
405 snapshot_ts: handle.snapshot_ts,
406 };
407
408 match db.get(txn, key) {
409 Ok(Some(val)) => {
410 let mut buf = val.into_boxed_slice();
412 unsafe {
413 *val_out = buf.as_mut_ptr();
414 *len_out = buf.len();
415 }
416 let _ = Box::into_raw(buf); 0
418 }
419 Ok(None) => 1, Err(_) => -1,
421 }
422}
423
424#[unsafe(no_mangle)]
428pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
429 if !ptr.is_null() {
430 unsafe {
431 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
432 }
433 }
434}
435
436#[unsafe(no_mangle)]
441pub unsafe extern "C" fn sochdb_delete(
442 ptr: *mut DatabasePtr,
443 handle: C_TxnHandle,
444 key_ptr: *const u8,
445 key_len: usize,
446) -> c_int {
447 if ptr.is_null() || key_ptr.is_null() {
448 return -1;
449 }
450 let db = unsafe { &(*ptr).0 };
451 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
452 let txn = TxnHandle {
453 txn_id: handle.txn_id,
454 snapshot_ts: handle.snapshot_ts,
455 };
456
457 match db.delete(txn, key) {
458 Ok(_) => 0,
459 Err(_) => -1,
460 }
461}
462
463#[unsafe(no_mangle)]
467pub unsafe extern "C" fn sochdb_put_path(
468 ptr: *mut DatabasePtr,
469 handle: C_TxnHandle,
470 path_ptr: *const c_char,
471 val_ptr: *const u8,
472 val_len: usize,
473) -> c_int {
474 if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
475 return -1;
476 }
477 let db = unsafe { &(*ptr).0 };
478 let c_str = unsafe { CStr::from_ptr(path_ptr) };
479 let path_str = match c_str.to_str() {
480 Ok(s) => s,
481 Err(_) => return -1,
482 };
483 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
484 let txn = TxnHandle {
485 txn_id: handle.txn_id,
486 snapshot_ts: handle.snapshot_ts,
487 };
488
489 match db.put_path(txn, path_str, val) {
490 Ok(_) => 0,
491 Err(_) => -1,
492 }
493}
494
495#[unsafe(no_mangle)]
499pub unsafe extern "C" fn sochdb_get_path(
500 ptr: *mut DatabasePtr,
501 handle: C_TxnHandle,
502 path_ptr: *const c_char,
503 val_out: *mut *mut u8,
504 len_out: *mut usize,
505) -> c_int {
506 if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
507 return -1;
508 }
509 let db = unsafe { &(*ptr).0 };
510 let c_str = unsafe { CStr::from_ptr(path_ptr) };
511 let path_str = match c_str.to_str() {
512 Ok(s) => s,
513 Err(_) => return -1,
514 };
515 let txn = TxnHandle {
516 txn_id: handle.txn_id,
517 snapshot_ts: handle.snapshot_ts,
518 };
519
520 match db.get_path(txn, path_str) {
521 Ok(Some(val)) => {
522 let mut buf = val.into_boxed_slice();
523 unsafe {
524 *val_out = buf.as_mut_ptr();
525 *len_out = buf.len();
526 }
527 let _ = Box::into_raw(buf);
528 0
529 }
530 Ok(None) => 1,
531 Err(_) => -1,
532 }
533}
534
535#[allow(clippy::type_complexity)]
537pub struct ScanIteratorPtr(
538 Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
539);
540
541#[unsafe(no_mangle)]
545pub unsafe extern "C" fn sochdb_scan(
546 ptr: *mut DatabasePtr,
547 handle: C_TxnHandle,
548 start_ptr: *const u8,
549 start_len: usize,
550 end_ptr: *const u8,
551 end_len: usize,
552) -> *mut ScanIteratorPtr {
553 if ptr.is_null() {
554 return ptr::null_mut();
555 }
556 let db = unsafe { &(*ptr).0 };
557 let txn = TxnHandle {
558 txn_id: handle.txn_id,
559 snapshot_ts: handle.snapshot_ts,
560 };
561
562 let start = if !start_ptr.is_null() && start_len > 0 {
563 unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
564 } else {
565 vec![]
566 };
567
568 let end = if !end_ptr.is_null() && end_len > 0 {
569 unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
570 } else {
571 vec![] };
573
574 match db.scan_range(txn, &start, &end) {
587 Ok(rows) => {
588 let iter = Box::new(rows.into_iter().map(Ok));
591
592 let ptr = Box::new(ScanIteratorPtr(iter));
593 Box::into_raw(ptr)
594 }
595 Err(_) => ptr::null_mut(),
596 }
597}
598
599#[unsafe(no_mangle)]
604pub unsafe extern "C" fn sochdb_scan_prefix(
605 ptr: *mut DatabasePtr,
606 handle: C_TxnHandle,
607 prefix_ptr: *const u8,
608 prefix_len: usize,
609) -> *mut ScanIteratorPtr {
610 if ptr.is_null() {
611 return ptr::null_mut();
612 }
613 let db = unsafe { &(*ptr).0 };
614 let txn = TxnHandle {
615 txn_id: handle.txn_id,
616 snapshot_ts: handle.snapshot_ts,
617 };
618
619 let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
620 unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
621 } else {
622 vec![]
623 };
624
625 match db.scan(txn, &prefix) {
627 Ok(rows) => {
628 let prefix_owned = prefix.clone();
631 let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
632 .into_iter()
633 .filter(|(k, _)| k.starts_with(&prefix_owned))
634 .collect();
635
636 let iter = Box::new(filtered.into_iter().map(Ok));
637 let ptr = Box::new(ScanIteratorPtr(iter));
638 Box::into_raw(ptr)
639 }
640 Err(_) => ptr::null_mut(),
641 }
642}
643
644#[unsafe(no_mangle)]
649pub unsafe extern "C" fn sochdb_scan_next(
650 iter_ptr: *mut ScanIteratorPtr,
651 key_out: *mut *mut u8,
652 key_len_out: *mut usize,
653 val_out: *mut *mut u8,
654 val_len_out: *mut usize,
655) -> c_int {
656 if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
657 return -1;
658 }
659 let iter = unsafe { &mut (*iter_ptr).0 };
660
661 match iter.next() {
662 Some(Ok((key, val))) => {
663 let mut key_buf = key.into_boxed_slice();
664 let mut val_buf = val.into_boxed_slice();
665 unsafe {
666 *key_out = key_buf.as_mut_ptr();
667 *key_len_out = key_buf.len();
668 *val_out = val_buf.as_mut_ptr();
669 *val_len_out = val_buf.len();
670 }
671 let _ = Box::into_raw(key_buf);
672 let _ = Box::into_raw(val_buf);
673 0
674 }
675 Some(Err(_)) => -1,
676 None => 1, }
678}
679
680#[unsafe(no_mangle)]
684pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
685 if !ptr.is_null() {
686 unsafe {
687 let _ = Box::from_raw(ptr);
688 }
689 }
690}
691
692#[unsafe(no_mangle)]
696pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
697 if ptr.is_null() {
698 return -1;
699 }
700 let db = unsafe { &(*ptr).0 };
701 match db.flush() {
702 Ok(_) => 0,
703 Err(_) => -1,
704 }
705}
706
707#[repr(C)]
709pub struct CStorageStats {
710 pub memtable_size_bytes: u64,
711 pub wal_size_bytes: u64,
712 pub active_transactions: usize,
713 pub min_active_snapshot: u64,
714 pub last_checkpoint_lsn: u64,
715}
716
717#[unsafe(no_mangle)]
721pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
722 if ptr.is_null() {
723 return CStorageStats {
724 memtable_size_bytes: 0,
725 wal_size_bytes: 0,
726 active_transactions: 0,
727 min_active_snapshot: 0,
728 last_checkpoint_lsn: 0,
729 };
730 }
731 let db = unsafe { &(*ptr).0 };
732 let stats = db.storage_stats();
733
734 CStorageStats {
735 memtable_size_bytes: stats.memtable_size_bytes,
736 wal_size_bytes: stats.wal_size_bytes,
737 active_transactions: stats.active_transactions,
738 min_active_snapshot: stats.min_active_snapshot,
739 last_checkpoint_lsn: stats.last_checkpoint_lsn,
740 }
741}
742
743#[repr(C)]
761pub struct CBatchPut {
762 pub data: *const u8,
764 pub len: usize,
766}
767
768#[unsafe(no_mangle)]
809pub unsafe extern "C" fn sochdb_put_many(
810 ptr: *mut DatabasePtr,
811 handle: C_TxnHandle,
812 batch: CBatchPut,
813) -> c_int {
814 if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
815 return -1;
816 }
817
818 let db = unsafe { &(*ptr).0 };
819 let txn = TxnHandle {
820 txn_id: handle.txn_id,
821 snapshot_ts: handle.snapshot_ts,
822 };
823
824 let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
826
827 if data.len() < 4 {
829 return -1;
830 }
831 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
832
833 let mut offset = 4;
834 let mut success_count = 0;
835
836 for _ in 0..num_entries {
837 if offset + 8 > data.len() {
839 return success_count;
840 }
841 let key_len = u32::from_le_bytes([
842 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
843 ]) as usize;
844 let val_len = u32::from_le_bytes([
845 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]
846 ]) as usize;
847 offset += 8;
848
849 if offset + key_len + val_len > data.len() {
851 return success_count;
852 }
853 let key = &data[offset..offset + key_len];
854 offset += key_len;
855 let value = &data[offset..offset + val_len];
856 offset += val_len;
857
858 match db.put(txn, key, value) {
860 Ok(_) => success_count += 1,
861 Err(_) => return success_count,
862 }
863 }
864
865 success_count
866}
867
868#[unsafe(no_mangle)]
888pub unsafe extern "C" fn sochdb_delete_many(
889 ptr: *mut DatabasePtr,
890 handle: C_TxnHandle,
891 keys_data: *const u8,
892 keys_len: usize,
893) -> c_int {
894 if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
895 return -1;
896 }
897
898 let db = unsafe { &(*ptr).0 };
899 let txn = TxnHandle {
900 txn_id: handle.txn_id,
901 snapshot_ts: handle.snapshot_ts,
902 };
903
904 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
905
906 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
907
908 let mut offset = 4;
909 let mut success_count = 0;
910
911 for _ in 0..num_entries {
912 if offset + 4 > data.len() {
913 return success_count;
914 }
915 let key_len = u32::from_le_bytes([
916 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
917 ]) as usize;
918 offset += 4;
919
920 if offset + key_len > data.len() {
921 return success_count;
922 }
923 let key = &data[offset..offset + key_len];
924 offset += key_len;
925
926 match db.delete(txn, key) {
927 Ok(_) => success_count += 1,
928 Err(_) => return success_count,
929 }
930 }
931
932 success_count
933}
934
935#[unsafe(no_mangle)]
963pub unsafe extern "C" fn sochdb_get_many(
964 ptr: *mut DatabasePtr,
965 handle: C_TxnHandle,
966 keys_data: *const u8,
967 keys_len: usize,
968 result_out: *mut *mut u8,
969 result_len_out: *mut usize,
970) -> c_int {
971 if ptr.is_null() || keys_data.is_null() || keys_len < 4
972 || result_out.is_null() || result_len_out.is_null() {
973 return -1;
974 }
975
976 let db = unsafe { &(*ptr).0 };
977 let txn = TxnHandle {
978 txn_id: handle.txn_id,
979 snapshot_ts: handle.snapshot_ts,
980 };
981
982 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
983
984 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
985
986 let mut result = Vec::with_capacity(4 + num_entries * 10); result.extend_from_slice(&(num_entries as u32).to_le_bytes());
989
990 let mut offset = 4;
991
992 for _ in 0..num_entries {
993 if offset + 4 > data.len() {
994 result.push(2); continue;
996 }
997 let key_len = u32::from_le_bytes([
998 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
999 ]) as usize;
1000 offset += 4;
1001
1002 if offset + key_len > data.len() {
1003 result.push(2); continue;
1005 }
1006 let key = &data[offset..offset + key_len];
1007 offset += key_len;
1008
1009 match db.get(txn, key) {
1010 Ok(Some(value)) => {
1011 result.push(0); result.extend_from_slice(&(value.len() as u32).to_le_bytes());
1013 result.extend_from_slice(&value);
1014 }
1015 Ok(None) => {
1016 result.push(1); }
1018 Err(_) => {
1019 result.push(2); }
1021 }
1022 }
1023
1024 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); 0
1033}
1034
1035#[unsafe(no_mangle)]
1092pub unsafe extern "C" fn sochdb_scan_batch(
1093 iter_ptr: *mut ScanIteratorPtr,
1094 batch_size: usize,
1095 result_out: *mut *mut u8,
1096 result_len_out: *mut usize,
1097) -> c_int {
1098 if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0 {
1099 return -1;
1100 }
1101
1102 let iter = unsafe { &mut (*iter_ptr).0 };
1103
1104 let estimated_size = 5 + batch_size * 108;
1107 let mut result = Vec::with_capacity(estimated_size);
1108
1109 result.extend_from_slice(&[0u8; 5]); let mut count = 0u32;
1113 let mut is_done = false;
1114
1115 for _ in 0..batch_size {
1116 match iter.next() {
1117 Some(Ok((key, val))) => {
1118 result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1120 result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1121 result.extend_from_slice(&key);
1122 result.extend_from_slice(&val);
1123 count += 1;
1124 }
1125 Some(Err(_)) => {
1126 result[0..4].copy_from_slice(&count.to_le_bytes());
1128 result[4] = 0; let mut boxed = result.into_boxed_slice();
1131 unsafe {
1132 *result_out = boxed.as_mut_ptr();
1133 *result_len_out = boxed.len();
1134 }
1135 let _ = Box::into_raw(boxed);
1136 return -1;
1137 }
1138 None => {
1139 is_done = true;
1140 break;
1141 }
1142 }
1143 }
1144
1145 result[0..4].copy_from_slice(&count.to_le_bytes());
1147 result[4] = if is_done { 1 } else { 0 };
1148
1149 if count == 0 && is_done {
1151 let mut boxed = result.into_boxed_slice();
1153 unsafe {
1154 *result_out = boxed.as_mut_ptr();
1155 *result_len_out = boxed.len();
1156 }
1157 let _ = Box::into_raw(boxed);
1158 return 1; }
1160
1161 let mut boxed = result.into_boxed_slice();
1163 unsafe {
1164 *result_out = boxed.as_mut_ptr();
1165 *result_len_out = boxed.len();
1166 }
1167 let _ = Box::into_raw(boxed);
1168
1169 0 }
1171
1172#[unsafe(no_mangle)]
1192pub unsafe extern "C" fn sochdb_set_table_index_policy(
1193 ptr: *mut DatabasePtr,
1194 table_name: *const c_char,
1195 policy: u8,
1196) -> c_int {
1197 if ptr.is_null() || table_name.is_null() {
1198 return -1;
1199 }
1200
1201 let c_str = unsafe { CStr::from_ptr(table_name) };
1202 let table = match c_str.to_str() {
1203 Ok(s) => s,
1204 Err(_) => return -1,
1205 };
1206
1207 let index_policy = match policy {
1208 0 => crate::index_policy::IndexPolicy::WriteOptimized,
1209 1 => crate::index_policy::IndexPolicy::Balanced,
1210 2 => crate::index_policy::IndexPolicy::ScanOptimized,
1211 3 => crate::index_policy::IndexPolicy::AppendOnly,
1212 _ => return -2,
1213 };
1214
1215 let db = unsafe { &(*ptr).0 };
1216
1217 let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1219 db.index_registry().configure_table(config);
1220
1221 0
1222}
1223
1224#[unsafe(no_mangle)]
1236pub unsafe extern "C" fn sochdb_get_table_index_policy(
1237 ptr: *mut DatabasePtr,
1238 table_name: *const c_char,
1239) -> u8 {
1240 if ptr.is_null() || table_name.is_null() {
1241 return 255;
1242 }
1243
1244 let c_str = unsafe { CStr::from_ptr(table_name) };
1245 let table = match c_str.to_str() {
1246 Ok(s) => s,
1247 Err(_) => return 255,
1248 };
1249
1250 let db = unsafe { &(*ptr).0 };
1251 let config = db.index_registry().get_config(table);
1252
1253 match config.policy {
1254 crate::index_policy::IndexPolicy::WriteOptimized => 0,
1255 crate::index_policy::IndexPolicy::Balanced => 1,
1256 crate::index_policy::IndexPolicy::ScanOptimized => 2,
1257 crate::index_policy::IndexPolicy::AppendOnly => 3,
1258 }
1259}
1260
1261#[repr(C)]
1263pub struct C_TemporalEdge {
1264 pub from_id: *const c_char,
1265 pub edge_type: *const c_char,
1266 pub to_id: *const c_char,
1267 pub valid_from: u64,
1268 pub valid_until: u64,
1269 pub properties_json: *const c_char, }
1271
1272#[unsafe(no_mangle)]
1276pub unsafe extern "C" fn sochdb_add_temporal_edge(
1277 ptr: *mut DatabasePtr,
1278 namespace: *const c_char,
1279 edge: C_TemporalEdge,
1280) -> c_int {
1281 if ptr.is_null() || namespace.is_null() || edge.from_id.is_null()
1282 || edge.edge_type.is_null() || edge.to_id.is_null() {
1283 return -1;
1284 }
1285
1286 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1287 Ok(s) => s,
1288 Err(_) => return -1,
1289 };
1290 let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1291 Ok(s) => s,
1292 Err(_) => return -1,
1293 };
1294 let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1295 Ok(s) => s,
1296 Err(_) => return -1,
1297 };
1298 let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1299 Ok(s) => s,
1300 Err(_) => return -1,
1301 };
1302
1303 let db = unsafe { &(*ptr).0 };
1304
1305 let txn = match db.begin_transaction() {
1307 Ok(t) => t,
1308 Err(_) => return -1,
1309 };
1310
1311 let key = format!(
1313 "_graph/{}/temporal/{}/{}/{}/{:016x}",
1314 ns, from, etype, to, edge.valid_from
1315 );
1316
1317 let props_str = if edge.properties_json.is_null() {
1318 "{}".to_string()
1319 } else {
1320 match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1321 Ok(s) => s.to_string(),
1322 Err(_) => return -1,
1323 }
1324 };
1325
1326 let value = format!(
1327 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1328 from, etype, to, edge.valid_from, edge.valid_until, props_str
1329 );
1330
1331 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1332 let _ = db.abort(txn);
1333 return -1;
1334 }
1335
1336 match db.commit(txn) {
1337 Ok(_) => 0,
1338 Err(_) => -1,
1339 }
1340}
1341
1342#[unsafe(no_mangle)]
1349pub unsafe extern "C" fn sochdb_query_temporal_graph(
1350 ptr: *mut DatabasePtr,
1351 namespace: *const c_char,
1352 node_id: *const c_char,
1353 query_mode: u8,
1354 timestamp: u64, start_time: u64, end_time: u64, edge_type: *const c_char, out_len: *mut usize,
1359) -> *mut c_char {
1360 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1361 return ptr::null_mut();
1362 }
1363
1364 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1365 Ok(s) => s,
1366 Err(_) => return ptr::null_mut(),
1367 };
1368 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1369 Ok(s) => s,
1370 Err(_) => return ptr::null_mut(),
1371 };
1372
1373 let edge_filter = if edge_type.is_null() {
1374 None
1375 } else {
1376 match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1377 Ok(s) => Some(s),
1378 Err(_) => return ptr::null_mut(),
1379 }
1380 };
1381
1382 let db = unsafe { &(*ptr).0 };
1383
1384 let txn = match db.begin_transaction() {
1386 Ok(t) => t,
1387 Err(_) => return ptr::null_mut(),
1388 };
1389
1390 let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1392 let pairs = match db.scan(txn, prefix.as_bytes()) {
1393 Ok(p) => p,
1394 Err(_) => {
1395 let _ = db.abort(txn);
1396 return ptr::null_mut();
1397 }
1398 };
1399
1400 if let Err(_) = db.commit(txn) {
1402 return ptr::null_mut();
1403 }
1404
1405 let mut results = Vec::new();
1406 let now = std::time::SystemTime::now()
1407 .duration_since(std::time::UNIX_EPOCH)
1408 .unwrap()
1409 .as_millis() as u64;
1410
1411 for (_key, value) in pairs {
1412 let value_str = match std::str::from_utf8(&value) {
1414 Ok(s) => s,
1415 Err(_) => continue,
1416 };
1417
1418 if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1420 if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1421 let vf_start = valid_from_pos + r#""valid_from":"#.len();
1422 let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1423 let vu_start = valid_until_pos + r#""valid_until":"#.len();
1424 let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1425
1426 let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1427 let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1428
1429 if let Some(filter) = edge_filter {
1431 if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1432 continue;
1433 }
1434 }
1435
1436 let matches = match query_mode {
1438 0 => timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until),
1439 1 => {
1440 let edge_end = if valid_until == 0 { u64::MAX } else { valid_until };
1441 valid_from < end_time && edge_end > start_time
1442 }
1443 2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1444 _ => false,
1445 };
1446
1447 if matches {
1448 results.push(value_str.to_string());
1449 }
1450 }
1451 }
1452 }
1453
1454 let json = format!("[{}]", results.join(","));
1456 let c_string = match std::ffi::CString::new(json) {
1457 Ok(s) => s,
1458 Err(_) => return ptr::null_mut(),
1459 };
1460
1461 unsafe { *out_len = c_string.as_bytes().len() };
1462 c_string.into_raw()
1463}
1464
1465#[unsafe(no_mangle)]
1469pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1470 if !ptr.is_null() {
1471 unsafe {
1472 let _ = std::ffi::CString::from_raw(ptr);
1473 }
1474 }
1475}
1476
1477#[unsafe(no_mangle)]
1492pub unsafe extern "C" fn sochdb_graph_add_node(
1493 ptr: *mut DatabasePtr,
1494 namespace: *const c_char,
1495 node_id: *const c_char,
1496 node_type: *const c_char,
1497 properties_json: *const c_char,
1498) -> c_int {
1499 if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1500 return -1;
1501 }
1502
1503 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1504 Ok(s) => s,
1505 Err(_) => return -1,
1506 };
1507 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1508 Ok(s) => s,
1509 Err(_) => return -1,
1510 };
1511 let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1512 Ok(s) => s,
1513 Err(_) => return -1,
1514 };
1515 let props = if properties_json.is_null() {
1516 "{}".to_string()
1517 } else {
1518 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1519 Ok(s) => s.to_string(),
1520 Err(_) => return -1,
1521 }
1522 };
1523
1524 let db = unsafe { &(*ptr).0 };
1525
1526 let txn = match db.begin_transaction() {
1527 Ok(t) => t,
1528 Err(_) => return -1,
1529 };
1530
1531 let key = format!("_graph/{}/nodes/{}", ns, id);
1532 let value = format!(
1533 r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1534 id, ntype, props
1535 );
1536
1537 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1538 let _ = db.abort(txn);
1539 return -1;
1540 }
1541
1542 match db.commit(txn) {
1543 Ok(_) => 0,
1544 Err(_) => -1,
1545 }
1546}
1547
1548#[unsafe(no_mangle)]
1559pub unsafe extern "C" fn sochdb_graph_add_edge(
1560 ptr: *mut DatabasePtr,
1561 namespace: *const c_char,
1562 from_id: *const c_char,
1563 edge_type: *const c_char,
1564 to_id: *const c_char,
1565 properties_json: *const c_char,
1566) -> c_int {
1567 if ptr.is_null() || namespace.is_null() || from_id.is_null()
1568 || edge_type.is_null() || to_id.is_null() {
1569 return -1;
1570 }
1571
1572 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1573 Ok(s) => s,
1574 Err(_) => return -1,
1575 };
1576 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1577 Ok(s) => s,
1578 Err(_) => return -1,
1579 };
1580 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1581 Ok(s) => s,
1582 Err(_) => return -1,
1583 };
1584 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1585 Ok(s) => s,
1586 Err(_) => return -1,
1587 };
1588 let props = if properties_json.is_null() {
1589 "{}".to_string()
1590 } else {
1591 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1592 Ok(s) => s.to_string(),
1593 Err(_) => return -1,
1594 }
1595 };
1596
1597 let db = unsafe { &(*ptr).0 };
1598
1599 let txn = match db.begin_transaction() {
1600 Ok(t) => t,
1601 Err(_) => return -1,
1602 };
1603
1604 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1605 let value = format!(
1606 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1607 from, etype, to, props
1608 );
1609
1610 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1611 let _ = db.abort(txn);
1612 return -1;
1613 }
1614
1615 match db.commit(txn) {
1616 Ok(_) => 0,
1617 Err(_) => -1,
1618 }
1619}
1620
1621#[unsafe(no_mangle)]
1631pub unsafe extern "C" fn sochdb_graph_traverse(
1632 ptr: *mut DatabasePtr,
1633 namespace: *const c_char,
1634 start_node: *const c_char,
1635 max_depth: usize,
1636 order: u8, out_len: *mut usize,
1638) -> *mut c_char {
1639 if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1640 return ptr::null_mut();
1641 }
1642
1643 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1644 Ok(s) => s,
1645 Err(_) => return ptr::null_mut(),
1646 };
1647 let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1648 Ok(s) => s,
1649 Err(_) => return ptr::null_mut(),
1650 };
1651
1652 let db = unsafe { &(*ptr).0 };
1653
1654 let txn = match db.begin_transaction() {
1655 Ok(t) => t,
1656 Err(_) => return ptr::null_mut(),
1657 };
1658
1659 let mut visited_nodes = std::collections::HashSet::new();
1661 let mut nodes_json = Vec::new();
1662 let mut edges_json = Vec::new();
1663
1664 let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1666
1667 while let Some((current_node, depth)) = if order == 0 {
1668 if frontier.is_empty() { None } else { Some(frontier.remove(0)) }
1670 } else {
1671 frontier.pop()
1673 } {
1674 if depth > max_depth || visited_nodes.contains(¤t_node) {
1675 continue;
1676 }
1677 visited_nodes.insert(current_node.clone());
1678
1679 let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1681 if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1682 if let Ok(s) = std::str::from_utf8(&node_data) {
1683 nodes_json.push(s.to_string());
1684 }
1685 }
1686
1687 let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1689 if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1690 for (_key, value) in edges {
1691 if let Ok(edge_str) = std::str::from_utf8(&value) {
1692 edges_json.push(edge_str.to_string());
1693
1694 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
1696 let start_idx = to_pos + r#""to_id":""#.len();
1697 if let Some(end_idx) = edge_str[start_idx..].find('"') {
1698 let to_id = &edge_str[start_idx..start_idx + end_idx];
1699 if !visited_nodes.contains(to_id) {
1700 frontier.push((to_id.to_string(), depth + 1));
1701 }
1702 }
1703 }
1704 }
1705 }
1706 }
1707 }
1708
1709 if let Err(_) = db.commit(txn) {
1710 return ptr::null_mut();
1711 }
1712
1713 let result = format!(
1714 r#"{{"nodes":[{}],"edges":[{}]}}"#,
1715 nodes_json.join(","),
1716 edges_json.join(",")
1717 );
1718
1719 let c_string = match std::ffi::CString::new(result) {
1720 Ok(s) => s,
1721 Err(_) => return ptr::null_mut(),
1722 };
1723
1724 unsafe { *out_len = c_string.as_bytes().len() };
1725 c_string.into_raw()
1726}
1727
1728#[unsafe(no_mangle)]
1741pub unsafe extern "C" fn sochdb_cache_put(
1742 ptr: *mut DatabasePtr,
1743 cache_name: *const c_char,
1744 key: *const c_char,
1745 value: *const c_char,
1746 embedding_ptr: *const f32,
1747 embedding_len: usize,
1748 ttl_seconds: u64,
1749) -> c_int {
1750 if ptr.is_null() || cache_name.is_null() || key.is_null()
1751 || value.is_null() || embedding_ptr.is_null() {
1752 return -1;
1753 }
1754
1755 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1756 Ok(s) => s,
1757 Err(_) => return -1,
1758 };
1759 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
1760 Ok(s) => s,
1761 Err(_) => return -1,
1762 };
1763 let v = match unsafe { CStr::from_ptr(value) }.to_str() {
1764 Ok(s) => s,
1765 Err(_) => return -1,
1766 };
1767 let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
1768
1769 let db = unsafe { &(*ptr).0 };
1770
1771 let txn = match db.begin_transaction() {
1772 Ok(t) => t,
1773 Err(_) => return -1,
1774 };
1775
1776 let expires_at = if ttl_seconds > 0 {
1778 std::time::SystemTime::now()
1779 .duration_since(std::time::UNIX_EPOCH)
1780 .unwrap()
1781 .as_secs() + ttl_seconds
1782 } else {
1783 0 };
1785
1786 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
1788 let cache_key = format!("_cache/{}/{}", cache, key_hash);
1789
1790 let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
1792
1793 let cache_value = format!(
1794 r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
1795 k, v, embedding_json.join(","), expires_at
1796 );
1797
1798 if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
1799 let _ = db.abort(txn);
1800 return -1;
1801 }
1802
1803 match db.commit(txn) {
1804 Ok(_) => 0,
1805 Err(_) => -1,
1806 }
1807}
1808
1809#[unsafe(no_mangle)]
1817pub unsafe extern "C" fn sochdb_cache_get(
1818 ptr: *mut DatabasePtr,
1819 cache_name: *const c_char,
1820 query_embedding_ptr: *const f32,
1821 embedding_len: usize,
1822 threshold: f32,
1823 out_len: *mut usize,
1824) -> *mut c_char {
1825 if ptr.is_null() || cache_name.is_null() || query_embedding_ptr.is_null() || out_len.is_null() {
1826 return ptr::null_mut();
1827 }
1828
1829 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1830 Ok(s) => s,
1831 Err(_) => return ptr::null_mut(),
1832 };
1833 let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
1834
1835 let db = unsafe { &(*ptr).0 };
1836
1837 let txn = match db.begin_transaction() {
1838 Ok(t) => t,
1839 Err(_) => return ptr::null_mut(),
1840 };
1841
1842 let prefix = format!("_cache/{}/", cache);
1843 let entries = match db.scan(txn, prefix.as_bytes()) {
1844 Ok(e) => e,
1845 Err(_) => {
1846 let _ = db.abort(txn);
1847 return ptr::null_mut();
1848 }
1849 };
1850
1851 let _ = db.commit(txn);
1852
1853 let now = std::time::SystemTime::now()
1854 .duration_since(std::time::UNIX_EPOCH)
1855 .unwrap()
1856 .as_secs();
1857
1858 let mut best_match: Option<(f32, String)> = None;
1859
1860 for (_key, value) in entries {
1861 let value_str = match std::str::from_utf8(&value) {
1862 Ok(s) => s,
1863 Err(_) => continue,
1864 };
1865
1866 if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
1868 let exp_start = exp_pos + r#""expires_at":"#.len();
1869 if let Some(exp_end) = value_str[exp_start..].find('}') {
1870 let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
1871 .parse()
1872 .unwrap_or(0);
1873 if expires_at > 0 && now > expires_at {
1874 continue; }
1876 }
1877 }
1878
1879 if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
1881 let emb_start = emb_pos + r#""embedding":["#.len();
1882 if let Some(emb_end) = value_str[emb_start..].find(']') {
1883 let emb_str = &value_str[emb_start..emb_start + emb_end];
1884 let cached_embedding: Vec<f32> = emb_str
1885 .split(',')
1886 .filter_map(|s| s.trim().parse().ok())
1887 .collect();
1888
1889 if cached_embedding.len() == query.len() {
1890 let similarity = cosine_similarity(query, &cached_embedding);
1891 if similarity >= threshold {
1892 if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
1893 if let Some(val_pos) = value_str.find(r#""value":""#) {
1895 let val_start = val_pos + r#""value":""#.len();
1896 if let Some(val_end) = value_str[val_start..].find('"') {
1897 let cached_value = &value_str[val_start..val_start + val_end];
1898 best_match = Some((similarity, cached_value.to_string()));
1899 }
1900 }
1901 }
1902 }
1903 }
1904 }
1905 }
1906 }
1907
1908 match best_match {
1909 Some((_, value)) => {
1910 let c_string = match std::ffi::CString::new(value) {
1911 Ok(s) => s,
1912 Err(_) => return ptr::null_mut(),
1913 };
1914 unsafe { *out_len = c_string.as_bytes().len() };
1915 c_string.into_raw()
1916 }
1917 None => ptr::null_mut(),
1918 }
1919}
1920
1921fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
1924 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1925 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
1926 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1927 if norm_a == 0.0 || norm_b == 0.0 {
1928 0.0
1929 } else {
1930 let similarity = dot / (norm_a * norm_b);
1931 (similarity + 1.0) / 2.0
1934 }
1935}
1936
1937#[unsafe(no_mangle)]
1952pub unsafe extern "C" fn sochdb_trace_start(
1953 ptr: *mut DatabasePtr,
1954 name: *const c_char,
1955 trace_id_out: *mut *mut c_char,
1956 span_id_out: *mut *mut c_char,
1957) -> c_int {
1958 if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
1959 return -1;
1960 }
1961
1962 let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
1963 Ok(s) => s,
1964 Err(_) => return -1,
1965 };
1966
1967 let db = unsafe { &(*ptr).0 };
1968
1969 let trace_id = format!("trace_{:016x}", rand_u64());
1971 let span_id = format!("span_{:016x}", rand_u64());
1972
1973 let txn = match db.begin_transaction() {
1974 Ok(t) => t,
1975 Err(_) => return -1,
1976 };
1977
1978 let now = std::time::SystemTime::now()
1979 .duration_since(std::time::UNIX_EPOCH)
1980 .unwrap()
1981 .as_micros() as u64;
1982
1983 let trace_key = format!("_traces/{}", trace_id);
1985 let trace_value = format!(
1986 r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
1987 trace_id, trace_name, now, span_id
1988 );
1989
1990 if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
1991 let _ = db.abort(txn);
1992 return -1;
1993 }
1994
1995 let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
1997 let span_value = format!(
1998 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
1999 span_id, trace_name, now
2000 );
2001
2002 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2003 let _ = db.abort(txn);
2004 return -1;
2005 }
2006
2007 if let Err(_) = db.commit(txn) {
2008 return -1;
2009 }
2010
2011 let trace_c = match std::ffi::CString::new(trace_id) {
2013 Ok(s) => s,
2014 Err(_) => return -1,
2015 };
2016 let span_c = match std::ffi::CString::new(span_id) {
2017 Ok(s) => s,
2018 Err(_) => return -1,
2019 };
2020
2021 unsafe {
2022 *trace_id_out = trace_c.into_raw();
2023 *span_id_out = span_c.into_raw();
2024 }
2025
2026 0
2027}
2028
2029#[unsafe(no_mangle)]
2036pub unsafe extern "C" fn sochdb_trace_span_start(
2037 ptr: *mut DatabasePtr,
2038 trace_id: *const c_char,
2039 parent_span_id: *const c_char,
2040 name: *const c_char,
2041 span_id_out: *mut *mut c_char,
2042) -> c_int {
2043 if ptr.is_null() || trace_id.is_null() || parent_span_id.is_null()
2044 || name.is_null() || span_id_out.is_null() {
2045 return -1;
2046 }
2047
2048 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2049 Ok(s) => s,
2050 Err(_) => return -1,
2051 };
2052 let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
2053 Ok(s) => s,
2054 Err(_) => return -1,
2055 };
2056 let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2057 Ok(s) => s,
2058 Err(_) => return -1,
2059 };
2060
2061 let db = unsafe { &(*ptr).0 };
2062 let span_id = format!("span_{:016x}", rand_u64());
2063
2064 let txn = match db.begin_transaction() {
2065 Ok(t) => t,
2066 Err(_) => return -1,
2067 };
2068
2069 let now = std::time::SystemTime::now()
2070 .duration_since(std::time::UNIX_EPOCH)
2071 .unwrap()
2072 .as_micros() as u64;
2073
2074 let span_key = format!("_traces/{}/spans/{}", tid, span_id);
2075 let span_value = format!(
2076 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
2077 span_id, span_name, now, pid
2078 );
2079
2080 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2081 let _ = db.abort(txn);
2082 return -1;
2083 }
2084
2085 if let Err(_) = db.commit(txn) {
2086 return -1;
2087 }
2088
2089 let span_c = match std::ffi::CString::new(span_id) {
2090 Ok(s) => s,
2091 Err(_) => return -1,
2092 };
2093
2094 unsafe { *span_id_out = span_c.into_raw() };
2095 0
2096}
2097
2098#[unsafe(no_mangle)]
2108pub unsafe extern "C" fn sochdb_trace_span_end(
2109 ptr: *mut DatabasePtr,
2110 trace_id: *const c_char,
2111 span_id: *const c_char,
2112 status: u8,
2113) -> i64 {
2114 if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2115 return -1;
2116 }
2117
2118 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2119 Ok(s) => s,
2120 Err(_) => return -1,
2121 };
2122 let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2123 Ok(s) => s,
2124 Err(_) => return -1,
2125 };
2126
2127 let db = unsafe { &(*ptr).0 };
2128
2129 let txn = match db.begin_transaction() {
2130 Ok(t) => t,
2131 Err(_) => return -1,
2132 };
2133
2134 let span_key = format!("_traces/{}/spans/{}", tid, sid);
2135
2136 let span_data = match db.get(txn, span_key.as_bytes()) {
2138 Ok(Some(data)) => data,
2139 _ => {
2140 let _ = db.abort(txn);
2141 return -1;
2142 }
2143 };
2144
2145 let span_str = match std::str::from_utf8(&span_data) {
2146 Ok(s) => s,
2147 Err(_) => {
2148 let _ = db.abort(txn);
2149 return -1;
2150 }
2151 };
2152
2153 let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2155 let start = pos + r#""start_us":"#.len();
2156 if let Some(end) = span_str[start..].find(',') {
2157 span_str[start..start + end].parse().unwrap_or(0u64)
2158 } else {
2159 0u64
2160 }
2161 } else {
2162 0u64
2163 };
2164
2165 let now = std::time::SystemTime::now()
2166 .duration_since(std::time::UNIX_EPOCH)
2167 .unwrap()
2168 .as_micros() as u64;
2169
2170 let duration_us = now.saturating_sub(start_us);
2171 let status_str = match status {
2172 1 => "ok",
2173 2 => "error",
2174 _ => "unset",
2175 };
2176
2177 let new_span = span_str
2179 .replace(r#""status":"active""#, &format!(r#""status":"{}","end_us":{},"duration_us":{}"#, status_str, now, duration_us));
2180
2181 if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2182 let _ = db.abort(txn);
2183 return -1;
2184 }
2185
2186 if let Err(_) = db.commit(txn) {
2187 return -1;
2188 }
2189
2190 duration_us as i64
2191}
2192
2193fn rand_u64() -> u64 {
2195 use std::sync::atomic::{AtomicU64, Ordering};
2196 static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2197
2198 let mut s = STATE.load(Ordering::Relaxed);
2199 if s == 0 {
2200 s = std::time::SystemTime::now()
2201 .duration_since(std::time::UNIX_EPOCH)
2202 .unwrap()
2203 .as_nanos() as u64;
2204 }
2205 s ^= s >> 12;
2206 s ^= s << 25;
2207 s ^= s >> 27;
2208 STATE.store(s, Ordering::Relaxed);
2209 s.wrapping_mul(0x2545F4914F6CDD1D)
2210}
2211
2212#[unsafe(no_mangle)]
2222pub unsafe extern "C" fn sochdb_collection_create(
2223 ptr: *mut DatabasePtr,
2224 namespace: *const c_char,
2225 collection: *const c_char,
2226 dimension: usize,
2227 dist_type: u8, ) -> c_int {
2229 if ptr.is_null() || namespace.is_null() || collection.is_null() {
2230 return -1;
2231 }
2232
2233 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2234 Ok(s) => s,
2235 Err(_) => return -1,
2236 };
2237 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2238 Ok(s) => s,
2239 Err(_) => return -1,
2240 };
2241
2242 let db = unsafe { &(*ptr).0 };
2243 let txn = match db.begin_transaction() {
2244 Ok(t) => t,
2245 Err(_) => return -1,
2246 };
2247
2248 let config_key = format!("{}/_collections/{}", ns, col);
2250 let config_value = format!(
2251 r#"{{"dimension":{},"metric":{}}}"#,
2252 dimension, dist_type
2253 );
2254
2255 if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2256 let _ = db.abort(txn);
2257 return -1;
2258 }
2259
2260 let result = match db.commit(txn) {
2261 Ok(_) => 0,
2262 Err(_) => -1,
2263 };
2264
2265 if result == 0 {
2266 let metric = match dist_type {
2267 1 => DistanceMetric::Euclidean,
2268 2 => DistanceMetric::DotProduct,
2269 _ => DistanceMetric::Cosine,
2270 };
2271 let _ = ensure_collection_index(db, ns, col, dimension, metric);
2272 }
2273
2274 result
2275}
2276
2277#[unsafe(no_mangle)]
2283pub unsafe extern "C" fn sochdb_collection_insert(
2284 ptr: *mut DatabasePtr,
2285 namespace: *const c_char,
2286 collection: *const c_char,
2287 id: *const c_char,
2288 vector_ptr: *const f32,
2289 vector_len: usize,
2290 metadata_json: *const c_char, ) -> c_int {
2292 if ptr.is_null() || namespace.is_null() || collection.is_null()
2293 || id.is_null() || vector_ptr.is_null() {
2294 return -1;
2295 }
2296
2297 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2298 Ok(s) => s,
2299 Err(_) => return -1,
2300 };
2301 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2302 Ok(s) => s,
2303 Err(_) => return -1,
2304 };
2305 let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2306 Ok(s) => s,
2307 Err(_) => return -1,
2308 };
2309 let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2310 let db = unsafe { &(*ptr).0 };
2311
2312 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2313 Some(config) => config,
2314 None => (vector_len, DistanceMetric::Cosine),
2315 };
2316 if vector_len != dimension {
2317 return -1;
2318 }
2319
2320 let metadata = if !metadata_json.is_null() {
2321 match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2322 Ok(s) => s.to_string(),
2323 Err(_) => "{}".to_string(),
2324 }
2325 } else {
2326 "{}".to_string()
2327 };
2328
2329 let txn = match db.begin_transaction() {
2330 Ok(t) => t,
2331 Err(_) => return -1,
2332 };
2333
2334 let id_hash = hash_id_to_u128(doc_id);
2335 let vec_key = vector_bin_key(ns, col, id_hash);
2336 let vec_value = serialize_vector_binary(vector);
2337
2338 if let Err(_) = db.put(txn, vec_key.as_bytes(), &vec_value) {
2339 let _ = db.abort(txn);
2340 return -1;
2341 }
2342
2343 let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2344 Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2345 Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2346 };
2347 let meta_key = metadata_key(ns, col, id_hash);
2348 if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2349 if let Err(_) = db.put(txn, meta_key.as_bytes(), &meta_bytes) {
2350 let _ = db.abort(txn);
2351 return -1;
2352 }
2353 }
2354
2355 if let Err(_) = db.commit(txn) {
2356 return -1;
2357 }
2358
2359 let index = ensure_collection_index(db, ns, col, dimension, metric);
2360 let _ = index.index.insert(id_hash, vector.to_vec());
2361
2362 0
2363}
2364
2365#[repr(C)]
2367pub struct CSearchResult {
2368 pub id_ptr: *mut c_char,
2369 pub score: f32,
2370 pub metadata_ptr: *mut c_char,
2371}
2372
2373#[unsafe(no_mangle)]
2379pub unsafe extern "C" fn sochdb_collection_search(
2380 ptr: *mut DatabasePtr,
2381 namespace: *const c_char,
2382 collection: *const c_char,
2383 query_ptr: *const f32,
2384 query_len: usize,
2385 k: usize,
2386 results_out: *mut CSearchResult,
2387) -> c_int {
2388 if ptr.is_null() || namespace.is_null() || collection.is_null()
2389 || query_ptr.is_null() || results_out.is_null() {
2390 return -1;
2391 }
2392 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2393 Ok(s) => s,
2394 Err(_) => return -1,
2395 };
2396 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2397 Ok(s) => s,
2398 Err(_) => return -1,
2399 };
2400 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2401 let db = unsafe { &(*ptr).0 };
2402 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2403 Some(config) => config,
2404 None => return 0,
2405 };
2406
2407 if query_len != dimension {
2408 return -1;
2409 }
2410
2411 let index = ensure_collection_index(db, ns, col, dimension, metric);
2412 let mut scored = match index.index.search(query, k) {
2413 Ok(results) => results,
2414 Err(_) => return -1,
2415 };
2416
2417 let result_count = scored.len().min(k);
2418 for (i, (id_hash, distance)) in scored.drain(..result_count).enumerate() {
2419 let meta_key = metadata_key(ns, col, id_hash);
2420 let txn = match db.begin_transaction() {
2421 Ok(t) => t,
2422 Err(_) => return -1,
2423 };
2424 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2425 let _ = db.commit(txn);
2426
2427 let mut id_value = String::new();
2428 let mut metadata_json = serde_json::json!({});
2429 if let Some(bytes) = meta_value.as_deref() {
2430 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(bytes) {
2431 id_value = parsed.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2432 metadata_json = parsed.get("metadata").cloned().unwrap_or(serde_json::json!({}));
2433 }
2434 }
2435 let metadata = serde_json::to_string(&metadata_json).unwrap_or_else(|_| "{}".to_string());
2436
2437 let c_id = match std::ffi::CString::new(id_value) {
2438 Ok(s) => s.into_raw(),
2439 Err(_) => ptr::null_mut(),
2440 };
2441 let c_meta = match std::ffi::CString::new(metadata) {
2442 Ok(s) => s.into_raw(),
2443 Err(_) => ptr::null_mut(),
2444 };
2445
2446 unsafe {
2447 (*results_out.add(i)).id_ptr = c_id;
2448 (*results_out.add(i)).score = decode_score(metric, distance);
2449 (*results_out.add(i)).metadata_ptr = c_meta;
2450 }
2451 }
2452
2453 result_count as c_int
2454}
2455
2456#[unsafe(no_mangle)]
2462pub unsafe extern "C" fn sochdb_collection_search_soa(
2463 ptr: *mut DatabasePtr,
2464 namespace: *const c_char,
2465 collection: *const c_char,
2466 query_ptr: *const f32,
2467 query_len: usize,
2468 k: usize,
2469 min_score: f32,
2470 filter_json: *const c_char,
2471 ids_hi_out: *mut *mut u64,
2472 ids_lo_out: *mut *mut u64,
2473 scores_out: *mut *mut f32,
2474 len_out: *mut usize,
2475) -> c_int {
2476 if ptr.is_null() || namespace.is_null() || collection.is_null()
2477 || query_ptr.is_null() || ids_hi_out.is_null() || ids_lo_out.is_null()
2478 || scores_out.is_null() || len_out.is_null() {
2479 return -1;
2480 }
2481
2482 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2483 Ok(s) => s,
2484 Err(_) => return -1,
2485 };
2486 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2487 Ok(s) => s,
2488 Err(_) => return -1,
2489 };
2490 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2491 let db = unsafe { &(*ptr).0 };
2492
2493 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2494 Some(config) => config,
2495 None => return 0,
2496 };
2497 if query_len != dimension {
2498 return -1;
2499 }
2500
2501 let filter = if !filter_json.is_null() {
2502 match unsafe { CStr::from_ptr(filter_json) }.to_str() {
2503 Ok(s) => serde_json::from_str::<serde_json::Value>(s).ok(),
2504 Err(_) => None,
2505 }
2506 } else {
2507 None
2508 };
2509
2510 let index = ensure_collection_index(db, ns, col, dimension, metric);
2511 let results = match index.index.search(query, k) {
2512 Ok(results) => results,
2513 Err(_) => return -1,
2514 };
2515
2516 let mut ids_hi: Vec<u64> = Vec::with_capacity(results.len());
2517 let mut ids_lo: Vec<u64> = Vec::with_capacity(results.len());
2518 let mut scores: Vec<f32> = Vec::with_capacity(results.len());
2519
2520 for (id_hash, distance) in results {
2521 let score = decode_score(metric, distance);
2522 if min_score > 0.0 && score < min_score {
2523 continue;
2524 }
2525
2526 if let Some(filter_value) = &filter {
2527 let meta_key = metadata_key(ns, col, id_hash);
2528 let txn = match db.begin_transaction() {
2529 Ok(t) => t,
2530 Err(_) => return -1,
2531 };
2532 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2533 let _ = db.commit(txn);
2534 let meta_value = match meta_value {
2535 Some(value) => value,
2536 None => continue,
2537 };
2538 let parsed = match serde_json::from_slice::<serde_json::Value>(&meta_value) {
2539 Ok(value) => value,
2540 Err(_) => continue,
2541 };
2542 let metadata = parsed.get("metadata").cloned().unwrap_or(Value::Null);
2543
2544 if !metadata_matches_filter(&metadata, filter_value) {
2545 continue;
2546 }
2547 }
2548
2549 ids_hi.push((id_hash >> 64) as u64);
2550 ids_lo.push((id_hash & u128::from(u64::MAX)) as u64);
2551 scores.push(score);
2552 if ids_hi.len() >= k {
2553 break;
2554 }
2555 }
2556
2557 let len = ids_hi.len();
2558 let mut ids_hi_box = ids_hi.into_boxed_slice();
2559 let mut ids_lo_box = ids_lo.into_boxed_slice();
2560 let mut scores_box = scores.into_boxed_slice();
2561
2562 unsafe {
2563 *len_out = len;
2564 *ids_hi_out = ids_hi_box.as_mut_ptr();
2565 *ids_lo_out = ids_lo_box.as_mut_ptr();
2566 *scores_out = scores_box.as_mut_ptr();
2567 }
2568
2569 std::mem::forget(ids_hi_box);
2570 std::mem::forget(ids_lo_box);
2571 std::mem::forget(scores_box);
2572
2573 len as c_int
2574}
2575
2576#[unsafe(no_mangle)]
2578pub unsafe extern "C" fn sochdb_collection_fetch_metadata_json(
2579 ptr: *mut DatabasePtr,
2580 namespace: *const c_char,
2581 collection: *const c_char,
2582 ids_hi_ptr: *const u64,
2583 ids_lo_ptr: *const u64,
2584 ids_len: usize,
2585) -> *mut c_char {
2586 if ptr.is_null() || namespace.is_null() || collection.is_null()
2587 || ids_hi_ptr.is_null() || ids_lo_ptr.is_null() {
2588 return ptr::null_mut();
2589 }
2590
2591 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2592 Ok(s) => s,
2593 Err(_) => return ptr::null_mut(),
2594 };
2595 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2596 Ok(s) => s,
2597 Err(_) => return ptr::null_mut(),
2598 };
2599 let ids_hi = unsafe { slice::from_raw_parts(ids_hi_ptr, ids_len) };
2600 let ids_lo = unsafe { slice::from_raw_parts(ids_lo_ptr, ids_len) };
2601 let db = unsafe { &(*ptr).0 };
2602
2603 let mut results = Vec::with_capacity(ids_len);
2604 for i in 0..ids_len {
2605 let id_hash = ((ids_hi[i] as u128) << 64) | (ids_lo[i] as u128);
2606 let meta_key = metadata_key(ns, col, id_hash);
2607 let txn = match db.begin_transaction() {
2608 Ok(t) => t,
2609 Err(_) => return ptr::null_mut(),
2610 };
2611 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2612 let _ = db.commit(txn);
2613 if let Some(bytes) = meta_value {
2614 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&bytes) {
2615 results.push(parsed);
2616 continue;
2617 }
2618 }
2619 results.push(serde_json::json!({"id": "", "metadata": {}}));
2620 }
2621
2622 match serde_json::to_string(&results) {
2623 Ok(json) => match std::ffi::CString::new(json) {
2624 Ok(cstr) => cstr.into_raw(),
2625 Err(_) => ptr::null_mut(),
2626 },
2627 Err(_) => ptr::null_mut(),
2628 }
2629}
2630
2631#[unsafe(no_mangle)]
2633pub unsafe extern "C" fn sochdb_collection_free_u64(ptr: *mut u64, len: usize) {
2634 if ptr.is_null() || len == 0 {
2635 return;
2636 }
2637 unsafe {
2638 let _ = Vec::from_raw_parts(ptr, len, len);
2639 }
2640}
2641
2642#[unsafe(no_mangle)]
2643pub unsafe extern "C" fn sochdb_collection_free_f32(ptr: *mut f32, len: usize) {
2644 if ptr.is_null() || len == 0 {
2645 return;
2646 }
2647 unsafe {
2648 let _ = Vec::from_raw_parts(ptr, len, len);
2649 }
2650}
2651
2652fn metadata_matches_filter(metadata: &Value, filter: &Value) -> bool {
2653 let filter_obj = match filter.as_object() {
2654 Some(obj) => obj,
2655 None => return true,
2656 };
2657 let metadata_obj = match metadata.as_object() {
2658 Some(obj) => obj,
2659 None => return false,
2660 };
2661
2662 for (key, expected) in filter_obj.iter() {
2663 match metadata_obj.get(key) {
2664 Some(actual) if actual == expected => {}
2665 _ => return false,
2666 }
2667 }
2668
2669 true
2670}
2671
2672#[unsafe(no_mangle)]
2678pub unsafe extern "C" fn sochdb_collection_keyword_search(
2679 ptr: *mut DatabasePtr,
2680 namespace: *const c_char,
2681 collection: *const c_char,
2682 query_ptr: *const c_char,
2683 k: usize,
2684 results_out: *mut CSearchResult,
2685) -> c_int {
2686 if ptr.is_null() || namespace.is_null() || collection.is_null()
2687 || query_ptr.is_null() || results_out.is_null() {
2688 return -1;
2689 }
2690
2691 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2692 Ok(s) => s,
2693 Err(_) => return -1,
2694 };
2695 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2696 Ok(s) => s,
2697 Err(_) => return -1,
2698 };
2699 let query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
2700 Ok(s) => s.to_lowercase(),
2701 Err(_) => return -1,
2702 };
2703 let terms: Vec<&str> = query_str.split_whitespace().collect();
2704 if terms.is_empty() {
2705 return 0;
2706 }
2707
2708 let db = unsafe { &(*ptr).0 };
2709 let txn = match db.begin_transaction() {
2710 Ok(t) => t,
2711 Err(_) => return -1,
2712 };
2713
2714 let prefix = format!("{}/collections/{}/vectors/", ns, col);
2716 let entries = match db.scan(txn, prefix.as_bytes()) {
2717 Ok(e) => e,
2718 Err(_) => {
2719 let _ = db.abort(txn);
2720 return -1;
2721 }
2722 };
2723 let _ = db.commit(txn);
2724
2725 let mut scored: Vec<(f32, String, String)> = Vec::new();
2727
2728 for (_key, value) in entries {
2729 let doc: Value = match serde_json::from_slice(&value) {
2731 Ok(v) => v,
2732 Err(_) => continue,
2733 };
2734
2735 let metadata_val = doc.get("metadata");
2737 let metadata_str = metadata_val.map(|v| v.to_string()).unwrap_or("{}".to_string());
2738
2739 let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
2741
2742 let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
2744
2745 let mut score = 0.0;
2746 for term in &terms {
2747 score += search_text.matches(term).count() as f32;
2748 }
2749
2750 if score > 0.0 {
2751 let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2752 if id.is_empty() { continue; }
2753
2754 scored.push((score, id, metadata_str));
2755 }
2756 }
2757
2758 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
2760
2761 let result_count = scored.len().min(k);
2763 for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
2764 let c_id = match std::ffi::CString::new(id) {
2765 Ok(s) => s.into_raw(),
2766 Err(_) => ptr::null_mut(),
2767 };
2768 let c_meta = match std::ffi::CString::new(metadata) {
2769 Ok(s) => s.into_raw(),
2770 Err(_) => ptr::null_mut(),
2771 };
2772
2773 unsafe {
2774 (*results_out.add(i)).id_ptr = c_id;
2775 (*results_out.add(i)).score = score;
2776 (*results_out.add(i)).metadata_ptr = c_meta;
2777 }
2778 }
2779
2780 result_count as c_int
2781}
2782
2783#[unsafe(no_mangle)]
2785pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
2786 if result.is_null() {
2787 return;
2788 }
2789
2790 for i in 0..count {
2791 let r = unsafe { &mut *result.add(i) };
2792 if !r.id_ptr.is_null() {
2793 let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
2794 }
2795 if !r.metadata_ptr.is_null() {
2796 let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
2797 }
2798 }
2799}
2800