Skip to main content

sochdb_storage/
ffi.rs

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