sochdb_storage/
ffi.rs

1// Copyright 2025 Sushanth (https://github.com/sushanthpy)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::database::{Database, TxnHandle};
16use std::ffi::CStr;
17use std::os::raw::{c_char, c_int};
18use std::ptr;
19use std::slice;
20use std::sync::Arc;
21use serde_json::Value;
22use std::collections::HashMap;
23use std::sync::Mutex;
24use std::sync::OnceLock;
25use sochdb_index::hnsw::{DistanceMetric, HnswConfig, HnswIndex};
26
27/// Opaque pointer to Database
28pub struct DatabasePtr(Arc<Database>);
29
30// =========================================================================
31// Collection Index Registry (in-memory)
32// =========================================================================
33
34struct CollectionIndex {
35    index: Arc<HnswIndex>,
36    dimension: usize,
37    metric: DistanceMetric,
38}
39
40static COLLECTION_INDEXES: OnceLock<Mutex<HashMap<String, Arc<CollectionIndex>>>> = OnceLock::new();
41
42fn collection_key(namespace: &str, collection: &str) -> String {
43    format!("{}/{}", namespace, collection)
44}
45
46fn vector_bin_key(namespace: &str, collection: &str, id_hash: u128) -> String {
47    format!("{}/collections/{}/vectors_bin/{:032x}", namespace, collection, id_hash)
48}
49
50fn metadata_key(namespace: &str, collection: &str, id_hash: u128) -> String {
51    format!("{}/collections/{}/meta/{:032x}", namespace, collection, id_hash)
52}
53
54fn hash_id_to_u128(id: &str) -> u128 {
55    let hash = blake3::hash(id.as_bytes());
56    let bytes = hash.as_bytes();
57    u128::from_le_bytes([
58        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
59        bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
60    ])
61}
62
63fn ensure_collection_index(
64    db: &Database,
65    namespace: &str,
66    collection: &str,
67    dimension: usize,
68    metric: DistanceMetric,
69) -> Arc<CollectionIndex> {
70    let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
71    let key = collection_key(namespace, collection);
72
73    let mut registry_guard = registry.lock().unwrap();
74    if let Some(existing) = registry_guard.get(&key) {
75        return existing.clone();
76    }
77
78    let mut config = HnswConfig::default();
79    config.metric = metric;
80    let index = Arc::new(HnswIndex::new(dimension, config));
81
82    let entry = Arc::new(CollectionIndex {
83        index,
84        dimension,
85        metric,
86    });
87    registry_guard.insert(key, entry.clone());
88
89    entry
90}
91
92fn resolve_collection_config(
93    db: &Database,
94    namespace: &str,
95    collection: &str,
96) -> Option<(usize, DistanceMetric)> {
97    let key = format!("{}/_collections/{}", namespace, collection);
98    let txn = db.begin_transaction().ok()?;
99    let value = db.get(txn, key.as_bytes()).ok().flatten();
100    let _ = db.commit(txn);
101    let value = value?;
102
103    let parsed: serde_json::Value = serde_json::from_slice(&value).ok()?;
104    let dimension = parsed.get("dimension")?.as_u64()? as usize;
105    let metric = match parsed.get("metric").and_then(|v| v.as_u64()).unwrap_or(0) {
106        1 => DistanceMetric::Euclidean,
107        2 => DistanceMetric::DotProduct,
108        _ => DistanceMetric::Cosine,
109    };
110    Some((dimension, metric))
111}
112
113fn serialize_vector_binary(vector: &[f32]) -> Vec<u8> {
114    let mut out = Vec::with_capacity(4 + vector.len() * 4);
115    let len = vector.len() as u32;
116    out.extend_from_slice(&len.to_le_bytes());
117    for value in vector {
118        out.extend_from_slice(&value.to_le_bytes());
119    }
120    out
121}
122
123fn decode_score(metric: DistanceMetric, distance: f32) -> f32 {
124    match metric {
125        DistanceMetric::Cosine => 1.0 - distance,
126        DistanceMetric::DotProduct => -distance,
127        DistanceMetric::Euclidean => -distance,
128    }
129}
130
131/// C-compatible Transaction Handle
132#[repr(C)]
133pub struct C_TxnHandle {
134    pub txn_id: u64,
135    pub snapshot_ts: u64,
136}
137
138/// C-compatible Commit Result
139/// Returns commit_ts on success, or 0 with error_code on failure
140#[repr(C)]
141pub struct C_CommitResult {
142    /// Commit timestamp (HLC-backed, monotonically increasing)
143    /// This is 0 if the commit failed.
144    pub commit_ts: u64,
145    /// Error code: 0 = success, -1 = error, -2 = SSI conflict
146    pub error_code: i32,
147}
148
149/// C-compatible Database Configuration
150/// 
151/// All fields have sensible defaults when set to 0/false.
152/// This allows clients to only set the fields they care about.
153#[repr(C)]
154pub struct C_DatabaseConfig {
155    /// Enable WAL for durability (default: true if wal_enabled_set is false)
156    pub wal_enabled: bool,
157    /// Whether wal_enabled was explicitly set
158    pub wal_enabled_set: bool,
159    /// Sync mode: 0=OFF, 1=NORMAL (default), 2=FULL
160    pub sync_mode: u8,
161    /// Whether sync_mode was explicitly set
162    pub sync_mode_set: bool,
163    /// Memtable size in bytes (0 = default 64MB)
164    pub memtable_size_bytes: u64,
165    /// Enable group commit for throughput (default: true if group_commit_set is false)
166    pub group_commit: bool,
167    /// Whether group_commit was explicitly set  
168    pub group_commit_set: bool,
169    /// Default index policy: 0=WriteOptimized, 1=Balanced (default), 2=ScanOptimized, 3=AppendOnly
170    pub default_index_policy: u8,
171    /// Whether default_index_policy was explicitly set
172    pub default_index_policy_set: bool,
173}
174
175/// Open the database with configuration.
176/// Returns a pointer to the database instance, or null on error.
177/// # Safety
178/// The path must be a valid C string.
179#[unsafe(no_mangle)]
180pub unsafe extern "C" fn sochdb_open_with_config(
181    path: *const c_char, 
182    config: C_DatabaseConfig
183) -> *mut DatabasePtr {
184    if path.is_null() {
185        return ptr::null_mut();
186    }
187
188    let c_str = unsafe { CStr::from_ptr(path) };
189    let path_str = match c_str.to_str() {
190        Ok(s) => s,
191        Err(_) => return ptr::null_mut(),
192    };
193
194    // Build config from C struct, using defaults for unset fields
195    let mut db_config = crate::database::DatabaseConfig::default();
196    
197    if config.wal_enabled_set {
198        db_config.wal_enabled = config.wal_enabled;
199    }
200    
201    if config.sync_mode_set {
202        db_config.sync_mode = match config.sync_mode {
203            0 => crate::database::SyncMode::Off,
204            1 => crate::database::SyncMode::Normal,
205            _ => crate::database::SyncMode::Full,
206        };
207    }
208    
209    if config.memtable_size_bytes > 0 {
210        db_config.memtable_size_limit = config.memtable_size_bytes as usize;
211    }
212    
213    if config.group_commit_set {
214        db_config.group_commit = config.group_commit;
215    }
216    
217    if config.default_index_policy_set {
218        db_config.default_index_policy = match config.default_index_policy {
219            0 => crate::index_policy::IndexPolicy::WriteOptimized,
220            1 => crate::index_policy::IndexPolicy::Balanced,
221            2 => crate::index_policy::IndexPolicy::ScanOptimized,
222            _ => crate::index_policy::IndexPolicy::AppendOnly,
223        };
224    }
225
226    match Database::open_with_config(path_str, db_config) {
227        Ok(db) => {
228            let ptr = Box::new(DatabasePtr(db));
229            Box::into_raw(ptr)
230        }
231        Err(_) => ptr::null_mut(),
232    }
233}
234
235/// Open the database.
236/// Returns a pointer to the database instance, or null on error.
237/// # Safety
238/// The path must be a valid C string.
239#[unsafe(no_mangle)]
240pub unsafe extern "C" fn sochdb_open(path: *const c_char) -> *mut DatabasePtr {
241    if path.is_null() {
242        return ptr::null_mut();
243    }
244
245    let c_str = unsafe { CStr::from_ptr(path) };
246    let path_str = match c_str.to_str() {
247        Ok(s) => s,
248        Err(_) => return ptr::null_mut(),
249    };
250
251    // Use default config for now
252    let config = crate::database::DatabaseConfig::default();
253
254    // Database::open returns Result<Arc<Database>>
255    match Database::open_with_config(path_str, config) {
256        Ok(db) => {
257            let ptr = Box::new(DatabasePtr(db));
258            Box::into_raw(ptr)
259        }
260        Err(_) => ptr::null_mut(),
261    }
262}
263
264/// Close the database and free the pointer.
265/// # Safety
266/// The ptr must be a valid pointer returned by sochdb_open.
267#[unsafe(no_mangle)]
268pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
269    if !ptr.is_null() {
270        unsafe {
271            let _ = Box::from_raw(ptr);
272        }
273    }
274}
275
276/// Begin a transaction.
277/// Returns C_TxnHandle. On error, txn_id will be 0.
278/// # Safety
279/// The ptr must be a valid pointer returned by sochdb_open.
280#[unsafe(no_mangle)]
281pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
282    if ptr.is_null() {
283        return C_TxnHandle {
284            txn_id: 0,
285            snapshot_ts: 0,
286        };
287    }
288    let db = unsafe { &(*ptr).0 };
289    match db.begin_transaction() {
290        Ok(txn) => C_TxnHandle {
291            txn_id: txn.txn_id,
292            snapshot_ts: txn.snapshot_ts,
293        },
294        Err(_) => C_TxnHandle {
295            txn_id: 0,
296            snapshot_ts: 0,
297        },
298    }
299}
300
301/// Commit a transaction.
302/// Returns C_CommitResult with commit_ts on success.
303/// The commit_ts is HLC-backed and monotonically increasing, suitable for 
304/// MVCC observability, replication, and audit trails.
305/// # Safety
306/// The ptr must be a valid pointer returned by sochdb_open.
307#[unsafe(no_mangle)]
308pub unsafe extern "C" fn sochdb_commit(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> C_CommitResult {
309    if ptr.is_null() {
310        return C_CommitResult {
311            commit_ts: 0,
312            error_code: -1,
313        };
314    }
315    let db = unsafe { &(*ptr).0 };
316    let txn = TxnHandle {
317        txn_id: handle.txn_id,
318        snapshot_ts: handle.snapshot_ts,
319    };
320    match db.commit(txn) {
321        Ok(commit_ts) => C_CommitResult {
322            commit_ts,
323            error_code: 0,
324        },
325        Err(_) => C_CommitResult {
326            commit_ts: 0,
327            error_code: -1,
328        },
329    }
330}
331
332/// Abort a transaction.
333/// Returns 0 on success, -1 on error.
334/// # Safety
335/// The ptr must be a valid pointer returned by sochdb_open.
336#[unsafe(no_mangle)]
337pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
338    if ptr.is_null() {
339        return -1;
340    }
341    let db = unsafe { &(*ptr).0 };
342    let txn = TxnHandle {
343        txn_id: handle.txn_id,
344        snapshot_ts: handle.snapshot_ts,
345    };
346    match db.abort(txn) {
347        Ok(_) => 0,
348        Err(_) => -1,
349    }
350}
351
352/// Put a key-value pair.
353/// Returns 0 on success, -1 on error.
354/// # Safety
355/// The ptr must be a valid pointer returned by sochdb_open.
356/// key_ptr and val_ptr must be valid pointers with the specified lengths.
357#[unsafe(no_mangle)]
358pub unsafe extern "C" fn sochdb_put(
359    ptr: *mut DatabasePtr,
360    handle: C_TxnHandle,
361    key_ptr: *const u8,
362    key_len: usize,
363    val_ptr: *const u8,
364    val_len: usize,
365) -> c_int {
366    if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
367        return -1;
368    }
369    let db = unsafe { &(*ptr).0 };
370    let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
371    let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
372    let txn = TxnHandle {
373        txn_id: handle.txn_id,
374        snapshot_ts: handle.snapshot_ts,
375    };
376
377    match db.put(txn, key, val) {
378        Ok(_) => 0,
379        Err(_) => -1,
380    }
381}
382
383/// Get a value.
384/// Writes pointer to val_out and length to len_out.
385/// The caller must free the returned bytes using sochdb_free_bytes.
386/// Returns 0 on success (found), 1 on not found, -1 on error.
387/// # Safety
388/// All pointer arguments must be valid.
389#[unsafe(no_mangle)]
390pub unsafe extern "C" fn sochdb_get(
391    ptr: *mut DatabasePtr,
392    handle: C_TxnHandle,
393    key_ptr: *const u8,
394    key_len: usize,
395    val_out: *mut *mut u8,
396    len_out: *mut usize,
397) -> c_int {
398    if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.is_null() {
399        return -1;
400    }
401    let db = unsafe { &(*ptr).0 };
402    let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
403    let txn = TxnHandle {
404        txn_id: handle.txn_id,
405        snapshot_ts: handle.snapshot_ts,
406    };
407
408    match db.get(txn, key) {
409        Ok(Some(val)) => {
410            // Copy value to heap to pass to C
411            let mut buf = val.into_boxed_slice();
412            unsafe {
413                *val_out = buf.as_mut_ptr();
414                *len_out = buf.len();
415            }
416            let _ = Box::into_raw(buf); // Leak memory, caller must free
417            0
418        }
419        Ok(None) => 1, // Not found
420        Err(_) => -1,
421    }
422}
423
424/// Free bytes allocated by sochdb_get.
425/// # Safety
426/// ptr must be a valid pointer returned by sochdb_get.
427#[unsafe(no_mangle)]
428pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
429    if !ptr.is_null() {
430        unsafe {
431            let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
432        }
433    }
434}
435
436/// Delete a key.
437/// Returns 0 on success, -1 on error.
438/// # Safety
439/// All pointer arguments must be valid.
440#[unsafe(no_mangle)]
441pub unsafe extern "C" fn sochdb_delete(
442    ptr: *mut DatabasePtr,
443    handle: C_TxnHandle,
444    key_ptr: *const u8,
445    key_len: usize,
446) -> c_int {
447    if ptr.is_null() || key_ptr.is_null() {
448        return -1;
449    }
450    let db = unsafe { &(*ptr).0 };
451    let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
452    let txn = TxnHandle {
453        txn_id: handle.txn_id,
454        snapshot_ts: handle.snapshot_ts,
455    };
456
457    match db.delete(txn, key) {
458        Ok(_) => 0,
459        Err(_) => -1,
460    }
461}
462
463/// Put path.
464/// # Safety
465/// All pointer arguments must be valid.
466#[unsafe(no_mangle)]
467pub unsafe extern "C" fn sochdb_put_path(
468    ptr: *mut DatabasePtr,
469    handle: C_TxnHandle,
470    path_ptr: *const c_char,
471    val_ptr: *const u8,
472    val_len: usize,
473) -> c_int {
474    if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
475        return -1;
476    }
477    let db = unsafe { &(*ptr).0 };
478    let c_str = unsafe { CStr::from_ptr(path_ptr) };
479    let path_str = match c_str.to_str() {
480        Ok(s) => s,
481        Err(_) => return -1,
482    };
483    let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
484    let txn = TxnHandle {
485        txn_id: handle.txn_id,
486        snapshot_ts: handle.snapshot_ts,
487    };
488
489    match db.put_path(txn, path_str, val) {
490        Ok(_) => 0,
491        Err(_) => -1,
492    }
493}
494
495/// Get path.
496/// # Safety
497/// All pointer arguments must be valid.
498#[unsafe(no_mangle)]
499pub unsafe extern "C" fn sochdb_get_path(
500    ptr: *mut DatabasePtr,
501    handle: C_TxnHandle,
502    path_ptr: *const c_char,
503    val_out: *mut *mut u8,
504    len_out: *mut usize,
505) -> c_int {
506    if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
507        return -1;
508    }
509    let db = unsafe { &(*ptr).0 };
510    let c_str = unsafe { CStr::from_ptr(path_ptr) };
511    let path_str = match c_str.to_str() {
512        Ok(s) => s,
513        Err(_) => return -1,
514    };
515    let txn = TxnHandle {
516        txn_id: handle.txn_id,
517        snapshot_ts: handle.snapshot_ts,
518    };
519
520    match db.get_path(txn, path_str) {
521        Ok(Some(val)) => {
522            let mut buf = val.into_boxed_slice();
523            unsafe {
524                *val_out = buf.as_mut_ptr();
525                *len_out = buf.len();
526            }
527            let _ = Box::into_raw(buf);
528            0
529        }
530        Ok(None) => 1,
531        Err(_) => -1,
532    }
533}
534
535/// Opaque pointer to Scan Iterator
536#[allow(clippy::type_complexity)]
537pub struct ScanIteratorPtr(
538    Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
539);
540
541/// Start a scan.
542/// # Safety
543/// All pointer arguments must be valid.
544#[unsafe(no_mangle)]
545pub unsafe extern "C" fn sochdb_scan(
546    ptr: *mut DatabasePtr,
547    handle: C_TxnHandle,
548    start_ptr: *const u8,
549    start_len: usize,
550    end_ptr: *const u8,
551    end_len: usize,
552) -> *mut ScanIteratorPtr {
553    if ptr.is_null() {
554        return ptr::null_mut();
555    }
556    let db = unsafe { &(*ptr).0 };
557    let txn = TxnHandle {
558        txn_id: handle.txn_id,
559        snapshot_ts: handle.snapshot_ts,
560    };
561
562    let start = if !start_ptr.is_null() && start_len > 0 {
563        unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
564    } else {
565        vec![]
566    };
567
568    let end = if !end_ptr.is_null() && end_len > 0 {
569        unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
570    } else {
571        vec![] // Empty end means unbounded in `scan` usually, or we need to handle it
572    };
573
574    // Note: The underlying `scan` method expects `Range<Vec<u8>>`.
575    // We need to handle empty start/end correctly.
576    // For now, let's assume the caller provides valid bounds or we use defaults.
577    // Ideally, we'd pass optionals.
578
579    // Using a simplified approach: if start is empty, use empty vec (start of db).
580    // If end is empty, use a "max" key or handle in `scan` impl.
581    // The `StorageEngine::scan` takes `Range<Vec<u8>>`.
582
583    // Using a simplified approach: if start is empty, use empty vec (start of db).
584    // If end is empty, use empty vec (unbounded).
585
586    match db.scan_range(txn, &start, &end) {
587        Ok(rows) => {
588            // Convert rows to an iterator of (key, value)
589            // scan_range returns Vec<(Vec<u8>, Vec<u8>)>
590            let iter = Box::new(rows.into_iter().map(Ok));
591
592            let ptr = Box::new(ScanIteratorPtr(iter));
593            Box::into_raw(ptr)
594        }
595        Err(_) => ptr::null_mut(),
596    }
597}
598
599/// Start a prefix scan - returns only keys that start with the given prefix.
600/// This is the safe method for multi-tenant isolation.
601/// # Safety
602/// All pointer arguments must be valid.
603#[unsafe(no_mangle)]
604pub unsafe extern "C" fn sochdb_scan_prefix(
605    ptr: *mut DatabasePtr,
606    handle: C_TxnHandle,
607    prefix_ptr: *const u8,
608    prefix_len: usize,
609) -> *mut ScanIteratorPtr {
610    if ptr.is_null() {
611        return ptr::null_mut();
612    }
613    let db = unsafe { &(*ptr).0 };
614    let txn = TxnHandle {
615        txn_id: handle.txn_id,
616        snapshot_ts: handle.snapshot_ts,
617    };
618
619    let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
620        unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
621    } else {
622        vec![]
623    };
624
625    // Use the proper scan method that filters by prefix
626    match db.scan(txn, &prefix) {
627        Ok(rows) => {
628            // The underlying scan already filters by prefix, but double-check
629            // to ensure no data leakage
630            let prefix_owned = prefix.clone();
631            let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
632                .into_iter()
633                .filter(|(k, _)| k.starts_with(&prefix_owned))
634                .collect();
635            
636            let iter = Box::new(filtered.into_iter().map(Ok));
637            let ptr = Box::new(ScanIteratorPtr(iter));
638            Box::into_raw(ptr)
639        }
640        Err(_) => ptr::null_mut(),
641    }
642}
643
644/// Get next item from scan iterator.
645/// Returns 0 on success, 1 on done, -1 on error.
646/// # Safety
647/// All pointer arguments must be valid.
648#[unsafe(no_mangle)]
649pub unsafe extern "C" fn sochdb_scan_next(
650    iter_ptr: *mut ScanIteratorPtr,
651    key_out: *mut *mut u8,
652    key_len_out: *mut usize,
653    val_out: *mut *mut u8,
654    val_len_out: *mut usize,
655) -> c_int {
656    if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
657        return -1;
658    }
659    let iter = unsafe { &mut (*iter_ptr).0 };
660
661    match iter.next() {
662        Some(Ok((key, val))) => {
663            let mut key_buf = key.into_boxed_slice();
664            let mut val_buf = val.into_boxed_slice();
665            unsafe {
666                *key_out = key_buf.as_mut_ptr();
667                *key_len_out = key_buf.len();
668                *val_out = val_buf.as_mut_ptr();
669                *val_len_out = val_buf.len();
670            }
671            let _ = Box::into_raw(key_buf);
672            let _ = Box::into_raw(val_buf);
673            0
674        }
675        Some(Err(_)) => -1,
676        None => 1, // Done
677    }
678}
679
680/// Free scan iterator.
681/// # Safety
682/// ptr must be a valid pointer returned by sochdb_scan.
683#[unsafe(no_mangle)]
684pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
685    if !ptr.is_null() {
686        unsafe {
687            let _ = Box::from_raw(ptr);
688        }
689    }
690}
691
692/// Checkpoint the database.
693/// # Safety
694/// ptr must be a valid pointer returned by sochdb_open.
695#[unsafe(no_mangle)]
696pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
697    if ptr.is_null() {
698        return -1;
699    }
700    let db = unsafe { &(*ptr).0 };
701    match db.flush() {
702        Ok(_) => 0,
703        Err(_) => -1,
704    }
705}
706
707/// Storage statistics
708#[repr(C)]
709pub struct CStorageStats {
710    pub memtable_size_bytes: u64,
711    pub wal_size_bytes: u64,
712    pub active_transactions: usize,
713    pub min_active_snapshot: u64,
714    pub last_checkpoint_lsn: u64,
715}
716
717/// Get storage statistics.
718/// # Safety
719/// ptr must be a valid pointer returned by sochdb_open.
720#[unsafe(no_mangle)]
721pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
722    if ptr.is_null() {
723        return CStorageStats {
724            memtable_size_bytes: 0,
725            wal_size_bytes: 0,
726            active_transactions: 0,
727            min_active_snapshot: 0,
728            last_checkpoint_lsn: 0,
729        };
730    }
731    let db = unsafe { &(*ptr).0 };
732    let stats = db.storage_stats();
733
734    CStorageStats {
735        memtable_size_bytes: stats.memtable_size_bytes,
736        wal_size_bytes: stats.wal_size_bytes,
737        active_transactions: stats.active_transactions,
738        min_active_snapshot: stats.min_active_snapshot,
739        last_checkpoint_lsn: stats.last_checkpoint_lsn,
740    }
741}
742
743// ============================================================================
744// Batched Operations - Minimize FFI Call Overhead
745// ============================================================================
746
747/// Batch descriptor for put_many operation
748///
749/// Memory layout for batch:
750/// ```text
751/// [num_entries: u32]
752/// For each entry:
753///   [key_len: u32][value_len: u32][key_bytes: ...][value_bytes: ...]
754/// ```
755///
756/// This packed format minimizes FFI crossing overhead:
757/// - One call instead of N calls
758/// - No per-entry pointer chasing
759/// - Contiguous memory for CPU cache efficiency
760#[repr(C)]
761pub struct CBatchPut {
762    /// Pointer to packed batch data
763    pub data: *const u8,
764    /// Total length of packed data
765    pub len: usize,
766}
767
768/// Put multiple key-value pairs in a single FFI call.
769///
770/// This is the high-performance path for Python and other FFI users.
771/// Instead of N individual sochdb_put calls (each with FFI overhead),
772/// the caller packs all writes into a single buffer and makes one call.
773///
774/// ## Performance
775///
776/// For N writes with per-call overhead c:
777/// - Individual puts: N × c (e.g., 100 × 500ns = 50µs overhead)
778/// - put_many: 1 × c (e.g., 1 × 500ns = 0.5µs overhead)
779/// - Speedup: 100× for FFI overhead alone
780///
781/// ## Buffer Format
782///
783/// ```text
784/// ┌────────────────────────────────────────────────────────────────┐
785/// │  num_entries (4 bytes, little-endian u32)                      │
786/// ├────────────────────────────────────────────────────────────────┤
787/// │  Entry 1:                                                      │
788/// │    key_len (4 bytes, u32) | val_len (4 bytes, u32)             │
789/// │    key_bytes (key_len bytes)                                   │
790/// │    value_bytes (val_len bytes)                                 │
791/// ├────────────────────────────────────────────────────────────────┤
792/// │  Entry 2: ...                                                  │
793/// ├────────────────────────────────────────────────────────────────┤
794/// │  Entry N: ...                                                  │
795/// └────────────────────────────────────────────────────────────────┘
796/// ```
797///
798/// ## Returns
799///
800/// - 0: All entries written successfully
801/// - -1: Error (null pointer, invalid format, write failure)
802/// - >0: Number of entries successfully written before error
803///
804/// # Safety
805///
806/// - `ptr` must be a valid DatabasePtr from sochdb_open
807/// - `batch` must point to a valid CBatchPut with correct format
808#[unsafe(no_mangle)]
809pub unsafe extern "C" fn sochdb_put_many(
810    ptr: *mut DatabasePtr,
811    handle: C_TxnHandle,
812    batch: CBatchPut,
813) -> c_int {
814    if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
815        return -1;
816    }
817
818    let db = unsafe { &(*ptr).0 };
819    let txn = TxnHandle {
820        txn_id: handle.txn_id,
821        snapshot_ts: handle.snapshot_ts,
822    };
823
824    // Parse batch
825    let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
826    
827    // Read number of entries
828    if data.len() < 4 {
829        return -1;
830    }
831    let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
832    
833    let mut offset = 4;
834    let mut success_count = 0;
835
836    for _ in 0..num_entries {
837        // Read key_len and value_len
838        if offset + 8 > data.len() {
839            return success_count;
840        }
841        let key_len = u32::from_le_bytes([
842            data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
843        ]) as usize;
844        let val_len = u32::from_le_bytes([
845            data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]
846        ]) as usize;
847        offset += 8;
848
849        // Read key and value
850        if offset + key_len + val_len > data.len() {
851            return success_count;
852        }
853        let key = &data[offset..offset + key_len];
854        offset += key_len;
855        let value = &data[offset..offset + val_len];
856        offset += val_len;
857
858        // Write to database
859        match db.put(txn, key, value) {
860            Ok(_) => success_count += 1,
861            Err(_) => return success_count,
862        }
863    }
864
865    success_count
866}
867
868/// Delete multiple keys in a single FFI call.
869///
870/// ## Buffer Format
871///
872/// ```text
873/// ┌────────────────────────────────────────────────────────────────┐
874/// │  num_entries (4 bytes, little-endian u32)                      │
875/// ├────────────────────────────────────────────────────────────────┤
876/// │  Entry 1:                                                      │
877/// │    key_len (4 bytes, u32)                                      │
878/// │    key_bytes (key_len bytes)                                   │
879/// ├────────────────────────────────────────────────────────────────┤
880/// │  Entry 2: ...                                                  │
881/// └────────────────────────────────────────────────────────────────┘
882/// ```
883///
884/// # Safety
885///
886/// Same as sochdb_put_many.
887#[unsafe(no_mangle)]
888pub unsafe extern "C" fn sochdb_delete_many(
889    ptr: *mut DatabasePtr,
890    handle: C_TxnHandle,
891    keys_data: *const u8,
892    keys_len: usize,
893) -> c_int {
894    if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
895        return -1;
896    }
897
898    let db = unsafe { &(*ptr).0 };
899    let txn = TxnHandle {
900        txn_id: handle.txn_id,
901        snapshot_ts: handle.snapshot_ts,
902    };
903
904    let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
905    
906    let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
907    
908    let mut offset = 4;
909    let mut success_count = 0;
910
911    for _ in 0..num_entries {
912        if offset + 4 > data.len() {
913            return success_count;
914        }
915        let key_len = u32::from_le_bytes([
916            data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
917        ]) as usize;
918        offset += 4;
919
920        if offset + key_len > data.len() {
921            return success_count;
922        }
923        let key = &data[offset..offset + key_len];
924        offset += key_len;
925
926        match db.delete(txn, key) {
927            Ok(_) => success_count += 1,
928            Err(_) => return success_count,
929        }
930    }
931
932    success_count
933}
934
935/// Get multiple values in a single FFI call.
936///
937/// ## Input Format
938///
939/// Same as delete_many: packed keys.
940///
941/// ## Output Format
942///
943/// ```text
944/// ┌────────────────────────────────────────────────────────────────┐
945/// │  num_results (4 bytes, u32)                                    │
946/// ├────────────────────────────────────────────────────────────────┤
947/// │  Entry 1:                                                      │
948/// │    status (1 byte): 0=found, 1=not_found, 2=error              │
949/// │    if found: val_len (4 bytes, u32), value_bytes               │
950/// ├────────────────────────────────────────────────────────────────┤
951/// │  Entry 2: ...                                                  │
952/// └────────────────────────────────────────────────────────────────┘
953/// ```
954///
955/// ## Returns
956///
957/// Pointer to allocated result buffer. Caller must free with sochdb_free_bytes.
958///
959/// # Safety
960///
961/// Same as sochdb_put_many.
962#[unsafe(no_mangle)]
963pub unsafe extern "C" fn sochdb_get_many(
964    ptr: *mut DatabasePtr,
965    handle: C_TxnHandle,
966    keys_data: *const u8,
967    keys_len: usize,
968    result_out: *mut *mut u8,
969    result_len_out: *mut usize,
970) -> c_int {
971    if ptr.is_null() || keys_data.is_null() || keys_len < 4 
972        || result_out.is_null() || result_len_out.is_null() {
973        return -1;
974    }
975
976    let db = unsafe { &(*ptr).0 };
977    let txn = TxnHandle {
978        txn_id: handle.txn_id,
979        snapshot_ts: handle.snapshot_ts,
980    };
981
982    let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
983    
984    let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
985    
986    // Build result buffer
987    let mut result = Vec::with_capacity(4 + num_entries * 10); // Estimate
988    result.extend_from_slice(&(num_entries as u32).to_le_bytes());
989    
990    let mut offset = 4;
991
992    for _ in 0..num_entries {
993        if offset + 4 > data.len() {
994            result.push(2); // Error
995            continue;
996        }
997        let key_len = u32::from_le_bytes([
998            data[offset], data[offset + 1], data[offset + 2], data[offset + 3]
999        ]) as usize;
1000        offset += 4;
1001
1002        if offset + key_len > data.len() {
1003            result.push(2); // Error
1004            continue;
1005        }
1006        let key = &data[offset..offset + key_len];
1007        offset += key_len;
1008
1009        match db.get(txn, key) {
1010            Ok(Some(value)) => {
1011                result.push(0); // Found
1012                result.extend_from_slice(&(value.len() as u32).to_le_bytes());
1013                result.extend_from_slice(&value);
1014            }
1015            Ok(None) => {
1016                result.push(1); // Not found
1017            }
1018            Err(_) => {
1019                result.push(2); // Error
1020            }
1021        }
1022    }
1023
1024    // Return result
1025    let mut boxed = result.into_boxed_slice();
1026    unsafe {
1027        *result_out = boxed.as_mut_ptr();
1028        *result_len_out = boxed.len();
1029    }
1030    let _ = Box::into_raw(boxed); // Leak for caller to free
1031    
1032    0
1033}
1034
1035// ============================================================================
1036// Batched Scan - Minimize FFI Call Overhead for Iterations
1037// ============================================================================
1038
1039/// Fetch a batch of results from scan iterator.
1040///
1041/// This dramatically reduces FFI overhead for scan operations.
1042/// Instead of N calls to `sochdb_scan_next` (each with FFI overhead),
1043/// fetch up to `batch_size` results in a single call.
1044///
1045/// ## Performance
1046///
1047/// For N results with per-call overhead c:
1048/// - Individual next calls: N × c (e.g., 10000 × 500ns = 5ms overhead)
1049/// - Batched (size=1000): 10 × c (e.g., 10 × 500ns = 5µs overhead)
1050/// - Speedup: 1000× for FFI overhead
1051///
1052/// ## Output Format
1053///
1054/// ```text
1055/// ┌────────────────────────────────────────────────────────────────┐
1056/// │  num_results (4 bytes, little-endian u32)                      │
1057/// │  is_done (1 byte): 0=more results available, 1=scan complete   │
1058/// ├────────────────────────────────────────────────────────────────┤
1059/// │  Entry 1:                                                      │
1060/// │    key_len (4 bytes, u32)                                      │
1061/// │    val_len (4 bytes, u32)                                      │
1062/// │    key_bytes (key_len bytes)                                   │
1063/// │    value_bytes (val_len bytes)                                 │
1064/// ├────────────────────────────────────────────────────────────────┤
1065/// │  Entry 2: ...                                                  │
1066/// └────────────────────────────────────────────────────────────────┘
1067/// ```
1068///
1069/// ## Returns
1070///
1071/// - 0: Batch fetched successfully (check is_done flag for completion)
1072/// - 1: Scan complete (no more results)
1073/// - -1: Error
1074///
1075/// ## Usage from Python
1076///
1077/// ```python
1078/// iter_ptr = lib.sochdb_scan(...)
1079/// while True:
1080///     result = lib.sochdb_scan_batch(iter_ptr, 1000, ...)
1081///     if result == 1:  # Done
1082///         break
1083///     # Parse batch buffer for up to 1000 results
1084/// lib.sochdb_scan_free(iter_ptr)
1085/// ```
1086///
1087/// # Safety
1088///
1089/// - `iter_ptr` must be a valid ScanIteratorPtr from sochdb_scan
1090/// - Output pointers must be valid
1091#[unsafe(no_mangle)]
1092pub unsafe extern "C" fn sochdb_scan_batch(
1093    iter_ptr: *mut ScanIteratorPtr,
1094    batch_size: usize,
1095    result_out: *mut *mut u8,
1096    result_len_out: *mut usize,
1097) -> c_int {
1098    if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0 {
1099        return -1;
1100    }
1101
1102    let iter = unsafe { &mut (*iter_ptr).0 };
1103    
1104    // Pre-allocate result buffer
1105    // Estimate: header (5 bytes) + batch_size * (8 bytes header + ~100 bytes avg data)
1106    let estimated_size = 5 + batch_size * 108;
1107    let mut result = Vec::with_capacity(estimated_size);
1108    
1109    // Reserve space for header (will fill in at end)
1110    result.extend_from_slice(&[0u8; 5]); // 4 bytes count + 1 byte is_done
1111    
1112    let mut count = 0u32;
1113    let mut is_done = false;
1114    
1115    for _ in 0..batch_size {
1116        match iter.next() {
1117            Some(Ok((key, val))) => {
1118                // Write key_len, val_len, key, value
1119                result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1120                result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1121                result.extend_from_slice(&key);
1122                result.extend_from_slice(&val);
1123                count += 1;
1124            }
1125            Some(Err(_)) => {
1126                // Write header with current count and return error
1127                result[0..4].copy_from_slice(&count.to_le_bytes());
1128                result[4] = 0; // Not done (error case)
1129                
1130                let mut boxed = result.into_boxed_slice();
1131                unsafe {
1132                    *result_out = boxed.as_mut_ptr();
1133                    *result_len_out = boxed.len();
1134                }
1135                let _ = Box::into_raw(boxed);
1136                return -1;
1137            }
1138            None => {
1139                is_done = true;
1140                break;
1141            }
1142        }
1143    }
1144    
1145    // Fill in header
1146    result[0..4].copy_from_slice(&count.to_le_bytes());
1147    result[4] = if is_done { 1 } else { 0 };
1148    
1149    // If no results and done, signal completion
1150    if count == 0 && is_done {
1151        // Still allocate minimal buffer so caller can free consistently
1152        let mut boxed = result.into_boxed_slice();
1153        unsafe {
1154            *result_out = boxed.as_mut_ptr();
1155            *result_len_out = boxed.len();
1156        }
1157        let _ = Box::into_raw(boxed);
1158        return 1; // Done
1159    }
1160    
1161    // Return buffer
1162    let mut boxed = result.into_boxed_slice();
1163    unsafe {
1164        *result_out = boxed.as_mut_ptr();
1165        *result_len_out = boxed.len();
1166    }
1167    let _ = Box::into_raw(boxed);
1168    
1169    0 // Success, check is_done flag for completion
1170}
1171
1172// ============================================================================
1173// Per-Table Index Policy API
1174// ============================================================================
1175
1176/// Set index policy for a table.
1177///
1178/// # Policy Values
1179/// - 0: WriteOptimized - O(1) writes, O(N) scans. For write-heavy tables.
1180/// - 1: Balanced (default) - O(1) amortized writes, O(output + log K) scans.
1181/// - 2: ScanOptimized - O(log N) writes, O(log N + K) scans. For analytics.
1182/// - 3: AppendOnly - O(1) writes, O(N) forward-only scans. For time-series.
1183///
1184/// # Returns
1185/// - 0: Success
1186/// - -1: Invalid pointer or table name
1187/// - -2: Invalid policy value
1188///
1189/// # Safety
1190/// ptr must be a valid DatabasePtr, table_name must be a valid C string.
1191#[unsafe(no_mangle)]
1192pub unsafe extern "C" fn sochdb_set_table_index_policy(
1193    ptr: *mut DatabasePtr,
1194    table_name: *const c_char,
1195    policy: u8,
1196) -> c_int {
1197    if ptr.is_null() || table_name.is_null() {
1198        return -1;
1199    }
1200    
1201    let c_str = unsafe { CStr::from_ptr(table_name) };
1202    let table = match c_str.to_str() {
1203        Ok(s) => s,
1204        Err(_) => return -1,
1205    };
1206    
1207    let index_policy = match policy {
1208        0 => crate::index_policy::IndexPolicy::WriteOptimized,
1209        1 => crate::index_policy::IndexPolicy::Balanced,
1210        2 => crate::index_policy::IndexPolicy::ScanOptimized,
1211        3 => crate::index_policy::IndexPolicy::AppendOnly,
1212        _ => return -2,
1213    };
1214    
1215    let db = unsafe { &(*ptr).0 };
1216    
1217    // Configure the table's index policy through the database registry
1218    let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1219    db.index_registry().configure_table(config);
1220    
1221    0
1222}
1223
1224/// Get index policy for a table.
1225///
1226/// # Returns
1227/// - 0: WriteOptimized
1228/// - 1: Balanced  
1229/// - 2: ScanOptimized
1230/// - 3: AppendOnly
1231/// - 255: Error (invalid pointer)
1232///
1233/// # Safety
1234/// ptr must be a valid DatabasePtr, table_name must be a valid C string.
1235#[unsafe(no_mangle)]
1236pub unsafe extern "C" fn sochdb_get_table_index_policy(
1237    ptr: *mut DatabasePtr,
1238    table_name: *const c_char,
1239) -> u8 {
1240    if ptr.is_null() || table_name.is_null() {
1241        return 255;
1242    }
1243    
1244    let c_str = unsafe { CStr::from_ptr(table_name) };
1245    let table = match c_str.to_str() {
1246        Ok(s) => s,
1247        Err(_) => return 255,
1248    };
1249    
1250    let db = unsafe { &(*ptr).0 };
1251    let config = db.index_registry().get_config(table);
1252    
1253    match config.policy {
1254        crate::index_policy::IndexPolicy::WriteOptimized => 0,
1255        crate::index_policy::IndexPolicy::Balanced => 1,
1256        crate::index_policy::IndexPolicy::ScanOptimized => 2,
1257        crate::index_policy::IndexPolicy::AppendOnly => 3,
1258    }
1259}
1260
1261/// C-compatible Temporal Edge
1262#[repr(C)]
1263pub struct C_TemporalEdge {
1264    pub from_id: *const c_char,
1265    pub edge_type: *const c_char,
1266    pub to_id: *const c_char,
1267    pub valid_from: u64,
1268    pub valid_until: u64,
1269    pub properties_json: *const c_char,  // JSON string of properties
1270}
1271
1272/// Add a temporal edge with validity interval.
1273/// # Safety
1274/// All pointers must be valid C strings. properties_json can be null.
1275#[unsafe(no_mangle)]
1276pub unsafe extern "C" fn sochdb_add_temporal_edge(
1277    ptr: *mut DatabasePtr,
1278    namespace: *const c_char,
1279    edge: C_TemporalEdge,
1280) -> c_int {
1281    if ptr.is_null() || namespace.is_null() || edge.from_id.is_null() 
1282        || edge.edge_type.is_null() || edge.to_id.is_null() {
1283        return -1;
1284    }
1285
1286    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1287        Ok(s) => s,
1288        Err(_) => return -1,
1289    };
1290    let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1291        Ok(s) => s,
1292        Err(_) => return -1,
1293    };
1294    let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1295        Ok(s) => s,
1296        Err(_) => return -1,
1297    };
1298    let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1299        Ok(s) => s,
1300        Err(_) => return -1,
1301    };
1302
1303    let db = unsafe { &(*ptr).0 };
1304    
1305    // Begin transaction for atomic write
1306    let txn = match db.begin_transaction() {
1307        Ok(t) => t,
1308        Err(_) => return -1,
1309    };
1310    
1311    // Store temporal edge: _graph/{ns}/temporal/{from}/{type}/{to}/{valid_from}
1312    let key = format!(
1313        "_graph/{}/temporal/{}/{}/{}/{:016x}",
1314        ns, from, etype, to, edge.valid_from
1315    );
1316    
1317    let props_str = if edge.properties_json.is_null() {
1318        "{}".to_string()
1319    } else {
1320        match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1321            Ok(s) => s.to_string(),
1322            Err(_) => return -1,
1323        }
1324    };
1325    
1326    let value = format!(
1327        r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1328        from, etype, to, edge.valid_from, edge.valid_until, props_str
1329    );
1330    
1331    if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1332        let _ = db.abort(txn);
1333        return -1;
1334    }
1335    
1336    match db.commit(txn) {
1337        Ok(_) => 0,
1338        Err(_) => -1,
1339    }
1340}
1341
1342/// Query temporal graph edges. Returns a JSON array of matching edges.
1343/// Caller must free the returned string with sochdb_free_string.
1344/// 
1345/// query_mode: 0=POINT_IN_TIME, 1=RANGE, 2=CURRENT
1346/// # Safety
1347/// All pointers must be valid C strings. edge_type can be null for no filter.
1348#[unsafe(no_mangle)]
1349pub unsafe extern "C" fn sochdb_query_temporal_graph(
1350    ptr: *mut DatabasePtr,
1351    namespace: *const c_char,
1352    node_id: *const c_char,
1353    query_mode: u8,
1354    timestamp: u64,      // For POINT_IN_TIME
1355    start_time: u64,     // For RANGE
1356    end_time: u64,       // For RANGE
1357    edge_type: *const c_char,  // Optional filter (null = all types)
1358    out_len: *mut usize,
1359) -> *mut c_char {
1360    if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1361        return ptr::null_mut();
1362    }
1363
1364    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1365        Ok(s) => s,
1366        Err(_) => return ptr::null_mut(),
1367    };
1368    let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1369        Ok(s) => s,
1370        Err(_) => return ptr::null_mut(),
1371    };
1372    
1373    let edge_filter = if edge_type.is_null() {
1374        None
1375    } else {
1376        match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1377            Ok(s) => Some(s),
1378            Err(_) => return ptr::null_mut(),
1379        }
1380    };
1381
1382    let db = unsafe { &(*ptr).0 };
1383    
1384    // Begin transaction for scan
1385    let txn = match db.begin_transaction() {
1386        Ok(t) => t,
1387        Err(_) => return ptr::null_mut(),
1388    };
1389    
1390    // Scan prefix: _graph/{ns}/temporal/{node}/
1391    let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1392    let pairs = match db.scan(txn, prefix.as_bytes()) {
1393        Ok(p) => p,
1394        Err(_) => {
1395            let _ = db.abort(txn);
1396            return ptr::null_mut();
1397        }
1398    };
1399    
1400    // Commit read transaction
1401    if let Err(_) = db.commit(txn) {
1402        return ptr::null_mut();
1403    }
1404    
1405    let mut results = Vec::new();
1406    let now = std::time::SystemTime::now()
1407        .duration_since(std::time::UNIX_EPOCH)
1408        .unwrap()
1409        .as_millis() as u64;
1410    
1411    for (_key, value) in pairs {
1412        // Parse the JSON value
1413        let value_str = match std::str::from_utf8(&value) {
1414            Ok(s) => s,
1415            Err(_) => continue,
1416        };
1417        
1418        // Simple JSON parsing (in production, use serde_json)
1419        if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1420            if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1421                let vf_start = valid_from_pos + r#""valid_from":"#.len();
1422                let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1423                let vu_start = valid_until_pos + r#""valid_until":"#.len();
1424                let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1425                
1426                let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1427                let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1428                
1429                // Filter by edge_type if specified
1430                if let Some(filter) = edge_filter {
1431                    if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1432                        continue;
1433                    }
1434                }
1435                
1436                // Filter by query mode
1437                let matches = match query_mode {
1438                    0 => timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until),
1439                    1 => {
1440                        let edge_end = if valid_until == 0 { u64::MAX } else { valid_until };
1441                        valid_from < end_time && edge_end > start_time
1442                    }
1443                    2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1444                    _ => false,
1445                };
1446                
1447                if matches {
1448                    results.push(value_str.to_string());
1449                }
1450            }
1451        }
1452    }
1453    
1454    // Build JSON array
1455    let json = format!("[{}]", results.join(","));
1456    let c_string = match std::ffi::CString::new(json) {
1457        Ok(s) => s,
1458        Err(_) => return ptr::null_mut(),
1459    };
1460    
1461    unsafe { *out_len = c_string.as_bytes().len() };
1462    c_string.into_raw()
1463}
1464
1465/// Free a string returned by sochdb_query_temporal_graph.
1466/// # Safety
1467/// The ptr must be a valid pointer returned by sochdb_query_temporal_graph.
1468#[unsafe(no_mangle)]
1469pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1470    if !ptr.is_null() {
1471        unsafe {
1472            let _ = std::ffi::CString::from_raw(ptr);
1473        }
1474    }
1475}
1476
1477// ============================================================================
1478// Graph Overlay FFI - Nodes, Edges, Traversal
1479// ============================================================================
1480
1481/// Add a node to the graph overlay.
1482/// 
1483/// Stores node as: _graph/{namespace}/nodes/{node_id}
1484/// 
1485/// # Returns
1486/// - 0: Success
1487/// - -1: Error
1488/// 
1489/// # Safety
1490/// All pointers must be valid C strings. properties_json can be null.
1491#[unsafe(no_mangle)]
1492pub unsafe extern "C" fn sochdb_graph_add_node(
1493    ptr: *mut DatabasePtr,
1494    namespace: *const c_char,
1495    node_id: *const c_char,
1496    node_type: *const c_char,
1497    properties_json: *const c_char,
1498) -> c_int {
1499    if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1500        return -1;
1501    }
1502
1503    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1504        Ok(s) => s,
1505        Err(_) => return -1,
1506    };
1507    let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1508        Ok(s) => s,
1509        Err(_) => return -1,
1510    };
1511    let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1512        Ok(s) => s,
1513        Err(_) => return -1,
1514    };
1515    let props = if properties_json.is_null() {
1516        "{}".to_string()
1517    } else {
1518        match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1519            Ok(s) => s.to_string(),
1520            Err(_) => return -1,
1521        }
1522    };
1523
1524    let db = unsafe { &(*ptr).0 };
1525    
1526    let txn = match db.begin_transaction() {
1527        Ok(t) => t,
1528        Err(_) => return -1,
1529    };
1530    
1531    let key = format!("_graph/{}/nodes/{}", ns, id);
1532    let value = format!(
1533        r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1534        id, ntype, props
1535    );
1536    
1537    if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1538        let _ = db.abort(txn);
1539        return -1;
1540    }
1541    
1542    match db.commit(txn) {
1543        Ok(_) => 0,
1544        Err(_) => -1,
1545    }
1546}
1547
1548/// Add an edge between nodes in the graph overlay.
1549/// 
1550/// Stores edge as: _graph/{namespace}/edges/{from_id}/{edge_type}/{to_id}
1551/// 
1552/// # Returns
1553/// - 0: Success
1554/// - -1: Error
1555/// 
1556/// # Safety
1557/// All pointers must be valid C strings. properties_json can be null.
1558#[unsafe(no_mangle)]
1559pub unsafe extern "C" fn sochdb_graph_add_edge(
1560    ptr: *mut DatabasePtr,
1561    namespace: *const c_char,
1562    from_id: *const c_char,
1563    edge_type: *const c_char,
1564    to_id: *const c_char,
1565    properties_json: *const c_char,
1566) -> c_int {
1567    if ptr.is_null() || namespace.is_null() || from_id.is_null() 
1568        || edge_type.is_null() || to_id.is_null() {
1569        return -1;
1570    }
1571
1572    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1573        Ok(s) => s,
1574        Err(_) => return -1,
1575    };
1576    let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1577        Ok(s) => s,
1578        Err(_) => return -1,
1579    };
1580    let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1581        Ok(s) => s,
1582        Err(_) => return -1,
1583    };
1584    let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1585        Ok(s) => s,
1586        Err(_) => return -1,
1587    };
1588    let props = if properties_json.is_null() {
1589        "{}".to_string()
1590    } else {
1591        match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1592            Ok(s) => s.to_string(),
1593            Err(_) => return -1,
1594        }
1595    };
1596
1597    let db = unsafe { &(*ptr).0 };
1598    
1599    let txn = match db.begin_transaction() {
1600        Ok(t) => t,
1601        Err(_) => return -1,
1602    };
1603    
1604    let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1605    let value = format!(
1606        r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1607        from, etype, to, props
1608    );
1609    
1610    if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1611        let _ = db.abort(txn);
1612        return -1;
1613    }
1614    
1615    match db.commit(txn) {
1616        Ok(_) => 0,
1617        Err(_) => -1,
1618    }
1619}
1620
1621/// Traverse the graph from a starting node.
1622/// 
1623/// Returns JSON: {"nodes": [...], "edges": [...]}
1624/// Caller must free the returned string with sochdb_free_string.
1625/// 
1626/// order: 0=BFS, 1=DFS
1627/// 
1628/// # Safety
1629/// All pointers must be valid.
1630#[unsafe(no_mangle)]
1631pub unsafe extern "C" fn sochdb_graph_traverse(
1632    ptr: *mut DatabasePtr,
1633    namespace: *const c_char,
1634    start_node: *const c_char,
1635    max_depth: usize,
1636    order: u8,  // 0=BFS, 1=DFS
1637    out_len: *mut usize,
1638) -> *mut c_char {
1639    if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1640        return ptr::null_mut();
1641    }
1642
1643    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1644        Ok(s) => s,
1645        Err(_) => return ptr::null_mut(),
1646    };
1647    let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1648        Ok(s) => s,
1649        Err(_) => return ptr::null_mut(),
1650    };
1651
1652    let db = unsafe { &(*ptr).0 };
1653    
1654    let txn = match db.begin_transaction() {
1655        Ok(t) => t,
1656        Err(_) => return ptr::null_mut(),
1657    };
1658    
1659    // Collect nodes and edges through traversal
1660    let mut visited_nodes = std::collections::HashSet::new();
1661    let mut nodes_json = Vec::new();
1662    let mut edges_json = Vec::new();
1663    
1664    // Use queue for BFS, stack for DFS
1665    let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1666    
1667    while let Some((current_node, depth)) = if order == 0 {
1668        // BFS: remove from front
1669        if frontier.is_empty() { None } else { Some(frontier.remove(0)) }
1670    } else {
1671        // DFS: remove from back
1672        frontier.pop()
1673    } {
1674        if depth > max_depth || visited_nodes.contains(&current_node) {
1675            continue;
1676        }
1677        visited_nodes.insert(current_node.clone());
1678        
1679        // Get node data
1680        let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1681        if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1682            if let Ok(s) = std::str::from_utf8(&node_data) {
1683                nodes_json.push(s.to_string());
1684            }
1685        }
1686        
1687        // Get outgoing edges
1688        let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1689        if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1690            for (_key, value) in edges {
1691                if let Ok(edge_str) = std::str::from_utf8(&value) {
1692                    edges_json.push(edge_str.to_string());
1693                    
1694                    // Extract to_id for traversal
1695                    if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
1696                        let start_idx = to_pos + r#""to_id":""#.len();
1697                        if let Some(end_idx) = edge_str[start_idx..].find('"') {
1698                            let to_id = &edge_str[start_idx..start_idx + end_idx];
1699                            if !visited_nodes.contains(to_id) {
1700                                frontier.push((to_id.to_string(), depth + 1));
1701                            }
1702                        }
1703                    }
1704                }
1705            }
1706        }
1707    }
1708    
1709    if let Err(_) = db.commit(txn) {
1710        return ptr::null_mut();
1711    }
1712    
1713    let result = format!(
1714        r#"{{"nodes":[{}],"edges":[{}]}}"#,
1715        nodes_json.join(","),
1716        edges_json.join(",")
1717    );
1718    
1719    let c_string = match std::ffi::CString::new(result) {
1720        Ok(s) => s,
1721        Err(_) => return ptr::null_mut(),
1722    };
1723    
1724    unsafe { *out_len = c_string.as_bytes().len() };
1725    c_string.into_raw()
1726}
1727
1728// ============================================================================
1729// Semantic Cache FFI
1730// ============================================================================
1731
1732/// Store a value in the semantic cache with its embedding.
1733/// 
1734/// # Returns
1735/// - 0: Success
1736/// - -1: Error
1737/// 
1738/// # Safety
1739/// All pointers must be valid.
1740#[unsafe(no_mangle)]
1741pub unsafe extern "C" fn sochdb_cache_put(
1742    ptr: *mut DatabasePtr,
1743    cache_name: *const c_char,
1744    key: *const c_char,
1745    value: *const c_char,
1746    embedding_ptr: *const f32,
1747    embedding_len: usize,
1748    ttl_seconds: u64,
1749) -> c_int {
1750    if ptr.is_null() || cache_name.is_null() || key.is_null() 
1751        || value.is_null() || embedding_ptr.is_null() {
1752        return -1;
1753    }
1754
1755    let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1756        Ok(s) => s,
1757        Err(_) => return -1,
1758    };
1759    let k = match unsafe { CStr::from_ptr(key) }.to_str() {
1760        Ok(s) => s,
1761        Err(_) => return -1,
1762    };
1763    let v = match unsafe { CStr::from_ptr(value) }.to_str() {
1764        Ok(s) => s,
1765        Err(_) => return -1,
1766    };
1767    let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
1768
1769    let db = unsafe { &(*ptr).0 };
1770    
1771    let txn = match db.begin_transaction() {
1772        Ok(t) => t,
1773        Err(_) => return -1,
1774    };
1775    
1776    // Compute expiry timestamp
1777    let expires_at = if ttl_seconds > 0 {
1778        std::time::SystemTime::now()
1779            .duration_since(std::time::UNIX_EPOCH)
1780            .unwrap()
1781            .as_secs() + ttl_seconds
1782    } else {
1783        0 // No expiry
1784    };
1785    
1786    // Store cache entry: _cache/{cache_name}/{key_hash}
1787    let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
1788    let cache_key = format!("_cache/{}/{}", cache, key_hash);
1789    
1790    // Serialize embedding as JSON array
1791    let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
1792    
1793    let cache_value = format!(
1794        r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
1795        k, v, embedding_json.join(","), expires_at
1796    );
1797    
1798    if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
1799        let _ = db.abort(txn);
1800        return -1;
1801    }
1802    
1803    match db.commit(txn) {
1804        Ok(_) => 0,
1805        Err(_) => -1,
1806    }
1807}
1808
1809/// Look up a value in the semantic cache by embedding similarity.
1810/// 
1811/// Returns the cached value if similarity >= threshold, null otherwise.
1812/// Caller must free the returned string with sochdb_free_string.
1813/// 
1814/// # Safety
1815/// All pointers must be valid.
1816#[unsafe(no_mangle)]
1817pub unsafe extern "C" fn sochdb_cache_get(
1818    ptr: *mut DatabasePtr,
1819    cache_name: *const c_char,
1820    query_embedding_ptr: *const f32,
1821    embedding_len: usize,
1822    threshold: f32,
1823    out_len: *mut usize,
1824) -> *mut c_char {
1825    if ptr.is_null() || cache_name.is_null() || query_embedding_ptr.is_null() || out_len.is_null() {
1826        return ptr::null_mut();
1827    }
1828
1829    let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
1830        Ok(s) => s,
1831        Err(_) => return ptr::null_mut(),
1832    };
1833    let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
1834
1835    let db = unsafe { &(*ptr).0 };
1836    
1837    let txn = match db.begin_transaction() {
1838        Ok(t) => t,
1839        Err(_) => return ptr::null_mut(),
1840    };
1841    
1842    let prefix = format!("_cache/{}/", cache);
1843    let entries = match db.scan(txn, prefix.as_bytes()) {
1844        Ok(e) => e,
1845        Err(_) => {
1846            let _ = db.abort(txn);
1847            return ptr::null_mut();
1848        }
1849    };
1850    
1851    let _ = db.commit(txn);
1852    
1853    let now = std::time::SystemTime::now()
1854        .duration_since(std::time::UNIX_EPOCH)
1855        .unwrap()
1856        .as_secs();
1857    
1858    let mut best_match: Option<(f32, String)> = None;
1859    
1860    for (_key, value) in entries {
1861        let value_str = match std::str::from_utf8(&value) {
1862            Ok(s) => s,
1863            Err(_) => continue,
1864        };
1865        
1866        // Parse expires_at
1867        if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
1868            let exp_start = exp_pos + r#""expires_at":"#.len();
1869            if let Some(exp_end) = value_str[exp_start..].find('}') {
1870                let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
1871                    .parse()
1872                    .unwrap_or(0);
1873                if expires_at > 0 && now > expires_at {
1874                    continue; // Expired
1875                }
1876            }
1877        }
1878        
1879        // Parse embedding and compute cosine similarity
1880        if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
1881            let emb_start = emb_pos + r#""embedding":["#.len();
1882            if let Some(emb_end) = value_str[emb_start..].find(']') {
1883                let emb_str = &value_str[emb_start..emb_start + emb_end];
1884                let cached_embedding: Vec<f32> = emb_str
1885                    .split(',')
1886                    .filter_map(|s| s.trim().parse().ok())
1887                    .collect();
1888                
1889                if cached_embedding.len() == query.len() {
1890                    let similarity = cosine_similarity(query, &cached_embedding);
1891                    if similarity >= threshold {
1892                        if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
1893                            // Extract value field
1894                            if let Some(val_pos) = value_str.find(r#""value":""#) {
1895                                let val_start = val_pos + r#""value":""#.len();
1896                                if let Some(val_end) = value_str[val_start..].find('"') {
1897                                    let cached_value = &value_str[val_start..val_start + val_end];
1898                                    best_match = Some((similarity, cached_value.to_string()));
1899                                }
1900                            }
1901                        }
1902                    }
1903                }
1904            }
1905        }
1906    }
1907    
1908    match best_match {
1909        Some((_, value)) => {
1910            let c_string = match std::ffi::CString::new(value) {
1911                Ok(s) => s,
1912                Err(_) => return ptr::null_mut(),
1913            };
1914            unsafe { *out_len = c_string.as_bytes().len() };
1915            c_string.into_raw()
1916        }
1917        None => ptr::null_mut(),
1918    }
1919}
1920
1921/// Compute cosine similarity between two vectors
1922/// Returns normalized similarity in [0, 1] range for threshold comparisons
1923fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
1924    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1925    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
1926    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1927    if norm_a == 0.0 || norm_b == 0.0 {
1928        0.0
1929    } else {
1930        let similarity = dot / (norm_a * norm_b);
1931        // Normalize from [-1, 1] to [0, 1] for threshold comparisons
1932        // This ensures consistent scoring across all SDKs (Python/Node.js/Go)
1933        (similarity + 1.0) / 2.0
1934    }
1935}
1936
1937// ============================================================================
1938// Trace Service FFI
1939// ============================================================================
1940
1941/// Start a new trace. Returns trace_id and root_span_id.
1942/// 
1943/// Caller must free the returned strings with sochdb_free_string.
1944/// 
1945/// # Returns
1946/// - 0: Success
1947/// - -1: Error
1948/// 
1949/// # Safety
1950/// All pointers must be valid.
1951#[unsafe(no_mangle)]
1952pub unsafe extern "C" fn sochdb_trace_start(
1953    ptr: *mut DatabasePtr,
1954    name: *const c_char,
1955    trace_id_out: *mut *mut c_char,
1956    span_id_out: *mut *mut c_char,
1957) -> c_int {
1958    if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
1959        return -1;
1960    }
1961
1962    let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
1963        Ok(s) => s,
1964        Err(_) => return -1,
1965    };
1966
1967    let db = unsafe { &(*ptr).0 };
1968    
1969    // Generate unique IDs
1970    let trace_id = format!("trace_{:016x}", rand_u64());
1971    let span_id = format!("span_{:016x}", rand_u64());
1972    
1973    let txn = match db.begin_transaction() {
1974        Ok(t) => t,
1975        Err(_) => return -1,
1976    };
1977    
1978    let now = std::time::SystemTime::now()
1979        .duration_since(std::time::UNIX_EPOCH)
1980        .unwrap()
1981        .as_micros() as u64;
1982    
1983    // Store trace: _traces/{trace_id}
1984    let trace_key = format!("_traces/{}", trace_id);
1985    let trace_value = format!(
1986        r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
1987        trace_id, trace_name, now, span_id
1988    );
1989    
1990    if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
1991        let _ = db.abort(txn);
1992        return -1;
1993    }
1994    
1995    // Store root span: _traces/{trace_id}/spans/{span_id}
1996    let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
1997    let span_value = format!(
1998        r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
1999        span_id, trace_name, now
2000    );
2001    
2002    if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2003        let _ = db.abort(txn);
2004        return -1;
2005    }
2006    
2007    if let Err(_) = db.commit(txn) {
2008        return -1;
2009    }
2010    
2011    // Return trace_id and span_id
2012    let trace_c = match std::ffi::CString::new(trace_id) {
2013        Ok(s) => s,
2014        Err(_) => return -1,
2015    };
2016    let span_c = match std::ffi::CString::new(span_id) {
2017        Ok(s) => s,
2018        Err(_) => return -1,
2019    };
2020    
2021    unsafe {
2022        *trace_id_out = trace_c.into_raw();
2023        *span_id_out = span_c.into_raw();
2024    }
2025    
2026    0
2027}
2028
2029/// Start a child span within a trace.
2030/// 
2031/// Caller must free the returned span_id with sochdb_free_string.
2032/// 
2033/// # Safety
2034/// All pointers must be valid.
2035#[unsafe(no_mangle)]
2036pub unsafe extern "C" fn sochdb_trace_span_start(
2037    ptr: *mut DatabasePtr,
2038    trace_id: *const c_char,
2039    parent_span_id: *const c_char,
2040    name: *const c_char,
2041    span_id_out: *mut *mut c_char,
2042) -> c_int {
2043    if ptr.is_null() || trace_id.is_null() || parent_span_id.is_null() 
2044        || name.is_null() || span_id_out.is_null() {
2045        return -1;
2046    }
2047
2048    let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2049        Ok(s) => s,
2050        Err(_) => return -1,
2051    };
2052    let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
2053        Ok(s) => s,
2054        Err(_) => return -1,
2055    };
2056    let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2057        Ok(s) => s,
2058        Err(_) => return -1,
2059    };
2060
2061    let db = unsafe { &(*ptr).0 };
2062    let span_id = format!("span_{:016x}", rand_u64());
2063    
2064    let txn = match db.begin_transaction() {
2065        Ok(t) => t,
2066        Err(_) => return -1,
2067    };
2068    
2069    let now = std::time::SystemTime::now()
2070        .duration_since(std::time::UNIX_EPOCH)
2071        .unwrap()
2072        .as_micros() as u64;
2073    
2074    let span_key = format!("_traces/{}/spans/{}", tid, span_id);
2075    let span_value = format!(
2076        r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
2077        span_id, span_name, now, pid
2078    );
2079    
2080    if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2081        let _ = db.abort(txn);
2082        return -1;
2083    }
2084    
2085    if let Err(_) = db.commit(txn) {
2086        return -1;
2087    }
2088    
2089    let span_c = match std::ffi::CString::new(span_id) {
2090        Ok(s) => s,
2091        Err(_) => return -1,
2092    };
2093    
2094    unsafe { *span_id_out = span_c.into_raw() };
2095    0
2096}
2097
2098/// End a span and record its duration.
2099/// 
2100/// status: 0=unset, 1=ok, 2=error
2101/// 
2102/// # Returns
2103/// Duration in microseconds on success, -1 on error.
2104/// 
2105/// # Safety
2106/// All pointers must be valid.
2107#[unsafe(no_mangle)]
2108pub unsafe extern "C" fn sochdb_trace_span_end(
2109    ptr: *mut DatabasePtr,
2110    trace_id: *const c_char,
2111    span_id: *const c_char,
2112    status: u8,
2113) -> i64 {
2114    if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2115        return -1;
2116    }
2117
2118    let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2119        Ok(s) => s,
2120        Err(_) => return -1,
2121    };
2122    let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2123        Ok(s) => s,
2124        Err(_) => return -1,
2125    };
2126
2127    let db = unsafe { &(*ptr).0 };
2128    
2129    let txn = match db.begin_transaction() {
2130        Ok(t) => t,
2131        Err(_) => return -1,
2132    };
2133    
2134    let span_key = format!("_traces/{}/spans/{}", tid, sid);
2135    
2136    // Read current span
2137    let span_data = match db.get(txn, span_key.as_bytes()) {
2138        Ok(Some(data)) => data,
2139        _ => {
2140            let _ = db.abort(txn);
2141            return -1;
2142        }
2143    };
2144    
2145    let span_str = match std::str::from_utf8(&span_data) {
2146        Ok(s) => s,
2147        Err(_) => {
2148            let _ = db.abort(txn);
2149            return -1;
2150        }
2151    };
2152    
2153    // Parse start_us
2154    let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2155        let start = pos + r#""start_us":"#.len();
2156        if let Some(end) = span_str[start..].find(',') {
2157            span_str[start..start + end].parse().unwrap_or(0u64)
2158        } else {
2159            0u64
2160        }
2161    } else {
2162        0u64
2163    };
2164    
2165    let now = std::time::SystemTime::now()
2166        .duration_since(std::time::UNIX_EPOCH)
2167        .unwrap()
2168        .as_micros() as u64;
2169    
2170    let duration_us = now.saturating_sub(start_us);
2171    let status_str = match status {
2172        1 => "ok",
2173        2 => "error",
2174        _ => "unset",
2175    };
2176    
2177    // Update span with end time and duration
2178    let new_span = span_str
2179        .replace(r#""status":"active""#, &format!(r#""status":"{}","end_us":{},"duration_us":{}"#, status_str, now, duration_us));
2180    
2181    if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2182        let _ = db.abort(txn);
2183        return -1;
2184    }
2185    
2186    if let Err(_) = db.commit(txn) {
2187        return -1;
2188    }
2189    
2190    duration_us as i64
2191}
2192
2193/// Generate a pseudo-random u64 (simple XorShift for trace IDs)
2194fn rand_u64() -> u64 {
2195    use std::sync::atomic::{AtomicU64, Ordering};
2196    static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2197    
2198    let mut s = STATE.load(Ordering::Relaxed);
2199    if s == 0 {
2200        s = std::time::SystemTime::now()
2201            .duration_since(std::time::UNIX_EPOCH)
2202            .unwrap()
2203            .as_nanos() as u64;
2204    }
2205    s ^= s >> 12;
2206    s ^= s << 25;
2207    s ^= s >> 27;
2208    STATE.store(s, Ordering::Relaxed);
2209    s.wrapping_mul(0x2545F4914F6CDD1D)
2210}
2211
2212// =========================================================================
2213// Vector Index Operations (KV-based, native Rust performance)
2214// =========================================================================
2215
2216/// Create a vector collection for storing embeddings
2217/// 
2218/// # Returns
2219/// - 0: Success (or already exists)
2220/// - -1: Error
2221#[unsafe(no_mangle)]
2222pub unsafe extern "C" fn sochdb_collection_create(
2223    ptr: *mut DatabasePtr,
2224    namespace: *const c_char,
2225    collection: *const c_char,
2226    dimension: usize,
2227    dist_type: u8, // 0=Cosine, 1=Euclidean, 2=Dot
2228) -> c_int {
2229    if ptr.is_null() || namespace.is_null() || collection.is_null() {
2230        return -1;
2231    }
2232    
2233    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2234        Ok(s) => s,
2235        Err(_) => return -1,
2236    };
2237    let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2238        Ok(s) => s,
2239        Err(_) => return -1,
2240    };
2241    
2242    let db = unsafe { &(*ptr).0 };
2243    let txn = match db.begin_transaction() {
2244        Ok(t) => t,
2245        Err(_) => return -1,
2246    };
2247    
2248    // Store collection config
2249    let config_key = format!("{}/_collections/{}", ns, col);
2250    let config_value = format!(
2251        r#"{{"dimension":{},"metric":{}}}"#,
2252        dimension, dist_type
2253    );
2254    
2255    if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2256        let _ = db.abort(txn);
2257        return -1;
2258    }
2259    
2260    let result = match db.commit(txn) {
2261        Ok(_) => 0,
2262        Err(_) => -1,
2263    };
2264
2265    if result == 0 {
2266        let metric = match dist_type {
2267            1 => DistanceMetric::Euclidean,
2268            2 => DistanceMetric::DotProduct,
2269            _ => DistanceMetric::Cosine,
2270        };
2271        let _ = ensure_collection_index(db, ns, col, dimension, metric);
2272    }
2273
2274    result
2275}
2276
2277/// Insert a vector into a collection
2278/// 
2279/// # Returns
2280/// - 0: Success
2281/// - -1: Error
2282#[unsafe(no_mangle)]
2283pub unsafe extern "C" fn sochdb_collection_insert(
2284    ptr: *mut DatabasePtr,
2285    namespace: *const c_char,
2286    collection: *const c_char,
2287    id: *const c_char,
2288    vector_ptr: *const f32,
2289    vector_len: usize,
2290    metadata_json: *const c_char, // Optional JSON metadata
2291) -> c_int {
2292    if ptr.is_null() || namespace.is_null() || collection.is_null() 
2293        || id.is_null() || vector_ptr.is_null() {
2294        return -1;
2295    }
2296    
2297    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2298        Ok(s) => s,
2299        Err(_) => return -1,
2300    };
2301    let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2302        Ok(s) => s,
2303        Err(_) => return -1,
2304    };
2305    let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2306        Ok(s) => s,
2307        Err(_) => return -1,
2308    };
2309    let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2310    let db = unsafe { &(*ptr).0 };
2311
2312    let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2313        Some(config) => config,
2314        None => (vector_len, DistanceMetric::Cosine),
2315    };
2316    if vector_len != dimension {
2317        return -1;
2318    }
2319    
2320    let metadata = if !metadata_json.is_null() {
2321        match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2322            Ok(s) => s.to_string(),
2323            Err(_) => "{}".to_string(),
2324        }
2325    } else {
2326        "{}".to_string()
2327    };
2328    
2329    let txn = match db.begin_transaction() {
2330        Ok(t) => t,
2331        Err(_) => return -1,
2332    };
2333    
2334    let id_hash = hash_id_to_u128(doc_id);
2335    let vec_key = vector_bin_key(ns, col, id_hash);
2336    let vec_value = serialize_vector_binary(vector);
2337
2338    if let Err(_) = db.put(txn, vec_key.as_bytes(), &vec_value) {
2339        let _ = db.abort(txn);
2340        return -1;
2341    }
2342
2343    let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2344        Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2345        Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2346    };
2347    let meta_key = metadata_key(ns, col, id_hash);
2348    if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2349        if let Err(_) = db.put(txn, meta_key.as_bytes(), &meta_bytes) {
2350            let _ = db.abort(txn);
2351            return -1;
2352        }
2353    }
2354    
2355    if let Err(_) = db.commit(txn) {
2356        return -1;
2357    }
2358
2359    let index = ensure_collection_index(db, ns, col, dimension, metric);
2360    let _ = index.index.insert(id_hash, vector.to_vec());
2361
2362    0
2363}
2364
2365/// C-compatible search result
2366#[repr(C)]
2367pub struct CSearchResult {
2368    pub id_ptr: *mut c_char,
2369    pub score: f32,
2370    pub metadata_ptr: *mut c_char,
2371}
2372
2373/// Search a collection for nearest vectors
2374/// 
2375/// # Returns
2376/// - >= 0: Number of results
2377/// - -1: Error
2378#[unsafe(no_mangle)]
2379pub unsafe extern "C" fn sochdb_collection_search(
2380    ptr: *mut DatabasePtr,
2381    namespace: *const c_char,
2382    collection: *const c_char,
2383    query_ptr: *const f32,
2384    query_len: usize,
2385    k: usize,
2386    results_out: *mut CSearchResult,
2387) -> c_int {
2388    if ptr.is_null() || namespace.is_null() || collection.is_null() 
2389        || query_ptr.is_null() || results_out.is_null() {
2390        return -1;
2391    }
2392    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2393        Ok(s) => s,
2394        Err(_) => return -1,
2395    };
2396    let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2397        Ok(s) => s,
2398        Err(_) => return -1,
2399    };
2400    let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2401    let db = unsafe { &(*ptr).0 };
2402    let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2403        Some(config) => config,
2404        None => return 0,
2405    };
2406
2407    if query_len != dimension {
2408        return -1;
2409    }
2410
2411    let index = ensure_collection_index(db, ns, col, dimension, metric);
2412    let mut scored = match index.index.search(query, k) {
2413        Ok(results) => results,
2414        Err(_) => return -1,
2415    };
2416
2417    let result_count = scored.len().min(k);
2418    for (i, (id_hash, distance)) in scored.drain(..result_count).enumerate() {
2419        let meta_key = metadata_key(ns, col, id_hash);
2420        let txn = match db.begin_transaction() {
2421            Ok(t) => t,
2422            Err(_) => return -1,
2423        };
2424        let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2425        let _ = db.commit(txn);
2426
2427        let mut id_value = String::new();
2428        let mut metadata_json = serde_json::json!({});
2429        if let Some(bytes) = meta_value.as_deref() {
2430            if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(bytes) {
2431                id_value = parsed.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2432                metadata_json = parsed.get("metadata").cloned().unwrap_or(serde_json::json!({}));
2433            }
2434        }
2435        let metadata = serde_json::to_string(&metadata_json).unwrap_or_else(|_| "{}".to_string());
2436
2437        let c_id = match std::ffi::CString::new(id_value) {
2438            Ok(s) => s.into_raw(),
2439            Err(_) => ptr::null_mut(),
2440        };
2441        let c_meta = match std::ffi::CString::new(metadata) {
2442            Ok(s) => s.into_raw(),
2443            Err(_) => ptr::null_mut(),
2444        };
2445
2446        unsafe {
2447            (*results_out.add(i)).id_ptr = c_id;
2448            (*results_out.add(i)).score = decode_score(metric, distance);
2449            (*results_out.add(i)).metadata_ptr = c_meta;
2450        }
2451    }
2452
2453    result_count as c_int
2454}
2455
2456/// Search a collection and return results as struct-of-arrays (ids + scores)
2457///
2458/// - ids_out: pointer to u64 array (allocated by Rust)
2459/// - scores_out: pointer to f32 array (allocated by Rust)
2460/// - len_out: number of results
2461#[unsafe(no_mangle)]
2462pub unsafe extern "C" fn sochdb_collection_search_soa(
2463    ptr: *mut DatabasePtr,
2464    namespace: *const c_char,
2465    collection: *const c_char,
2466    query_ptr: *const f32,
2467    query_len: usize,
2468    k: usize,
2469    min_score: f32,
2470    filter_json: *const c_char,
2471    ids_hi_out: *mut *mut u64,
2472    ids_lo_out: *mut *mut u64,
2473    scores_out: *mut *mut f32,
2474    len_out: *mut usize,
2475) -> c_int {
2476    if ptr.is_null() || namespace.is_null() || collection.is_null()
2477        || query_ptr.is_null() || ids_hi_out.is_null() || ids_lo_out.is_null()
2478        || scores_out.is_null() || len_out.is_null() {
2479        return -1;
2480    }
2481
2482    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2483        Ok(s) => s,
2484        Err(_) => return -1,
2485    };
2486    let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2487        Ok(s) => s,
2488        Err(_) => return -1,
2489    };
2490    let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2491    let db = unsafe { &(*ptr).0 };
2492
2493    let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2494        Some(config) => config,
2495        None => return 0,
2496    };
2497    if query_len != dimension {
2498        return -1;
2499    }
2500
2501    let filter = if !filter_json.is_null() {
2502        match unsafe { CStr::from_ptr(filter_json) }.to_str() {
2503            Ok(s) => serde_json::from_str::<serde_json::Value>(s).ok(),
2504            Err(_) => None,
2505        }
2506    } else {
2507        None
2508    };
2509
2510    let index = ensure_collection_index(db, ns, col, dimension, metric);
2511    let results = match index.index.search(query, k) {
2512        Ok(results) => results,
2513        Err(_) => return -1,
2514    };
2515
2516    let mut ids_hi: Vec<u64> = Vec::with_capacity(results.len());
2517    let mut ids_lo: Vec<u64> = Vec::with_capacity(results.len());
2518    let mut scores: Vec<f32> = Vec::with_capacity(results.len());
2519
2520    for (id_hash, distance) in results {
2521        let score = decode_score(metric, distance);
2522        if min_score > 0.0 && score < min_score {
2523            continue;
2524        }
2525
2526        if let Some(filter_value) = &filter {
2527            let meta_key = metadata_key(ns, col, id_hash);
2528            let txn = match db.begin_transaction() {
2529                Ok(t) => t,
2530                Err(_) => return -1,
2531            };
2532            let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2533            let _ = db.commit(txn);
2534            let meta_value = match meta_value {
2535                Some(value) => value,
2536                None => continue,
2537            };
2538            let parsed = match serde_json::from_slice::<serde_json::Value>(&meta_value) {
2539                Ok(value) => value,
2540                Err(_) => continue,
2541            };
2542            let metadata = parsed.get("metadata").cloned().unwrap_or(Value::Null);
2543
2544            if !metadata_matches_filter(&metadata, filter_value) {
2545                continue;
2546            }
2547        }
2548
2549        ids_hi.push((id_hash >> 64) as u64);
2550        ids_lo.push((id_hash & u128::from(u64::MAX)) as u64);
2551        scores.push(score);
2552        if ids_hi.len() >= k {
2553            break;
2554        }
2555    }
2556
2557    let len = ids_hi.len();
2558    let mut ids_hi_box = ids_hi.into_boxed_slice();
2559    let mut ids_lo_box = ids_lo.into_boxed_slice();
2560    let mut scores_box = scores.into_boxed_slice();
2561
2562    unsafe {
2563        *len_out = len;
2564        *ids_hi_out = ids_hi_box.as_mut_ptr();
2565        *ids_lo_out = ids_lo_box.as_mut_ptr();
2566        *scores_out = scores_box.as_mut_ptr();
2567    }
2568
2569    std::mem::forget(ids_hi_box);
2570    std::mem::forget(ids_lo_box);
2571    std::mem::forget(scores_box);
2572
2573    len as c_int
2574}
2575
2576/// Fetch metadata JSON for a list of ids (u64 hashes)
2577#[unsafe(no_mangle)]
2578pub unsafe extern "C" fn sochdb_collection_fetch_metadata_json(
2579    ptr: *mut DatabasePtr,
2580    namespace: *const c_char,
2581    collection: *const c_char,
2582    ids_hi_ptr: *const u64,
2583    ids_lo_ptr: *const u64,
2584    ids_len: usize,
2585) -> *mut c_char {
2586    if ptr.is_null() || namespace.is_null() || collection.is_null()
2587        || ids_hi_ptr.is_null() || ids_lo_ptr.is_null() {
2588        return ptr::null_mut();
2589    }
2590
2591    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2592        Ok(s) => s,
2593        Err(_) => return ptr::null_mut(),
2594    };
2595    let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2596        Ok(s) => s,
2597        Err(_) => return ptr::null_mut(),
2598    };
2599    let ids_hi = unsafe { slice::from_raw_parts(ids_hi_ptr, ids_len) };
2600    let ids_lo = unsafe { slice::from_raw_parts(ids_lo_ptr, ids_len) };
2601    let db = unsafe { &(*ptr).0 };
2602
2603    let mut results = Vec::with_capacity(ids_len);
2604    for i in 0..ids_len {
2605        let id_hash = ((ids_hi[i] as u128) << 64) | (ids_lo[i] as u128);
2606        let meta_key = metadata_key(ns, col, id_hash);
2607        let txn = match db.begin_transaction() {
2608            Ok(t) => t,
2609            Err(_) => return ptr::null_mut(),
2610        };
2611        let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2612        let _ = db.commit(txn);
2613        if let Some(bytes) = meta_value {
2614            if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&bytes) {
2615                results.push(parsed);
2616                continue;
2617            }
2618        }
2619        results.push(serde_json::json!({"id": "", "metadata": {}}));
2620    }
2621
2622    match serde_json::to_string(&results) {
2623        Ok(json) => match std::ffi::CString::new(json) {
2624            Ok(cstr) => cstr.into_raw(),
2625            Err(_) => ptr::null_mut(),
2626        },
2627        Err(_) => ptr::null_mut(),
2628    }
2629}
2630
2631/// Free arrays returned by sochdb_collection_search_soa
2632#[unsafe(no_mangle)]
2633pub unsafe extern "C" fn sochdb_collection_free_u64(ptr: *mut u64, len: usize) {
2634    if ptr.is_null() || len == 0 {
2635        return;
2636    }
2637    unsafe {
2638        let _ = Vec::from_raw_parts(ptr, len, len);
2639    }
2640}
2641
2642#[unsafe(no_mangle)]
2643pub unsafe extern "C" fn sochdb_collection_free_f32(ptr: *mut f32, len: usize) {
2644    if ptr.is_null() || len == 0 {
2645        return;
2646    }
2647    unsafe {
2648        let _ = Vec::from_raw_parts(ptr, len, len);
2649    }
2650}
2651
2652fn metadata_matches_filter(metadata: &Value, filter: &Value) -> bool {
2653    let filter_obj = match filter.as_object() {
2654        Some(obj) => obj,
2655        None => return true,
2656    };
2657    let metadata_obj = match metadata.as_object() {
2658        Some(obj) => obj,
2659        None => return false,
2660    };
2661
2662    for (key, expected) in filter_obj.iter() {
2663        match metadata_obj.get(key) {
2664            Some(actual) if actual == expected => {}
2665            _ => return false,
2666        }
2667    }
2668
2669    true
2670}
2671
2672/// Search a collection for keywords (simple term match)
2673/// 
2674/// # Returns
2675/// - >= 0: Number of results
2676/// - -1: Error
2677#[unsafe(no_mangle)]
2678pub unsafe extern "C" fn sochdb_collection_keyword_search(
2679    ptr: *mut DatabasePtr,
2680    namespace: *const c_char,
2681    collection: *const c_char,
2682    query_ptr: *const c_char,
2683    k: usize,
2684    results_out: *mut CSearchResult,
2685) -> c_int {
2686    if ptr.is_null() || namespace.is_null() || collection.is_null() 
2687        || query_ptr.is_null() || results_out.is_null() {
2688        return -1;
2689    }
2690    
2691    let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2692        Ok(s) => s,
2693        Err(_) => return -1,
2694    };
2695    let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2696        Ok(s) => s,
2697        Err(_) => return -1,
2698    };
2699    let query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
2700        Ok(s) => s.to_lowercase(),
2701        Err(_) => return -1,
2702    };
2703    let terms: Vec<&str> = query_str.split_whitespace().collect();
2704    if terms.is_empty() {
2705        return 0;
2706    }
2707    
2708    let db = unsafe { &(*ptr).0 };
2709    let txn = match db.begin_transaction() {
2710        Ok(t) => t,
2711        Err(_) => return -1,
2712    };
2713    
2714    // Scan all vectors in collection (we assume vectors & metadata are stored together)
2715    let prefix = format!("{}/collections/{}/vectors/", ns, col);
2716    let entries = match db.scan(txn, prefix.as_bytes()) {
2717        Ok(e) => e,
2718        Err(_) => {
2719            let _ = db.abort(txn);
2720            return -1;
2721        }
2722    };
2723    let _ = db.commit(txn);
2724    
2725    // Score documents based on term frequency
2726    let mut scored: Vec<(f32, String, String)> = Vec::new();
2727    
2728    for (_key, value) in entries {
2729        // Parse whole JSON (robust)
2730        let doc: Value = match serde_json::from_slice(&value) {
2731            Ok(v) => v,
2732            Err(_) => continue,
2733        };
2734        
2735        // Search in metadata string (includes values)
2736        let metadata_val = doc.get("metadata");
2737        let metadata_str = metadata_val.map(|v| v.to_string()).unwrap_or("{}".to_string());
2738        
2739        // Also check "content" field if present (fallback compat)
2740        let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
2741        
2742        // Combine text to search
2743        let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
2744         
2745        let mut score = 0.0;
2746        for term in &terms {
2747            score += search_text.matches(term).count() as f32;
2748        }
2749        
2750        if score > 0.0 {
2751            let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
2752            if id.is_empty() { continue; }
2753            
2754            scored.push((score, id, metadata_str));
2755        }
2756    }
2757    
2758    // Sort by score descending
2759    scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
2760    
2761    // Return top k
2762    let result_count = scored.len().min(k);
2763    for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
2764        let c_id = match std::ffi::CString::new(id) {
2765            Ok(s) => s.into_raw(),
2766            Err(_) => ptr::null_mut(),
2767        };
2768        let c_meta = match std::ffi::CString::new(metadata) {
2769            Ok(s) => s.into_raw(),
2770            Err(_) => ptr::null_mut(),
2771        };
2772        
2773        unsafe {
2774            (*results_out.add(i)).id_ptr = c_id;
2775            (*results_out.add(i)).score = score;
2776            (*results_out.add(i)).metadata_ptr = c_meta;
2777        }
2778    }
2779    
2780    result_count as c_int
2781}
2782
2783/// Free a search result
2784#[unsafe(no_mangle)]
2785pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
2786    if result.is_null() {
2787        return;
2788    }
2789    
2790    for i in 0..count {
2791        let r = unsafe { &mut *result.add(i) };
2792        if !r.id_ptr.is_null() {
2793            let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
2794        }
2795        if !r.metadata_ptr.is_null() {
2796            let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
2797        }
2798    }
2799}
2800