sochdb_storage/
ffi.rs

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