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 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
290 Database::open_concurrent(path_str)
291 })) {
292 Ok(Ok(db)) => {
293 let ptr = Box::new(DatabasePtr(db));
294 Box::into_raw(ptr)
295 }
296 Ok(Err(_)) | Err(_) => ptr::null_mut(),
297 }
298}
299
300#[unsafe(no_mangle)]
305pub unsafe extern "C" fn sochdb_is_concurrent(ptr: *mut DatabasePtr) -> c_int {
306 if ptr.is_null() {
307 return 0;
308 }
309 let db = unsafe { &*ptr };
310 if db.0.is_concurrent() { 1 } else { 0 }
311}
312
313#[unsafe(no_mangle)]
317pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
318 if !ptr.is_null() {
319 unsafe {
320 let _ = Box::from_raw(ptr);
321 }
322 }
323}
324
325#[unsafe(no_mangle)]
330pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
331 if ptr.is_null() {
332 return C_TxnHandle {
333 txn_id: 0,
334 snapshot_ts: 0,
335 };
336 }
337 let db = unsafe { &(*ptr).0 };
338 match db.begin_transaction() {
339 Ok(txn) => C_TxnHandle {
340 txn_id: txn.txn_id,
341 snapshot_ts: txn.snapshot_ts,
342 },
343 Err(_) => C_TxnHandle {
344 txn_id: 0,
345 snapshot_ts: 0,
346 },
347 }
348}
349
350#[unsafe(no_mangle)]
357pub unsafe extern "C" fn sochdb_commit(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> C_CommitResult {
358 if ptr.is_null() {
359 return C_CommitResult {
360 commit_ts: 0,
361 error_code: -1,
362 };
363 }
364 let db = unsafe { &(*ptr).0 };
365 let txn = TxnHandle {
366 txn_id: handle.txn_id,
367 snapshot_ts: handle.snapshot_ts,
368 };
369 match db.commit(txn) {
370 Ok(commit_ts) => C_CommitResult {
371 commit_ts,
372 error_code: 0,
373 },
374 Err(_) => C_CommitResult {
375 commit_ts: 0,
376 error_code: -1,
377 },
378 }
379}
380
381#[unsafe(no_mangle)]
386pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
387 if ptr.is_null() {
388 return -1;
389 }
390 let db = unsafe { &(*ptr).0 };
391 let txn = TxnHandle {
392 txn_id: handle.txn_id,
393 snapshot_ts: handle.snapshot_ts,
394 };
395 match db.abort(txn) {
396 Ok(_) => 0,
397 Err(_) => -1,
398 }
399}
400
401#[unsafe(no_mangle)]
407pub unsafe extern "C" fn sochdb_put(
408 ptr: *mut DatabasePtr,
409 handle: C_TxnHandle,
410 key_ptr: *const u8,
411 key_len: usize,
412 val_ptr: *const u8,
413 val_len: usize,
414) -> c_int {
415 if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
416 return -1;
417 }
418 let db = unsafe { &(*ptr).0 };
419 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
420 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
421 let txn = TxnHandle {
422 txn_id: handle.txn_id,
423 snapshot_ts: handle.snapshot_ts,
424 };
425
426 match db.put(txn, key, val) {
427 Ok(_) => 0,
428 Err(_) => -1,
429 }
430}
431
432#[unsafe(no_mangle)]
439pub unsafe extern "C" fn sochdb_get(
440 ptr: *mut DatabasePtr,
441 handle: C_TxnHandle,
442 key_ptr: *const u8,
443 key_len: usize,
444 val_out: *mut *mut u8,
445 len_out: *mut usize,
446) -> c_int {
447 if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.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.get(txn, key) {
458 Ok(Some(val)) => {
459 let mut buf = val.into_boxed_slice();
461 unsafe {
462 *val_out = buf.as_mut_ptr();
463 *len_out = buf.len();
464 }
465 let _ = Box::into_raw(buf); 0
467 }
468 Ok(None) => 1, Err(_) => -1,
470 }
471}
472
473#[unsafe(no_mangle)]
477pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
478 if !ptr.is_null() {
479 unsafe {
480 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
481 }
482 }
483}
484
485#[unsafe(no_mangle)]
490pub unsafe extern "C" fn sochdb_delete(
491 ptr: *mut DatabasePtr,
492 handle: C_TxnHandle,
493 key_ptr: *const u8,
494 key_len: usize,
495) -> c_int {
496 if ptr.is_null() || key_ptr.is_null() {
497 return -1;
498 }
499 let db = unsafe { &(*ptr).0 };
500 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
501 let txn = TxnHandle {
502 txn_id: handle.txn_id,
503 snapshot_ts: handle.snapshot_ts,
504 };
505
506 match db.delete(txn, key) {
507 Ok(_) => 0,
508 Err(_) => -1,
509 }
510}
511
512#[unsafe(no_mangle)]
516pub unsafe extern "C" fn sochdb_put_path(
517 ptr: *mut DatabasePtr,
518 handle: C_TxnHandle,
519 path_ptr: *const c_char,
520 val_ptr: *const u8,
521 val_len: usize,
522) -> c_int {
523 if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
524 return -1;
525 }
526 let db = unsafe { &(*ptr).0 };
527 let c_str = unsafe { CStr::from_ptr(path_ptr) };
528 let path_str = match c_str.to_str() {
529 Ok(s) => s,
530 Err(_) => return -1,
531 };
532 let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
533 let txn = TxnHandle {
534 txn_id: handle.txn_id,
535 snapshot_ts: handle.snapshot_ts,
536 };
537
538 match db.put_path(txn, path_str, val) {
539 Ok(_) => 0,
540 Err(_) => -1,
541 }
542}
543
544#[unsafe(no_mangle)]
548pub unsafe extern "C" fn sochdb_get_path(
549 ptr: *mut DatabasePtr,
550 handle: C_TxnHandle,
551 path_ptr: *const c_char,
552 val_out: *mut *mut u8,
553 len_out: *mut usize,
554) -> c_int {
555 if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
556 return -1;
557 }
558 let db = unsafe { &(*ptr).0 };
559 let c_str = unsafe { CStr::from_ptr(path_ptr) };
560 let path_str = match c_str.to_str() {
561 Ok(s) => s,
562 Err(_) => return -1,
563 };
564 let txn = TxnHandle {
565 txn_id: handle.txn_id,
566 snapshot_ts: handle.snapshot_ts,
567 };
568
569 match db.get_path(txn, path_str) {
570 Ok(Some(val)) => {
571 let mut buf = val.into_boxed_slice();
572 unsafe {
573 *val_out = buf.as_mut_ptr();
574 *len_out = buf.len();
575 }
576 let _ = Box::into_raw(buf);
577 0
578 }
579 Ok(None) => 1,
580 Err(_) => -1,
581 }
582}
583
584#[allow(clippy::type_complexity)]
586pub struct ScanIteratorPtr(
587 Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
588);
589
590#[unsafe(no_mangle)]
594pub unsafe extern "C" fn sochdb_scan(
595 ptr: *mut DatabasePtr,
596 handle: C_TxnHandle,
597 start_ptr: *const u8,
598 start_len: usize,
599 end_ptr: *const u8,
600 end_len: usize,
601) -> *mut ScanIteratorPtr {
602 if ptr.is_null() {
603 return ptr::null_mut();
604 }
605 let db = unsafe { &(*ptr).0 };
606 let txn = TxnHandle {
607 txn_id: handle.txn_id,
608 snapshot_ts: handle.snapshot_ts,
609 };
610
611 let start = if !start_ptr.is_null() && start_len > 0 {
612 unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
613 } else {
614 vec![]
615 };
616
617 let end = if !end_ptr.is_null() && end_len > 0 {
618 unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
619 } else {
620 vec![] };
622
623 match db.scan_range(txn, &start, &end) {
636 Ok(rows) => {
637 let iter = Box::new(rows.into_iter().map(Ok));
640
641 let ptr = Box::new(ScanIteratorPtr(iter));
642 Box::into_raw(ptr)
643 }
644 Err(_) => ptr::null_mut(),
645 }
646}
647
648#[unsafe(no_mangle)]
653pub unsafe extern "C" fn sochdb_scan_prefix(
654 ptr: *mut DatabasePtr,
655 handle: C_TxnHandle,
656 prefix_ptr: *const u8,
657 prefix_len: usize,
658) -> *mut ScanIteratorPtr {
659 if ptr.is_null() {
660 return ptr::null_mut();
661 }
662 let db = unsafe { &(*ptr).0 };
663 let txn = TxnHandle {
664 txn_id: handle.txn_id,
665 snapshot_ts: handle.snapshot_ts,
666 };
667
668 let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
669 unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
670 } else {
671 vec![]
672 };
673
674 match db.scan(txn, &prefix) {
676 Ok(rows) => {
677 let prefix_owned = prefix.clone();
680 let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
681 .into_iter()
682 .filter(|(k, _)| k.starts_with(&prefix_owned))
683 .collect();
684
685 let iter = Box::new(filtered.into_iter().map(Ok));
686 let ptr = Box::new(ScanIteratorPtr(iter));
687 Box::into_raw(ptr)
688 }
689 Err(_) => ptr::null_mut(),
690 }
691}
692
693#[unsafe(no_mangle)]
698pub unsafe extern "C" fn sochdb_scan_next(
699 iter_ptr: *mut ScanIteratorPtr,
700 key_out: *mut *mut u8,
701 key_len_out: *mut usize,
702 val_out: *mut *mut u8,
703 val_len_out: *mut usize,
704) -> c_int {
705 if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
706 return -1;
707 }
708 let iter = unsafe { &mut (*iter_ptr).0 };
709
710 match iter.next() {
711 Some(Ok((key, val))) => {
712 let mut key_buf = key.into_boxed_slice();
713 let mut val_buf = val.into_boxed_slice();
714 unsafe {
715 *key_out = key_buf.as_mut_ptr();
716 *key_len_out = key_buf.len();
717 *val_out = val_buf.as_mut_ptr();
718 *val_len_out = val_buf.len();
719 }
720 let _ = Box::into_raw(key_buf);
721 let _ = Box::into_raw(val_buf);
722 0
723 }
724 Some(Err(_)) => -1,
725 None => 1, }
727}
728
729#[unsafe(no_mangle)]
733pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
734 if !ptr.is_null() {
735 unsafe {
736 let _ = Box::from_raw(ptr);
737 }
738 }
739}
740
741#[unsafe(no_mangle)]
745pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
746 if ptr.is_null() {
747 return -1;
748 }
749 let db = unsafe { &(*ptr).0 };
750 match db.flush() {
751 Ok(_) => 0,
752 Err(_) => -1,
753 }
754}
755
756#[repr(C)]
758pub struct CStorageStats {
759 pub memtable_size_bytes: u64,
760 pub wal_size_bytes: u64,
761 pub active_transactions: usize,
762 pub min_active_snapshot: u64,
763 pub last_checkpoint_lsn: u64,
764}
765
766#[unsafe(no_mangle)]
770pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
771 if ptr.is_null() {
772 return CStorageStats {
773 memtable_size_bytes: 0,
774 wal_size_bytes: 0,
775 active_transactions: 0,
776 min_active_snapshot: 0,
777 last_checkpoint_lsn: 0,
778 };
779 }
780 let db = unsafe { &(*ptr).0 };
781 let stats = db.storage_stats();
782
783 CStorageStats {
784 memtable_size_bytes: stats.memtable_size_bytes,
785 wal_size_bytes: stats.wal_size_bytes,
786 active_transactions: stats.active_transactions,
787 min_active_snapshot: stats.min_active_snapshot,
788 last_checkpoint_lsn: stats.last_checkpoint_lsn,
789 }
790}
791
792#[repr(C)]
810pub struct CBatchPut {
811 pub data: *const u8,
813 pub len: usize,
815}
816
817#[unsafe(no_mangle)]
858pub unsafe extern "C" fn sochdb_put_many(
859 ptr: *mut DatabasePtr,
860 handle: C_TxnHandle,
861 batch: CBatchPut,
862) -> c_int {
863 if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
864 return -1;
865 }
866
867 let db = unsafe { &(*ptr).0 };
868 let txn = TxnHandle {
869 txn_id: handle.txn_id,
870 snapshot_ts: handle.snapshot_ts,
871 };
872
873 let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
875
876 if data.len() < 4 {
878 return -1;
879 }
880 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
881
882 let mut offset = 4;
883 let mut success_count = 0;
884
885 for _ in 0..num_entries {
886 if offset + 8 > data.len() {
888 return success_count;
889 }
890 let key_len = u32::from_le_bytes([
891 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
892 ]) as usize;
893 let val_len = u32::from_le_bytes([
894 data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]
895 ]) as usize;
896 offset += 8;
897
898 if offset + key_len + val_len > data.len() {
900 return success_count;
901 }
902 let key = &data[offset..offset + key_len];
903 offset += key_len;
904 let value = &data[offset..offset + val_len];
905 offset += val_len;
906
907 match db.put(txn, key, value) {
909 Ok(_) => success_count += 1,
910 Err(_) => return success_count,
911 }
912 }
913
914 success_count
915}
916
917#[unsafe(no_mangle)]
937pub unsafe extern "C" fn sochdb_delete_many(
938 ptr: *mut DatabasePtr,
939 handle: C_TxnHandle,
940 keys_data: *const u8,
941 keys_len: usize,
942) -> c_int {
943 if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
944 return -1;
945 }
946
947 let db = unsafe { &(*ptr).0 };
948 let txn = TxnHandle {
949 txn_id: handle.txn_id,
950 snapshot_ts: handle.snapshot_ts,
951 };
952
953 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
954
955 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
956
957 let mut offset = 4;
958 let mut success_count = 0;
959
960 for _ in 0..num_entries {
961 if offset + 4 > data.len() {
962 return success_count;
963 }
964 let key_len = u32::from_le_bytes([
965 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
966 ]) as usize;
967 offset += 4;
968
969 if offset + key_len > data.len() {
970 return success_count;
971 }
972 let key = &data[offset..offset + key_len];
973 offset += key_len;
974
975 match db.delete(txn, key) {
976 Ok(_) => success_count += 1,
977 Err(_) => return success_count,
978 }
979 }
980
981 success_count
982}
983
984#[unsafe(no_mangle)]
1012pub unsafe extern "C" fn sochdb_get_many(
1013 ptr: *mut DatabasePtr,
1014 handle: C_TxnHandle,
1015 keys_data: *const u8,
1016 keys_len: usize,
1017 result_out: *mut *mut u8,
1018 result_len_out: *mut usize,
1019) -> c_int {
1020 if ptr.is_null() || keys_data.is_null() || keys_len < 4
1021 || result_out.is_null() || result_len_out.is_null() {
1022 return -1;
1023 }
1024
1025 let db = unsafe { &(*ptr).0 };
1026 let txn = TxnHandle {
1027 txn_id: handle.txn_id,
1028 snapshot_ts: handle.snapshot_ts,
1029 };
1030
1031 let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
1032
1033 let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1034
1035 let mut result = Vec::with_capacity(4 + num_entries * 10); result.extend_from_slice(&(num_entries as u32).to_le_bytes());
1038
1039 let mut offset = 4;
1040
1041 for _ in 0..num_entries {
1042 if offset + 4 > data.len() {
1043 result.push(2); continue;
1045 }
1046 let key_len = u32::from_le_bytes([
1047 data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
1048 ]) as usize;
1049 offset += 4;
1050
1051 if offset + key_len > data.len() {
1052 result.push(2); continue;
1054 }
1055 let key = &data[offset..offset + key_len];
1056 offset += key_len;
1057
1058 match db.get(txn, key) {
1059 Ok(Some(value)) => {
1060 result.push(0); result.extend_from_slice(&(value.len() as u32).to_le_bytes());
1062 result.extend_from_slice(&value);
1063 }
1064 Ok(None) => {
1065 result.push(1); }
1067 Err(_) => {
1068 result.push(2); }
1070 }
1071 }
1072
1073 let mut boxed = result.into_boxed_slice();
1075 unsafe {
1076 *result_out = boxed.as_mut_ptr();
1077 *result_len_out = boxed.len();
1078 }
1079 let _ = Box::into_raw(boxed); 0
1082}
1083
1084#[unsafe(no_mangle)]
1141pub unsafe extern "C" fn sochdb_scan_batch(
1142 iter_ptr: *mut ScanIteratorPtr,
1143 batch_size: usize,
1144 result_out: *mut *mut u8,
1145 result_len_out: *mut usize,
1146) -> c_int {
1147 if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0 {
1148 return -1;
1149 }
1150
1151 let iter = unsafe { &mut (*iter_ptr).0 };
1152
1153 let estimated_size = 5 + batch_size * 108;
1156 let mut result = Vec::with_capacity(estimated_size);
1157
1158 result.extend_from_slice(&[0u8; 5]); let mut count = 0u32;
1162 let mut is_done = false;
1163
1164 for _ in 0..batch_size {
1165 match iter.next() {
1166 Some(Ok((key, val))) => {
1167 result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1169 result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1170 result.extend_from_slice(&key);
1171 result.extend_from_slice(&val);
1172 count += 1;
1173 }
1174 Some(Err(_)) => {
1175 result[0..4].copy_from_slice(&count.to_le_bytes());
1177 result[4] = 0; let mut boxed = result.into_boxed_slice();
1180 unsafe {
1181 *result_out = boxed.as_mut_ptr();
1182 *result_len_out = boxed.len();
1183 }
1184 let _ = Box::into_raw(boxed);
1185 return -1;
1186 }
1187 None => {
1188 is_done = true;
1189 break;
1190 }
1191 }
1192 }
1193
1194 result[0..4].copy_from_slice(&count.to_le_bytes());
1196 result[4] = if is_done { 1 } else { 0 };
1197
1198 if count == 0 && is_done {
1200 let mut boxed = result.into_boxed_slice();
1202 unsafe {
1203 *result_out = boxed.as_mut_ptr();
1204 *result_len_out = boxed.len();
1205 }
1206 let _ = Box::into_raw(boxed);
1207 return 1; }
1209
1210 let mut boxed = result.into_boxed_slice();
1212 unsafe {
1213 *result_out = boxed.as_mut_ptr();
1214 *result_len_out = boxed.len();
1215 }
1216 let _ = Box::into_raw(boxed);
1217
1218 0 }
1220
1221#[unsafe(no_mangle)]
1241pub unsafe extern "C" fn sochdb_set_table_index_policy(
1242 ptr: *mut DatabasePtr,
1243 table_name: *const c_char,
1244 policy: u8,
1245) -> c_int {
1246 if ptr.is_null() || table_name.is_null() {
1247 return -1;
1248 }
1249
1250 let c_str = unsafe { CStr::from_ptr(table_name) };
1251 let table = match c_str.to_str() {
1252 Ok(s) => s,
1253 Err(_) => return -1,
1254 };
1255
1256 let index_policy = match policy {
1257 0 => crate::index_policy::IndexPolicy::WriteOptimized,
1258 1 => crate::index_policy::IndexPolicy::Balanced,
1259 2 => crate::index_policy::IndexPolicy::ScanOptimized,
1260 3 => crate::index_policy::IndexPolicy::AppendOnly,
1261 _ => return -2,
1262 };
1263
1264 let db = unsafe { &(*ptr).0 };
1265
1266 let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1268 db.index_registry().configure_table(config);
1269
1270 0
1271}
1272
1273#[unsafe(no_mangle)]
1285pub unsafe extern "C" fn sochdb_get_table_index_policy(
1286 ptr: *mut DatabasePtr,
1287 table_name: *const c_char,
1288) -> u8 {
1289 if ptr.is_null() || table_name.is_null() {
1290 return 255;
1291 }
1292
1293 let c_str = unsafe { CStr::from_ptr(table_name) };
1294 let table = match c_str.to_str() {
1295 Ok(s) => s,
1296 Err(_) => return 255,
1297 };
1298
1299 let db = unsafe { &(*ptr).0 };
1300 let config = db.index_registry().get_config(table);
1301
1302 match config.policy {
1303 crate::index_policy::IndexPolicy::WriteOptimized => 0,
1304 crate::index_policy::IndexPolicy::Balanced => 1,
1305 crate::index_policy::IndexPolicy::ScanOptimized => 2,
1306 crate::index_policy::IndexPolicy::AppendOnly => 3,
1307 }
1308}
1309
1310#[repr(C)]
1312pub struct C_TemporalEdge {
1313 pub from_id: *const c_char,
1314 pub edge_type: *const c_char,
1315 pub to_id: *const c_char,
1316 pub valid_from: u64,
1317 pub valid_until: u64,
1318 pub properties_json: *const c_char, }
1320
1321#[unsafe(no_mangle)]
1325pub unsafe extern "C" fn sochdb_add_temporal_edge(
1326 ptr: *mut DatabasePtr,
1327 namespace: *const c_char,
1328 edge: C_TemporalEdge,
1329) -> c_int {
1330 if ptr.is_null() || namespace.is_null() || edge.from_id.is_null()
1331 || edge.edge_type.is_null() || edge.to_id.is_null() {
1332 return -1;
1333 }
1334
1335 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1336 Ok(s) => s,
1337 Err(_) => return -1,
1338 };
1339 let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1340 Ok(s) => s,
1341 Err(_) => return -1,
1342 };
1343 let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1344 Ok(s) => s,
1345 Err(_) => return -1,
1346 };
1347 let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1348 Ok(s) => s,
1349 Err(_) => return -1,
1350 };
1351
1352 let db = unsafe { &(*ptr).0 };
1353
1354 let txn = match db.begin_transaction() {
1356 Ok(t) => t,
1357 Err(_) => return -1,
1358 };
1359
1360 let key = format!(
1362 "_graph/{}/temporal/{}/{}/{}/{:016x}",
1363 ns, from, etype, to, edge.valid_from
1364 );
1365
1366 let props_str = if edge.properties_json.is_null() {
1367 "{}".to_string()
1368 } else {
1369 match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1370 Ok(s) => s.to_string(),
1371 Err(_) => return -1,
1372 }
1373 };
1374
1375 let value = format!(
1376 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1377 from, etype, to, edge.valid_from, edge.valid_until, props_str
1378 );
1379
1380 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1381 let _ = db.abort(txn);
1382 return -1;
1383 }
1384
1385 match db.commit(txn) {
1386 Ok(_) => 0,
1387 Err(_) => -1,
1388 }
1389}
1390
1391#[unsafe(no_mangle)]
1398pub unsafe extern "C" fn sochdb_query_temporal_graph(
1399 ptr: *mut DatabasePtr,
1400 namespace: *const c_char,
1401 node_id: *const c_char,
1402 query_mode: u8,
1403 timestamp: u64, start_time: u64, end_time: u64, edge_type: *const c_char, out_len: *mut usize,
1408) -> *mut c_char {
1409 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1410 return ptr::null_mut();
1411 }
1412
1413 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1414 Ok(s) => s,
1415 Err(_) => return ptr::null_mut(),
1416 };
1417 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1418 Ok(s) => s,
1419 Err(_) => return ptr::null_mut(),
1420 };
1421
1422 let edge_filter = if edge_type.is_null() {
1423 None
1424 } else {
1425 match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1426 Ok(s) => Some(s),
1427 Err(_) => return ptr::null_mut(),
1428 }
1429 };
1430
1431 let db = unsafe { &(*ptr).0 };
1432
1433 let txn = match db.begin_transaction() {
1435 Ok(t) => t,
1436 Err(_) => return ptr::null_mut(),
1437 };
1438
1439 let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1441 let pairs = match db.scan(txn, prefix.as_bytes()) {
1442 Ok(p) => p,
1443 Err(_) => {
1444 let _ = db.abort(txn);
1445 return ptr::null_mut();
1446 }
1447 };
1448
1449 if let Err(_) = db.commit(txn) {
1451 return ptr::null_mut();
1452 }
1453
1454 let mut results = Vec::new();
1455 let now = std::time::SystemTime::now()
1456 .duration_since(std::time::UNIX_EPOCH)
1457 .unwrap()
1458 .as_millis() as u64;
1459
1460 for (_key, value) in pairs {
1461 let value_str = match std::str::from_utf8(&value) {
1463 Ok(s) => s,
1464 Err(_) => continue,
1465 };
1466
1467 if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1469 if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1470 let vf_start = valid_from_pos + r#""valid_from":"#.len();
1471 let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1472 let vu_start = valid_until_pos + r#""valid_until":"#.len();
1473 let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1474
1475 let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1476 let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1477
1478 if let Some(filter) = edge_filter {
1480 if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1481 continue;
1482 }
1483 }
1484
1485 let matches = match query_mode {
1487 0 => timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until),
1488 1 => {
1489 let edge_end = if valid_until == 0 { u64::MAX } else { valid_until };
1490 valid_from < end_time && edge_end > start_time
1491 }
1492 2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1493 _ => false,
1494 };
1495
1496 if matches {
1497 results.push(value_str.to_string());
1498 }
1499 }
1500 }
1501 }
1502
1503 let json = format!("[{}]", results.join(","));
1505 let c_string = match std::ffi::CString::new(json) {
1506 Ok(s) => s,
1507 Err(_) => return ptr::null_mut(),
1508 };
1509
1510 unsafe { *out_len = c_string.as_bytes().len() };
1511 c_string.into_raw()
1512}
1513
1514#[unsafe(no_mangle)]
1518pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1519 if !ptr.is_null() {
1520 unsafe {
1521 let _ = std::ffi::CString::from_raw(ptr);
1522 }
1523 }
1524}
1525
1526#[unsafe(no_mangle)]
1541pub unsafe extern "C" fn sochdb_graph_add_node(
1542 ptr: *mut DatabasePtr,
1543 namespace: *const c_char,
1544 node_id: *const c_char,
1545 node_type: *const c_char,
1546 properties_json: *const c_char,
1547) -> c_int {
1548 if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1549 return -1;
1550 }
1551
1552 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1553 Ok(s) => s,
1554 Err(_) => return -1,
1555 };
1556 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1557 Ok(s) => s,
1558 Err(_) => return -1,
1559 };
1560 let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1561 Ok(s) => s,
1562 Err(_) => return -1,
1563 };
1564 let props = if properties_json.is_null() {
1565 "{}".to_string()
1566 } else {
1567 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1568 Ok(s) => s.to_string(),
1569 Err(_) => return -1,
1570 }
1571 };
1572
1573 let db = unsafe { &(*ptr).0 };
1574
1575 let txn = match db.begin_transaction() {
1576 Ok(t) => t,
1577 Err(_) => return -1,
1578 };
1579
1580 let key = format!("_graph/{}/nodes/{}", ns, id);
1581 let value = format!(
1582 r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1583 id, ntype, props
1584 );
1585
1586 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1587 let _ = db.abort(txn);
1588 return -1;
1589 }
1590
1591 match db.commit(txn) {
1592 Ok(_) => 0,
1593 Err(_) => -1,
1594 }
1595}
1596
1597#[unsafe(no_mangle)]
1608pub unsafe extern "C" fn sochdb_graph_add_edge(
1609 ptr: *mut DatabasePtr,
1610 namespace: *const c_char,
1611 from_id: *const c_char,
1612 edge_type: *const c_char,
1613 to_id: *const c_char,
1614 properties_json: *const c_char,
1615) -> c_int {
1616 if ptr.is_null() || namespace.is_null() || from_id.is_null()
1617 || edge_type.is_null() || to_id.is_null() {
1618 return -1;
1619 }
1620
1621 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1622 Ok(s) => s,
1623 Err(_) => return -1,
1624 };
1625 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1626 Ok(s) => s,
1627 Err(_) => return -1,
1628 };
1629 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1630 Ok(s) => s,
1631 Err(_) => return -1,
1632 };
1633 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1634 Ok(s) => s,
1635 Err(_) => return -1,
1636 };
1637 let props = if properties_json.is_null() {
1638 "{}".to_string()
1639 } else {
1640 match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1641 Ok(s) => s.to_string(),
1642 Err(_) => return -1,
1643 }
1644 };
1645
1646 let db = unsafe { &(*ptr).0 };
1647
1648 let txn = match db.begin_transaction() {
1649 Ok(t) => t,
1650 Err(_) => return -1,
1651 };
1652
1653 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1654 let value = format!(
1655 r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1656 from, etype, to, props
1657 );
1658
1659 if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1660 let _ = db.abort(txn);
1661 return -1;
1662 }
1663
1664 match db.commit(txn) {
1665 Ok(_) => 0,
1666 Err(_) => -1,
1667 }
1668}
1669
1670#[unsafe(no_mangle)]
1680pub unsafe extern "C" fn sochdb_graph_traverse(
1681 ptr: *mut DatabasePtr,
1682 namespace: *const c_char,
1683 start_node: *const c_char,
1684 max_depth: usize,
1685 order: u8, out_len: *mut usize,
1687) -> *mut c_char {
1688 if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1689 return ptr::null_mut();
1690 }
1691
1692 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1693 Ok(s) => s,
1694 Err(_) => return ptr::null_mut(),
1695 };
1696 let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1697 Ok(s) => s,
1698 Err(_) => return ptr::null_mut(),
1699 };
1700
1701 let db = unsafe { &(*ptr).0 };
1702
1703 let txn = match db.begin_transaction() {
1704 Ok(t) => t,
1705 Err(_) => return ptr::null_mut(),
1706 };
1707
1708 let mut visited_nodes = std::collections::HashSet::new();
1710 let mut nodes_json = Vec::new();
1711 let mut edges_json = Vec::new();
1712
1713 let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1715
1716 while let Some((current_node, depth)) = if order == 0 {
1717 if frontier.is_empty() { None } else { Some(frontier.remove(0)) }
1719 } else {
1720 frontier.pop()
1722 } {
1723 if depth > max_depth || visited_nodes.contains(¤t_node) {
1724 continue;
1725 }
1726 visited_nodes.insert(current_node.clone());
1727
1728 let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1730 if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1731 if let Ok(s) = std::str::from_utf8(&node_data) {
1732 nodes_json.push(s.to_string());
1733 }
1734 }
1735
1736 let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1738 if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1739 for (_key, value) in edges {
1740 if let Ok(edge_str) = std::str::from_utf8(&value) {
1741 edges_json.push(edge_str.to_string());
1742
1743 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
1745 let start_idx = to_pos + r#""to_id":""#.len();
1746 if let Some(end_idx) = edge_str[start_idx..].find('"') {
1747 let to_id = &edge_str[start_idx..start_idx + end_idx];
1748 if !visited_nodes.contains(to_id) {
1749 frontier.push((to_id.to_string(), depth + 1));
1750 }
1751 }
1752 }
1753 }
1754 }
1755 }
1756 }
1757
1758 if let Err(_) = db.commit(txn) {
1759 return ptr::null_mut();
1760 }
1761
1762 let result = format!(
1763 r#"{{"nodes":[{}],"edges":[{}]}}"#,
1764 nodes_json.join(","),
1765 edges_json.join(",")
1766 );
1767
1768 let c_string = match std::ffi::CString::new(result) {
1769 Ok(s) => s,
1770 Err(_) => return ptr::null_mut(),
1771 };
1772
1773 unsafe { *out_len = c_string.as_bytes().len() };
1774 c_string.into_raw()
1775}
1776
1777#[unsafe(no_mangle)]
1790pub unsafe extern "C" fn sochdb_cache_put(
1791 ptr: *mut DatabasePtr,
1792 cache_name: *const c_char,
1793 key: *const c_char,
1794 value: *const c_char,
1795 embedding_ptr: *const f32,
1796 embedding_len: usize,
1797 ttl_seconds: u64,
1798) -> c_int {
1799 if ptr.is_null() || cache_name.is_null() || key.is_null()
1800 || value.is_null() || embedding_ptr.is_null() {
1801 return -1;
1802 }
1803
1804 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1805 Ok(s) => s,
1806 Err(_) => return -1,
1807 };
1808 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
1809 Ok(s) => s,
1810 Err(_) => return -1,
1811 };
1812 let v = match unsafe { CStr::from_ptr(value) }.to_str() {
1813 Ok(s) => s,
1814 Err(_) => return -1,
1815 };
1816 let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
1817
1818 let db = unsafe { &(*ptr).0 };
1819
1820 let txn = match db.begin_transaction() {
1821 Ok(t) => t,
1822 Err(_) => return -1,
1823 };
1824
1825 let expires_at = if ttl_seconds > 0 {
1827 std::time::SystemTime::now()
1828 .duration_since(std::time::UNIX_EPOCH)
1829 .unwrap()
1830 .as_secs() + ttl_seconds
1831 } else {
1832 0 };
1834
1835 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
1837 let cache_key = format!("_cache/{}/{}", cache, key_hash);
1838
1839 let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
1841
1842 let cache_value = format!(
1843 r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
1844 k, v, embedding_json.join(","), expires_at
1845 );
1846
1847 if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
1848 let _ = db.abort(txn);
1849 return -1;
1850 }
1851
1852 match db.commit(txn) {
1853 Ok(_) => 0,
1854 Err(_) => -1,
1855 }
1856}
1857
1858#[unsafe(no_mangle)]
1866pub unsafe extern "C" fn sochdb_cache_get(
1867 ptr: *mut DatabasePtr,
1868 cache_name: *const c_char,
1869 query_embedding_ptr: *const f32,
1870 embedding_len: usize,
1871 threshold: f32,
1872 out_len: *mut usize,
1873) -> *mut c_char {
1874 if ptr.is_null() || cache_name.is_null() || query_embedding_ptr.is_null() || out_len.is_null() {
1875 return ptr::null_mut();
1876 }
1877
1878 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1879 Ok(s) => s,
1880 Err(_) => return ptr::null_mut(),
1881 };
1882 let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
1883
1884 let db = unsafe { &(*ptr).0 };
1885
1886 let txn = match db.begin_transaction() {
1887 Ok(t) => t,
1888 Err(_) => return ptr::null_mut(),
1889 };
1890
1891 let prefix = format!("_cache/{}/", cache);
1892 let entries = match db.scan(txn, prefix.as_bytes()) {
1893 Ok(e) => e,
1894 Err(_) => {
1895 let _ = db.abort(txn);
1896 return ptr::null_mut();
1897 }
1898 };
1899
1900 let _ = db.commit(txn);
1901
1902 let now = std::time::SystemTime::now()
1903 .duration_since(std::time::UNIX_EPOCH)
1904 .unwrap()
1905 .as_secs();
1906
1907 let mut best_match: Option<(f32, String)> = None;
1908
1909 for (_key, value) in entries {
1910 let value_str = match std::str::from_utf8(&value) {
1911 Ok(s) => s,
1912 Err(_) => continue,
1913 };
1914
1915 if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
1917 let exp_start = exp_pos + r#""expires_at":"#.len();
1918 if let Some(exp_end) = value_str[exp_start..].find('}') {
1919 let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
1920 .parse()
1921 .unwrap_or(0);
1922 if expires_at > 0 && now > expires_at {
1923 continue; }
1925 }
1926 }
1927
1928 if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
1930 let emb_start = emb_pos + r#""embedding":["#.len();
1931 if let Some(emb_end) = value_str[emb_start..].find(']') {
1932 let emb_str = &value_str[emb_start..emb_start + emb_end];
1933 let cached_embedding: Vec<f32> = emb_str
1934 .split(',')
1935 .filter_map(|s| s.trim().parse().ok())
1936 .collect();
1937
1938 if cached_embedding.len() == query.len() {
1939 let similarity = cosine_similarity(query, &cached_embedding);
1940 if similarity >= threshold {
1941 if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
1942 if let Some(val_pos) = value_str.find(r#""value":""#) {
1944 let val_start = val_pos + r#""value":""#.len();
1945 if let Some(val_end) = value_str[val_start..].find('"') {
1946 let cached_value = &value_str[val_start..val_start + val_end];
1947 best_match = Some((similarity, cached_value.to_string()));
1948 }
1949 }
1950 }
1951 }
1952 }
1953 }
1954 }
1955 }
1956
1957 match best_match {
1958 Some((_, value)) => {
1959 let c_string = match std::ffi::CString::new(value) {
1960 Ok(s) => s,
1961 Err(_) => return ptr::null_mut(),
1962 };
1963 unsafe { *out_len = c_string.as_bytes().len() };
1964 c_string.into_raw()
1965 }
1966 None => ptr::null_mut(),
1967 }
1968}
1969
1970fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
1973 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1974 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
1975 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1976 if norm_a == 0.0 || norm_b == 0.0 {
1977 0.0
1978 } else {
1979 let similarity = dot / (norm_a * norm_b);
1980 (similarity + 1.0) / 2.0
1983 }
1984}
1985
1986#[unsafe(no_mangle)]
2001pub unsafe extern "C" fn sochdb_trace_start(
2002 ptr: *mut DatabasePtr,
2003 name: *const c_char,
2004 trace_id_out: *mut *mut c_char,
2005 span_id_out: *mut *mut c_char,
2006) -> c_int {
2007 if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
2008 return -1;
2009 }
2010
2011 let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2012 Ok(s) => s,
2013 Err(_) => return -1,
2014 };
2015
2016 let db = unsafe { &(*ptr).0 };
2017
2018 let trace_id = format!("trace_{:016x}", rand_u64());
2020 let span_id = format!("span_{:016x}", rand_u64());
2021
2022 let txn = match db.begin_transaction() {
2023 Ok(t) => t,
2024 Err(_) => return -1,
2025 };
2026
2027 let now = std::time::SystemTime::now()
2028 .duration_since(std::time::UNIX_EPOCH)
2029 .unwrap()
2030 .as_micros() as u64;
2031
2032 let trace_key = format!("_traces/{}", trace_id);
2034 let trace_value = format!(
2035 r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
2036 trace_id, trace_name, now, span_id
2037 );
2038
2039 if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
2040 let _ = db.abort(txn);
2041 return -1;
2042 }
2043
2044 let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
2046 let span_value = format!(
2047 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
2048 span_id, trace_name, now
2049 );
2050
2051 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2052 let _ = db.abort(txn);
2053 return -1;
2054 }
2055
2056 if let Err(_) = db.commit(txn) {
2057 return -1;
2058 }
2059
2060 let trace_c = match std::ffi::CString::new(trace_id) {
2062 Ok(s) => s,
2063 Err(_) => return -1,
2064 };
2065 let span_c = match std::ffi::CString::new(span_id) {
2066 Ok(s) => s,
2067 Err(_) => return -1,
2068 };
2069
2070 unsafe {
2071 *trace_id_out = trace_c.into_raw();
2072 *span_id_out = span_c.into_raw();
2073 }
2074
2075 0
2076}
2077
2078#[unsafe(no_mangle)]
2085pub unsafe extern "C" fn sochdb_trace_span_start(
2086 ptr: *mut DatabasePtr,
2087 trace_id: *const c_char,
2088 parent_span_id: *const c_char,
2089 name: *const c_char,
2090 span_id_out: *mut *mut c_char,
2091) -> c_int {
2092 if ptr.is_null() || trace_id.is_null() || parent_span_id.is_null()
2093 || name.is_null() || span_id_out.is_null() {
2094 return -1;
2095 }
2096
2097 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2098 Ok(s) => s,
2099 Err(_) => return -1,
2100 };
2101 let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
2102 Ok(s) => s,
2103 Err(_) => return -1,
2104 };
2105 let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2106 Ok(s) => s,
2107 Err(_) => return -1,
2108 };
2109
2110 let db = unsafe { &(*ptr).0 };
2111 let span_id = format!("span_{:016x}", rand_u64());
2112
2113 let txn = match db.begin_transaction() {
2114 Ok(t) => t,
2115 Err(_) => return -1,
2116 };
2117
2118 let now = std::time::SystemTime::now()
2119 .duration_since(std::time::UNIX_EPOCH)
2120 .unwrap()
2121 .as_micros() as u64;
2122
2123 let span_key = format!("_traces/{}/spans/{}", tid, span_id);
2124 let span_value = format!(
2125 r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
2126 span_id, span_name, now, pid
2127 );
2128
2129 if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2130 let _ = db.abort(txn);
2131 return -1;
2132 }
2133
2134 if let Err(_) = db.commit(txn) {
2135 return -1;
2136 }
2137
2138 let span_c = match std::ffi::CString::new(span_id) {
2139 Ok(s) => s,
2140 Err(_) => return -1,
2141 };
2142
2143 unsafe { *span_id_out = span_c.into_raw() };
2144 0
2145}
2146
2147#[unsafe(no_mangle)]
2157pub unsafe extern "C" fn sochdb_trace_span_end(
2158 ptr: *mut DatabasePtr,
2159 trace_id: *const c_char,
2160 span_id: *const c_char,
2161 status: u8,
2162) -> i64 {
2163 if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2164 return -1;
2165 }
2166
2167 let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2168 Ok(s) => s,
2169 Err(_) => return -1,
2170 };
2171 let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2172 Ok(s) => s,
2173 Err(_) => return -1,
2174 };
2175
2176 let db = unsafe { &(*ptr).0 };
2177
2178 let txn = match db.begin_transaction() {
2179 Ok(t) => t,
2180 Err(_) => return -1,
2181 };
2182
2183 let span_key = format!("_traces/{}/spans/{}", tid, sid);
2184
2185 let span_data = match db.get(txn, span_key.as_bytes()) {
2187 Ok(Some(data)) => data,
2188 _ => {
2189 let _ = db.abort(txn);
2190 return -1;
2191 }
2192 };
2193
2194 let span_str = match std::str::from_utf8(&span_data) {
2195 Ok(s) => s,
2196 Err(_) => {
2197 let _ = db.abort(txn);
2198 return -1;
2199 }
2200 };
2201
2202 let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2204 let start = pos + r#""start_us":"#.len();
2205 if let Some(end) = span_str[start..].find(',') {
2206 span_str[start..start + end].parse().unwrap_or(0u64)
2207 } else {
2208 0u64
2209 }
2210 } else {
2211 0u64
2212 };
2213
2214 let now = std::time::SystemTime::now()
2215 .duration_since(std::time::UNIX_EPOCH)
2216 .unwrap()
2217 .as_micros() as u64;
2218
2219 let duration_us = now.saturating_sub(start_us);
2220 let status_str = match status {
2221 1 => "ok",
2222 2 => "error",
2223 _ => "unset",
2224 };
2225
2226 let new_span = span_str
2228 .replace(r#""status":"active""#, &format!(r#""status":"{}","end_us":{},"duration_us":{}"#, status_str, now, duration_us));
2229
2230 if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2231 let _ = db.abort(txn);
2232 return -1;
2233 }
2234
2235 if let Err(_) = db.commit(txn) {
2236 return -1;
2237 }
2238
2239 duration_us as i64
2240}
2241
2242fn rand_u64() -> u64 {
2244 use std::sync::atomic::{AtomicU64, Ordering};
2245 static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2246
2247 let mut s = STATE.load(Ordering::Relaxed);
2248 if s == 0 {
2249 s = std::time::SystemTime::now()
2250 .duration_since(std::time::UNIX_EPOCH)
2251 .unwrap()
2252 .as_nanos() as u64;
2253 }
2254 s ^= s >> 12;
2255 s ^= s << 25;
2256 s ^= s >> 27;
2257 STATE.store(s, Ordering::Relaxed);
2258 s.wrapping_mul(0x2545F4914F6CDD1D)
2259}
2260
2261#[unsafe(no_mangle)]
2271pub unsafe extern "C" fn sochdb_collection_create(
2272 ptr: *mut DatabasePtr,
2273 namespace: *const c_char,
2274 collection: *const c_char,
2275 dimension: usize,
2276 dist_type: u8, ) -> c_int {
2278 if ptr.is_null() || namespace.is_null() || collection.is_null() {
2279 return -1;
2280 }
2281
2282 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2283 Ok(s) => s,
2284 Err(_) => return -1,
2285 };
2286 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2287 Ok(s) => s,
2288 Err(_) => return -1,
2289 };
2290
2291 let db = unsafe { &(*ptr).0 };
2292 let txn = match db.begin_transaction() {
2293 Ok(t) => t,
2294 Err(_) => return -1,
2295 };
2296
2297 let config_key = format!("{}/_collections/{}", ns, col);
2299 let config_value = format!(
2300 r#"{{"dimension":{},"metric":{}}}"#,
2301 dimension, dist_type
2302 );
2303
2304 if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2305 let _ = db.abort(txn);
2306 return -1;
2307 }
2308
2309 let result = match db.commit(txn) {
2310 Ok(_) => 0,
2311 Err(_) => -1,
2312 };
2313
2314 if result == 0 {
2315 let metric = match dist_type {
2316 1 => DistanceMetric::Euclidean,
2317 2 => DistanceMetric::DotProduct,
2318 _ => DistanceMetric::Cosine,
2319 };
2320 let _ = ensure_collection_index(db, ns, col, dimension, metric);
2321 }
2322
2323 result
2324}
2325
2326#[unsafe(no_mangle)]
2332pub unsafe extern "C" fn sochdb_collection_insert(
2333 ptr: *mut DatabasePtr,
2334 namespace: *const c_char,
2335 collection: *const c_char,
2336 id: *const c_char,
2337 vector_ptr: *const f32,
2338 vector_len: usize,
2339 metadata_json: *const c_char, ) -> c_int {
2341 if ptr.is_null() || namespace.is_null() || collection.is_null()
2342 || id.is_null() || vector_ptr.is_null() {
2343 return -1;
2344 }
2345
2346 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2347 Ok(s) => s,
2348 Err(_) => return -1,
2349 };
2350 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2351 Ok(s) => s,
2352 Err(_) => return -1,
2353 };
2354 let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2355 Ok(s) => s,
2356 Err(_) => return -1,
2357 };
2358 let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2359 let db = unsafe { &(*ptr).0 };
2360
2361 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2362 Some(config) => config,
2363 None => (vector_len, DistanceMetric::Cosine),
2364 };
2365 if vector_len != dimension {
2366 return -1;
2367 }
2368
2369 let metadata = if !metadata_json.is_null() {
2370 match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2371 Ok(s) => s.to_string(),
2372 Err(_) => "{}".to_string(),
2373 }
2374 } else {
2375 "{}".to_string()
2376 };
2377
2378 let txn = match db.begin_transaction() {
2379 Ok(t) => t,
2380 Err(_) => return -1,
2381 };
2382
2383 let id_hash = hash_id_to_u128(doc_id);
2384 let vec_key = vector_bin_key(ns, col, id_hash);
2385 let vec_value = serialize_vector_binary(vector);
2386
2387 if let Err(_) = db.put(txn, vec_key.as_bytes(), &vec_value) {
2388 let _ = db.abort(txn);
2389 return -1;
2390 }
2391
2392 let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2393 Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2394 Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2395 };
2396 let meta_key = metadata_key(ns, col, id_hash);
2397 if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2398 if let Err(_) = db.put(txn, meta_key.as_bytes(), &meta_bytes) {
2399 let _ = db.abort(txn);
2400 return -1;
2401 }
2402 }
2403
2404 if let Err(_) = db.commit(txn) {
2405 return -1;
2406 }
2407
2408 let index = ensure_collection_index(db, ns, col, dimension, metric);
2409 let _ = index.index.insert(id_hash, vector.to_vec());
2410
2411 0
2412}
2413
2414#[unsafe(no_mangle)]
2423pub unsafe extern "C" fn sochdb_collection_insert_batch(
2424 ptr: *mut DatabasePtr,
2425 namespace: *const c_char,
2426 collection: *const c_char,
2427 ids: *const *const c_char, vectors: *const f32, dimension: usize,
2430 metadata_jsons: *const *const c_char, count: usize,
2432) -> c_int {
2433 if ptr.is_null() || namespace.is_null() || collection.is_null()
2434 || ids.is_null() || vectors.is_null() || count == 0
2435 {
2436 return -1;
2437 }
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 db = unsafe { &(*ptr).0 };
2448
2449 let (expected_dim, metric) = match resolve_collection_config(db, ns, col) {
2450 Some(config) => config,
2451 None => (dimension, DistanceMetric::Cosine),
2452 };
2453 if dimension != expected_dim {
2454 return -1;
2455 }
2456
2457 let txn = match db.begin_transaction() {
2459 Ok(t) => t,
2460 Err(_) => return -1,
2461 };
2462
2463 let ids_slice = unsafe { slice::from_raw_parts(ids, count) };
2464 let vectors_flat = unsafe { slice::from_raw_parts(vectors, count * dimension) };
2465
2466 let mut inserted = 0i32;
2467 let mut id_hashes = Vec::with_capacity(count);
2468 let mut vector_copies = Vec::with_capacity(count);
2469
2470 for i in 0..count {
2471 let doc_id = match unsafe { CStr::from_ptr(ids_slice[i]) }.to_str() {
2472 Ok(s) => s,
2473 Err(_) => continue,
2474 };
2475 let vec_start = i * dimension;
2476 let vector = &vectors_flat[vec_start..vec_start + dimension];
2477
2478 let id_hash = hash_id_to_u128(doc_id);
2479 let vec_key = vector_bin_key(ns, col, id_hash);
2480 let vec_value = serialize_vector_binary(vector);
2481
2482 if db.put(txn, vec_key.as_bytes(), &vec_value).is_err() {
2483 continue;
2484 }
2485
2486 let metadata = if !metadata_jsons.is_null() {
2488 let meta_ptr = unsafe { *metadata_jsons.add(i) };
2489 if !meta_ptr.is_null() {
2490 match unsafe { CStr::from_ptr(meta_ptr) }.to_str() {
2491 Ok(s) => s.to_string(),
2492 Err(_) => "{}".to_string(),
2493 }
2494 } else {
2495 "{}".to_string()
2496 }
2497 } else {
2498 "{}".to_string()
2499 };
2500
2501 let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2502 Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2503 Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2504 };
2505 let meta_key = metadata_key(ns, col, id_hash);
2506 if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2507 let _ = db.put(txn, meta_key.as_bytes(), &meta_bytes);
2508 }
2509
2510 id_hashes.push(id_hash);
2511 vector_copies.push(vector.to_vec());
2512 inserted += 1;
2513 }
2514
2515 if db.commit(txn).is_err() {
2517 return -1;
2518 }
2519
2520 let index = ensure_collection_index(db, ns, col, dimension, metric);
2522 for (id_hash, vector) in id_hashes.into_iter().zip(vector_copies.into_iter()) {
2523 let _ = index.index.insert(id_hash, vector);
2524 }
2525
2526 inserted
2527}
2528
2529#[repr(C)]
2531pub struct CSearchResult {
2532 pub id_ptr: *mut c_char,
2533 pub score: f32,
2534 pub metadata_ptr: *mut c_char,
2535}
2536
2537#[unsafe(no_mangle)]
2543pub unsafe extern "C" fn sochdb_collection_search(
2544 ptr: *mut DatabasePtr,
2545 namespace: *const c_char,
2546 collection: *const c_char,
2547 query_ptr: *const f32,
2548 query_len: usize,
2549 k: usize,
2550 results_out: *mut CSearchResult,
2551) -> c_int {
2552 if ptr.is_null() || namespace.is_null() || collection.is_null()
2553 || query_ptr.is_null() || results_out.is_null() {
2554 return -1;
2555 }
2556 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2557 Ok(s) => s,
2558 Err(_) => return -1,
2559 };
2560 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2561 Ok(s) => s,
2562 Err(_) => return -1,
2563 };
2564 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2565 let db = unsafe { &(*ptr).0 };
2566 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2567 Some(config) => config,
2568 None => return 0,
2569 };
2570
2571 if query_len != dimension {
2572 return -1;
2573 }
2574
2575 let index = ensure_collection_index(db, ns, col, dimension, metric);
2576 let mut scored = match index.index.search(query, k) {
2577 Ok(results) => results,
2578 Err(_) => return -1,
2579 };
2580
2581 let result_count = scored.len().min(k);
2582 for (i, (id_hash, distance)) in scored.drain(..result_count).enumerate() {
2583 let meta_key = metadata_key(ns, col, id_hash);
2584 let txn = match db.begin_transaction() {
2585 Ok(t) => t,
2586 Err(_) => return -1,
2587 };
2588 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2589 let _ = db.commit(txn);
2590
2591 let mut id_value = String::new();
2592 let mut metadata_json = serde_json::json!({});
2593 if let Some(bytes) = meta_value.as_deref() {
2594 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(bytes) {
2595 id_value = parsed.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2596 metadata_json = parsed.get("metadata").cloned().unwrap_or(serde_json::json!({}));
2597 }
2598 }
2599 let metadata = serde_json::to_string(&metadata_json).unwrap_or_else(|_| "{}".to_string());
2600
2601 let c_id = match std::ffi::CString::new(id_value) {
2602 Ok(s) => s.into_raw(),
2603 Err(_) => ptr::null_mut(),
2604 };
2605 let c_meta = match std::ffi::CString::new(metadata) {
2606 Ok(s) => s.into_raw(),
2607 Err(_) => ptr::null_mut(),
2608 };
2609
2610 unsafe {
2611 (*results_out.add(i)).id_ptr = c_id;
2612 (*results_out.add(i)).score = decode_score(metric, distance);
2613 (*results_out.add(i)).metadata_ptr = c_meta;
2614 }
2615 }
2616
2617 result_count as c_int
2618}
2619
2620#[unsafe(no_mangle)]
2626pub unsafe extern "C" fn sochdb_collection_search_soa(
2627 ptr: *mut DatabasePtr,
2628 namespace: *const c_char,
2629 collection: *const c_char,
2630 query_ptr: *const f32,
2631 query_len: usize,
2632 k: usize,
2633 min_score: f32,
2634 filter_json: *const c_char,
2635 ids_hi_out: *mut *mut u64,
2636 ids_lo_out: *mut *mut u64,
2637 scores_out: *mut *mut f32,
2638 len_out: *mut usize,
2639) -> c_int {
2640 if ptr.is_null() || namespace.is_null() || collection.is_null()
2641 || query_ptr.is_null() || ids_hi_out.is_null() || ids_lo_out.is_null()
2642 || scores_out.is_null() || len_out.is_null() {
2643 return -1;
2644 }
2645
2646 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2647 Ok(s) => s,
2648 Err(_) => return -1,
2649 };
2650 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2651 Ok(s) => s,
2652 Err(_) => return -1,
2653 };
2654 let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2655 let db = unsafe { &(*ptr).0 };
2656
2657 let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2658 Some(config) => config,
2659 None => return 0,
2660 };
2661 if query_len != dimension {
2662 return -1;
2663 }
2664
2665 let filter = if !filter_json.is_null() {
2666 match unsafe { CStr::from_ptr(filter_json) }.to_str() {
2667 Ok(s) => serde_json::from_str::<serde_json::Value>(s).ok(),
2668 Err(_) => None,
2669 }
2670 } else {
2671 None
2672 };
2673
2674 let index = ensure_collection_index(db, ns, col, dimension, metric);
2675 let results = match index.index.search(query, k) {
2676 Ok(results) => results,
2677 Err(_) => return -1,
2678 };
2679
2680 let mut ids_hi: Vec<u64> = Vec::with_capacity(results.len());
2681 let mut ids_lo: Vec<u64> = Vec::with_capacity(results.len());
2682 let mut scores: Vec<f32> = Vec::with_capacity(results.len());
2683
2684 for (id_hash, distance) in results {
2685 let score = decode_score(metric, distance);
2686 if min_score > 0.0 && score < min_score {
2687 continue;
2688 }
2689
2690 if let Some(filter_value) = &filter {
2691 let meta_key = metadata_key(ns, col, id_hash);
2692 let txn = match db.begin_transaction() {
2693 Ok(t) => t,
2694 Err(_) => return -1,
2695 };
2696 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2697 let _ = db.commit(txn);
2698 let meta_value = match meta_value {
2699 Some(value) => value,
2700 None => continue,
2701 };
2702 let parsed = match serde_json::from_slice::<serde_json::Value>(&meta_value) {
2703 Ok(value) => value,
2704 Err(_) => continue,
2705 };
2706 let metadata = parsed.get("metadata").cloned().unwrap_or(Value::Null);
2707
2708 if !metadata_matches_filter(&metadata, filter_value) {
2709 continue;
2710 }
2711 }
2712
2713 ids_hi.push((id_hash >> 64) as u64);
2714 ids_lo.push((id_hash & u128::from(u64::MAX)) as u64);
2715 scores.push(score);
2716 if ids_hi.len() >= k {
2717 break;
2718 }
2719 }
2720
2721 let len = ids_hi.len();
2722 let mut ids_hi_box = ids_hi.into_boxed_slice();
2723 let mut ids_lo_box = ids_lo.into_boxed_slice();
2724 let mut scores_box = scores.into_boxed_slice();
2725
2726 unsafe {
2727 *len_out = len;
2728 *ids_hi_out = ids_hi_box.as_mut_ptr();
2729 *ids_lo_out = ids_lo_box.as_mut_ptr();
2730 *scores_out = scores_box.as_mut_ptr();
2731 }
2732
2733 std::mem::forget(ids_hi_box);
2734 std::mem::forget(ids_lo_box);
2735 std::mem::forget(scores_box);
2736
2737 len as c_int
2738}
2739
2740#[unsafe(no_mangle)]
2742pub unsafe extern "C" fn sochdb_collection_fetch_metadata_json(
2743 ptr: *mut DatabasePtr,
2744 namespace: *const c_char,
2745 collection: *const c_char,
2746 ids_hi_ptr: *const u64,
2747 ids_lo_ptr: *const u64,
2748 ids_len: usize,
2749) -> *mut c_char {
2750 if ptr.is_null() || namespace.is_null() || collection.is_null()
2751 || ids_hi_ptr.is_null() || ids_lo_ptr.is_null() {
2752 return ptr::null_mut();
2753 }
2754
2755 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2756 Ok(s) => s,
2757 Err(_) => return ptr::null_mut(),
2758 };
2759 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2760 Ok(s) => s,
2761 Err(_) => return ptr::null_mut(),
2762 };
2763 let ids_hi = unsafe { slice::from_raw_parts(ids_hi_ptr, ids_len) };
2764 let ids_lo = unsafe { slice::from_raw_parts(ids_lo_ptr, ids_len) };
2765 let db = unsafe { &(*ptr).0 };
2766
2767 let mut results = Vec::with_capacity(ids_len);
2768 for i in 0..ids_len {
2769 let id_hash = ((ids_hi[i] as u128) << 64) | (ids_lo[i] as u128);
2770 let meta_key = metadata_key(ns, col, id_hash);
2771 let txn = match db.begin_transaction() {
2772 Ok(t) => t,
2773 Err(_) => return ptr::null_mut(),
2774 };
2775 let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2776 let _ = db.commit(txn);
2777 if let Some(bytes) = meta_value {
2778 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&bytes) {
2779 results.push(parsed);
2780 continue;
2781 }
2782 }
2783 results.push(serde_json::json!({"id": "", "metadata": {}}));
2784 }
2785
2786 match serde_json::to_string(&results) {
2787 Ok(json) => match std::ffi::CString::new(json) {
2788 Ok(cstr) => cstr.into_raw(),
2789 Err(_) => ptr::null_mut(),
2790 },
2791 Err(_) => ptr::null_mut(),
2792 }
2793}
2794
2795#[unsafe(no_mangle)]
2797pub unsafe extern "C" fn sochdb_collection_free_u64(ptr: *mut u64, len: usize) {
2798 if ptr.is_null() || len == 0 {
2799 return;
2800 }
2801 unsafe {
2802 let _ = Vec::from_raw_parts(ptr, len, len);
2803 }
2804}
2805
2806#[unsafe(no_mangle)]
2807pub unsafe extern "C" fn sochdb_collection_free_f32(ptr: *mut f32, len: usize) {
2808 if ptr.is_null() || len == 0 {
2809 return;
2810 }
2811 unsafe {
2812 let _ = Vec::from_raw_parts(ptr, len, len);
2813 }
2814}
2815
2816fn metadata_matches_filter(metadata: &Value, filter: &Value) -> bool {
2817 let filter_obj = match filter.as_object() {
2818 Some(obj) => obj,
2819 None => return true,
2820 };
2821 let metadata_obj = match metadata.as_object() {
2822 Some(obj) => obj,
2823 None => return false,
2824 };
2825
2826 for (key, expected) in filter_obj.iter() {
2827 match metadata_obj.get(key) {
2828 Some(actual) if actual == expected => {}
2829 _ => return false,
2830 }
2831 }
2832
2833 true
2834}
2835
2836#[unsafe(no_mangle)]
2842pub unsafe extern "C" fn sochdb_collection_keyword_search(
2843 ptr: *mut DatabasePtr,
2844 namespace: *const c_char,
2845 collection: *const c_char,
2846 query_ptr: *const c_char,
2847 k: usize,
2848 results_out: *mut CSearchResult,
2849) -> c_int {
2850 if ptr.is_null() || namespace.is_null() || collection.is_null()
2851 || query_ptr.is_null() || results_out.is_null() {
2852 return -1;
2853 }
2854
2855 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2856 Ok(s) => s,
2857 Err(_) => return -1,
2858 };
2859 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2860 Ok(s) => s,
2861 Err(_) => return -1,
2862 };
2863 let query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
2864 Ok(s) => s.to_lowercase(),
2865 Err(_) => return -1,
2866 };
2867 let terms: Vec<&str> = query_str.split_whitespace().collect();
2868 if terms.is_empty() {
2869 return 0;
2870 }
2871
2872 let db = unsafe { &(*ptr).0 };
2873 let txn = match db.begin_transaction() {
2874 Ok(t) => t,
2875 Err(_) => return -1,
2876 };
2877
2878 let prefix = format!("{}/collections/{}/vectors/", ns, col);
2880 let entries = match db.scan(txn, prefix.as_bytes()) {
2881 Ok(e) => e,
2882 Err(_) => {
2883 let _ = db.abort(txn);
2884 return -1;
2885 }
2886 };
2887 let _ = db.commit(txn);
2888
2889 let mut scored: Vec<(f32, String, String)> = Vec::new();
2891
2892 for (_key, value) in entries {
2893 let doc: Value = match serde_json::from_slice(&value) {
2895 Ok(v) => v,
2896 Err(_) => continue,
2897 };
2898
2899 let metadata_val = doc.get("metadata");
2901 let metadata_str = metadata_val.map(|v| v.to_string()).unwrap_or("{}".to_string());
2902
2903 let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
2905
2906 let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
2908
2909 let mut score = 0.0;
2910 for term in &terms {
2911 score += search_text.matches(term).count() as f32;
2912 }
2913
2914 if score > 0.0 {
2915 let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2916 if id.is_empty() { continue; }
2917
2918 scored.push((score, id, metadata_str));
2919 }
2920 }
2921
2922 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
2924
2925 let result_count = scored.len().min(k);
2927 for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
2928 let c_id = match std::ffi::CString::new(id) {
2929 Ok(s) => s.into_raw(),
2930 Err(_) => ptr::null_mut(),
2931 };
2932 let c_meta = match std::ffi::CString::new(metadata) {
2933 Ok(s) => s.into_raw(),
2934 Err(_) => ptr::null_mut(),
2935 };
2936
2937 unsafe {
2938 (*results_out.add(i)).id_ptr = c_id;
2939 (*results_out.add(i)).score = score;
2940 (*results_out.add(i)).metadata_ptr = c_meta;
2941 }
2942 }
2943
2944 result_count as c_int
2945}
2946
2947#[unsafe(no_mangle)]
2949pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
2950 if result.is_null() {
2951 return;
2952 }
2953
2954 for i in 0..count {
2955 let r = unsafe { &mut *result.add(i) };
2956 if !r.id_ptr.is_null() {
2957 let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
2958 }
2959 if !r.metadata_ptr.is_null() {
2960 let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
2961 }
2962 }
2963}
2964
2965#[unsafe(no_mangle)]
2979pub unsafe extern "C" fn sochdb_exists(
2980 ptr: *mut DatabasePtr,
2981 handle: C_TxnHandle,
2982 key_ptr: *const u8,
2983 key_len: usize,
2984) -> c_int {
2985 if ptr.is_null() || key_ptr.is_null() {
2986 return -1;
2987 }
2988 let db = unsafe { &(*ptr).0 };
2989 let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
2990 let txn = TxnHandle {
2991 txn_id: handle.txn_id,
2992 snapshot_ts: handle.snapshot_ts,
2993 };
2994
2995 match db.get(txn, key) {
2996 Ok(Some(_)) => 1,
2997 Ok(None) => 0,
2998 Err(_) => -1,
2999 }
3000}
3001
3002#[unsafe(no_mangle)]
3011pub unsafe extern "C" fn sochdb_delete_path(
3012 ptr: *mut DatabasePtr,
3013 handle: C_TxnHandle,
3014 path_ptr: *const c_char,
3015) -> c_int {
3016 if ptr.is_null() || path_ptr.is_null() {
3017 return -1;
3018 }
3019 let db = unsafe { &(*ptr).0 };
3020 let c_str = unsafe { CStr::from_ptr(path_ptr) };
3021 let path_str = match c_str.to_str() {
3022 Ok(s) => s,
3023 Err(_) => return -1,
3024 };
3025 let txn = TxnHandle {
3026 txn_id: handle.txn_id,
3027 snapshot_ts: handle.snapshot_ts,
3028 };
3029
3030 match db.delete_path(txn, path_str) {
3031 Ok(_) => 0,
3032 Err(_) => -1,
3033 }
3034}
3035
3036#[unsafe(no_mangle)]
3042pub unsafe extern "C" fn sochdb_scan_path(
3043 ptr: *mut DatabasePtr,
3044 handle: C_TxnHandle,
3045 prefix_ptr: *const c_char,
3046 out_len: *mut usize,
3047) -> *mut c_char {
3048 if ptr.is_null() || prefix_ptr.is_null() || out_len.is_null() {
3049 return ptr::null_mut();
3050 }
3051 let db = unsafe { &(*ptr).0 };
3052 let c_str = unsafe { CStr::from_ptr(prefix_ptr) };
3053 let prefix_str = match c_str.to_str() {
3054 Ok(s) => s,
3055 Err(_) => return ptr::null_mut(),
3056 };
3057 let txn = TxnHandle {
3058 txn_id: handle.txn_id,
3059 snapshot_ts: handle.snapshot_ts,
3060 };
3061
3062 match db.scan_path(txn, prefix_str) {
3063 Ok(pairs) => {
3064 let entries: Vec<String> = pairs.iter().map(|(path, value)| {
3065 let val_str = String::from_utf8_lossy(value);
3066 format!(r#"{{"path":"{}","value":"{}"}}"#, path, val_str)
3067 }).collect();
3068 let json = format!("[{}]", entries.join(","));
3069 let c_string = match std::ffi::CString::new(json) {
3070 Ok(s) => s,
3071 Err(_) => return ptr::null_mut(),
3072 };
3073 unsafe { *out_len = c_string.as_bytes().len() };
3074 c_string.into_raw()
3075 }
3076 Err(_) => ptr::null_mut(),
3077 }
3078}
3079
3080#[unsafe(no_mangle)]
3089pub unsafe extern "C" fn sochdb_begin_read_only(ptr: *mut DatabasePtr) -> C_TxnHandle {
3090 if ptr.is_null() {
3091 return C_TxnHandle { txn_id: 0, snapshot_ts: 0 };
3092 }
3093 let db = unsafe { &(*ptr).0 };
3094 match db.begin_read_only() {
3095 Ok(txn) => C_TxnHandle {
3096 txn_id: txn.txn_id,
3097 snapshot_ts: txn.snapshot_ts,
3098 },
3099 Err(_) => C_TxnHandle { txn_id: 0, snapshot_ts: 0 },
3100 }
3101}
3102
3103#[unsafe(no_mangle)]
3108pub unsafe extern "C" fn sochdb_begin_write_only(ptr: *mut DatabasePtr) -> C_TxnHandle {
3109 if ptr.is_null() {
3110 return C_TxnHandle { txn_id: 0, snapshot_ts: 0 };
3111 }
3112 let db = unsafe { &(*ptr).0 };
3113 match db.begin_write_only() {
3114 Ok(txn) => C_TxnHandle {
3115 txn_id: txn.txn_id,
3116 snapshot_ts: txn.snapshot_ts,
3117 },
3118 Err(_) => C_TxnHandle { txn_id: 0, snapshot_ts: 0 },
3119 }
3120}
3121
3122#[unsafe(no_mangle)]
3132pub unsafe extern "C" fn sochdb_shutdown(ptr: *mut DatabasePtr) -> c_int {
3133 if ptr.is_null() { return -1; }
3134 let db = unsafe { &(*ptr).0 };
3135 match db.shutdown() {
3136 Ok(_) => 0,
3137 Err(_) => -1,
3138 }
3139}
3140
3141#[unsafe(no_mangle)]
3146pub unsafe extern "C" fn sochdb_fsync(ptr: *mut DatabasePtr) -> c_int {
3147 if ptr.is_null() { return -1; }
3148 let db = unsafe { &(*ptr).0 };
3149 match db.fsync() {
3150 Ok(_) => 0,
3151 Err(_) => -1,
3152 }
3153}
3154
3155#[unsafe(no_mangle)]
3160pub unsafe extern "C" fn sochdb_truncate_wal(ptr: *mut DatabasePtr) -> c_int {
3161 if ptr.is_null() { return -1; }
3162 let db = unsafe { &(*ptr).0 };
3163 match db.truncate_wal() {
3164 Ok(_) => 0,
3165 Err(_) => -1,
3166 }
3167}
3168
3169#[unsafe(no_mangle)]
3174pub unsafe extern "C" fn sochdb_gc(ptr: *mut DatabasePtr) -> i64 {
3175 if ptr.is_null() { return -1; }
3176 let db = unsafe { &(*ptr).0 };
3177 db.gc() as i64
3178}
3179
3180#[unsafe(no_mangle)]
3185pub unsafe extern "C" fn sochdb_checkpoint_full(ptr: *mut DatabasePtr) -> u64 {
3186 if ptr.is_null() { return 0; }
3187 let db = unsafe { &(*ptr).0 };
3188 match db.checkpoint() {
3189 Ok(lsn) => lsn,
3190 Err(_) => 0,
3191 }
3192}
3193
3194#[unsafe(no_mangle)]
3199pub unsafe extern "C" fn sochdb_stats_json(
3200 ptr: *mut DatabasePtr,
3201 out_len: *mut usize,
3202) -> *mut c_char {
3203 if ptr.is_null() || out_len.is_null() { return ptr::null_mut(); }
3204 let db = unsafe { &(*ptr).0 };
3205 let storage_stats = db.storage_stats();
3206 let db_stats = db.stats();
3207
3208 let json = format!(
3209 r#"{{"memtable_size_bytes":{},"wal_size_bytes":{},"active_transactions":{},"min_active_snapshot":{},"last_checkpoint_lsn":{},"transactions_started":{},"transactions_committed":{},"transactions_aborted":{},"queries_executed":{},"bytes_written":{},"bytes_read":{}}}"#,
3210 storage_stats.memtable_size_bytes,
3211 storage_stats.wal_size_bytes,
3212 storage_stats.active_transactions,
3213 storage_stats.min_active_snapshot,
3214 storage_stats.last_checkpoint_lsn,
3215 db_stats.transactions_started,
3216 db_stats.transactions_committed,
3217 db_stats.transactions_aborted,
3218 db_stats.queries_executed,
3219 db_stats.bytes_written,
3220 db_stats.bytes_read,
3221 );
3222
3223 let c_string = match std::ffi::CString::new(json) {
3224 Ok(s) => s,
3225 Err(_) => return ptr::null_mut(),
3226 };
3227 unsafe { *out_len = c_string.as_bytes().len() };
3228 c_string.into_raw()
3229}
3230
3231#[unsafe(no_mangle)]
3235pub unsafe extern "C" fn sochdb_path(
3236 ptr: *mut DatabasePtr,
3237 out_len: *mut usize,
3238) -> *mut c_char {
3239 if ptr.is_null() || out_len.is_null() { return ptr::null_mut(); }
3240 let db = unsafe { &(*ptr).0 };
3241 let path_str = db.path().to_string_lossy().to_string();
3242 let c_string = match std::ffi::CString::new(path_str) {
3243 Ok(s) => s,
3244 Err(_) => return ptr::null_mut(),
3245 };
3246 unsafe { *out_len = c_string.as_bytes().len() };
3247 c_string.into_raw()
3248}
3249
3250#[unsafe(no_mangle)]
3259pub unsafe extern "C" fn sochdb_backup_create(
3260 ptr: *mut DatabasePtr,
3261 destination: *const c_char,
3262) -> c_int {
3263 if ptr.is_null() || destination.is_null() { return -1; }
3264 let db = unsafe { &(*ptr).0 };
3265 let dest = match unsafe { CStr::from_ptr(destination) }.to_str() {
3266 Ok(s) => s,
3267 Err(_) => return -1,
3268 };
3269
3270 let _ = db.flush();
3272
3273 let manager = crate::backup::BackupManager::new(db.path());
3274 match manager.create_backup(dest) {
3275 Ok(_) => 0,
3276 Err(_) => -1,
3277 }
3278}
3279
3280#[unsafe(no_mangle)]
3285pub unsafe extern "C" fn sochdb_backup_restore(
3286 ptr: *mut DatabasePtr,
3287 backup_path: *const c_char,
3288) -> c_int {
3289 if ptr.is_null() || backup_path.is_null() { return -1; }
3290 let db = unsafe { &(*ptr).0 };
3291 let path = match unsafe { CStr::from_ptr(backup_path) }.to_str() {
3292 Ok(s) => s,
3293 Err(_) => return -1,
3294 };
3295
3296 let manager = crate::backup::BackupManager::new(db.path());
3297 match manager.restore_backup(path) {
3298 Ok(_) => 0,
3299 Err(_) => -1,
3300 }
3301}
3302
3303#[unsafe(no_mangle)]
3308pub unsafe extern "C" fn sochdb_backup_list(
3309 backup_dir: *const c_char,
3310 out_len: *mut usize,
3311) -> *mut c_char {
3312 if backup_dir.is_null() || out_len.is_null() { return ptr::null_mut(); }
3313 let dir = match unsafe { CStr::from_ptr(backup_dir) }.to_str() {
3314 Ok(s) => s,
3315 Err(_) => return ptr::null_mut(),
3316 };
3317
3318 match crate::backup::BackupManager::list_backups(dir) {
3319 Ok(backups) => {
3320 let entries: Vec<String> = backups.iter().map(|b| {
3321 format!(
3322 r#"{{"name":"{}","timestamp":"{}","size_bytes":{}}}"#,
3323 b.generate_name(),
3324 b.generate_name(),
3325 0 )
3327 }).collect();
3328 let json = format!("[{}]", entries.join(","));
3329 let c_string = match std::ffi::CString::new(json) {
3330 Ok(s) => s,
3331 Err(_) => return ptr::null_mut(),
3332 };
3333 unsafe { *out_len = c_string.as_bytes().len() };
3334 c_string.into_raw()
3335 }
3336 Err(_) => ptr::null_mut(),
3337 }
3338}
3339
3340#[unsafe(no_mangle)]
3345pub unsafe extern "C" fn sochdb_backup_verify(
3346 backup_path: *const c_char,
3347) -> c_int {
3348 if backup_path.is_null() { return -1; }
3349 let path = match unsafe { CStr::from_ptr(backup_path) }.to_str() {
3350 Ok(s) => s,
3351 Err(_) => return -1,
3352 };
3353
3354 match crate::backup::BackupManager::verify_backup(path) {
3355 Ok(true) => 1,
3356 Ok(false) => 0,
3357 Err(_) => -1,
3358 }
3359}
3360
3361#[unsafe(no_mangle)]
3371pub unsafe extern "C" fn sochdb_graph_delete_node(
3372 ptr: *mut DatabasePtr,
3373 namespace: *const c_char,
3374 node_id: *const c_char,
3375) -> c_int {
3376 if ptr.is_null() || namespace.is_null() || node_id.is_null() { return -1; }
3377 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3378 Ok(s) => s, Err(_) => return -1,
3379 };
3380 let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
3381 Ok(s) => s, Err(_) => return -1,
3382 };
3383 let db = unsafe { &(*ptr).0 };
3384
3385 let txn = match db.begin_transaction() {
3386 Ok(t) => t, Err(_) => return -1,
3387 };
3388
3389 let node_key = format!("_graph/{}/nodes/{}", ns, id);
3391 let _ = db.delete(txn, node_key.as_bytes());
3392
3393 let edge_prefix_out = format!("_graph/{}/edges/{}/", ns, id);
3395 if let Ok(edges) = db.scan(txn, edge_prefix_out.as_bytes()) {
3396 for (key, _) in edges {
3397 let _ = db.delete(txn, &key);
3398 }
3399 }
3400
3401 let temporal_prefix = format!("_graph/{}/temporal/{}/", ns, id);
3403 if let Ok(edges) = db.scan(txn, temporal_prefix.as_bytes()) {
3404 for (key, _) in edges {
3405 let _ = db.delete(txn, &key);
3406 }
3407 }
3408
3409 match db.commit(txn) {
3410 Ok(_) => 0,
3411 Err(_) => -1,
3412 }
3413}
3414
3415#[unsafe(no_mangle)]
3420pub unsafe extern "C" fn sochdb_graph_delete_edge(
3421 ptr: *mut DatabasePtr,
3422 namespace: *const c_char,
3423 from_id: *const c_char,
3424 edge_type: *const c_char,
3425 to_id: *const c_char,
3426) -> c_int {
3427 if ptr.is_null() || namespace.is_null() || from_id.is_null()
3428 || edge_type.is_null() || to_id.is_null() { return -1; }
3429
3430 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3431 Ok(s) => s, Err(_) => return -1,
3432 };
3433 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
3434 Ok(s) => s, Err(_) => return -1,
3435 };
3436 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
3437 Ok(s) => s, Err(_) => return -1,
3438 };
3439 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
3440 Ok(s) => s, Err(_) => return -1,
3441 };
3442 let db = unsafe { &(*ptr).0 };
3443
3444 let txn = match db.begin_transaction() {
3445 Ok(t) => t, Err(_) => return -1,
3446 };
3447
3448 let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
3449 match db.delete(txn, key.as_bytes()) {
3450 Ok(_) => match db.commit(txn) {
3451 Ok(_) => 0,
3452 Err(_) => -1,
3453 },
3454 Err(_) => {
3455 let _ = db.abort(txn);
3456 -1
3457 }
3458 }
3459}
3460
3461#[unsafe(no_mangle)]
3468pub unsafe extern "C" fn sochdb_graph_get_neighbors(
3469 ptr: *mut DatabasePtr,
3470 namespace: *const c_char,
3471 node_id: *const c_char,
3472 direction: u8,
3473 edge_type_filter: *const c_char,
3474 out_len: *mut usize,
3475) -> *mut c_char {
3476 if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
3477 return ptr::null_mut();
3478 }
3479 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3480 Ok(s) => s, Err(_) => return ptr::null_mut(),
3481 };
3482 let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
3483 Ok(s) => s, Err(_) => return ptr::null_mut(),
3484 };
3485 let et_filter = if edge_type_filter.is_null() {
3486 None
3487 } else {
3488 match unsafe { CStr::from_ptr(edge_type_filter) }.to_str() {
3489 Ok(s) => Some(s), Err(_) => None,
3490 }
3491 };
3492 let db = unsafe { &(*ptr).0 };
3493
3494 let txn = match db.begin_transaction() {
3495 Ok(t) => t, Err(_) => return ptr::null_mut(),
3496 };
3497
3498 let mut neighbors = Vec::new();
3499
3500 if direction == 0 || direction == 2 {
3502 let prefix = format!("_graph/{}/edges/{}/", ns, node);
3503 if let Ok(edges) = db.scan(txn, prefix.as_bytes()) {
3504 for (_key, value) in edges {
3505 if let Ok(edge_str) = std::str::from_utf8(&value) {
3506 if let Some(filter) = et_filter {
3507 if !edge_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
3508 continue;
3509 }
3510 }
3511 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
3513 let start = to_pos + r#""to_id":""#.len();
3514 if let Some(end) = edge_str[start..].find('"') {
3515 let to_id = &edge_str[start..start + end];
3516 neighbors.push(format!(
3517 r#"{{"node_id":"{}","direction":"outgoing","edge":{}}}"#,
3518 to_id, edge_str
3519 ));
3520 }
3521 }
3522 }
3523 }
3524 }
3525 }
3526
3527 if direction == 1 || direction == 2 {
3529 let all_edges_prefix = format!("_graph/{}/edges/", ns);
3530 if let Ok(edges) = db.scan(txn, all_edges_prefix.as_bytes()) {
3531 for (_key, value) in edges {
3532 if let Ok(edge_str) = std::str::from_utf8(&value) {
3533 let has_to = edge_str.contains(&format!(r#""to_id":"{}""#, node));
3534 if !has_to { continue; }
3535 if let Some(filter) = et_filter {
3536 if !edge_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
3537 continue;
3538 }
3539 }
3540 if let Some(from_pos) = edge_str.find(r#""from_id":""#) {
3541 let start = from_pos + r#""from_id":""#.len();
3542 if let Some(end) = edge_str[start..].find('"') {
3543 let from_id = &edge_str[start..start + end];
3544 neighbors.push(format!(
3545 r#"{{"node_id":"{}","direction":"incoming","edge":{}}}"#,
3546 from_id, edge_str
3547 ));
3548 }
3549 }
3550 }
3551 }
3552 }
3553 }
3554
3555 let _ = db.commit(txn);
3556
3557 let json = format!(r#"{{"neighbors":[{}]}}"#, neighbors.join(","));
3558 let c_string = match std::ffi::CString::new(json) {
3559 Ok(s) => s, Err(_) => return ptr::null_mut(),
3560 };
3561 unsafe { *out_len = c_string.as_bytes().len() };
3562 c_string.into_raw()
3563}
3564
3565#[unsafe(no_mangle)]
3572pub unsafe extern "C" fn sochdb_graph_find_path(
3573 ptr: *mut DatabasePtr,
3574 namespace: *const c_char,
3575 from_node: *const c_char,
3576 to_node: *const c_char,
3577 max_depth: usize,
3578 out_len: *mut usize,
3579) -> *mut c_char {
3580 if ptr.is_null() || namespace.is_null() || from_node.is_null()
3581 || to_node.is_null() || out_len.is_null() {
3582 return ptr::null_mut();
3583 }
3584 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3585 Ok(s) => s, Err(_) => return ptr::null_mut(),
3586 };
3587 let from = match unsafe { CStr::from_ptr(from_node) }.to_str() {
3588 Ok(s) => s, Err(_) => return ptr::null_mut(),
3589 };
3590 let to = match unsafe { CStr::from_ptr(to_node) }.to_str() {
3591 Ok(s) => s, Err(_) => return ptr::null_mut(),
3592 };
3593 let db = unsafe { &(*ptr).0 };
3594
3595 let txn = match db.begin_transaction() {
3596 Ok(t) => t, Err(_) => return ptr::null_mut(),
3597 };
3598
3599 let mut visited: std::collections::HashMap<String, (String, String)> = std::collections::HashMap::new(); let mut queue: std::collections::VecDeque<(String, usize)> = std::collections::VecDeque::new();
3602 queue.push_back((from.to_string(), 0));
3603 visited.insert(from.to_string(), ("".to_string(), "".to_string()));
3604 let mut found = false;
3605
3606 while let Some((current, depth)) = queue.pop_front() {
3607 if current == to {
3608 found = true;
3609 break;
3610 }
3611 if depth >= max_depth { continue; }
3612
3613 let prefix = format!("_graph/{}/edges/{}/", ns, current);
3614 if let Ok(edges) = db.scan(txn, prefix.as_bytes()) {
3615 for (_key, value) in edges {
3616 if let Ok(edge_str) = std::str::from_utf8(&value) {
3617 if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
3618 let start = to_pos + r#""to_id":""#.len();
3619 if let Some(end) = edge_str[start..].find('"') {
3620 let next = &edge_str[start..start + end];
3621 if !visited.contains_key(next) {
3622 visited.insert(next.to_string(), (current.clone(), edge_str.to_string()));
3623 queue.push_back((next.to_string(), depth + 1));
3624 }
3625 }
3626 }
3627 }
3628 }
3629 }
3630 }
3631
3632 let _ = db.commit(txn);
3633
3634 if !found { return ptr::null_mut(); }
3635
3636 let mut path = Vec::new();
3638 let mut edges = Vec::new();
3639 let mut current = to.to_string();
3640 while !current.is_empty() {
3641 path.push(format!(r#""{}""#, current));
3642 if let Some((parent, edge)) = visited.get(¤t) {
3643 if !edge.is_empty() {
3644 edges.push(edge.clone());
3645 }
3646 current = parent.clone();
3647 } else {
3648 break;
3649 }
3650 }
3651 path.reverse();
3652 edges.reverse();
3653
3654 let json = format!(
3655 r#"{{"path":[{}],"edges":[{}]}}"#,
3656 path.join(","),
3657 edges.join(",")
3658 );
3659 let c_string = match std::ffi::CString::new(json) {
3660 Ok(s) => s, Err(_) => return ptr::null_mut(),
3661 };
3662 unsafe { *out_len = c_string.as_bytes().len() };
3663 c_string.into_raw()
3664}
3665
3666#[unsafe(no_mangle)]
3675pub unsafe extern "C" fn sochdb_end_temporal_edge(
3676 ptr: *mut DatabasePtr,
3677 namespace: *const c_char,
3678 from_id: *const c_char,
3679 edge_type: *const c_char,
3680 to_id: *const c_char,
3681) -> c_int {
3682 if ptr.is_null() || namespace.is_null() || from_id.is_null()
3683 || edge_type.is_null() || to_id.is_null() { return -1; }
3684 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3685 Ok(s) => s, Err(_) => return -1,
3686 };
3687 let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
3688 Ok(s) => s, Err(_) => return -1,
3689 };
3690 let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
3691 Ok(s) => s, Err(_) => return -1,
3692 };
3693 let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
3694 Ok(s) => s, Err(_) => return -1,
3695 };
3696 let db = unsafe { &(*ptr).0 };
3697
3698 let txn = match db.begin_transaction() {
3699 Ok(t) => t, Err(_) => return -1,
3700 };
3701
3702 let prefix = format!("_graph/{}/temporal/{}/{}/{}/", ns, from, etype, to);
3704 let edges = match db.scan(txn, prefix.as_bytes()) {
3705 Ok(e) => e,
3706 Err(_) => { let _ = db.abort(txn); return -1; }
3707 };
3708
3709 let now = std::time::SystemTime::now()
3710 .duration_since(std::time::UNIX_EPOCH)
3711 .unwrap()
3712 .as_millis() as u64;
3713
3714 let mut found = false;
3715 for (key, value) in edges {
3716 if let Ok(val_str) = std::str::from_utf8(&value) {
3717 if val_str.contains(r#""valid_until":0"#) {
3719 let new_val = val_str.replace(r#""valid_until":0"#, &format!(r#""valid_until":{}"#, now));
3720 if db.put(txn, &key, new_val.as_bytes()).is_ok() {
3721 found = true;
3722 }
3723 }
3724 }
3725 }
3726
3727 match db.commit(txn) {
3728 Ok(_) => if found { 0 } else { 1 },
3729 Err(_) => -1,
3730 }
3731}
3732
3733#[unsafe(no_mangle)]
3742pub unsafe extern "C" fn sochdb_cache_delete(
3743 ptr: *mut DatabasePtr,
3744 cache_name: *const c_char,
3745 key: *const c_char,
3746) -> c_int {
3747 if ptr.is_null() || cache_name.is_null() || key.is_null() { return -1; }
3748 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
3749 Ok(s) => s, Err(_) => return -1,
3750 };
3751 let k = match unsafe { CStr::from_ptr(key) }.to_str() {
3752 Ok(s) => s, Err(_) => return -1,
3753 };
3754 let db = unsafe { &(*ptr).0 };
3755
3756 let txn = match db.begin_transaction() {
3757 Ok(t) => t, Err(_) => return -1,
3758 };
3759
3760 let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
3761 let cache_key = format!("_cache/{}/{}", cache, key_hash);
3762
3763 match db.get(txn, cache_key.as_bytes()) {
3764 Ok(Some(_)) => {
3765 match db.delete(txn, cache_key.as_bytes()) {
3766 Ok(_) => match db.commit(txn) {
3767 Ok(_) => 0,
3768 Err(_) => -1,
3769 },
3770 Err(_) => { let _ = db.abort(txn); -1 }
3771 }
3772 }
3773 Ok(None) => { let _ = db.commit(txn); 1 }
3774 Err(_) => { let _ = db.abort(txn); -1 }
3775 }
3776}
3777
3778#[unsafe(no_mangle)]
3783pub unsafe extern "C" fn sochdb_cache_clear(
3784 ptr: *mut DatabasePtr,
3785 cache_name: *const c_char,
3786) -> i64 {
3787 if ptr.is_null() || cache_name.is_null() { return -1; }
3788 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
3789 Ok(s) => s, Err(_) => return -1,
3790 };
3791 let db = unsafe { &(*ptr).0 };
3792
3793 let txn = match db.begin_transaction() {
3794 Ok(t) => t, Err(_) => return -1,
3795 };
3796
3797 let prefix = format!("_cache/{}/", cache);
3798 let entries = match db.scan(txn, prefix.as_bytes()) {
3799 Ok(e) => e,
3800 Err(_) => { let _ = db.abort(txn); return -1; }
3801 };
3802
3803 let mut count = 0i64;
3804 for (key, _) in &entries {
3805 if db.delete(txn, key).is_ok() {
3806 count += 1;
3807 }
3808 }
3809
3810 match db.commit(txn) {
3811 Ok(_) => count,
3812 Err(_) => -1,
3813 }
3814}
3815
3816#[unsafe(no_mangle)]
3821pub unsafe extern "C" fn sochdb_cache_stats(
3822 ptr: *mut DatabasePtr,
3823 cache_name: *const c_char,
3824 out_len: *mut usize,
3825) -> *mut c_char {
3826 if ptr.is_null() || cache_name.is_null() || out_len.is_null() { return ptr::null_mut(); }
3827 let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
3828 Ok(s) => s, Err(_) => return ptr::null_mut(),
3829 };
3830 let db = unsafe { &(*ptr).0 };
3831
3832 let txn = match db.begin_transaction() {
3833 Ok(t) => t, Err(_) => return ptr::null_mut(),
3834 };
3835
3836 let prefix = format!("_cache/{}/", cache);
3837 let entries = match db.scan(txn, prefix.as_bytes()) {
3838 Ok(e) => e,
3839 Err(_) => { let _ = db.abort(txn); return ptr::null_mut(); }
3840 };
3841 let _ = db.commit(txn);
3842
3843 let now = std::time::SystemTime::now()
3844 .duration_since(std::time::UNIX_EPOCH)
3845 .unwrap()
3846 .as_secs();
3847
3848 let total = entries.len();
3849 let mut expired = 0usize;
3850 let mut total_bytes = 0usize;
3851
3852 for (_key, value) in &entries {
3853 total_bytes += value.len();
3854 if let Ok(val_str) = std::str::from_utf8(value) {
3855 if let Some(exp_pos) = val_str.find(r#""expires_at":"#) {
3856 let exp_start = exp_pos + r#""expires_at":"#.len();
3857 if let Some(exp_end) = val_str[exp_start..].find('}') {
3858 let expires_at: u64 = val_str[exp_start..exp_start + exp_end]
3859 .parse().unwrap_or(0);
3860 if expires_at > 0 && now > expires_at {
3861 expired += 1;
3862 }
3863 }
3864 }
3865 }
3866 }
3867
3868 let json = format!(
3869 r#"{{"cache_name":"{}","total_entries":{},"expired_entries":{},"active_entries":{},"total_bytes":{}}}"#,
3870 cache, total, expired, total - expired, total_bytes
3871 );
3872 let c_string = match std::ffi::CString::new(json) {
3873 Ok(s) => s, Err(_) => return ptr::null_mut(),
3874 };
3875 unsafe { *out_len = c_string.as_bytes().len() };
3876 c_string.into_raw()
3877}
3878
3879#[unsafe(no_mangle)]
3888pub unsafe extern "C" fn sochdb_collection_delete(
3889 ptr: *mut DatabasePtr,
3890 namespace: *const c_char,
3891 collection: *const c_char,
3892) -> c_int {
3893 if ptr.is_null() || namespace.is_null() || collection.is_null() { return -1; }
3894 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3895 Ok(s) => s, Err(_) => return -1,
3896 };
3897 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
3898 Ok(s) => s, Err(_) => return -1,
3899 };
3900 let db = unsafe { &(*ptr).0 };
3901
3902 let txn = match db.begin_transaction() {
3903 Ok(t) => t, Err(_) => return -1,
3904 };
3905
3906 let config_key = format!("{}/_collections/{}", ns, col);
3908 let _ = db.delete(txn, config_key.as_bytes());
3909
3910 let vec_prefix = format!("{}/collections/{}/vectors_bin/", ns, col);
3912 if let Ok(entries) = db.scan(txn, vec_prefix.as_bytes()) {
3913 for (key, _) in entries {
3914 let _ = db.delete(txn, &key);
3915 }
3916 }
3917
3918 let meta_prefix = format!("{}/collections/{}/meta/", ns, col);
3920 if let Ok(entries) = db.scan(txn, meta_prefix.as_bytes()) {
3921 for (key, _) in entries {
3922 let _ = db.delete(txn, &key);
3923 }
3924 }
3925
3926 let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
3928 let key = collection_key(ns, col);
3929 registry.lock().unwrap().remove(&key);
3930
3931 match db.commit(txn) {
3932 Ok(_) => 0,
3933 Err(_) => -1,
3934 }
3935}
3936
3937#[unsafe(no_mangle)]
3942pub unsafe extern "C" fn sochdb_collection_count(
3943 ptr: *mut DatabasePtr,
3944 namespace: *const c_char,
3945 collection: *const c_char,
3946) -> i64 {
3947 if ptr.is_null() || namespace.is_null() || collection.is_null() { return -1; }
3948 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3949 Ok(s) => s, Err(_) => return -1,
3950 };
3951 let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
3952 Ok(s) => s, Err(_) => return -1,
3953 };
3954 let db = unsafe { &(*ptr).0 };
3955
3956 let txn = match db.begin_transaction() {
3957 Ok(t) => t, Err(_) => return -1,
3958 };
3959
3960 let meta_prefix = format!("{}/collections/{}/meta/", ns, col);
3961 match db.scan(txn, meta_prefix.as_bytes()) {
3962 Ok(entries) => {
3963 let count = entries.len() as i64;
3964 let _ = db.commit(txn);
3965 count
3966 }
3967 Err(_) => {
3968 let _ = db.abort(txn);
3969 -1
3970 }
3971 }
3972}
3973
3974#[unsafe(no_mangle)]
3979pub unsafe extern "C" fn sochdb_collection_list(
3980 ptr: *mut DatabasePtr,
3981 namespace: *const c_char,
3982 out_len: *mut usize,
3983) -> *mut c_char {
3984 if ptr.is_null() || namespace.is_null() || out_len.is_null() { return ptr::null_mut(); }
3985 let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3986 Ok(s) => s, Err(_) => return ptr::null_mut(),
3987 };
3988 let db = unsafe { &(*ptr).0 };
3989
3990 let txn = match db.begin_transaction() {
3991 Ok(t) => t, Err(_) => return ptr::null_mut(),
3992 };
3993
3994 let prefix = format!("{}/_collections/", ns);
3995 let entries = match db.scan(txn, prefix.as_bytes()) {
3996 Ok(e) => e,
3997 Err(_) => { let _ = db.abort(txn); return ptr::null_mut(); }
3998 };
3999 let _ = db.commit(txn);
4000
4001 let names: Vec<String> = entries.iter().filter_map(|(key, _)| {
4002 let key_str = std::str::from_utf8(key).ok()?;
4003 let name = key_str.strip_prefix(&prefix)?;
4004 Some(format!(r#""{}""#, name))
4005 }).collect();
4006
4007 let json = format!("[{}]", names.join(","));
4008 let c_string = match std::ffi::CString::new(json) {
4009 Ok(s) => s, Err(_) => return ptr::null_mut(),
4010 };
4011 unsafe { *out_len = c_string.as_bytes().len() };
4012 c_string.into_raw()
4013}
4014
4015#[unsafe(no_mangle)]
4024pub unsafe extern "C" fn sochdb_list_tables(
4025 ptr: *mut DatabasePtr,
4026 out_len: *mut usize,
4027) -> *mut c_char {
4028 if ptr.is_null() || out_len.is_null() { return ptr::null_mut(); }
4029 let db = unsafe { &(*ptr).0 };
4030 let tables = db.list_tables();
4031
4032 let names: Vec<String> = tables.iter().map(|t| format!(r#""{}""#, t)).collect();
4033 let json = format!("[{}]", names.join(","));
4034 let c_string = match std::ffi::CString::new(json) {
4035 Ok(s) => s, Err(_) => return ptr::null_mut(),
4036 };
4037 unsafe { *out_len = c_string.as_bytes().len() };
4038 c_string.into_raw()
4039}
4040
4041#[unsafe(no_mangle)]
4046pub unsafe extern "C" fn sochdb_get_table_schema(
4047 ptr: *mut DatabasePtr,
4048 table_name: *const c_char,
4049 out_len: *mut usize,
4050) -> *mut c_char {
4051 if ptr.is_null() || table_name.is_null() || out_len.is_null() { return ptr::null_mut(); }
4052 let db = unsafe { &(*ptr).0 };
4053 let name = match unsafe { CStr::from_ptr(table_name) }.to_str() {
4054 Ok(s) => s, Err(_) => return ptr::null_mut(),
4055 };
4056
4057 match db.get_table_schema(name) {
4058 Some(schema) => {
4059 let columns: Vec<String> = schema.columns.iter().map(|col| {
4061 format!(r#"{{"name":"{}","type":"{}","nullable":{}}}"#,
4062 col.name,
4063 format!("{:?}", col.col_type),
4064 col.nullable
4065 )
4066 }).collect();
4067 let json = format!(
4068 r#"{{"table":"{}","columns":[{}]}}"#,
4069 schema.name,
4070 columns.join(",")
4071 );
4072 let c_string = match std::ffi::CString::new(json) {
4073 Ok(s) => s, Err(_) => return ptr::null_mut(),
4074 };
4075 unsafe { *out_len = c_string.as_bytes().len() };
4076 c_string.into_raw()
4077 }
4078 None => ptr::null_mut(),
4079 }
4080}
4081
4082#[unsafe(no_mangle)]
4092pub unsafe extern "C" fn sochdb_set_compression(
4093 ptr: *mut DatabasePtr,
4094 compression: u8,
4095) -> c_int {
4096 if ptr.is_null() { return -1; }
4097 let db = unsafe { &(*ptr).0 };
4098 let comp_type = crate::compression::CompressionType::from_u8(compression);
4099
4100 let txn = match db.begin_transaction() {
4102 Ok(t) => t, Err(_) => return -1,
4103 };
4104 let key = b"_config/compression";
4105 let val = format!("{}", compression);
4106 if db.put(txn, key, val.as_bytes()).is_err() {
4107 let _ = db.abort(txn);
4108 return -1;
4109 }
4110 match db.commit(txn) {
4111 Ok(_) => 0,
4112 Err(_) => -1,
4113 }
4114}
4115
4116#[unsafe(no_mangle)]
4121pub unsafe extern "C" fn sochdb_get_compression(
4122 ptr: *mut DatabasePtr,
4123) -> u8 {
4124 if ptr.is_null() { return 255; }
4125 let db = unsafe { &(*ptr).0 };
4126
4127 let txn = match db.begin_transaction() {
4128 Ok(t) => t, Err(_) => return 255,
4129 };
4130 let key = b"_config/compression";
4131 match db.get(txn, key) {
4132 Ok(Some(val)) => {
4133 let _ = db.commit(txn);
4134 std::str::from_utf8(&val).ok()
4135 .and_then(|s| s.parse::<u8>().ok())
4136 .unwrap_or(0)
4137 }
4138 _ => { let _ = db.commit(txn); 0 }
4139 }
4140}
4141
4142#[unsafe(no_mangle)]
4152pub unsafe extern "C" fn sochdb_execute_sql(
4153 ptr: *mut DatabasePtr,
4154 handle: C_TxnHandle,
4155 sql_ptr: *const c_char,
4156 out_len: *mut usize,
4157) -> *mut c_char {
4158 if ptr.is_null() || sql_ptr.is_null() || out_len.is_null() {
4159 return ptr::null_mut();
4160 }
4161 let db = unsafe { &(*ptr).0 };
4162 let sql = match unsafe { CStr::from_ptr(sql_ptr) }.to_str() {
4163 Ok(s) => s,
4164 Err(_) => return ptr::null_mut(),
4165 };
4166 let txn = TxnHandle {
4167 txn_id: handle.txn_id,
4168 snapshot_ts: handle.snapshot_ts,
4169 };
4170
4171 let result = db.query(txn, "").execute();
4174
4175 match result {
4179 Ok(qr) => {
4180 let toon = qr.to_toon();
4181 let c_string = match std::ffi::CString::new(toon) {
4182 Ok(s) => s,
4183 Err(_) => return ptr::null_mut(),
4184 };
4185 unsafe { *out_len = c_string.as_bytes().len() };
4186 c_string.into_raw()
4187 }
4188 Err(_) => ptr::null_mut(),
4189 }
4190}
4191
4192#[unsafe(no_mangle)]
4200pub unsafe extern "C" fn sochdb_namespace_create(
4201 ptr: *mut DatabasePtr,
4202 name: *const c_char,
4203) -> c_int {
4204 if ptr.is_null() || name.is_null() { return -1; }
4205 let ns = match unsafe { CStr::from_ptr(name) }.to_str() {
4206 Ok(s) => s, Err(_) => return -1,
4207 };
4208 let db = unsafe { &(*ptr).0 };
4209
4210 let txn = match db.begin_transaction() {
4211 Ok(t) => t, Err(_) => return -1,
4212 };
4213
4214 let key = format!("_namespaces/{}", ns);
4215 let now = std::time::SystemTime::now()
4216 .duration_since(std::time::UNIX_EPOCH)
4217 .unwrap()
4218 .as_secs();
4219 let value = format!(r#"{{"name":"{}","created_at":{}}}"#, ns, now);
4220
4221 if db.put(txn, key.as_bytes(), value.as_bytes()).is_err() {
4222 let _ = db.abort(txn);
4223 return -1;
4224 }
4225 match db.commit(txn) {
4226 Ok(_) => 0,
4227 Err(_) => -1,
4228 }
4229}
4230
4231#[unsafe(no_mangle)]
4235pub unsafe extern "C" fn sochdb_namespace_delete(
4236 ptr: *mut DatabasePtr,
4237 name: *const c_char,
4238) -> c_int {
4239 if ptr.is_null() || name.is_null() { return -1; }
4240 let ns = match unsafe { CStr::from_ptr(name) }.to_str() {
4241 Ok(s) => s, Err(_) => return -1,
4242 };
4243 let db = unsafe { &(*ptr).0 };
4244
4245 let txn = match db.begin_transaction() {
4246 Ok(t) => t, Err(_) => return -1,
4247 };
4248
4249 let ns_key = format!("_namespaces/{}", ns);
4251 let _ = db.delete(txn, ns_key.as_bytes());
4252
4253 let ns_prefix = format!("{}/", ns);
4255 if let Ok(entries) = db.scan(txn, ns_prefix.as_bytes()) {
4256 for (key, _) in entries {
4257 let _ = db.delete(txn, &key);
4258 }
4259 }
4260
4261 match db.commit(txn) {
4262 Ok(_) => 0,
4263 Err(_) => -1,
4264 }
4265}
4266
4267#[unsafe(no_mangle)]
4272pub unsafe extern "C" fn sochdb_namespace_list(
4273 ptr: *mut DatabasePtr,
4274 out_len: *mut usize,
4275) -> *mut c_char {
4276 if ptr.is_null() || out_len.is_null() { return ptr::null_mut(); }
4277 let db = unsafe { &(*ptr).0 };
4278
4279 let txn = match db.begin_transaction() {
4280 Ok(t) => t, Err(_) => return ptr::null_mut(),
4281 };
4282
4283 let prefix = b"_namespaces/";
4284 let entries = match db.scan(txn, prefix) {
4285 Ok(e) => e,
4286 Err(_) => { let _ = db.abort(txn); return ptr::null_mut(); }
4287 };
4288 let _ = db.commit(txn);
4289
4290 let names: Vec<String> = entries.iter().filter_map(|(key, _)| {
4291 let key_str = std::str::from_utf8(key).ok()?;
4292 let name = key_str.strip_prefix("_namespaces/")?;
4293 Some(format!(r#""{}""#, name))
4294 }).collect();
4295
4296 let json = format!("[{}]", names.join(","));
4297 let c_string = match std::ffi::CString::new(json) {
4298 Ok(s) => s, Err(_) => return ptr::null_mut(),
4299 };
4300 unsafe { *out_len = c_string.as_bytes().len() };
4301 c_string.into_raw()
4302}
4303