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 parking_lot::Mutex;
20use serde_json::Value;
21use sochdb_index::hnsw::{DistanceMetric, HnswConfig, HnswIndex};
22use std::collections::HashMap;
23use std::ffi::CStr;
24use std::os::raw::{c_char, c_int};
25use std::ptr;
26use std::slice;
27use std::sync::Arc;
28use std::sync::OnceLock;
29
30// ===========================================================================
31// FFI Panic Firewall (Task 3 — Total-Function Errors + FFI Panic Firewall)
32//
33// Unwinding a Rust panic across an `extern "C"` boundary is undefined behavior.
34// SochDB is an *embedded* engine linked into long-lived host processes (e.g.
35// the Python interpreter via the SDK), so a panic that escapes the ABI either
36// invokes UB (under `panic = "unwind"`) or aborts the entire host process
37// (under `panic = "abort"`). Neither is acceptable for a library: bad input
38// must surface as a typed/sentinel error, not kill the host.
39//
40// Every public entry point therefore wraps its body in [`ffi_guard!`], which
41// uses `catch_unwind` to stop any panic at the boundary and return a stable
42// [`FfiDefault`] sentinel for the function's return type. With this firewall in
43// place at *every* boundary, building with `panic = "unwind"` is sound — a
44// caught panic becomes an error code rather than a process abort, and no panic
45// can ever propagate into foreign frames.
46// ===========================================================================
47
48/// A stable, safe error sentinel returned from an FFI entry point when its body
49/// panics. Implemented for every C return type used at the boundary.
50pub trait FfiDefault {
51    /// The value to return to the C caller when the Rust body panicked.
52    fn ffi_default() -> Self;
53}
54
55impl FfiDefault for () {
56    #[inline]
57    fn ffi_default() -> Self {}
58}
59
60impl<T> FfiDefault for *mut T {
61    #[inline]
62    fn ffi_default() -> Self {
63        std::ptr::null_mut()
64    }
65}
66
67impl<T> FfiDefault for *const T {
68    #[inline]
69    fn ffi_default() -> Self {
70        std::ptr::null()
71    }
72}
73
74impl FfiDefault for c_int {
75    #[inline]
76    fn ffi_default() -> Self {
77        -1
78    }
79}
80
81impl FfiDefault for i64 {
82    #[inline]
83    fn ffi_default() -> Self {
84        -1
85    }
86}
87
88impl FfiDefault for u8 {
89    #[inline]
90    fn ffi_default() -> Self {
91        0
92    }
93}
94
95impl FfiDefault for u64 {
96    #[inline]
97    fn ffi_default() -> Self {
98        0
99    }
100}
101
102impl FfiDefault for usize {
103    #[inline]
104    fn ffi_default() -> Self {
105        0
106    }
107}
108
109impl FfiDefault for bool {
110    #[inline]
111    fn ffi_default() -> Self {
112        false
113    }
114}
115
116impl FfiDefault for f32 {
117    #[inline]
118    fn ffi_default() -> Self {
119        0.0
120    }
121}
122
123impl FfiDefault for f64 {
124    #[inline]
125    fn ffi_default() -> Self {
126        0.0
127    }
128}
129
130impl FfiDefault for C_TxnHandle {
131    #[inline]
132    fn ffi_default() -> Self {
133        C_TxnHandle {
134            txn_id: 0,
135            snapshot_ts: 0,
136        }
137    }
138}
139
140impl FfiDefault for C_CommitResult {
141    #[inline]
142    fn ffi_default() -> Self {
143        C_CommitResult {
144            commit_ts: 0,
145            error_code: -1,
146        }
147    }
148}
149
150impl FfiDefault for CStorageStats {
151    #[inline]
152    fn ffi_default() -> Self {
153        CStorageStats {
154            memtable_size_bytes: 0,
155            wal_size_bytes: 0,
156            active_transactions: 0,
157            min_active_snapshot: 0,
158            last_checkpoint_lsn: 0,
159        }
160    }
161}
162
163/// Wrap an FFI entry-point body so that no panic can cross the C ABI.
164///
165/// On a caught panic, returns the [`FfiDefault`] sentinel for the function's
166/// return type instead of unwinding into foreign frames. `AssertUnwindSafe` is
167/// required because raw pointers captured from the C caller are not
168/// `UnwindSafe`; this is sound because the firewall converts the panic into a
169/// value return and performs no further work on the poisoned state.
170macro_rules! ffi_guard {
171    ($body:block) => {{
172        match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(move || $body)) {
173            ::std::result::Result::Ok(__ffi_ok) => __ffi_ok,
174            ::std::result::Result::Err(_) => FfiDefault::ffi_default(),
175        }
176    }};
177}
178
179/// Opaque pointer to Database
180pub struct DatabasePtr(Arc<Database>);
181
182// =========================================================================
183// Collection Index Registry (in-memory)
184// =========================================================================
185
186struct CollectionIndex {
187    index: Arc<HnswIndex>,
188    dimension: usize,
189    metric: DistanceMetric,
190}
191
192static COLLECTION_INDEXES: OnceLock<Mutex<HashMap<String, Arc<CollectionIndex>>>> = OnceLock::new();
193
194fn collection_key(namespace: &str, collection: &str) -> String {
195    format!("{}/{}", namespace, collection)
196}
197
198fn vector_bin_key(namespace: &str, collection: &str, id_hash: u128) -> String {
199    format!(
200        "{}/collections/{}/vectors_bin/{:032x}",
201        namespace, collection, id_hash
202    )
203}
204
205fn metadata_key(namespace: &str, collection: &str, id_hash: u128) -> String {
206    format!(
207        "{}/collections/{}/meta/{:032x}",
208        namespace, collection, id_hash
209    )
210}
211
212fn hash_id_to_u128(id: &str) -> u128 {
213    let hash = blake3::hash(id.as_bytes());
214    let bytes = hash.as_bytes();
215    u128::from_le_bytes([
216        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
217        bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
218    ])
219}
220
221fn ensure_collection_index(
222    _db: &Database,
223    namespace: &str,
224    collection: &str,
225    dimension: usize,
226    metric: DistanceMetric,
227) -> Arc<CollectionIndex> {
228    let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
229    let key = collection_key(namespace, collection);
230
231    let mut registry_guard = registry.lock();
232    if let Some(existing) = registry_guard.get(&key) {
233        return existing.clone();
234    }
235
236    let mut config = HnswConfig::default();
237    config.metric = metric;
238    let index = Arc::new(HnswIndex::new(dimension, config));
239
240    let entry = Arc::new(CollectionIndex {
241        index,
242        dimension,
243        metric,
244    });
245    registry_guard.insert(key, entry.clone());
246
247    entry
248}
249
250fn resolve_collection_config(
251    db: &Database,
252    namespace: &str,
253    collection: &str,
254) -> Option<(usize, DistanceMetric)> {
255    let key = format!("{}/_collections/{}", namespace, collection);
256    let txn = db.begin_transaction().ok()?;
257    let value = db.get(txn, key.as_bytes()).ok().flatten();
258    let _ = db.commit(txn);
259    let value = value?;
260
261    let parsed: serde_json::Value = serde_json::from_slice(&value).ok()?;
262    let dimension = parsed.get("dimension")?.as_u64()? as usize;
263    let metric = match parsed.get("metric").and_then(|v| v.as_u64()).unwrap_or(0) {
264        1 => DistanceMetric::Euclidean,
265        2 => DistanceMetric::DotProduct,
266        _ => DistanceMetric::Cosine,
267    };
268    Some((dimension, metric))
269}
270
271fn serialize_vector_binary(vector: &[f32]) -> Vec<u8> {
272    let mut out = Vec::with_capacity(4 + vector.len() * 4);
273    let len = vector.len() as u32;
274    out.extend_from_slice(&len.to_le_bytes());
275    for value in vector {
276        out.extend_from_slice(&value.to_le_bytes());
277    }
278    out
279}
280
281fn decode_score(metric: DistanceMetric, distance: f32) -> f32 {
282    match metric {
283        DistanceMetric::Cosine => 1.0 - distance,
284        DistanceMetric::DotProduct => -distance,
285        DistanceMetric::Euclidean => -distance,
286    }
287}
288
289/// C-compatible Transaction Handle
290#[repr(C)]
291pub struct C_TxnHandle {
292    pub txn_id: u64,
293    pub snapshot_ts: u64,
294}
295
296/// C-compatible Commit Result
297/// Returns commit_ts on success, or 0 with error_code on failure
298#[repr(C)]
299pub struct C_CommitResult {
300    /// Commit timestamp (HLC-backed, monotonically increasing)
301    /// This is 0 if the commit failed.
302    pub commit_ts: u64,
303    /// Error code: 0 = success, -1 = error, -2 = SSI conflict
304    pub error_code: i32,
305}
306
307/// C-compatible Database Configuration
308///
309/// All fields have sensible defaults when set to 0/false.
310/// This allows clients to only set the fields they care about.
311#[repr(C)]
312pub struct C_DatabaseConfig {
313    /// Enable WAL for durability (default: true if wal_enabled_set is false)
314    pub wal_enabled: bool,
315    /// Whether wal_enabled was explicitly set
316    pub wal_enabled_set: bool,
317    /// Sync mode: 0=OFF, 1=NORMAL (default), 2=FULL
318    pub sync_mode: u8,
319    /// Whether sync_mode was explicitly set
320    pub sync_mode_set: bool,
321    /// Memtable size in bytes (0 = default 64MB)
322    pub memtable_size_bytes: u64,
323    /// Enable group commit for throughput (default: true if group_commit_set is false)
324    pub group_commit: bool,
325    /// Whether group_commit was explicitly set  
326    pub group_commit_set: bool,
327    /// Default index policy: 0=WriteOptimized, 1=Balanced (default), 2=ScanOptimized, 3=AppendOnly
328    pub default_index_policy: u8,
329    /// Whether default_index_policy was explicitly set
330    pub default_index_policy_set: bool,
331}
332
333/// Open the database with configuration.
334/// Returns a pointer to the database instance, or null on error.
335/// # Safety
336/// The path must be a valid C string.
337#[unsafe(no_mangle)]
338pub unsafe extern "C" fn sochdb_open_with_config(
339    path: *const c_char,
340    config: C_DatabaseConfig,
341) -> *mut DatabasePtr {
342    ffi_guard!({
343        if path.is_null() {
344            return ptr::null_mut();
345        }
346
347        let c_str = unsafe { CStr::from_ptr(path) };
348        let path_str = match c_str.to_str() {
349            Ok(s) => s,
350            Err(_) => return ptr::null_mut(),
351        };
352
353        // Build config from C struct, using defaults for unset fields
354        let mut db_config = crate::database::DatabaseConfig::default();
355
356        if config.wal_enabled_set {
357            db_config.wal_enabled = config.wal_enabled;
358        }
359
360        if config.sync_mode_set {
361            db_config.sync_mode = match config.sync_mode {
362                0 => crate::database::SyncMode::Off,
363                1 => crate::database::SyncMode::Normal,
364                _ => crate::database::SyncMode::Full,
365            };
366        }
367
368        if config.memtable_size_bytes > 0 {
369            db_config.memtable_size_limit = config.memtable_size_bytes as usize;
370        }
371
372        if config.group_commit_set {
373            db_config.group_commit = config.group_commit;
374        }
375
376        if config.default_index_policy_set {
377            db_config.default_index_policy = match config.default_index_policy {
378                0 => crate::index_policy::IndexPolicy::WriteOptimized,
379                1 => crate::index_policy::IndexPolicy::Balanced,
380                2 => crate::index_policy::IndexPolicy::ScanOptimized,
381                _ => crate::index_policy::IndexPolicy::AppendOnly,
382            };
383        }
384
385        match Database::open_with_config(path_str, db_config) {
386            Ok(db) => {
387                let ptr = Box::new(DatabasePtr(db));
388                Box::into_raw(ptr)
389            }
390            Err(_) => ptr::null_mut(),
391        }
392    })
393}
394
395/// Open the database.
396/// Returns a pointer to the database instance, or null on error.
397/// # Safety
398/// The path must be a valid C string.
399#[unsafe(no_mangle)]
400pub unsafe extern "C" fn sochdb_open(path: *const c_char) -> *mut DatabasePtr {
401    ffi_guard!({
402        if path.is_null() {
403            return ptr::null_mut();
404        }
405
406        let c_str = unsafe { CStr::from_ptr(path) };
407        let path_str = match c_str.to_str() {
408            Ok(s) => s,
409            Err(_) => return ptr::null_mut(),
410        };
411
412        // Use default config for now
413        let config = crate::database::DatabaseConfig::default();
414
415        // Database::open returns Result<Arc<Database>>
416        match Database::open_with_config(path_str, config) {
417            Ok(db) => {
418                let ptr = Box::new(DatabasePtr(db));
419                Box::into_raw(ptr)
420            }
421            Err(_) => ptr::null_mut(),
422        }
423    })
424}
425
426/// Open the database in concurrent mode (multi-reader, single-writer).
427///
428/// This mode allows multiple processes to access the database simultaneously:
429/// - Readers: Lock-free, concurrent access via MVCC snapshots
430/// - Writers: Single-writer coordination through atomic locks
431///
432/// Use this for web applications, hot reloading, or any multi-process scenario.
433///
434/// # Safety
435/// The path must be a valid C string.
436#[unsafe(no_mangle)]
437pub unsafe extern "C" fn sochdb_open_concurrent(path: *const c_char) -> *mut DatabasePtr {
438    ffi_guard!({
439        if path.is_null() {
440            return ptr::null_mut();
441        }
442
443        let c_str = unsafe { CStr::from_ptr(path) };
444        let path_str = match c_str.to_str() {
445            Ok(s) => s,
446            Err(_) => return ptr::null_mut(),
447        };
448
449        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
450            Database::open_concurrent(path_str)
451        })) {
452            Ok(Ok(db)) => {
453                let ptr = Box::new(DatabasePtr(db));
454                Box::into_raw(ptr)
455            }
456            Ok(Err(_)) | Err(_) => ptr::null_mut(),
457        }
458    })
459}
460
461/// Check if database is in concurrent mode.
462/// Returns 1 if concurrent mode, 0 otherwise.
463/// # Safety
464/// The ptr must be a valid pointer returned by sochdb_open or sochdb_open_concurrent.
465#[unsafe(no_mangle)]
466pub unsafe extern "C" fn sochdb_is_concurrent(ptr: *mut DatabasePtr) -> c_int {
467    ffi_guard!({
468        if ptr.is_null() {
469            return 0;
470        }
471        let db = unsafe { &*ptr };
472        if db.0.is_concurrent() { 1 } else { 0 }
473    })
474}
475
476/// Close the database and free the pointer.
477/// # Safety
478/// The ptr must be a valid pointer returned by sochdb_open.
479#[unsafe(no_mangle)]
480pub unsafe extern "C" fn sochdb_close(ptr: *mut DatabasePtr) {
481    ffi_guard!({
482        if !ptr.is_null() {
483            unsafe {
484                let _ = Box::from_raw(ptr);
485            }
486        }
487    })
488}
489
490/// Begin a transaction.
491/// Returns C_TxnHandle. On error, txn_id will be 0.
492/// # Safety
493/// The ptr must be a valid pointer returned by sochdb_open.
494#[unsafe(no_mangle)]
495pub unsafe extern "C" fn sochdb_begin_txn(ptr: *mut DatabasePtr) -> C_TxnHandle {
496    ffi_guard!({
497        if ptr.is_null() {
498            return C_TxnHandle {
499                txn_id: 0,
500                snapshot_ts: 0,
501            };
502        }
503        let db = unsafe { &(*ptr).0 };
504        match db.begin_transaction() {
505            Ok(txn) => C_TxnHandle {
506                txn_id: txn.txn_id,
507                snapshot_ts: txn.snapshot_ts,
508            },
509            Err(_) => C_TxnHandle {
510                txn_id: 0,
511                snapshot_ts: 0,
512            },
513        }
514    })
515}
516
517/// Commit a transaction.
518/// Returns C_CommitResult with commit_ts on success.
519/// The commit_ts is HLC-backed and monotonically increasing, suitable for
520/// MVCC observability, replication, and audit trails.
521/// # Safety
522/// The ptr must be a valid pointer returned by sochdb_open.
523#[unsafe(no_mangle)]
524pub unsafe extern "C" fn sochdb_commit(
525    ptr: *mut DatabasePtr,
526    handle: C_TxnHandle,
527) -> C_CommitResult {
528    ffi_guard!({
529        if ptr.is_null() {
530            return C_CommitResult {
531                commit_ts: 0,
532                error_code: -1,
533            };
534        }
535        let db = unsafe { &(*ptr).0 };
536        let txn = TxnHandle {
537            txn_id: handle.txn_id,
538            snapshot_ts: handle.snapshot_ts,
539        };
540        match db.commit(txn) {
541            Ok(commit_ts) => C_CommitResult {
542                commit_ts,
543                error_code: 0,
544            },
545            Err(_) => C_CommitResult {
546                commit_ts: 0,
547                error_code: -1,
548            },
549        }
550    })
551}
552
553/// Abort a transaction.
554/// Returns 0 on success, -1 on error.
555/// # Safety
556/// The ptr must be a valid pointer returned by sochdb_open.
557#[unsafe(no_mangle)]
558pub unsafe extern "C" fn sochdb_abort(ptr: *mut DatabasePtr, handle: C_TxnHandle) -> c_int {
559    ffi_guard!({
560        if ptr.is_null() {
561            return -1;
562        }
563        let db = unsafe { &(*ptr).0 };
564        let txn = TxnHandle {
565            txn_id: handle.txn_id,
566            snapshot_ts: handle.snapshot_ts,
567        };
568        match db.abort(txn) {
569            Ok(_) => 0,
570            Err(_) => -1,
571        }
572    })
573}
574
575/// Put a key-value pair.
576/// Returns 0 on success, -1 on error.
577/// # Safety
578/// The ptr must be a valid pointer returned by sochdb_open.
579/// key_ptr and val_ptr must be valid pointers with the specified lengths.
580#[unsafe(no_mangle)]
581pub unsafe extern "C" fn sochdb_put(
582    ptr: *mut DatabasePtr,
583    handle: C_TxnHandle,
584    key_ptr: *const u8,
585    key_len: usize,
586    val_ptr: *const u8,
587    val_len: usize,
588) -> c_int {
589    ffi_guard!({
590        if ptr.is_null() || key_ptr.is_null() || val_ptr.is_null() {
591            return -1;
592        }
593        let db = unsafe { &(*ptr).0 };
594        let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
595        let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
596        let txn = TxnHandle {
597            txn_id: handle.txn_id,
598            snapshot_ts: handle.snapshot_ts,
599        };
600
601        match db.put(txn, key, val) {
602            Ok(_) => 0,
603            Err(_) => -1,
604        }
605    })
606}
607
608/// Get a value.
609/// Writes pointer to val_out and length to len_out.
610/// The caller must free the returned bytes using sochdb_free_bytes.
611/// Returns 0 on success (found), 1 on not found, -1 on error.
612/// # Safety
613/// All pointer arguments must be valid.
614#[unsafe(no_mangle)]
615pub unsafe extern "C" fn sochdb_get(
616    ptr: *mut DatabasePtr,
617    handle: C_TxnHandle,
618    key_ptr: *const u8,
619    key_len: usize,
620    val_out: *mut *mut u8,
621    len_out: *mut usize,
622) -> c_int {
623    ffi_guard!({
624        if ptr.is_null() || key_ptr.is_null() || val_out.is_null() || len_out.is_null() {
625            return -1;
626        }
627        let db = unsafe { &(*ptr).0 };
628        let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
629        let txn = TxnHandle {
630            txn_id: handle.txn_id,
631            snapshot_ts: handle.snapshot_ts,
632        };
633
634        match db.get(txn, key) {
635            Ok(Some(val)) => {
636                // Copy value to heap to pass to C
637                let mut buf = val.into_boxed_slice();
638                unsafe {
639                    *val_out = buf.as_mut_ptr();
640                    *len_out = buf.len();
641                }
642                let _ = Box::into_raw(buf); // Leak memory, caller must free
643                0
644            }
645            Ok(None) => 1, // Not found
646            Err(_) => -1,
647        }
648    })
649}
650
651/// Free bytes allocated by sochdb_get.
652/// # Safety
653/// ptr must be a valid pointer returned by sochdb_get.
654#[unsafe(no_mangle)]
655pub unsafe extern "C" fn sochdb_free_bytes(ptr: *mut u8, len: usize) {
656    ffi_guard!({
657        if !ptr.is_null() {
658            unsafe {
659                let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len));
660            }
661        }
662    })
663}
664
665/// Delete a key.
666/// Returns 0 on success, -1 on error.
667/// # Safety
668/// All pointer arguments must be valid.
669#[unsafe(no_mangle)]
670pub unsafe extern "C" fn sochdb_delete(
671    ptr: *mut DatabasePtr,
672    handle: C_TxnHandle,
673    key_ptr: *const u8,
674    key_len: usize,
675) -> c_int {
676    ffi_guard!({
677        if ptr.is_null() || key_ptr.is_null() {
678            return -1;
679        }
680        let db = unsafe { &(*ptr).0 };
681        let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
682        let txn = TxnHandle {
683            txn_id: handle.txn_id,
684            snapshot_ts: handle.snapshot_ts,
685        };
686
687        match db.delete(txn, key) {
688            Ok(_) => 0,
689            Err(_) => -1,
690        }
691    })
692}
693
694/// Put path.
695/// # Safety
696/// All pointer arguments must be valid.
697#[unsafe(no_mangle)]
698pub unsafe extern "C" fn sochdb_put_path(
699    ptr: *mut DatabasePtr,
700    handle: C_TxnHandle,
701    path_ptr: *const c_char,
702    val_ptr: *const u8,
703    val_len: usize,
704) -> c_int {
705    ffi_guard!({
706        if ptr.is_null() || path_ptr.is_null() || val_ptr.is_null() {
707            return -1;
708        }
709        let db = unsafe { &(*ptr).0 };
710        let c_str = unsafe { CStr::from_ptr(path_ptr) };
711        let path_str = match c_str.to_str() {
712            Ok(s) => s,
713            Err(_) => return -1,
714        };
715        let val = unsafe { slice::from_raw_parts(val_ptr, val_len) };
716        let txn = TxnHandle {
717            txn_id: handle.txn_id,
718            snapshot_ts: handle.snapshot_ts,
719        };
720
721        match db.put_path(txn, path_str, val) {
722            Ok(_) => 0,
723            Err(_) => -1,
724        }
725    })
726}
727
728/// Get path.
729/// # Safety
730/// All pointer arguments must be valid.
731#[unsafe(no_mangle)]
732pub unsafe extern "C" fn sochdb_get_path(
733    ptr: *mut DatabasePtr,
734    handle: C_TxnHandle,
735    path_ptr: *const c_char,
736    val_out: *mut *mut u8,
737    len_out: *mut usize,
738) -> c_int {
739    ffi_guard!({
740        if ptr.is_null() || path_ptr.is_null() || val_out.is_null() || len_out.is_null() {
741            return -1;
742        }
743        let db = unsafe { &(*ptr).0 };
744        let c_str = unsafe { CStr::from_ptr(path_ptr) };
745        let path_str = match c_str.to_str() {
746            Ok(s) => s,
747            Err(_) => return -1,
748        };
749        let txn = TxnHandle {
750            txn_id: handle.txn_id,
751            snapshot_ts: handle.snapshot_ts,
752        };
753
754        match db.get_path(txn, path_str) {
755            Ok(Some(val)) => {
756                let mut buf = val.into_boxed_slice();
757                unsafe {
758                    *val_out = buf.as_mut_ptr();
759                    *len_out = buf.len();
760                }
761                let _ = Box::into_raw(buf);
762                0
763            }
764            Ok(None) => 1,
765            Err(_) => -1,
766        }
767    })
768}
769
770/// Opaque pointer to Scan Iterator
771#[allow(clippy::type_complexity)]
772pub struct ScanIteratorPtr(
773    Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), sochdb_core::SochDBError>>>,
774);
775
776/// Start a scan.
777/// # Safety
778/// All pointer arguments must be valid.
779#[unsafe(no_mangle)]
780pub unsafe extern "C" fn sochdb_scan(
781    ptr: *mut DatabasePtr,
782    handle: C_TxnHandle,
783    start_ptr: *const u8,
784    start_len: usize,
785    end_ptr: *const u8,
786    end_len: usize,
787) -> *mut ScanIteratorPtr {
788    ffi_guard!({
789        if ptr.is_null() {
790            return ptr::null_mut();
791        }
792        let db = unsafe { &(*ptr).0 };
793        let txn = TxnHandle {
794            txn_id: handle.txn_id,
795            snapshot_ts: handle.snapshot_ts,
796        };
797
798        let start = if !start_ptr.is_null() && start_len > 0 {
799            unsafe { slice::from_raw_parts(start_ptr, start_len).to_vec() }
800        } else {
801            vec![]
802        };
803
804        let end = if !end_ptr.is_null() && end_len > 0 {
805            unsafe { slice::from_raw_parts(end_ptr, end_len).to_vec() }
806        } else {
807            vec![] // Empty end means unbounded in `scan` usually, or we need to handle it
808        };
809
810        // Note: The underlying `scan` method expects `Range<Vec<u8>>`.
811        // We need to handle empty start/end correctly.
812        // For now, let's assume the caller provides valid bounds or we use defaults.
813        // Ideally, we'd pass optionals.
814
815        // Using a simplified approach: if start is empty, use empty vec (start of db).
816        // If end is empty, use a "max" key or handle in `scan` impl.
817        // The `StorageEngine::scan` takes `Range<Vec<u8>>`.
818
819        // Using a simplified approach: if start is empty, use empty vec (start of db).
820        // If end is empty, use empty vec (unbounded).
821
822        match db.scan_range(txn, &start, &end) {
823            Ok(rows) => {
824                // Convert rows to an iterator of (key, value)
825                // scan_range returns Vec<(Vec<u8>, Vec<u8>)>
826                let iter = Box::new(rows.into_iter().map(Ok));
827
828                let ptr = Box::new(ScanIteratorPtr(iter));
829                Box::into_raw(ptr)
830            }
831            Err(_) => ptr::null_mut(),
832        }
833    })
834}
835
836/// Start a prefix scan - returns only keys that start with the given prefix.
837/// This is the safe method for multi-tenant isolation.
838/// # Safety
839/// All pointer arguments must be valid.
840#[unsafe(no_mangle)]
841pub unsafe extern "C" fn sochdb_scan_prefix(
842    ptr: *mut DatabasePtr,
843    handle: C_TxnHandle,
844    prefix_ptr: *const u8,
845    prefix_len: usize,
846) -> *mut ScanIteratorPtr {
847    ffi_guard!({
848        if ptr.is_null() {
849            return ptr::null_mut();
850        }
851        let db = unsafe { &(*ptr).0 };
852        let txn = TxnHandle {
853            txn_id: handle.txn_id,
854            snapshot_ts: handle.snapshot_ts,
855        };
856
857        let prefix = if !prefix_ptr.is_null() && prefix_len > 0 {
858            unsafe { slice::from_raw_parts(prefix_ptr, prefix_len).to_vec() }
859        } else {
860            vec![]
861        };
862
863        // Use the proper scan method that filters by prefix
864        match db.scan(txn, &prefix) {
865            Ok(rows) => {
866                // The underlying scan already filters by prefix, but double-check
867                // to ensure no data leakage
868                let prefix_owned = prefix.clone();
869                let filtered: Vec<(Vec<u8>, Vec<u8>)> = rows
870                    .into_iter()
871                    .filter(|(k, _)| k.starts_with(&prefix_owned))
872                    .collect();
873
874                let iter = Box::new(filtered.into_iter().map(Ok));
875                let ptr = Box::new(ScanIteratorPtr(iter));
876                Box::into_raw(ptr)
877            }
878            Err(_) => ptr::null_mut(),
879        }
880    })
881}
882
883/// Get next item from scan iterator.
884/// Returns 0 on success, 1 on done, -1 on error.
885/// # Safety
886/// All pointer arguments must be valid.
887#[unsafe(no_mangle)]
888pub unsafe extern "C" fn sochdb_scan_next(
889    iter_ptr: *mut ScanIteratorPtr,
890    key_out: *mut *mut u8,
891    key_len_out: *mut usize,
892    val_out: *mut *mut u8,
893    val_len_out: *mut usize,
894) -> c_int {
895    ffi_guard!({
896        if iter_ptr.is_null() || key_out.is_null() || val_out.is_null() {
897            return -1;
898        }
899        let iter = unsafe { &mut (*iter_ptr).0 };
900
901        match iter.next() {
902            Some(Ok((key, val))) => {
903                let mut key_buf = key.into_boxed_slice();
904                let mut val_buf = val.into_boxed_slice();
905                unsafe {
906                    *key_out = key_buf.as_mut_ptr();
907                    *key_len_out = key_buf.len();
908                    *val_out = val_buf.as_mut_ptr();
909                    *val_len_out = val_buf.len();
910                }
911                let _ = Box::into_raw(key_buf);
912                let _ = Box::into_raw(val_buf);
913                0
914            }
915            Some(Err(_)) => -1,
916            None => 1, // Done
917        }
918    })
919}
920
921/// Free scan iterator.
922/// # Safety
923/// ptr must be a valid pointer returned by sochdb_scan.
924#[unsafe(no_mangle)]
925pub unsafe extern "C" fn sochdb_scan_free(ptr: *mut ScanIteratorPtr) {
926    ffi_guard!({
927        if !ptr.is_null() {
928            unsafe {
929                let _ = Box::from_raw(ptr);
930            }
931        }
932    })
933}
934
935/// Checkpoint the database.
936/// # Safety
937/// ptr must be a valid pointer returned by sochdb_open.
938#[unsafe(no_mangle)]
939pub unsafe extern "C" fn sochdb_checkpoint(ptr: *mut DatabasePtr) -> c_int {
940    ffi_guard!({
941        if ptr.is_null() {
942            return -1;
943        }
944        let db = unsafe { &(*ptr).0 };
945        match db.flush() {
946            Ok(_) => 0,
947            Err(_) => -1,
948        }
949    })
950}
951
952/// Storage statistics
953#[repr(C)]
954pub struct CStorageStats {
955    pub memtable_size_bytes: u64,
956    pub wal_size_bytes: u64,
957    pub active_transactions: usize,
958    pub min_active_snapshot: u64,
959    pub last_checkpoint_lsn: u64,
960}
961
962/// Get storage statistics.
963/// # Safety
964/// ptr must be a valid pointer returned by sochdb_open.
965#[unsafe(no_mangle)]
966pub unsafe extern "C" fn sochdb_stats(ptr: *mut DatabasePtr) -> CStorageStats {
967    ffi_guard!({
968        if ptr.is_null() {
969            return CStorageStats {
970                memtable_size_bytes: 0,
971                wal_size_bytes: 0,
972                active_transactions: 0,
973                min_active_snapshot: 0,
974                last_checkpoint_lsn: 0,
975            };
976        }
977        let db = unsafe { &(*ptr).0 };
978        let stats = db.storage_stats();
979
980        CStorageStats {
981            memtable_size_bytes: stats.memtable_size_bytes,
982            wal_size_bytes: stats.wal_size_bytes,
983            active_transactions: stats.active_transactions,
984            min_active_snapshot: stats.min_active_snapshot,
985            last_checkpoint_lsn: stats.last_checkpoint_lsn,
986        }
987    })
988}
989
990// ============================================================================
991// Batched Operations - Minimize FFI Call Overhead
992// ============================================================================
993
994/// Batch descriptor for put_many operation
995///
996/// Memory layout for batch:
997/// ```text
998/// [num_entries: u32]
999/// For each entry:
1000///   [key_len: u32][value_len: u32][key_bytes: ...][value_bytes: ...]
1001/// ```
1002///
1003/// This packed format minimizes FFI crossing overhead:
1004/// - One call instead of N calls
1005/// - No per-entry pointer chasing
1006/// - Contiguous memory for CPU cache efficiency
1007#[repr(C)]
1008pub struct CBatchPut {
1009    /// Pointer to packed batch data
1010    pub data: *const u8,
1011    /// Total length of packed data
1012    pub len: usize,
1013}
1014
1015/// Put multiple key-value pairs in a single FFI call.
1016///
1017/// This is the high-performance path for Python and other FFI users.
1018/// Instead of N individual sochdb_put calls (each with FFI overhead),
1019/// the caller packs all writes into a single buffer and makes one call.
1020///
1021/// ## Performance
1022///
1023/// For N writes with per-call overhead c:
1024/// - Individual puts: N × c (e.g., 100 × 500ns = 50µs overhead)
1025/// - put_many: 1 × c (e.g., 1 × 500ns = 0.5µs overhead)
1026/// - Speedup: 100× for FFI overhead alone
1027///
1028/// ## Buffer Format
1029///
1030/// ```text
1031/// ┌────────────────────────────────────────────────────────────────┐
1032/// │  num_entries (4 bytes, little-endian u32)                      │
1033/// ├────────────────────────────────────────────────────────────────┤
1034/// │  Entry 1:                                                      │
1035/// │    key_len (4 bytes, u32) | val_len (4 bytes, u32)             │
1036/// │    key_bytes (key_len bytes)                                   │
1037/// │    value_bytes (val_len bytes)                                 │
1038/// ├────────────────────────────────────────────────────────────────┤
1039/// │  Entry 2: ...                                                  │
1040/// ├────────────────────────────────────────────────────────────────┤
1041/// │  Entry N: ...                                                  │
1042/// └────────────────────────────────────────────────────────────────┘
1043/// ```
1044///
1045/// ## Returns
1046///
1047/// - 0: All entries written successfully
1048/// - -1: Error (null pointer, invalid format, write failure)
1049/// - >0: Number of entries successfully written before error
1050///
1051/// # Safety
1052///
1053/// - `ptr` must be a valid DatabasePtr from sochdb_open
1054/// - `batch` must point to a valid CBatchPut with correct format
1055#[unsafe(no_mangle)]
1056pub unsafe extern "C" fn sochdb_put_many(
1057    ptr: *mut DatabasePtr,
1058    handle: C_TxnHandle,
1059    batch: CBatchPut,
1060) -> c_int {
1061    ffi_guard!({
1062        if ptr.is_null() || batch.data.is_null() || batch.len < 4 {
1063            return -1;
1064        }
1065
1066        let db = unsafe { &(*ptr).0 };
1067        let txn = TxnHandle {
1068            txn_id: handle.txn_id,
1069            snapshot_ts: handle.snapshot_ts,
1070        };
1071
1072        // Parse batch
1073        let data = unsafe { slice::from_raw_parts(batch.data, batch.len) };
1074
1075        // Read number of entries
1076        if data.len() < 4 {
1077            return -1;
1078        }
1079        let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1080
1081        let mut offset = 4;
1082        let mut success_count = 0;
1083
1084        for _ in 0..num_entries {
1085            // Read key_len and value_len
1086            if offset + 8 > data.len() {
1087                return success_count;
1088            }
1089            let key_len = u32::from_le_bytes([
1090                data[offset],
1091                data[offset + 1],
1092                data[offset + 2],
1093                data[offset + 3],
1094            ]) as usize;
1095            let val_len = u32::from_le_bytes([
1096                data[offset + 4],
1097                data[offset + 5],
1098                data[offset + 6],
1099                data[offset + 7],
1100            ]) as usize;
1101            offset += 8;
1102
1103            // Read key and value
1104            if offset + key_len + val_len > data.len() {
1105                return success_count;
1106            }
1107            let key = &data[offset..offset + key_len];
1108            offset += key_len;
1109            let value = &data[offset..offset + val_len];
1110            offset += val_len;
1111
1112            // Write to database
1113            match db.put(txn, key, value) {
1114                Ok(_) => success_count += 1,
1115                Err(_) => return success_count,
1116            }
1117        }
1118
1119        success_count
1120    })
1121}
1122
1123/// Delete multiple keys in a single FFI call.
1124///
1125/// ## Buffer Format
1126///
1127/// ```text
1128/// ┌────────────────────────────────────────────────────────────────┐
1129/// │  num_entries (4 bytes, little-endian u32)                      │
1130/// ├────────────────────────────────────────────────────────────────┤
1131/// │  Entry 1:                                                      │
1132/// │    key_len (4 bytes, u32)                                      │
1133/// │    key_bytes (key_len bytes)                                   │
1134/// ├────────────────────────────────────────────────────────────────┤
1135/// │  Entry 2: ...                                                  │
1136/// └────────────────────────────────────────────────────────────────┘
1137/// ```
1138///
1139/// # Safety
1140///
1141/// Same as sochdb_put_many.
1142#[unsafe(no_mangle)]
1143pub unsafe extern "C" fn sochdb_delete_many(
1144    ptr: *mut DatabasePtr,
1145    handle: C_TxnHandle,
1146    keys_data: *const u8,
1147    keys_len: usize,
1148) -> c_int {
1149    ffi_guard!({
1150        if ptr.is_null() || keys_data.is_null() || keys_len < 4 {
1151            return -1;
1152        }
1153
1154        let db = unsafe { &(*ptr).0 };
1155        let txn = TxnHandle {
1156            txn_id: handle.txn_id,
1157            snapshot_ts: handle.snapshot_ts,
1158        };
1159
1160        let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
1161
1162        let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1163
1164        let mut offset = 4;
1165        let mut success_count = 0;
1166
1167        for _ in 0..num_entries {
1168            if offset + 4 > data.len() {
1169                return success_count;
1170            }
1171            let key_len = u32::from_le_bytes([
1172                data[offset],
1173                data[offset + 1],
1174                data[offset + 2],
1175                data[offset + 3],
1176            ]) as usize;
1177            offset += 4;
1178
1179            if offset + key_len > data.len() {
1180                return success_count;
1181            }
1182            let key = &data[offset..offset + key_len];
1183            offset += key_len;
1184
1185            match db.delete(txn, key) {
1186                Ok(_) => success_count += 1,
1187                Err(_) => return success_count,
1188            }
1189        }
1190
1191        success_count
1192    })
1193}
1194
1195/// Get multiple values in a single FFI call.
1196///
1197/// ## Input Format
1198///
1199/// Same as delete_many: packed keys.
1200///
1201/// ## Output Format
1202///
1203/// ```text
1204/// ┌────────────────────────────────────────────────────────────────┐
1205/// │  num_results (4 bytes, u32)                                    │
1206/// ├────────────────────────────────────────────────────────────────┤
1207/// │  Entry 1:                                                      │
1208/// │    status (1 byte): 0=found, 1=not_found, 2=error              │
1209/// │    if found: val_len (4 bytes, u32), value_bytes               │
1210/// ├────────────────────────────────────────────────────────────────┤
1211/// │  Entry 2: ...                                                  │
1212/// └────────────────────────────────────────────────────────────────┘
1213/// ```
1214///
1215/// ## Returns
1216///
1217/// Pointer to allocated result buffer. Caller must free with sochdb_free_bytes.
1218///
1219/// # Safety
1220///
1221/// Same as sochdb_put_many.
1222#[unsafe(no_mangle)]
1223pub unsafe extern "C" fn sochdb_get_many(
1224    ptr: *mut DatabasePtr,
1225    handle: C_TxnHandle,
1226    keys_data: *const u8,
1227    keys_len: usize,
1228    result_out: *mut *mut u8,
1229    result_len_out: *mut usize,
1230) -> c_int {
1231    ffi_guard!({
1232        if ptr.is_null()
1233            || keys_data.is_null()
1234            || keys_len < 4
1235            || result_out.is_null()
1236            || result_len_out.is_null()
1237        {
1238            return -1;
1239        }
1240
1241        let db = unsafe { &(*ptr).0 };
1242        let txn = TxnHandle {
1243            txn_id: handle.txn_id,
1244            snapshot_ts: handle.snapshot_ts,
1245        };
1246
1247        let data = unsafe { slice::from_raw_parts(keys_data, keys_len) };
1248
1249        let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1250
1251        // Build result buffer
1252        let mut result = Vec::with_capacity(4 + num_entries * 10); // Estimate
1253        result.extend_from_slice(&(num_entries as u32).to_le_bytes());
1254
1255        let mut offset = 4;
1256
1257        for _ in 0..num_entries {
1258            if offset + 4 > data.len() {
1259                result.push(2); // Error
1260                continue;
1261            }
1262            let key_len = u32::from_le_bytes([
1263                data[offset],
1264                data[offset + 1],
1265                data[offset + 2],
1266                data[offset + 3],
1267            ]) as usize;
1268            offset += 4;
1269
1270            if offset + key_len > data.len() {
1271                result.push(2); // Error
1272                continue;
1273            }
1274            let key = &data[offset..offset + key_len];
1275            offset += key_len;
1276
1277            match db.get(txn, key) {
1278                Ok(Some(value)) => {
1279                    result.push(0); // Found
1280                    result.extend_from_slice(&(value.len() as u32).to_le_bytes());
1281                    result.extend_from_slice(&value);
1282                }
1283                Ok(None) => {
1284                    result.push(1); // Not found
1285                }
1286                Err(_) => {
1287                    result.push(2); // Error
1288                }
1289            }
1290        }
1291
1292        // Return result
1293        let mut boxed = result.into_boxed_slice();
1294        unsafe {
1295            *result_out = boxed.as_mut_ptr();
1296            *result_len_out = boxed.len();
1297        }
1298        let _ = Box::into_raw(boxed); // Leak for caller to free
1299
1300        0
1301    })
1302}
1303
1304// ============================================================================
1305// Batched Scan - Minimize FFI Call Overhead for Iterations
1306// ============================================================================
1307
1308/// Fetch a batch of results from scan iterator.
1309///
1310/// This dramatically reduces FFI overhead for scan operations.
1311/// Instead of N calls to `sochdb_scan_next` (each with FFI overhead),
1312/// fetch up to `batch_size` results in a single call.
1313///
1314/// ## Performance
1315///
1316/// For N results with per-call overhead c:
1317/// - Individual next calls: N × c (e.g., 10000 × 500ns = 5ms overhead)
1318/// - Batched (size=1000): 10 × c (e.g., 10 × 500ns = 5µs overhead)
1319/// - Speedup: 1000× for FFI overhead
1320///
1321/// ## Output Format
1322///
1323/// ```text
1324/// ┌────────────────────────────────────────────────────────────────┐
1325/// │  num_results (4 bytes, little-endian u32)                      │
1326/// │  is_done (1 byte): 0=more results available, 1=scan complete   │
1327/// ├────────────────────────────────────────────────────────────────┤
1328/// │  Entry 1:                                                      │
1329/// │    key_len (4 bytes, u32)                                      │
1330/// │    val_len (4 bytes, u32)                                      │
1331/// │    key_bytes (key_len bytes)                                   │
1332/// │    value_bytes (val_len bytes)                                 │
1333/// ├────────────────────────────────────────────────────────────────┤
1334/// │  Entry 2: ...                                                  │
1335/// └────────────────────────────────────────────────────────────────┘
1336/// ```
1337///
1338/// ## Returns
1339///
1340/// - 0: Batch fetched successfully (check is_done flag for completion)
1341/// - 1: Scan complete (no more results)
1342/// - -1: Error
1343///
1344/// ## Usage from Python
1345///
1346/// ```python
1347/// iter_ptr = lib.sochdb_scan(...)
1348/// while True:
1349///     result = lib.sochdb_scan_batch(iter_ptr, 1000, ...)
1350///     if result == 1:  # Done
1351///         break
1352///     # Parse batch buffer for up to 1000 results
1353/// lib.sochdb_scan_free(iter_ptr)
1354/// ```
1355///
1356/// # Safety
1357///
1358/// - `iter_ptr` must be a valid ScanIteratorPtr from sochdb_scan
1359/// - Output pointers must be valid
1360#[unsafe(no_mangle)]
1361pub unsafe extern "C" fn sochdb_scan_batch(
1362    iter_ptr: *mut ScanIteratorPtr,
1363    batch_size: usize,
1364    result_out: *mut *mut u8,
1365    result_len_out: *mut usize,
1366) -> c_int {
1367    ffi_guard!({
1368        if iter_ptr.is_null() || result_out.is_null() || result_len_out.is_null() || batch_size == 0
1369        {
1370            return -1;
1371        }
1372
1373        let iter = unsafe { &mut (*iter_ptr).0 };
1374
1375        // Pre-allocate result buffer
1376        // Estimate: header (5 bytes) + batch_size * (8 bytes header + ~100 bytes avg data)
1377        let estimated_size = 5 + batch_size * 108;
1378        let mut result = Vec::with_capacity(estimated_size);
1379
1380        // Reserve space for header (will fill in at end)
1381        result.extend_from_slice(&[0u8; 5]); // 4 bytes count + 1 byte is_done
1382
1383        let mut count = 0u32;
1384        let mut is_done = false;
1385
1386        for _ in 0..batch_size {
1387            match iter.next() {
1388                Some(Ok((key, val))) => {
1389                    // Write key_len, val_len, key, value
1390                    result.extend_from_slice(&(key.len() as u32).to_le_bytes());
1391                    result.extend_from_slice(&(val.len() as u32).to_le_bytes());
1392                    result.extend_from_slice(&key);
1393                    result.extend_from_slice(&val);
1394                    count += 1;
1395                }
1396                Some(Err(_)) => {
1397                    // Write header with current count and return error
1398                    result[0..4].copy_from_slice(&count.to_le_bytes());
1399                    result[4] = 0; // Not done (error case)
1400
1401                    let mut boxed = result.into_boxed_slice();
1402                    unsafe {
1403                        *result_out = boxed.as_mut_ptr();
1404                        *result_len_out = boxed.len();
1405                    }
1406                    let _ = Box::into_raw(boxed);
1407                    return -1;
1408                }
1409                None => {
1410                    is_done = true;
1411                    break;
1412                }
1413            }
1414        }
1415
1416        // Fill in header
1417        result[0..4].copy_from_slice(&count.to_le_bytes());
1418        result[4] = if is_done { 1 } else { 0 };
1419
1420        // If no results and done, signal completion
1421        if count == 0 && is_done {
1422            // Still allocate minimal buffer so caller can free consistently
1423            let mut boxed = result.into_boxed_slice();
1424            unsafe {
1425                *result_out = boxed.as_mut_ptr();
1426                *result_len_out = boxed.len();
1427            }
1428            let _ = Box::into_raw(boxed);
1429            return 1; // Done
1430        }
1431
1432        // Return buffer
1433        let mut boxed = result.into_boxed_slice();
1434        unsafe {
1435            *result_out = boxed.as_mut_ptr();
1436            *result_len_out = boxed.len();
1437        }
1438        let _ = Box::into_raw(boxed);
1439
1440        0 // Success, check is_done flag for completion
1441    })
1442}
1443
1444// ============================================================================
1445// Per-Table Index Policy API
1446// ============================================================================
1447
1448/// Set index policy for a table.
1449///
1450/// # Policy Values
1451/// - 0: WriteOptimized - O(1) writes, O(N) scans. For write-heavy tables.
1452/// - 1: Balanced (default) - O(1) amortized writes, O(output + log K) scans.
1453/// - 2: ScanOptimized - O(log N) writes, O(log N + K) scans. For analytics.
1454/// - 3: AppendOnly - O(1) writes, O(N) forward-only scans. For time-series.
1455///
1456/// # Returns
1457/// - 0: Success
1458/// - -1: Invalid pointer or table name
1459/// - -2: Invalid policy value
1460///
1461/// # Safety
1462/// ptr must be a valid DatabasePtr, table_name must be a valid C string.
1463#[unsafe(no_mangle)]
1464pub unsafe extern "C" fn sochdb_set_table_index_policy(
1465    ptr: *mut DatabasePtr,
1466    table_name: *const c_char,
1467    policy: u8,
1468) -> c_int {
1469    ffi_guard!({
1470        if ptr.is_null() || table_name.is_null() {
1471            return -1;
1472        }
1473
1474        let c_str = unsafe { CStr::from_ptr(table_name) };
1475        let table = match c_str.to_str() {
1476            Ok(s) => s,
1477            Err(_) => return -1,
1478        };
1479
1480        let index_policy = match policy {
1481            0 => crate::index_policy::IndexPolicy::WriteOptimized,
1482            1 => crate::index_policy::IndexPolicy::Balanced,
1483            2 => crate::index_policy::IndexPolicy::ScanOptimized,
1484            3 => crate::index_policy::IndexPolicy::AppendOnly,
1485            _ => return -2,
1486        };
1487
1488        let db = unsafe { &(*ptr).0 };
1489
1490        // Configure the table's index policy through the database registry
1491        let config = crate::index_policy::TableIndexConfig::new(table, index_policy);
1492        db.index_registry().configure_table(config);
1493
1494        0
1495    })
1496}
1497
1498/// Get index policy for a table.
1499///
1500/// # Returns
1501/// - 0: WriteOptimized
1502/// - 1: Balanced  
1503/// - 2: ScanOptimized
1504/// - 3: AppendOnly
1505/// - 255: Error (invalid pointer)
1506///
1507/// # Safety
1508/// ptr must be a valid DatabasePtr, table_name must be a valid C string.
1509#[unsafe(no_mangle)]
1510pub unsafe extern "C" fn sochdb_get_table_index_policy(
1511    ptr: *mut DatabasePtr,
1512    table_name: *const c_char,
1513) -> u8 {
1514    ffi_guard!({
1515        if ptr.is_null() || table_name.is_null() {
1516            return 255;
1517        }
1518
1519        let c_str = unsafe { CStr::from_ptr(table_name) };
1520        let table = match c_str.to_str() {
1521            Ok(s) => s,
1522            Err(_) => return 255,
1523        };
1524
1525        let db = unsafe { &(*ptr).0 };
1526        let config = db.index_registry().get_config(table);
1527
1528        match config.policy {
1529            crate::index_policy::IndexPolicy::WriteOptimized => 0,
1530            crate::index_policy::IndexPolicy::Balanced => 1,
1531            crate::index_policy::IndexPolicy::ScanOptimized => 2,
1532            crate::index_policy::IndexPolicy::AppendOnly => 3,
1533        }
1534    })
1535}
1536
1537/// C-compatible Temporal Edge
1538#[repr(C)]
1539pub struct C_TemporalEdge {
1540    pub from_id: *const c_char,
1541    pub edge_type: *const c_char,
1542    pub to_id: *const c_char,
1543    pub valid_from: u64,
1544    pub valid_until: u64,
1545    pub properties_json: *const c_char, // JSON string of properties
1546}
1547
1548/// Add a temporal edge with validity interval.
1549/// # Safety
1550/// All pointers must be valid C strings. properties_json can be null.
1551#[unsafe(no_mangle)]
1552pub unsafe extern "C" fn sochdb_add_temporal_edge(
1553    ptr: *mut DatabasePtr,
1554    namespace: *const c_char,
1555    edge: C_TemporalEdge,
1556) -> c_int {
1557    ffi_guard!({
1558        if ptr.is_null()
1559            || namespace.is_null()
1560            || edge.from_id.is_null()
1561            || edge.edge_type.is_null()
1562            || edge.to_id.is_null()
1563        {
1564            return -1;
1565        }
1566
1567        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1568            Ok(s) => s,
1569            Err(_) => return -1,
1570        };
1571        let from = match unsafe { CStr::from_ptr(edge.from_id) }.to_str() {
1572            Ok(s) => s,
1573            Err(_) => return -1,
1574        };
1575        let etype = match unsafe { CStr::from_ptr(edge.edge_type) }.to_str() {
1576            Ok(s) => s,
1577            Err(_) => return -1,
1578        };
1579        let to = match unsafe { CStr::from_ptr(edge.to_id) }.to_str() {
1580            Ok(s) => s,
1581            Err(_) => return -1,
1582        };
1583
1584        let db = unsafe { &(*ptr).0 };
1585
1586        // Begin transaction for atomic write
1587        let txn = match db.begin_transaction() {
1588            Ok(t) => t,
1589            Err(_) => return -1,
1590        };
1591
1592        // Store temporal edge: _graph/{ns}/temporal/{from}/{type}/{to}/{valid_from}
1593        let key = format!(
1594            "_graph/{}/temporal/{}/{}/{}/{:016x}",
1595            ns, from, etype, to, edge.valid_from
1596        );
1597
1598        let props_str = if edge.properties_json.is_null() {
1599            "{}".to_string()
1600        } else {
1601            match unsafe { CStr::from_ptr(edge.properties_json) }.to_str() {
1602                Ok(s) => s.to_string(),
1603                Err(_) => return -1,
1604            }
1605        };
1606
1607        let value = format!(
1608            r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","valid_from":{},"valid_until":{},"properties":{}}}"#,
1609            from, etype, to, edge.valid_from, edge.valid_until, props_str
1610        );
1611
1612        if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1613            let _ = db.abort(txn);
1614            return -1;
1615        }
1616
1617        match db.commit(txn) {
1618            Ok(_) => 0,
1619            Err(_) => -1,
1620        }
1621    })
1622}
1623
1624/// Query temporal graph edges. Returns a JSON array of matching edges.
1625/// Caller must free the returned string with sochdb_free_string.
1626///
1627/// query_mode: 0=POINT_IN_TIME, 1=RANGE, 2=CURRENT
1628/// # Safety
1629/// All pointers must be valid C strings. edge_type can be null for no filter.
1630#[unsafe(no_mangle)]
1631pub unsafe extern "C" fn sochdb_query_temporal_graph(
1632    ptr: *mut DatabasePtr,
1633    namespace: *const c_char,
1634    node_id: *const c_char,
1635    query_mode: u8,
1636    timestamp: u64,           // For POINT_IN_TIME
1637    start_time: u64,          // For RANGE
1638    end_time: u64,            // For RANGE
1639    edge_type: *const c_char, // Optional filter (null = all types)
1640    out_len: *mut usize,
1641) -> *mut c_char {
1642    ffi_guard!({
1643        if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
1644            return ptr::null_mut();
1645        }
1646
1647        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1648            Ok(s) => s,
1649            Err(_) => return ptr::null_mut(),
1650        };
1651        let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1652            Ok(s) => s,
1653            Err(_) => return ptr::null_mut(),
1654        };
1655
1656        let edge_filter = if edge_type.is_null() {
1657            None
1658        } else {
1659            match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1660                Ok(s) => Some(s),
1661                Err(_) => return ptr::null_mut(),
1662            }
1663        };
1664
1665        let db = unsafe { &(*ptr).0 };
1666
1667        // Begin transaction for scan
1668        let txn = match db.begin_transaction() {
1669            Ok(t) => t,
1670            Err(_) => return ptr::null_mut(),
1671        };
1672
1673        // Scan prefix: _graph/{ns}/temporal/{node}/
1674        let prefix = format!("_graph/{}/temporal/{}/", ns, node);
1675        let pairs = match db.scan(txn, prefix.as_bytes()) {
1676            Ok(p) => p,
1677            Err(_) => {
1678                let _ = db.abort(txn);
1679                return ptr::null_mut();
1680            }
1681        };
1682
1683        // Commit read transaction
1684        if let Err(_) = db.commit(txn) {
1685            return ptr::null_mut();
1686        }
1687
1688        let mut results = Vec::new();
1689        let now = std::time::SystemTime::now()
1690            .duration_since(std::time::UNIX_EPOCH)
1691            .unwrap()
1692            .as_millis() as u64;
1693
1694        for (_key, value) in pairs {
1695            // Parse the JSON value
1696            let value_str = match std::str::from_utf8(&value) {
1697                Ok(s) => s,
1698                Err(_) => continue,
1699            };
1700
1701            // Simple JSON parsing (in production, use serde_json)
1702            if let Some(valid_from_pos) = value_str.find(r#""valid_from":"#) {
1703                if let Some(valid_until_pos) = value_str.find(r#""valid_until":"#) {
1704                    let vf_start = valid_from_pos + r#""valid_from":"#.len();
1705                    let vf_end = value_str[vf_start..].find(',').unwrap_or(0) + vf_start;
1706                    let vu_start = valid_until_pos + r#""valid_until":"#.len();
1707                    let vu_end = value_str[vu_start..].find(',').unwrap_or(0) + vu_start;
1708
1709                    let valid_from: u64 = value_str[vf_start..vf_end].parse().unwrap_or(0);
1710                    let valid_until: u64 = value_str[vu_start..vu_end].parse().unwrap_or(0);
1711
1712                    // Filter by edge_type if specified
1713                    if let Some(filter) = edge_filter {
1714                        if !value_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
1715                            continue;
1716                        }
1717                    }
1718
1719                    // Filter by query mode
1720                    let matches = match query_mode {
1721                        0 => {
1722                            timestamp >= valid_from && (valid_until == 0 || timestamp < valid_until)
1723                        }
1724                        1 => {
1725                            let edge_end = if valid_until == 0 {
1726                                u64::MAX
1727                            } else {
1728                                valid_until
1729                            };
1730                            valid_from < end_time && edge_end > start_time
1731                        }
1732                        2 => now >= valid_from && (valid_until == 0 || now < valid_until),
1733                        _ => false,
1734                    };
1735
1736                    if matches {
1737                        results.push(value_str.to_string());
1738                    }
1739                }
1740            }
1741        }
1742
1743        // Build JSON array
1744        let json = format!("[{}]", results.join(","));
1745        let c_string = match std::ffi::CString::new(json) {
1746            Ok(s) => s,
1747            Err(_) => return ptr::null_mut(),
1748        };
1749
1750        unsafe { *out_len = c_string.as_bytes().len() };
1751        c_string.into_raw()
1752    })
1753}
1754
1755/// Free a string returned by sochdb_query_temporal_graph.
1756/// # Safety
1757/// The ptr must be a valid pointer returned by sochdb_query_temporal_graph.
1758#[unsafe(no_mangle)]
1759pub unsafe extern "C" fn sochdb_free_string(ptr: *mut c_char) {
1760    ffi_guard!({
1761        if !ptr.is_null() {
1762            unsafe {
1763                let _ = std::ffi::CString::from_raw(ptr);
1764            }
1765        }
1766    })
1767}
1768
1769// ============================================================================
1770// Graph Overlay FFI - Nodes, Edges, Traversal
1771// ============================================================================
1772
1773/// Add a node to the graph overlay.
1774///
1775/// Stores node as: _graph/{namespace}/nodes/{node_id}
1776///
1777/// # Returns
1778/// - 0: Success
1779/// - -1: Error
1780///
1781/// # Safety
1782/// All pointers must be valid C strings. properties_json can be null.
1783#[unsafe(no_mangle)]
1784pub unsafe extern "C" fn sochdb_graph_add_node(
1785    ptr: *mut DatabasePtr,
1786    namespace: *const c_char,
1787    node_id: *const c_char,
1788    node_type: *const c_char,
1789    properties_json: *const c_char,
1790) -> c_int {
1791    ffi_guard!({
1792        if ptr.is_null() || namespace.is_null() || node_id.is_null() || node_type.is_null() {
1793            return -1;
1794        }
1795
1796        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1797            Ok(s) => s,
1798            Err(_) => return -1,
1799        };
1800        let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
1801            Ok(s) => s,
1802            Err(_) => return -1,
1803        };
1804        let ntype = match unsafe { CStr::from_ptr(node_type) }.to_str() {
1805            Ok(s) => s,
1806            Err(_) => return -1,
1807        };
1808        let props = if properties_json.is_null() {
1809            "{}".to_string()
1810        } else {
1811            match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1812                Ok(s) => s.to_string(),
1813                Err(_) => return -1,
1814            }
1815        };
1816
1817        let db = unsafe { &(*ptr).0 };
1818
1819        let txn = match db.begin_transaction() {
1820            Ok(t) => t,
1821            Err(_) => return -1,
1822        };
1823
1824        let key = format!("_graph/{}/nodes/{}", ns, id);
1825        let value = format!(
1826            r#"{{"id":"{}","node_type":"{}","properties":{}}}"#,
1827            id, ntype, props
1828        );
1829
1830        if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1831            let _ = db.abort(txn);
1832            return -1;
1833        }
1834
1835        match db.commit(txn) {
1836            Ok(_) => 0,
1837            Err(_) => -1,
1838        }
1839    })
1840}
1841
1842/// Add an edge between nodes in the graph overlay.
1843///
1844/// Stores edge as: _graph/{namespace}/edges/{from_id}/{edge_type}/{to_id}
1845///
1846/// # Returns
1847/// - 0: Success
1848/// - -1: Error
1849///
1850/// # Safety
1851/// All pointers must be valid C strings. properties_json can be null.
1852#[unsafe(no_mangle)]
1853pub unsafe extern "C" fn sochdb_graph_add_edge(
1854    ptr: *mut DatabasePtr,
1855    namespace: *const c_char,
1856    from_id: *const c_char,
1857    edge_type: *const c_char,
1858    to_id: *const c_char,
1859    properties_json: *const c_char,
1860) -> c_int {
1861    ffi_guard!({
1862        if ptr.is_null()
1863            || namespace.is_null()
1864            || from_id.is_null()
1865            || edge_type.is_null()
1866            || to_id.is_null()
1867        {
1868            return -1;
1869        }
1870
1871        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1872            Ok(s) => s,
1873            Err(_) => return -1,
1874        };
1875        let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
1876            Ok(s) => s,
1877            Err(_) => return -1,
1878        };
1879        let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
1880            Ok(s) => s,
1881            Err(_) => return -1,
1882        };
1883        let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
1884            Ok(s) => s,
1885            Err(_) => return -1,
1886        };
1887        let props = if properties_json.is_null() {
1888            "{}".to_string()
1889        } else {
1890            match unsafe { CStr::from_ptr(properties_json) }.to_str() {
1891                Ok(s) => s.to_string(),
1892                Err(_) => return -1,
1893            }
1894        };
1895
1896        let db = unsafe { &(*ptr).0 };
1897
1898        let txn = match db.begin_transaction() {
1899            Ok(t) => t,
1900            Err(_) => return -1,
1901        };
1902
1903        let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
1904        let value = format!(
1905            r#"{{"from_id":"{}","edge_type":"{}","to_id":"{}","properties":{}}}"#,
1906            from, etype, to, props
1907        );
1908
1909        if let Err(_) = db.put(txn, key.as_bytes(), value.as_bytes()) {
1910            let _ = db.abort(txn);
1911            return -1;
1912        }
1913
1914        match db.commit(txn) {
1915            Ok(_) => 0,
1916            Err(_) => -1,
1917        }
1918    })
1919}
1920
1921/// Traverse the graph from a starting node.
1922///
1923/// Returns JSON: {"nodes": [...], "edges": [...]}
1924/// Caller must free the returned string with sochdb_free_string.
1925///
1926/// order: 0=BFS, 1=DFS
1927///
1928/// # Safety
1929/// All pointers must be valid.
1930#[unsafe(no_mangle)]
1931pub unsafe extern "C" fn sochdb_graph_traverse(
1932    ptr: *mut DatabasePtr,
1933    namespace: *const c_char,
1934    start_node: *const c_char,
1935    max_depth: usize,
1936    order: u8, // 0=BFS, 1=DFS
1937    out_len: *mut usize,
1938) -> *mut c_char {
1939    ffi_guard!({
1940        if ptr.is_null() || namespace.is_null() || start_node.is_null() || out_len.is_null() {
1941            return ptr::null_mut();
1942        }
1943
1944        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
1945            Ok(s) => s,
1946            Err(_) => return ptr::null_mut(),
1947        };
1948        let start = match unsafe { CStr::from_ptr(start_node) }.to_str() {
1949            Ok(s) => s,
1950            Err(_) => return ptr::null_mut(),
1951        };
1952
1953        let db = unsafe { &(*ptr).0 };
1954
1955        let txn = match db.begin_transaction() {
1956            Ok(t) => t,
1957            Err(_) => return ptr::null_mut(),
1958        };
1959
1960        // Collect nodes and edges through traversal
1961        let mut visited_nodes = std::collections::HashSet::new();
1962        let mut nodes_json = Vec::new();
1963        let mut edges_json = Vec::new();
1964
1965        // Use queue for BFS, stack for DFS
1966        let mut frontier: Vec<(String, usize)> = vec![(start.to_string(), 0)];
1967
1968        while let Some((current_node, depth)) = if order == 0 {
1969            // BFS: remove from front
1970            if frontier.is_empty() {
1971                None
1972            } else {
1973                Some(frontier.remove(0))
1974            }
1975        } else {
1976            // DFS: remove from back
1977            frontier.pop()
1978        } {
1979            if depth > max_depth || visited_nodes.contains(&current_node) {
1980                continue;
1981            }
1982            visited_nodes.insert(current_node.clone());
1983
1984            // Get node data
1985            let node_key = format!("_graph/{}/nodes/{}", ns, current_node);
1986            if let Ok(Some(node_data)) = db.get(txn, node_key.as_bytes()) {
1987                if let Ok(s) = std::str::from_utf8(&node_data) {
1988                    nodes_json.push(s.to_string());
1989                }
1990            }
1991
1992            // Get outgoing edges
1993            let edge_prefix = format!("_graph/{}/edges/{}/", ns, current_node);
1994            if let Ok(edges) = db.scan(txn, edge_prefix.as_bytes()) {
1995                for (_key, value) in edges {
1996                    if let Ok(edge_str) = std::str::from_utf8(&value) {
1997                        edges_json.push(edge_str.to_string());
1998
1999                        // Extract to_id for traversal
2000                        if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
2001                            let start_idx = to_pos + r#""to_id":""#.len();
2002                            if let Some(end_idx) = edge_str[start_idx..].find('"') {
2003                                let to_id = &edge_str[start_idx..start_idx + end_idx];
2004                                if !visited_nodes.contains(to_id) {
2005                                    frontier.push((to_id.to_string(), depth + 1));
2006                                }
2007                            }
2008                        }
2009                    }
2010                }
2011            }
2012        }
2013
2014        if let Err(_) = db.commit(txn) {
2015            return ptr::null_mut();
2016        }
2017
2018        let result = format!(
2019            r#"{{"nodes":[{}],"edges":[{}]}}"#,
2020            nodes_json.join(","),
2021            edges_json.join(",")
2022        );
2023
2024        let c_string = match std::ffi::CString::new(result) {
2025            Ok(s) => s,
2026            Err(_) => return ptr::null_mut(),
2027        };
2028
2029        unsafe { *out_len = c_string.as_bytes().len() };
2030        c_string.into_raw()
2031    })
2032}
2033
2034// ============================================================================
2035// Semantic Cache FFI
2036// ============================================================================
2037
2038/// Store a value in the semantic cache with its embedding.
2039///
2040/// # Returns
2041/// - 0: Success
2042/// - -1: Error
2043///
2044/// # Safety
2045/// All pointers must be valid.
2046#[unsafe(no_mangle)]
2047pub unsafe extern "C" fn sochdb_cache_put(
2048    ptr: *mut DatabasePtr,
2049    cache_name: *const c_char,
2050    key: *const c_char,
2051    value: *const c_char,
2052    embedding_ptr: *const f32,
2053    embedding_len: usize,
2054    ttl_seconds: u64,
2055) -> c_int {
2056    ffi_guard!({
2057        if ptr.is_null()
2058            || cache_name.is_null()
2059            || key.is_null()
2060            || value.is_null()
2061            || embedding_ptr.is_null()
2062        {
2063            return -1;
2064        }
2065
2066        let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
2067            Ok(s) => s,
2068            Err(_) => return -1,
2069        };
2070        let k = match unsafe { CStr::from_ptr(key) }.to_str() {
2071            Ok(s) => s,
2072            Err(_) => return -1,
2073        };
2074        let v = match unsafe { CStr::from_ptr(value) }.to_str() {
2075            Ok(s) => s,
2076            Err(_) => return -1,
2077        };
2078        let embedding = unsafe { slice::from_raw_parts(embedding_ptr, embedding_len) };
2079
2080        let db = unsafe { &(*ptr).0 };
2081
2082        let txn = match db.begin_transaction() {
2083            Ok(t) => t,
2084            Err(_) => return -1,
2085        };
2086
2087        // Compute expiry timestamp
2088        let expires_at = if ttl_seconds > 0 {
2089            std::time::SystemTime::now()
2090                .duration_since(std::time::UNIX_EPOCH)
2091                .unwrap()
2092                .as_secs()
2093                + ttl_seconds
2094        } else {
2095            0 // No expiry
2096        };
2097
2098        // Store cache entry: _cache/{cache_name}/{key_hash}
2099        let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
2100        let cache_key = format!("_cache/{}/{}", cache, key_hash);
2101
2102        // Serialize embedding as JSON array
2103        let embedding_json: Vec<String> = embedding.iter().map(|f| f.to_string()).collect();
2104
2105        let cache_value = format!(
2106            r#"{{"key":"{}","value":"{}","embedding":[{}],"expires_at":{}}}"#,
2107            k,
2108            v,
2109            embedding_json.join(","),
2110            expires_at
2111        );
2112
2113        if let Err(_) = db.put(txn, cache_key.as_bytes(), cache_value.as_bytes()) {
2114            let _ = db.abort(txn);
2115            return -1;
2116        }
2117
2118        match db.commit(txn) {
2119            Ok(_) => 0,
2120            Err(_) => -1,
2121        }
2122    })
2123}
2124
2125/// Look up a value in the semantic cache by embedding similarity.
2126///
2127/// Returns the cached value if similarity >= threshold, null otherwise.
2128/// Caller must free the returned string with sochdb_free_string.
2129///
2130/// # Safety
2131/// All pointers must be valid.
2132#[unsafe(no_mangle)]
2133pub unsafe extern "C" fn sochdb_cache_get(
2134    ptr: *mut DatabasePtr,
2135    cache_name: *const c_char,
2136    query_embedding_ptr: *const f32,
2137    embedding_len: usize,
2138    threshold: f32,
2139    out_len: *mut usize,
2140) -> *mut c_char {
2141    ffi_guard!({
2142        if ptr.is_null()
2143            || cache_name.is_null()
2144            || query_embedding_ptr.is_null()
2145            || out_len.is_null()
2146        {
2147            return ptr::null_mut();
2148        }
2149
2150        let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
2151            Ok(s) => s,
2152            Err(_) => return ptr::null_mut(),
2153        };
2154        let query = unsafe { slice::from_raw_parts(query_embedding_ptr, embedding_len) };
2155
2156        let db = unsafe { &(*ptr).0 };
2157
2158        let txn = match db.begin_transaction() {
2159            Ok(t) => t,
2160            Err(_) => return ptr::null_mut(),
2161        };
2162
2163        let prefix = format!("_cache/{}/", cache);
2164        let entries = match db.scan(txn, prefix.as_bytes()) {
2165            Ok(e) => e,
2166            Err(_) => {
2167                let _ = db.abort(txn);
2168                return ptr::null_mut();
2169            }
2170        };
2171
2172        let _ = db.commit(txn);
2173
2174        let now = std::time::SystemTime::now()
2175            .duration_since(std::time::UNIX_EPOCH)
2176            .unwrap()
2177            .as_secs();
2178
2179        let mut best_match: Option<(f32, String)> = None;
2180
2181        for (_key, value) in entries {
2182            let value_str = match std::str::from_utf8(&value) {
2183                Ok(s) => s,
2184                Err(_) => continue,
2185            };
2186
2187            // Parse expires_at
2188            if let Some(exp_pos) = value_str.find(r#""expires_at":"#) {
2189                let exp_start = exp_pos + r#""expires_at":"#.len();
2190                if let Some(exp_end) = value_str[exp_start..].find('}') {
2191                    let expires_at: u64 = value_str[exp_start..exp_start + exp_end]
2192                        .parse()
2193                        .unwrap_or(0);
2194                    if expires_at > 0 && now > expires_at {
2195                        continue; // Expired
2196                    }
2197                }
2198            }
2199
2200            // Parse embedding and compute cosine similarity
2201            if let Some(emb_pos) = value_str.find(r#""embedding":["#) {
2202                let emb_start = emb_pos + r#""embedding":["#.len();
2203                if let Some(emb_end) = value_str[emb_start..].find(']') {
2204                    let emb_str = &value_str[emb_start..emb_start + emb_end];
2205                    let cached_embedding: Vec<f32> = emb_str
2206                        .split(',')
2207                        .filter_map(|s| s.trim().parse().ok())
2208                        .collect();
2209
2210                    if cached_embedding.len() == query.len() {
2211                        let similarity = cosine_similarity(query, &cached_embedding);
2212                        if similarity >= threshold {
2213                            if best_match.is_none() || similarity > best_match.as_ref().unwrap().0 {
2214                                // Extract value field
2215                                if let Some(val_pos) = value_str.find(r#""value":""#) {
2216                                    let val_start = val_pos + r#""value":""#.len();
2217                                    if let Some(val_end) = value_str[val_start..].find('"') {
2218                                        let cached_value =
2219                                            &value_str[val_start..val_start + val_end];
2220                                        best_match = Some((similarity, cached_value.to_string()));
2221                                    }
2222                                }
2223                            }
2224                        }
2225                    }
2226                }
2227            }
2228        }
2229
2230        match best_match {
2231            Some((_, value)) => {
2232                let c_string = match std::ffi::CString::new(value) {
2233                    Ok(s) => s,
2234                    Err(_) => return ptr::null_mut(),
2235                };
2236                unsafe { *out_len = c_string.as_bytes().len() };
2237                c_string.into_raw()
2238            }
2239            None => ptr::null_mut(),
2240        }
2241    })
2242}
2243
2244/// Compute cosine similarity between two vectors
2245/// Returns normalized similarity in [0, 1] range for threshold comparisons
2246fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
2247    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
2248    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
2249    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
2250    if norm_a == 0.0 || norm_b == 0.0 {
2251        0.0
2252    } else {
2253        let similarity = dot / (norm_a * norm_b);
2254        // Normalize from [-1, 1] to [0, 1] for threshold comparisons
2255        // This ensures consistent scoring across all SDKs (Python/Node.js/Go)
2256        (similarity + 1.0) / 2.0
2257    }
2258}
2259
2260// ============================================================================
2261// Trace Service FFI
2262// ============================================================================
2263
2264/// Start a new trace. Returns trace_id and root_span_id.
2265///
2266/// Caller must free the returned strings with sochdb_free_string.
2267///
2268/// # Returns
2269/// - 0: Success
2270/// - -1: Error
2271///
2272/// # Safety
2273/// All pointers must be valid.
2274#[unsafe(no_mangle)]
2275pub unsafe extern "C" fn sochdb_trace_start(
2276    ptr: *mut DatabasePtr,
2277    name: *const c_char,
2278    trace_id_out: *mut *mut c_char,
2279    span_id_out: *mut *mut c_char,
2280) -> c_int {
2281    ffi_guard!({
2282        if ptr.is_null() || name.is_null() || trace_id_out.is_null() || span_id_out.is_null() {
2283            return -1;
2284        }
2285
2286        let trace_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2287            Ok(s) => s,
2288            Err(_) => return -1,
2289        };
2290
2291        let db = unsafe { &(*ptr).0 };
2292
2293        // Generate unique IDs
2294        let trace_id = format!("trace_{:016x}", rand_u64());
2295        let span_id = format!("span_{:016x}", rand_u64());
2296
2297        let txn = match db.begin_transaction() {
2298            Ok(t) => t,
2299            Err(_) => return -1,
2300        };
2301
2302        let now = std::time::SystemTime::now()
2303            .duration_since(std::time::UNIX_EPOCH)
2304            .unwrap()
2305            .as_micros() as u64;
2306
2307        // Store trace: _traces/{trace_id}
2308        let trace_key = format!("_traces/{}", trace_id);
2309        let trace_value = format!(
2310            r#"{{"trace_id":"{}","name":"{}","start_us":{},"root_span_id":"{}"}}"#,
2311            trace_id, trace_name, now, span_id
2312        );
2313
2314        if let Err(_) = db.put(txn, trace_key.as_bytes(), trace_value.as_bytes()) {
2315            let _ = db.abort(txn);
2316            return -1;
2317        }
2318
2319        // Store root span: _traces/{trace_id}/spans/{span_id}
2320        let span_key = format!("_traces/{}/spans/{}", trace_id, span_id);
2321        let span_value = format!(
2322            r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":null,"status":"active"}}"#,
2323            span_id, trace_name, now
2324        );
2325
2326        if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2327            let _ = db.abort(txn);
2328            return -1;
2329        }
2330
2331        if let Err(_) = db.commit(txn) {
2332            return -1;
2333        }
2334
2335        // Return trace_id and span_id
2336        let trace_c = match std::ffi::CString::new(trace_id) {
2337            Ok(s) => s,
2338            Err(_) => return -1,
2339        };
2340        let span_c = match std::ffi::CString::new(span_id) {
2341            Ok(s) => s,
2342            Err(_) => return -1,
2343        };
2344
2345        unsafe {
2346            *trace_id_out = trace_c.into_raw();
2347            *span_id_out = span_c.into_raw();
2348        }
2349
2350        0
2351    })
2352}
2353
2354/// Start a child span within a trace.
2355///
2356/// Caller must free the returned span_id with sochdb_free_string.
2357///
2358/// # Safety
2359/// All pointers must be valid.
2360#[unsafe(no_mangle)]
2361pub unsafe extern "C" fn sochdb_trace_span_start(
2362    ptr: *mut DatabasePtr,
2363    trace_id: *const c_char,
2364    parent_span_id: *const c_char,
2365    name: *const c_char,
2366    span_id_out: *mut *mut c_char,
2367) -> c_int {
2368    ffi_guard!({
2369        if ptr.is_null()
2370            || trace_id.is_null()
2371            || parent_span_id.is_null()
2372            || name.is_null()
2373            || span_id_out.is_null()
2374        {
2375            return -1;
2376        }
2377
2378        let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2379            Ok(s) => s,
2380            Err(_) => return -1,
2381        };
2382        let pid = match unsafe { CStr::from_ptr(parent_span_id) }.to_str() {
2383            Ok(s) => s,
2384            Err(_) => return -1,
2385        };
2386        let span_name = match unsafe { CStr::from_ptr(name) }.to_str() {
2387            Ok(s) => s,
2388            Err(_) => return -1,
2389        };
2390
2391        let db = unsafe { &(*ptr).0 };
2392        let span_id = format!("span_{:016x}", rand_u64());
2393
2394        let txn = match db.begin_transaction() {
2395            Ok(t) => t,
2396            Err(_) => return -1,
2397        };
2398
2399        let now = std::time::SystemTime::now()
2400            .duration_since(std::time::UNIX_EPOCH)
2401            .unwrap()
2402            .as_micros() as u64;
2403
2404        let span_key = format!("_traces/{}/spans/{}", tid, span_id);
2405        let span_value = format!(
2406            r#"{{"span_id":"{}","name":"{}","start_us":{},"parent_span_id":"{}","status":"active"}}"#,
2407            span_id, span_name, now, pid
2408        );
2409
2410        if let Err(_) = db.put(txn, span_key.as_bytes(), span_value.as_bytes()) {
2411            let _ = db.abort(txn);
2412            return -1;
2413        }
2414
2415        if let Err(_) = db.commit(txn) {
2416            return -1;
2417        }
2418
2419        let span_c = match std::ffi::CString::new(span_id) {
2420            Ok(s) => s,
2421            Err(_) => return -1,
2422        };
2423
2424        unsafe { *span_id_out = span_c.into_raw() };
2425        0
2426    })
2427}
2428
2429/// End a span and record its duration.
2430///
2431/// status: 0=unset, 1=ok, 2=error
2432///
2433/// # Returns
2434/// Duration in microseconds on success, -1 on error.
2435///
2436/// # Safety
2437/// All pointers must be valid.
2438#[unsafe(no_mangle)]
2439pub unsafe extern "C" fn sochdb_trace_span_end(
2440    ptr: *mut DatabasePtr,
2441    trace_id: *const c_char,
2442    span_id: *const c_char,
2443    status: u8,
2444) -> i64 {
2445    ffi_guard!({
2446        if ptr.is_null() || trace_id.is_null() || span_id.is_null() {
2447            return -1;
2448        }
2449
2450        let tid = match unsafe { CStr::from_ptr(trace_id) }.to_str() {
2451            Ok(s) => s,
2452            Err(_) => return -1,
2453        };
2454        let sid = match unsafe { CStr::from_ptr(span_id) }.to_str() {
2455            Ok(s) => s,
2456            Err(_) => return -1,
2457        };
2458
2459        let db = unsafe { &(*ptr).0 };
2460
2461        let txn = match db.begin_transaction() {
2462            Ok(t) => t,
2463            Err(_) => return -1,
2464        };
2465
2466        let span_key = format!("_traces/{}/spans/{}", tid, sid);
2467
2468        // Read current span
2469        let span_data = match db.get(txn, span_key.as_bytes()) {
2470            Ok(Some(data)) => data,
2471            _ => {
2472                let _ = db.abort(txn);
2473                return -1;
2474            }
2475        };
2476
2477        let span_str = match std::str::from_utf8(&span_data) {
2478            Ok(s) => s,
2479            Err(_) => {
2480                let _ = db.abort(txn);
2481                return -1;
2482            }
2483        };
2484
2485        // Parse start_us
2486        let start_us = if let Some(pos) = span_str.find(r#""start_us":"#) {
2487            let start = pos + r#""start_us":"#.len();
2488            if let Some(end) = span_str[start..].find(',') {
2489                span_str[start..start + end].parse().unwrap_or(0u64)
2490            } else {
2491                0u64
2492            }
2493        } else {
2494            0u64
2495        };
2496
2497        let now = std::time::SystemTime::now()
2498            .duration_since(std::time::UNIX_EPOCH)
2499            .unwrap()
2500            .as_micros() as u64;
2501
2502        let duration_us = now.saturating_sub(start_us);
2503        let status_str = match status {
2504            1 => "ok",
2505            2 => "error",
2506            _ => "unset",
2507        };
2508
2509        // Update span with end time and duration
2510        let new_span = span_str.replace(
2511            r#""status":"active""#,
2512            &format!(
2513                r#""status":"{}","end_us":{},"duration_us":{}"#,
2514                status_str, now, duration_us
2515            ),
2516        );
2517
2518        if let Err(_) = db.put(txn, span_key.as_bytes(), new_span.as_bytes()) {
2519            let _ = db.abort(txn);
2520            return -1;
2521        }
2522
2523        if let Err(_) = db.commit(txn) {
2524            return -1;
2525        }
2526
2527        duration_us as i64
2528    })
2529}
2530
2531/// Generate a pseudo-random u64 (simple XorShift for trace IDs)
2532fn rand_u64() -> u64 {
2533    use std::sync::atomic::{AtomicU64, Ordering};
2534    static STATE: AtomicU64 = AtomicU64::new(0x853c49e6748fea9b);
2535
2536    let mut s = STATE.load(Ordering::Relaxed);
2537    if s == 0 {
2538        s = std::time::SystemTime::now()
2539            .duration_since(std::time::UNIX_EPOCH)
2540            .unwrap()
2541            .as_nanos() as u64;
2542    }
2543    s ^= s >> 12;
2544    s ^= s << 25;
2545    s ^= s >> 27;
2546    STATE.store(s, Ordering::Relaxed);
2547    s.wrapping_mul(0x2545F4914F6CDD1D)
2548}
2549
2550// =========================================================================
2551// Vector Index Operations (KV-based, native Rust performance)
2552// =========================================================================
2553
2554/// Create a vector collection for storing embeddings
2555///
2556/// # Returns
2557/// - 0: Success (or already exists)
2558/// - -1: Error
2559#[unsafe(no_mangle)]
2560pub unsafe extern "C" fn sochdb_collection_create(
2561    ptr: *mut DatabasePtr,
2562    namespace: *const c_char,
2563    collection: *const c_char,
2564    dimension: usize,
2565    dist_type: u8, // 0=Cosine, 1=Euclidean, 2=Dot
2566) -> c_int {
2567    ffi_guard!({
2568        if ptr.is_null() || namespace.is_null() || collection.is_null() {
2569            return -1;
2570        }
2571
2572        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2573            Ok(s) => s,
2574            Err(_) => return -1,
2575        };
2576        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2577            Ok(s) => s,
2578            Err(_) => return -1,
2579        };
2580
2581        let db = unsafe { &(*ptr).0 };
2582        let txn = match db.begin_transaction() {
2583            Ok(t) => t,
2584            Err(_) => return -1,
2585        };
2586
2587        // Store collection config
2588        let config_key = format!("{}/_collections/{}", ns, col);
2589        let config_value = format!(r#"{{"dimension":{},"metric":{}}}"#, dimension, dist_type);
2590
2591        if let Err(_) = db.put(txn, config_key.as_bytes(), config_value.as_bytes()) {
2592            let _ = db.abort(txn);
2593            return -1;
2594        }
2595
2596        let result = match db.commit(txn) {
2597            Ok(_) => 0,
2598            Err(_) => -1,
2599        };
2600
2601        if result == 0 {
2602            let metric = match dist_type {
2603                1 => DistanceMetric::Euclidean,
2604                2 => DistanceMetric::DotProduct,
2605                _ => DistanceMetric::Cosine,
2606            };
2607            let _ = ensure_collection_index(db, ns, col, dimension, metric);
2608        }
2609
2610        result
2611    })
2612}
2613
2614/// Insert a vector into a collection
2615///
2616/// # Returns
2617/// - 0: Success
2618/// - -1: Error
2619#[unsafe(no_mangle)]
2620pub unsafe extern "C" fn sochdb_collection_insert(
2621    ptr: *mut DatabasePtr,
2622    namespace: *const c_char,
2623    collection: *const c_char,
2624    id: *const c_char,
2625    vector_ptr: *const f32,
2626    vector_len: usize,
2627    metadata_json: *const c_char, // Optional JSON metadata
2628) -> c_int {
2629    ffi_guard!({
2630        if ptr.is_null()
2631            || namespace.is_null()
2632            || collection.is_null()
2633            || id.is_null()
2634            || vector_ptr.is_null()
2635        {
2636            return -1;
2637        }
2638
2639        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2640            Ok(s) => s,
2641            Err(_) => return -1,
2642        };
2643        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2644            Ok(s) => s,
2645            Err(_) => return -1,
2646        };
2647        let doc_id = match unsafe { CStr::from_ptr(id) }.to_str() {
2648            Ok(s) => s,
2649            Err(_) => return -1,
2650        };
2651        let vector = unsafe { slice::from_raw_parts(vector_ptr, vector_len) };
2652        let db = unsafe { &(*ptr).0 };
2653
2654        let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2655            Some(config) => config,
2656            None => (vector_len, DistanceMetric::Cosine),
2657        };
2658        if vector_len != dimension {
2659            return -1;
2660        }
2661
2662        let metadata = if !metadata_json.is_null() {
2663            match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2664                Ok(s) => s.to_string(),
2665                Err(_) => "{}".to_string(),
2666            }
2667        } else {
2668            "{}".to_string()
2669        };
2670
2671        let txn = match db.begin_transaction() {
2672            Ok(t) => t,
2673            Err(_) => return -1,
2674        };
2675
2676        let id_hash = hash_id_to_u128(doc_id);
2677        let vec_key = vector_bin_key(ns, col, id_hash);
2678        let vec_value = serialize_vector_binary(vector);
2679
2680        if let Err(_) = db.put(txn, vec_key.as_bytes(), &vec_value) {
2681            let _ = db.abort(txn);
2682            return -1;
2683        }
2684
2685        let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2686            Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2687            Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2688        };
2689        let meta_key = metadata_key(ns, col, id_hash);
2690        if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2691            if let Err(_) = db.put(txn, meta_key.as_bytes(), &meta_bytes) {
2692                let _ = db.abort(txn);
2693                return -1;
2694            }
2695        }
2696
2697        if let Err(_) = db.commit(txn) {
2698            return -1;
2699        }
2700
2701        let index = ensure_collection_index(db, ns, col, dimension, metric);
2702        let _ = index.index.insert(id_hash, vector.to_vec());
2703
2704        0
2705    })
2706}
2707
2708/// Batch insert vectors into a collection.
2709///
2710/// Takes parallel arrays of ids, vectors, and optional metadata.
2711/// Uses a single transaction for all inserts → much faster than per-vector.
2712///
2713/// # Returns
2714/// - >= 0: Number of successfully inserted vectors
2715/// - -1: Error
2716#[unsafe(no_mangle)]
2717pub unsafe extern "C" fn sochdb_collection_insert_batch(
2718    ptr: *mut DatabasePtr,
2719    namespace: *const c_char,
2720    collection: *const c_char,
2721    ids: *const *const c_char, // Array of C strings
2722    vectors: *const f32,       // Flat array: count * dimension floats
2723    dimension: usize,
2724    metadata_jsons: *const *const c_char, // Array of C strings (nullable entries)
2725    count: usize,
2726) -> c_int {
2727    ffi_guard!({
2728        if ptr.is_null()
2729            || namespace.is_null()
2730            || collection.is_null()
2731            || ids.is_null()
2732            || vectors.is_null()
2733            || count == 0
2734        {
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 db = unsafe { &(*ptr).0 };
2747
2748        let (expected_dim, metric) = match resolve_collection_config(db, ns, col) {
2749            Some(config) => config,
2750            None => (dimension, DistanceMetric::Cosine),
2751        };
2752        if dimension != expected_dim {
2753            return -1;
2754        }
2755
2756        // Single transaction for the entire batch
2757        let txn = match db.begin_transaction() {
2758            Ok(t) => t,
2759            Err(_) => return -1,
2760        };
2761
2762        let ids_slice = unsafe { slice::from_raw_parts(ids, count) };
2763        let vectors_flat = unsafe { slice::from_raw_parts(vectors, count * dimension) };
2764
2765        let mut inserted = 0i32;
2766        let mut id_hashes = Vec::with_capacity(count);
2767        let mut vector_copies = Vec::with_capacity(count);
2768
2769        for i in 0..count {
2770            let doc_id = match unsafe { CStr::from_ptr(ids_slice[i]) }.to_str() {
2771                Ok(s) => s,
2772                Err(_) => continue,
2773            };
2774            let vec_start = i * dimension;
2775            let vector = &vectors_flat[vec_start..vec_start + dimension];
2776
2777            let id_hash = hash_id_to_u128(doc_id);
2778            let vec_key = vector_bin_key(ns, col, id_hash);
2779            let vec_value = serialize_vector_binary(vector);
2780
2781            if db.put(txn, vec_key.as_bytes(), &vec_value).is_err() {
2782                continue;
2783            }
2784
2785            // Metadata
2786            let metadata = if !metadata_jsons.is_null() {
2787                let meta_ptr = unsafe { *metadata_jsons.add(i) };
2788                if !meta_ptr.is_null() {
2789                    match unsafe { CStr::from_ptr(meta_ptr) }.to_str() {
2790                        Ok(s) => s.to_string(),
2791                        Err(_) => "{}".to_string(),
2792                    }
2793                } else {
2794                    "{}".to_string()
2795                }
2796            } else {
2797                "{}".to_string()
2798            };
2799
2800            let metadata_value = match serde_json::from_str::<serde_json::Value>(&metadata) {
2801                Ok(value) => serde_json::json!({"id": doc_id, "metadata": value}),
2802                Err(_) => serde_json::json!({"id": doc_id, "metadata": serde_json::json!({})}),
2803            };
2804            let meta_key = metadata_key(ns, col, id_hash);
2805            if let Ok(meta_bytes) = serde_json::to_vec(&metadata_value) {
2806                let _ = db.put(txn, meta_key.as_bytes(), &meta_bytes);
2807            }
2808
2809            id_hashes.push(id_hash);
2810            vector_copies.push(vector.to_vec());
2811            inserted += 1;
2812        }
2813
2814        // Commit single transaction for entire batch
2815        if db.commit(txn).is_err() {
2816            return -1;
2817        }
2818
2819        // Insert into HNSW index (after commit succeeds)
2820        let index = ensure_collection_index(db, ns, col, dimension, metric);
2821        for (id_hash, vector) in id_hashes.into_iter().zip(vector_copies.into_iter()) {
2822            let _ = index.index.insert(id_hash, vector);
2823        }
2824
2825        inserted
2826    })
2827}
2828
2829/// C-compatible search result
2830#[repr(C)]
2831pub struct CSearchResult {
2832    pub id_ptr: *mut c_char,
2833    pub score: f32,
2834    pub metadata_ptr: *mut c_char,
2835}
2836
2837/// Search a collection for nearest vectors
2838///
2839/// # Returns
2840/// - >= 0: Number of results
2841/// - -1: Error
2842#[unsafe(no_mangle)]
2843pub unsafe extern "C" fn sochdb_collection_search(
2844    ptr: *mut DatabasePtr,
2845    namespace: *const c_char,
2846    collection: *const c_char,
2847    query_ptr: *const f32,
2848    query_len: usize,
2849    k: usize,
2850    results_out: *mut CSearchResult,
2851) -> c_int {
2852    ffi_guard!({
2853        if ptr.is_null()
2854            || namespace.is_null()
2855            || collection.is_null()
2856            || query_ptr.is_null()
2857            || results_out.is_null()
2858        {
2859            return -1;
2860        }
2861        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2862            Ok(s) => s,
2863            Err(_) => return -1,
2864        };
2865        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2866            Ok(s) => s,
2867            Err(_) => return -1,
2868        };
2869        let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2870        let db = unsafe { &(*ptr).0 };
2871        let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2872            Some(config) => config,
2873            None => return 0,
2874        };
2875
2876        if query_len != dimension {
2877            return -1;
2878        }
2879
2880        let index = ensure_collection_index(db, ns, col, dimension, metric);
2881        let mut scored = match index.index.search(query, k) {
2882            Ok(results) => results,
2883            Err(_) => return -1,
2884        };
2885
2886        let result_count = scored.len().min(k);
2887        for (i, (id_hash, distance)) in scored.drain(..result_count).enumerate() {
2888            let meta_key = metadata_key(ns, col, id_hash);
2889            let txn = match db.begin_transaction() {
2890                Ok(t) => t,
2891                Err(_) => return -1,
2892            };
2893            let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
2894            let _ = db.commit(txn);
2895
2896            let mut id_value = String::new();
2897            let mut metadata_json = serde_json::json!({});
2898            if let Some(bytes) = meta_value.as_deref() {
2899                if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(bytes) {
2900                    id_value = parsed
2901                        .get("id")
2902                        .and_then(|v| v.as_str())
2903                        .unwrap_or("")
2904                        .to_string();
2905                    metadata_json = parsed
2906                        .get("metadata")
2907                        .cloned()
2908                        .unwrap_or(serde_json::json!({}));
2909                }
2910            }
2911            let metadata =
2912                serde_json::to_string(&metadata_json).unwrap_or_else(|_| "{}".to_string());
2913
2914            let c_id = match std::ffi::CString::new(id_value) {
2915                Ok(s) => s.into_raw(),
2916                Err(_) => ptr::null_mut(),
2917            };
2918            let c_meta = match std::ffi::CString::new(metadata) {
2919                Ok(s) => s.into_raw(),
2920                Err(_) => ptr::null_mut(),
2921            };
2922
2923            unsafe {
2924                (*results_out.add(i)).id_ptr = c_id;
2925                (*results_out.add(i)).score = decode_score(metric, distance);
2926                (*results_out.add(i)).metadata_ptr = c_meta;
2927            }
2928        }
2929
2930        result_count as c_int
2931    })
2932}
2933
2934/// Search a collection and return results as struct-of-arrays (ids + scores)
2935///
2936/// - ids_out: pointer to u64 array (allocated by Rust)
2937/// - scores_out: pointer to f32 array (allocated by Rust)
2938/// - len_out: number of results
2939#[unsafe(no_mangle)]
2940pub unsafe extern "C" fn sochdb_collection_search_soa(
2941    ptr: *mut DatabasePtr,
2942    namespace: *const c_char,
2943    collection: *const c_char,
2944    query_ptr: *const f32,
2945    query_len: usize,
2946    k: usize,
2947    min_score: f32,
2948    filter_json: *const c_char,
2949    ids_hi_out: *mut *mut u64,
2950    ids_lo_out: *mut *mut u64,
2951    scores_out: *mut *mut f32,
2952    len_out: *mut usize,
2953) -> c_int {
2954    ffi_guard!({
2955        if ptr.is_null()
2956            || namespace.is_null()
2957            || collection.is_null()
2958            || query_ptr.is_null()
2959            || ids_hi_out.is_null()
2960            || ids_lo_out.is_null()
2961            || scores_out.is_null()
2962            || len_out.is_null()
2963        {
2964            return -1;
2965        }
2966
2967        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
2968            Ok(s) => s,
2969            Err(_) => return -1,
2970        };
2971        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
2972            Ok(s) => s,
2973            Err(_) => return -1,
2974        };
2975        let query = unsafe { slice::from_raw_parts(query_ptr, query_len) };
2976        let db = unsafe { &(*ptr).0 };
2977
2978        let (dimension, metric) = match resolve_collection_config(db, ns, col) {
2979            Some(config) => config,
2980            None => return 0,
2981        };
2982        if query_len != dimension {
2983            return -1;
2984        }
2985
2986        let filter = if !filter_json.is_null() {
2987            match unsafe { CStr::from_ptr(filter_json) }.to_str() {
2988                Ok(s) => serde_json::from_str::<serde_json::Value>(s).ok(),
2989                Err(_) => None,
2990            }
2991        } else {
2992            None
2993        };
2994
2995        let index = ensure_collection_index(db, ns, col, dimension, metric);
2996        let results = match index.index.search(query, k) {
2997            Ok(results) => results,
2998            Err(_) => return -1,
2999        };
3000
3001        let mut ids_hi: Vec<u64> = Vec::with_capacity(results.len());
3002        let mut ids_lo: Vec<u64> = Vec::with_capacity(results.len());
3003        let mut scores: Vec<f32> = Vec::with_capacity(results.len());
3004
3005        for (id_hash, distance) in results {
3006            let score = decode_score(metric, distance);
3007            if min_score > 0.0 && score < min_score {
3008                continue;
3009            }
3010
3011            if let Some(filter_value) = &filter {
3012                let meta_key = metadata_key(ns, col, id_hash);
3013                let txn = match db.begin_transaction() {
3014                    Ok(t) => t,
3015                    Err(_) => return -1,
3016                };
3017                let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
3018                let _ = db.commit(txn);
3019                let meta_value = match meta_value {
3020                    Some(value) => value,
3021                    None => continue,
3022                };
3023                let parsed = match serde_json::from_slice::<serde_json::Value>(&meta_value) {
3024                    Ok(value) => value,
3025                    Err(_) => continue,
3026                };
3027                let metadata = parsed.get("metadata").cloned().unwrap_or(Value::Null);
3028
3029                if !metadata_matches_filter(&metadata, filter_value) {
3030                    continue;
3031                }
3032            }
3033
3034            ids_hi.push((id_hash >> 64) as u64);
3035            ids_lo.push((id_hash & u128::from(u64::MAX)) as u64);
3036            scores.push(score);
3037            if ids_hi.len() >= k {
3038                break;
3039            }
3040        }
3041
3042        let len = ids_hi.len();
3043        let mut ids_hi_box = ids_hi.into_boxed_slice();
3044        let mut ids_lo_box = ids_lo.into_boxed_slice();
3045        let mut scores_box = scores.into_boxed_slice();
3046
3047        unsafe {
3048            *len_out = len;
3049            *ids_hi_out = ids_hi_box.as_mut_ptr();
3050            *ids_lo_out = ids_lo_box.as_mut_ptr();
3051            *scores_out = scores_box.as_mut_ptr();
3052        }
3053
3054        std::mem::forget(ids_hi_box);
3055        std::mem::forget(ids_lo_box);
3056        std::mem::forget(scores_box);
3057
3058        len as c_int
3059    })
3060}
3061
3062/// Fetch metadata JSON for a list of ids (u64 hashes)
3063#[unsafe(no_mangle)]
3064pub unsafe extern "C" fn sochdb_collection_fetch_metadata_json(
3065    ptr: *mut DatabasePtr,
3066    namespace: *const c_char,
3067    collection: *const c_char,
3068    ids_hi_ptr: *const u64,
3069    ids_lo_ptr: *const u64,
3070    ids_len: usize,
3071) -> *mut c_char {
3072    ffi_guard!({
3073        if ptr.is_null()
3074            || namespace.is_null()
3075            || collection.is_null()
3076            || ids_hi_ptr.is_null()
3077            || ids_lo_ptr.is_null()
3078        {
3079            return ptr::null_mut();
3080        }
3081
3082        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3083            Ok(s) => s,
3084            Err(_) => return ptr::null_mut(),
3085        };
3086        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
3087            Ok(s) => s,
3088            Err(_) => return ptr::null_mut(),
3089        };
3090        let ids_hi = unsafe { slice::from_raw_parts(ids_hi_ptr, ids_len) };
3091        let ids_lo = unsafe { slice::from_raw_parts(ids_lo_ptr, ids_len) };
3092        let db = unsafe { &(*ptr).0 };
3093
3094        let mut results = Vec::with_capacity(ids_len);
3095        for i in 0..ids_len {
3096            let id_hash = ((ids_hi[i] as u128) << 64) | (ids_lo[i] as u128);
3097            let meta_key = metadata_key(ns, col, id_hash);
3098            let txn = match db.begin_transaction() {
3099                Ok(t) => t,
3100                Err(_) => return ptr::null_mut(),
3101            };
3102            let meta_value = db.get(txn, meta_key.as_bytes()).ok().flatten();
3103            let _ = db.commit(txn);
3104            if let Some(bytes) = meta_value {
3105                if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&bytes) {
3106                    results.push(parsed);
3107                    continue;
3108                }
3109            }
3110            results.push(serde_json::json!({"id": "", "metadata": {}}));
3111        }
3112
3113        match serde_json::to_string(&results) {
3114            Ok(json) => match std::ffi::CString::new(json) {
3115                Ok(cstr) => cstr.into_raw(),
3116                Err(_) => ptr::null_mut(),
3117            },
3118            Err(_) => ptr::null_mut(),
3119        }
3120    })
3121}
3122
3123/// Free arrays returned by sochdb_collection_search_soa
3124#[unsafe(no_mangle)]
3125pub unsafe extern "C" fn sochdb_collection_free_u64(ptr: *mut u64, len: usize) {
3126    ffi_guard!({
3127        if ptr.is_null() || len == 0 {
3128            return;
3129        }
3130        unsafe {
3131            let _ = Vec::from_raw_parts(ptr, len, len);
3132        }
3133    })
3134}
3135
3136#[unsafe(no_mangle)]
3137pub unsafe extern "C" fn sochdb_collection_free_f32(ptr: *mut f32, len: usize) {
3138    ffi_guard!({
3139        if ptr.is_null() || len == 0 {
3140            return;
3141        }
3142        unsafe {
3143            let _ = Vec::from_raw_parts(ptr, len, len);
3144        }
3145    })
3146}
3147
3148fn metadata_matches_filter(metadata: &Value, filter: &Value) -> bool {
3149    let filter_obj = match filter.as_object() {
3150        Some(obj) => obj,
3151        None => return true,
3152    };
3153    let metadata_obj = match metadata.as_object() {
3154        Some(obj) => obj,
3155        None => return false,
3156    };
3157
3158    for (key, expected) in filter_obj.iter() {
3159        match metadata_obj.get(key) {
3160            Some(actual) if actual == expected => {}
3161            _ => return false,
3162        }
3163    }
3164
3165    true
3166}
3167
3168/// Search a collection for keywords (simple term match)
3169///
3170/// # Returns
3171/// - >= 0: Number of results
3172/// - -1: Error
3173#[unsafe(no_mangle)]
3174pub unsafe extern "C" fn sochdb_collection_keyword_search(
3175    ptr: *mut DatabasePtr,
3176    namespace: *const c_char,
3177    collection: *const c_char,
3178    query_ptr: *const c_char,
3179    k: usize,
3180    results_out: *mut CSearchResult,
3181) -> c_int {
3182    ffi_guard!({
3183        if ptr.is_null()
3184            || namespace.is_null()
3185            || collection.is_null()
3186            || query_ptr.is_null()
3187            || results_out.is_null()
3188        {
3189            return -1;
3190        }
3191
3192        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3193            Ok(s) => s,
3194            Err(_) => return -1,
3195        };
3196        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
3197            Ok(s) => s,
3198            Err(_) => return -1,
3199        };
3200        let query_str = match unsafe { CStr::from_ptr(query_ptr) }.to_str() {
3201            Ok(s) => s.to_lowercase(),
3202            Err(_) => return -1,
3203        };
3204        let terms: Vec<&str> = query_str.split_whitespace().collect();
3205        if terms.is_empty() {
3206            return 0;
3207        }
3208
3209        let db = unsafe { &(*ptr).0 };
3210        let txn = match db.begin_transaction() {
3211            Ok(t) => t,
3212            Err(_) => return -1,
3213        };
3214
3215        // Scan all vectors in collection (we assume vectors & metadata are stored together)
3216        let prefix = format!("{}/collections/{}/vectors/", ns, col);
3217        let entries = match db.scan(txn, prefix.as_bytes()) {
3218            Ok(e) => e,
3219            Err(_) => {
3220                let _ = db.abort(txn);
3221                return -1;
3222            }
3223        };
3224        let _ = db.commit(txn);
3225
3226        // Score documents based on term frequency
3227        let mut scored: Vec<(f32, String, String)> = Vec::new();
3228
3229        for (_key, value) in entries {
3230            // Parse whole JSON (robust)
3231            let doc: Value = match serde_json::from_slice(&value) {
3232                Ok(v) => v,
3233                Err(_) => continue,
3234            };
3235
3236            // Search in metadata string (includes values)
3237            let metadata_val = doc.get("metadata");
3238            let metadata_str = metadata_val
3239                .map(|v| v.to_string())
3240                .unwrap_or("{}".to_string());
3241
3242            // Also check "content" field if present (fallback compat)
3243            let content_str = doc.get("content").and_then(|v| v.as_str()).unwrap_or("");
3244
3245            // Combine text to search
3246            let search_text = format!("{} {}", metadata_str, content_str).to_lowercase();
3247
3248            let mut score = 0.0;
3249            for term in &terms {
3250                score += search_text.matches(term).count() as f32;
3251            }
3252
3253            if score > 0.0 {
3254                let id = doc
3255                    .get("id")
3256                    .and_then(|v| v.as_str())
3257                    .unwrap_or("")
3258                    .to_string();
3259                if id.is_empty() {
3260                    continue;
3261                }
3262
3263                scored.push((score, id, metadata_str));
3264            }
3265        }
3266
3267        // Sort by score descending
3268        scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
3269
3270        // Return top k
3271        let result_count = scored.len().min(k);
3272        for (i, (score, id, metadata)) in scored.into_iter().take(k).enumerate() {
3273            let c_id = match std::ffi::CString::new(id) {
3274                Ok(s) => s.into_raw(),
3275                Err(_) => ptr::null_mut(),
3276            };
3277            let c_meta = match std::ffi::CString::new(metadata) {
3278                Ok(s) => s.into_raw(),
3279                Err(_) => ptr::null_mut(),
3280            };
3281
3282            unsafe {
3283                (*results_out.add(i)).id_ptr = c_id;
3284                (*results_out.add(i)).score = score;
3285                (*results_out.add(i)).metadata_ptr = c_meta;
3286            }
3287        }
3288
3289        result_count as c_int
3290    })
3291}
3292
3293/// Free a search result
3294#[unsafe(no_mangle)]
3295pub unsafe extern "C" fn sochdb_search_result_free(result: *mut CSearchResult, count: usize) {
3296    ffi_guard!({
3297        if result.is_null() {
3298            return;
3299        }
3300
3301        for i in 0..count {
3302            let r = unsafe { &mut *result.add(i) };
3303            if !r.id_ptr.is_null() {
3304                let _ = unsafe { std::ffi::CString::from_raw(r.id_ptr) };
3305            }
3306            if !r.metadata_ptr.is_null() {
3307                let _ = unsafe { std::ffi::CString::from_raw(r.metadata_ptr) };
3308            }
3309        }
3310    })
3311}
3312
3313// ============================================================================
3314// NEW FFI: Key Existence Check
3315// ============================================================================
3316
3317/// Check if a key exists without retrieving its value.
3318///
3319/// # Returns
3320/// - 1: Key exists
3321/// - 0: Key does not exist
3322/// - -1: Error
3323///
3324/// # Safety
3325/// All pointer arguments must be valid.
3326#[unsafe(no_mangle)]
3327pub unsafe extern "C" fn sochdb_exists(
3328    ptr: *mut DatabasePtr,
3329    handle: C_TxnHandle,
3330    key_ptr: *const u8,
3331    key_len: usize,
3332) -> c_int {
3333    ffi_guard!({
3334        if ptr.is_null() || key_ptr.is_null() {
3335            return -1;
3336        }
3337        let db = unsafe { &(*ptr).0 };
3338        let key = unsafe { slice::from_raw_parts(key_ptr, key_len) };
3339        let txn = TxnHandle {
3340            txn_id: handle.txn_id,
3341            snapshot_ts: handle.snapshot_ts,
3342        };
3343
3344        match db.get(txn, key) {
3345            Ok(Some(_)) => 1,
3346            Ok(None) => 0,
3347            Err(_) => -1,
3348        }
3349    })
3350}
3351
3352// ============================================================================
3353// NEW FFI: Path-based delete and scan
3354// ============================================================================
3355
3356/// Delete a key by path.
3357/// Returns 0 on success, -1 on error.
3358/// # Safety
3359/// All pointer arguments must be valid.
3360#[unsafe(no_mangle)]
3361pub unsafe extern "C" fn sochdb_delete_path(
3362    ptr: *mut DatabasePtr,
3363    handle: C_TxnHandle,
3364    path_ptr: *const c_char,
3365) -> c_int {
3366    ffi_guard!({
3367        if ptr.is_null() || path_ptr.is_null() {
3368            return -1;
3369        }
3370        let db = unsafe { &(*ptr).0 };
3371        let c_str = unsafe { CStr::from_ptr(path_ptr) };
3372        let path_str = match c_str.to_str() {
3373            Ok(s) => s,
3374            Err(_) => return -1,
3375        };
3376        let txn = TxnHandle {
3377            txn_id: handle.txn_id,
3378            snapshot_ts: handle.snapshot_ts,
3379        };
3380
3381        match db.delete_path(txn, path_str) {
3382            Ok(_) => 0,
3383            Err(_) => -1,
3384        }
3385    })
3386}
3387
3388/// Scan keys by path prefix. Returns JSON array of [{"path":"...", "value":"base64..."}]
3389/// Caller must free the returned string with sochdb_free_string.
3390///
3391/// # Safety
3392/// All pointer arguments must be valid.
3393#[unsafe(no_mangle)]
3394pub unsafe extern "C" fn sochdb_scan_path(
3395    ptr: *mut DatabasePtr,
3396    handle: C_TxnHandle,
3397    prefix_ptr: *const c_char,
3398    out_len: *mut usize,
3399) -> *mut c_char {
3400    ffi_guard!({
3401        if ptr.is_null() || prefix_ptr.is_null() || out_len.is_null() {
3402            return ptr::null_mut();
3403        }
3404        let db = unsafe { &(*ptr).0 };
3405        let c_str = unsafe { CStr::from_ptr(prefix_ptr) };
3406        let prefix_str = match c_str.to_str() {
3407            Ok(s) => s,
3408            Err(_) => return ptr::null_mut(),
3409        };
3410        let txn = TxnHandle {
3411            txn_id: handle.txn_id,
3412            snapshot_ts: handle.snapshot_ts,
3413        };
3414
3415        match db.scan_path(txn, prefix_str) {
3416            Ok(pairs) => {
3417                let entries: Vec<String> = pairs
3418                    .iter()
3419                    .map(|(path, value)| {
3420                        let val_str = String::from_utf8_lossy(value);
3421                        format!(r#"{{"path":"{}","value":"{}"}}"#, path, val_str)
3422                    })
3423                    .collect();
3424                let json = format!("[{}]", entries.join(","));
3425                let c_string = match std::ffi::CString::new(json) {
3426                    Ok(s) => s,
3427                    Err(_) => return ptr::null_mut(),
3428                };
3429                unsafe { *out_len = c_string.as_bytes().len() };
3430                c_string.into_raw()
3431            }
3432            Err(_) => ptr::null_mut(),
3433        }
3434    })
3435}
3436
3437// ============================================================================
3438// NEW FFI: Transaction Modes
3439// ============================================================================
3440
3441/// Begin a read-only transaction (no write-set tracking, lower overhead).
3442/// Returns C_TxnHandle. On error, txn_id will be 0.
3443/// # Safety
3444/// ptr must be a valid pointer returned by sochdb_open.
3445#[unsafe(no_mangle)]
3446pub unsafe extern "C" fn sochdb_begin_read_only(ptr: *mut DatabasePtr) -> C_TxnHandle {
3447    ffi_guard!({
3448        if ptr.is_null() {
3449            return C_TxnHandle {
3450                txn_id: 0,
3451                snapshot_ts: 0,
3452            };
3453        }
3454        let db = unsafe { &(*ptr).0 };
3455        match db.begin_read_only() {
3456            Ok(txn) => C_TxnHandle {
3457                txn_id: txn.txn_id,
3458                snapshot_ts: txn.snapshot_ts,
3459            },
3460            Err(_) => C_TxnHandle {
3461                txn_id: 0,
3462                snapshot_ts: 0,
3463            },
3464        }
3465    })
3466}
3467
3468/// Begin a write-only transaction (no snapshot reads, optimized for bulk loads).
3469/// Returns C_TxnHandle. On error, txn_id will be 0.
3470/// # Safety
3471/// ptr must be a valid pointer returned by sochdb_open.
3472#[unsafe(no_mangle)]
3473pub unsafe extern "C" fn sochdb_begin_write_only(ptr: *mut DatabasePtr) -> C_TxnHandle {
3474    ffi_guard!({
3475        if ptr.is_null() {
3476            return C_TxnHandle {
3477                txn_id: 0,
3478                snapshot_ts: 0,
3479            };
3480        }
3481        let db = unsafe { &(*ptr).0 };
3482        match db.begin_write_only() {
3483            Ok(txn) => C_TxnHandle {
3484                txn_id: txn.txn_id,
3485                snapshot_ts: txn.snapshot_ts,
3486            },
3487            Err(_) => C_TxnHandle {
3488                txn_id: 0,
3489                snapshot_ts: 0,
3490            },
3491        }
3492    })
3493}
3494
3495// ============================================================================
3496// NEW FFI: Database Maintenance
3497// ============================================================================
3498
3499/// Gracefully shut down the database, flushing all pending writes.
3500/// After this call the DatabasePtr is still valid but no new operations should be started.
3501/// Returns 0 on success, -1 on error.
3502/// # Safety
3503/// ptr must be a valid pointer returned by sochdb_open.
3504#[unsafe(no_mangle)]
3505pub unsafe extern "C" fn sochdb_shutdown(ptr: *mut DatabasePtr) -> c_int {
3506    ffi_guard!({
3507        if ptr.is_null() {
3508            return -1;
3509        }
3510        let db = unsafe { &(*ptr).0 };
3511        match db.shutdown() {
3512            Ok(_) => 0,
3513            Err(_) => -1,
3514        }
3515    })
3516}
3517
3518/// Force fsync all data to durable storage.
3519/// Returns 0 on success, -1 on error.
3520/// # Safety
3521/// ptr must be a valid pointer returned by sochdb_open.
3522#[unsafe(no_mangle)]
3523pub unsafe extern "C" fn sochdb_fsync(ptr: *mut DatabasePtr) -> c_int {
3524    ffi_guard!({
3525        if ptr.is_null() {
3526            return -1;
3527        }
3528        let db = unsafe { &(*ptr).0 };
3529        match db.fsync() {
3530            Ok(_) => 0,
3531            Err(_) => -1,
3532        }
3533    })
3534}
3535
3536/// Truncate the write-ahead log (WAL) up to the last checkpoint.
3537/// Returns 0 on success, -1 on error.
3538/// # Safety
3539/// ptr must be a valid pointer returned by sochdb_open.
3540#[unsafe(no_mangle)]
3541pub unsafe extern "C" fn sochdb_truncate_wal(ptr: *mut DatabasePtr) -> c_int {
3542    ffi_guard!({
3543        if ptr.is_null() {
3544            return -1;
3545        }
3546        let db = unsafe { &(*ptr).0 };
3547        match db.truncate_wal() {
3548            Ok(_) => 0,
3549            Err(_) => -1,
3550        }
3551    })
3552}
3553
3554/// Run garbage collection (remove dead MVCC versions).
3555/// Returns the number of versions reclaimed, or -1 on error.
3556/// # Safety
3557/// ptr must be a valid pointer returned by sochdb_open.
3558#[unsafe(no_mangle)]
3559pub unsafe extern "C" fn sochdb_gc(ptr: *mut DatabasePtr) -> i64 {
3560    ffi_guard!({
3561        if ptr.is_null() {
3562            return -1;
3563        }
3564        let db = unsafe { &(*ptr).0 };
3565        db.gc() as i64
3566    })
3567}
3568
3569/// Perform a full checkpoint (flush memtable + dirty pages to durable storage).
3570/// Returns the checkpoint LSN on success, or 0 on error.
3571/// # Safety
3572/// ptr must be a valid pointer returned by sochdb_open.
3573#[unsafe(no_mangle)]
3574pub unsafe extern "C" fn sochdb_checkpoint_full(ptr: *mut DatabasePtr) -> u64 {
3575    ffi_guard!({
3576        if ptr.is_null() {
3577            return 0;
3578        }
3579        let db = unsafe { &(*ptr).0 };
3580        match db.checkpoint() {
3581            Ok(lsn) => lsn,
3582            Err(_) => 0,
3583        }
3584    })
3585}
3586
3587/// Extended storage statistics (JSON).
3588/// Returns JSON string with all stats. Caller must free with sochdb_free_string.
3589/// # Safety
3590/// ptr must be a valid pointer returned by sochdb_open.
3591#[unsafe(no_mangle)]
3592pub unsafe extern "C" fn sochdb_stats_json(
3593    ptr: *mut DatabasePtr,
3594    out_len: *mut usize,
3595) -> *mut c_char {
3596    ffi_guard!({
3597        if ptr.is_null() || out_len.is_null() {
3598            return ptr::null_mut();
3599        }
3600        let db = unsafe { &(*ptr).0 };
3601        let storage_stats = db.storage_stats();
3602        let db_stats = db.stats();
3603
3604        let json = format!(
3605            r#"{{"memtable_size_bytes":{},"wal_size_bytes":{},"active_transactions":{},"min_active_snapshot":{},"last_checkpoint_lsn":{},"transactions_started":{},"transactions_committed":{},"transactions_aborted":{},"queries_executed":{},"bytes_written":{},"bytes_read":{}}}"#,
3606            storage_stats.memtable_size_bytes,
3607            storage_stats.wal_size_bytes,
3608            storage_stats.active_transactions,
3609            storage_stats.min_active_snapshot,
3610            storage_stats.last_checkpoint_lsn,
3611            db_stats.transactions_started,
3612            db_stats.transactions_committed,
3613            db_stats.transactions_aborted,
3614            db_stats.queries_executed,
3615            db_stats.bytes_written,
3616            db_stats.bytes_read,
3617        );
3618
3619        let c_string = match std::ffi::CString::new(json) {
3620            Ok(s) => s,
3621            Err(_) => return ptr::null_mut(),
3622        };
3623        unsafe { *out_len = c_string.as_bytes().len() };
3624        c_string.into_raw()
3625    })
3626}
3627
3628/// Get the database file path. Caller must free with sochdb_free_string.
3629/// # Safety
3630/// ptr must be a valid pointer returned by sochdb_open.
3631#[unsafe(no_mangle)]
3632pub unsafe extern "C" fn sochdb_path(ptr: *mut DatabasePtr, out_len: *mut usize) -> *mut c_char {
3633    ffi_guard!({
3634        if ptr.is_null() || out_len.is_null() {
3635            return ptr::null_mut();
3636        }
3637        let db = unsafe { &(*ptr).0 };
3638        let path_str = db.path().to_string_lossy().to_string();
3639        let c_string = match std::ffi::CString::new(path_str) {
3640            Ok(s) => s,
3641            Err(_) => return ptr::null_mut(),
3642        };
3643        unsafe { *out_len = c_string.as_bytes().len() };
3644        c_string.into_raw()
3645    })
3646}
3647
3648// ============================================================================
3649// NEW FFI: Backup & Snapshot Operations
3650// ============================================================================
3651
3652/// Create a backup of the database.
3653/// Returns 0 on success, -1 on error.
3654/// # Safety
3655/// All pointer arguments must be valid C strings.
3656#[unsafe(no_mangle)]
3657pub unsafe extern "C" fn sochdb_backup_create(
3658    ptr: *mut DatabasePtr,
3659    destination: *const c_char,
3660) -> c_int {
3661    ffi_guard!({
3662        if ptr.is_null() || destination.is_null() {
3663            return -1;
3664        }
3665        let db = unsafe { &(*ptr).0 };
3666        let dest = match unsafe { CStr::from_ptr(destination) }.to_str() {
3667            Ok(s) => s,
3668            Err(_) => return -1,
3669        };
3670
3671        // Flush before backup to ensure all data is on disk
3672        let _ = db.flush();
3673
3674        let manager = crate::backup::BackupManager::new(db.path());
3675        match manager.create_backup(dest) {
3676            Ok(_) => 0,
3677            Err(_) => -1,
3678        }
3679    })
3680}
3681
3682/// Restore a database from a backup.
3683/// Returns 0 on success, -1 on error.
3684/// # Safety
3685/// All pointer arguments must be valid C strings.
3686#[unsafe(no_mangle)]
3687pub unsafe extern "C" fn sochdb_backup_restore(
3688    ptr: *mut DatabasePtr,
3689    backup_path: *const c_char,
3690) -> c_int {
3691    ffi_guard!({
3692        if ptr.is_null() || backup_path.is_null() {
3693            return -1;
3694        }
3695        let db = unsafe { &(*ptr).0 };
3696        let path = match unsafe { CStr::from_ptr(backup_path) }.to_str() {
3697            Ok(s) => s,
3698            Err(_) => return -1,
3699        };
3700
3701        let manager = crate::backup::BackupManager::new(db.path());
3702        match manager.restore_backup(path) {
3703            Ok(_) => 0,
3704            Err(_) => -1,
3705        }
3706    })
3707}
3708
3709/// List backups in a directory. Returns JSON array of backup metadata.
3710/// Caller must free the returned string with sochdb_free_string.
3711/// # Safety
3712/// backup_dir must be a valid C string. out_len must be a valid pointer.
3713#[unsafe(no_mangle)]
3714pub unsafe extern "C" fn sochdb_backup_list(
3715    backup_dir: *const c_char,
3716    out_len: *mut usize,
3717) -> *mut c_char {
3718    ffi_guard!({
3719        if backup_dir.is_null() || out_len.is_null() {
3720            return ptr::null_mut();
3721        }
3722        let dir = match unsafe { CStr::from_ptr(backup_dir) }.to_str() {
3723            Ok(s) => s,
3724            Err(_) => return ptr::null_mut(),
3725        };
3726
3727        match crate::backup::BackupManager::list_backups(dir) {
3728            Ok(backups) => {
3729                let entries: Vec<String> = backups
3730                    .iter()
3731                    .map(|b| {
3732                        format!(
3733                            r#"{{"name":"{}","timestamp":"{}","size_bytes":{}}}"#,
3734                            b.generate_name(),
3735                            b.generate_name(),
3736                            0 // BackupMetadata may not expose size, use 0 placeholder
3737                        )
3738                    })
3739                    .collect();
3740                let json = format!("[{}]", entries.join(","));
3741                let c_string = match std::ffi::CString::new(json) {
3742                    Ok(s) => s,
3743                    Err(_) => return ptr::null_mut(),
3744                };
3745                unsafe { *out_len = c_string.as_bytes().len() };
3746                c_string.into_raw()
3747            }
3748            Err(_) => ptr::null_mut(),
3749        }
3750    })
3751}
3752
3753/// Verify a backup is valid and not corrupted.
3754/// Returns 1 if valid, 0 if invalid, -1 on error.
3755/// # Safety
3756/// backup_path must be a valid C string.
3757#[unsafe(no_mangle)]
3758pub unsafe extern "C" fn sochdb_backup_verify(backup_path: *const c_char) -> c_int {
3759    ffi_guard!({
3760        if backup_path.is_null() {
3761            return -1;
3762        }
3763        let path = match unsafe { CStr::from_ptr(backup_path) }.to_str() {
3764            Ok(s) => s,
3765            Err(_) => return -1,
3766        };
3767
3768        match crate::backup::BackupManager::verify_backup(path) {
3769            Ok(true) => 1,
3770            Ok(false) => 0,
3771            Err(_) => -1,
3772        }
3773    })
3774}
3775
3776// ============================================================================
3777// NEW FFI: Graph Operations (delete, neighbors, find_path)
3778// ============================================================================
3779
3780/// Delete a node from the graph overlay.
3781/// Also removes all edges involving this node.
3782/// Returns 0 on success, -1 on error.
3783/// # Safety
3784/// All pointers must be valid C strings.
3785#[unsafe(no_mangle)]
3786pub unsafe extern "C" fn sochdb_graph_delete_node(
3787    ptr: *mut DatabasePtr,
3788    namespace: *const c_char,
3789    node_id: *const c_char,
3790) -> c_int {
3791    ffi_guard!({
3792        if ptr.is_null() || namespace.is_null() || node_id.is_null() {
3793            return -1;
3794        }
3795        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3796            Ok(s) => s,
3797            Err(_) => return -1,
3798        };
3799        let id = match unsafe { CStr::from_ptr(node_id) }.to_str() {
3800            Ok(s) => s,
3801            Err(_) => return -1,
3802        };
3803        let db = unsafe { &(*ptr).0 };
3804
3805        let txn = match db.begin_transaction() {
3806            Ok(t) => t,
3807            Err(_) => return -1,
3808        };
3809
3810        // Delete the node itself
3811        let node_key = format!("_graph/{}/nodes/{}", ns, id);
3812        let _ = db.delete(txn, node_key.as_bytes());
3813
3814        // Delete outgoing edges
3815        let edge_prefix_out = format!("_graph/{}/edges/{}/", ns, id);
3816        if let Ok(edges) = db.scan(txn, edge_prefix_out.as_bytes()) {
3817            for (key, _) in edges {
3818                let _ = db.delete(txn, &key);
3819            }
3820        }
3821
3822        // Delete temporal edges from this node
3823        let temporal_prefix = format!("_graph/{}/temporal/{}/", ns, id);
3824        if let Ok(edges) = db.scan(txn, temporal_prefix.as_bytes()) {
3825            for (key, _) in edges {
3826                let _ = db.delete(txn, &key);
3827            }
3828        }
3829
3830        match db.commit(txn) {
3831            Ok(_) => 0,
3832            Err(_) => -1,
3833        }
3834    })
3835}
3836
3837/// Delete a specific edge from the graph overlay.
3838/// Returns 0 on success, -1 on error.
3839/// # Safety
3840/// All pointers must be valid C strings.
3841#[unsafe(no_mangle)]
3842pub unsafe extern "C" fn sochdb_graph_delete_edge(
3843    ptr: *mut DatabasePtr,
3844    namespace: *const c_char,
3845    from_id: *const c_char,
3846    edge_type: *const c_char,
3847    to_id: *const c_char,
3848) -> c_int {
3849    ffi_guard!({
3850        if ptr.is_null()
3851            || namespace.is_null()
3852            || from_id.is_null()
3853            || edge_type.is_null()
3854            || to_id.is_null()
3855        {
3856            return -1;
3857        }
3858
3859        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3860            Ok(s) => s,
3861            Err(_) => return -1,
3862        };
3863        let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
3864            Ok(s) => s,
3865            Err(_) => return -1,
3866        };
3867        let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
3868            Ok(s) => s,
3869            Err(_) => return -1,
3870        };
3871        let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
3872            Ok(s) => s,
3873            Err(_) => return -1,
3874        };
3875        let db = unsafe { &(*ptr).0 };
3876
3877        let txn = match db.begin_transaction() {
3878            Ok(t) => t,
3879            Err(_) => return -1,
3880        };
3881
3882        let key = format!("_graph/{}/edges/{}/{}/{}", ns, from, etype, to);
3883        match db.delete(txn, key.as_bytes()) {
3884            Ok(_) => match db.commit(txn) {
3885                Ok(_) => 0,
3886                Err(_) => -1,
3887            },
3888            Err(_) => {
3889                let _ = db.abort(txn);
3890                -1
3891            }
3892        }
3893    })
3894}
3895
3896/// Get neighbors of a node. Returns JSON: {"neighbors": [...]}
3897/// direction: 0=outgoing, 1=incoming, 2=both
3898/// edge_type can be null for all edge types.
3899/// Caller must free the returned string with sochdb_free_string.
3900/// # Safety
3901/// All pointers must be valid.
3902#[unsafe(no_mangle)]
3903pub unsafe extern "C" fn sochdb_graph_get_neighbors(
3904    ptr: *mut DatabasePtr,
3905    namespace: *const c_char,
3906    node_id: *const c_char,
3907    direction: u8,
3908    edge_type_filter: *const c_char,
3909    out_len: *mut usize,
3910) -> *mut c_char {
3911    ffi_guard!({
3912        if ptr.is_null() || namespace.is_null() || node_id.is_null() || out_len.is_null() {
3913            return ptr::null_mut();
3914        }
3915        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
3916            Ok(s) => s,
3917            Err(_) => return ptr::null_mut(),
3918        };
3919        let node = match unsafe { CStr::from_ptr(node_id) }.to_str() {
3920            Ok(s) => s,
3921            Err(_) => return ptr::null_mut(),
3922        };
3923        let et_filter = if edge_type_filter.is_null() {
3924            None
3925        } else {
3926            match unsafe { CStr::from_ptr(edge_type_filter) }.to_str() {
3927                Ok(s) => Some(s),
3928                Err(_) => None,
3929            }
3930        };
3931        let db = unsafe { &(*ptr).0 };
3932
3933        let txn = match db.begin_transaction() {
3934            Ok(t) => t,
3935            Err(_) => return ptr::null_mut(),
3936        };
3937
3938        let mut neighbors = Vec::new();
3939
3940        // Outgoing edges (direction 0 or 2)
3941        if direction == 0 || direction == 2 {
3942            let prefix = format!("_graph/{}/edges/{}/", ns, node);
3943            if let Ok(edges) = db.scan(txn, prefix.as_bytes()) {
3944                for (_key, value) in edges {
3945                    if let Ok(edge_str) = std::str::from_utf8(&value) {
3946                        if let Some(filter) = et_filter {
3947                            if !edge_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
3948                                continue;
3949                            }
3950                        }
3951                        // Extract to_id
3952                        if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
3953                            let start = to_pos + r#""to_id":""#.len();
3954                            if let Some(end) = edge_str[start..].find('"') {
3955                                let to_id = &edge_str[start..start + end];
3956                                neighbors.push(format!(
3957                                    r#"{{"node_id":"{}","direction":"outgoing","edge":{}}}"#,
3958                                    to_id, edge_str
3959                                ));
3960                            }
3961                        }
3962                    }
3963                }
3964            }
3965        }
3966
3967        // Incoming edges (direction 1 or 2) — scan ALL edges and filter by to_id
3968        if direction == 1 || direction == 2 {
3969            let all_edges_prefix = format!("_graph/{}/edges/", ns);
3970            if let Ok(edges) = db.scan(txn, all_edges_prefix.as_bytes()) {
3971                for (_key, value) in edges {
3972                    if let Ok(edge_str) = std::str::from_utf8(&value) {
3973                        let has_to = edge_str.contains(&format!(r#""to_id":"{}""#, node));
3974                        if !has_to {
3975                            continue;
3976                        }
3977                        if let Some(filter) = et_filter {
3978                            if !edge_str.contains(&format!(r#""edge_type":"{}""#, filter)) {
3979                                continue;
3980                            }
3981                        }
3982                        if let Some(from_pos) = edge_str.find(r#""from_id":""#) {
3983                            let start = from_pos + r#""from_id":""#.len();
3984                            if let Some(end) = edge_str[start..].find('"') {
3985                                let from_id = &edge_str[start..start + end];
3986                                neighbors.push(format!(
3987                                    r#"{{"node_id":"{}","direction":"incoming","edge":{}}}"#,
3988                                    from_id, edge_str
3989                                ));
3990                            }
3991                        }
3992                    }
3993                }
3994            }
3995        }
3996
3997        let _ = db.commit(txn);
3998
3999        let json = format!(r#"{{"neighbors":[{}]}}"#, neighbors.join(","));
4000        let c_string = match std::ffi::CString::new(json) {
4001            Ok(s) => s,
4002            Err(_) => return ptr::null_mut(),
4003        };
4004        unsafe { *out_len = c_string.as_bytes().len() };
4005        c_string.into_raw()
4006    })
4007}
4008
4009/// Find shortest path between two nodes using BFS.
4010/// Returns JSON: {"path": ["node1","node2",...], "edges": [...]}
4011/// Returns null if no path found.
4012/// Caller must free the returned string with sochdb_free_string.
4013/// # Safety
4014/// All pointers must be valid.
4015#[unsafe(no_mangle)]
4016pub unsafe extern "C" fn sochdb_graph_find_path(
4017    ptr: *mut DatabasePtr,
4018    namespace: *const c_char,
4019    from_node: *const c_char,
4020    to_node: *const c_char,
4021    max_depth: usize,
4022    out_len: *mut usize,
4023) -> *mut c_char {
4024    ffi_guard!({
4025        if ptr.is_null()
4026            || namespace.is_null()
4027            || from_node.is_null()
4028            || to_node.is_null()
4029            || out_len.is_null()
4030        {
4031            return ptr::null_mut();
4032        }
4033        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4034            Ok(s) => s,
4035            Err(_) => return ptr::null_mut(),
4036        };
4037        let from = match unsafe { CStr::from_ptr(from_node) }.to_str() {
4038            Ok(s) => s,
4039            Err(_) => return ptr::null_mut(),
4040        };
4041        let to = match unsafe { CStr::from_ptr(to_node) }.to_str() {
4042            Ok(s) => s,
4043            Err(_) => return ptr::null_mut(),
4044        };
4045        let db = unsafe { &(*ptr).0 };
4046
4047        let txn = match db.begin_transaction() {
4048            Ok(t) => t,
4049            Err(_) => return ptr::null_mut(),
4050        };
4051
4052        // BFS with parent tracking
4053        let mut visited: std::collections::HashMap<String, (String, String)> =
4054            std::collections::HashMap::new(); // node -> (parent, edge_json)
4055        let mut queue: std::collections::VecDeque<(String, usize)> =
4056            std::collections::VecDeque::new();
4057        queue.push_back((from.to_string(), 0));
4058        visited.insert(from.to_string(), ("".to_string(), "".to_string()));
4059        let mut found = false;
4060
4061        while let Some((current, depth)) = queue.pop_front() {
4062            if current == to {
4063                found = true;
4064                break;
4065            }
4066            if depth >= max_depth {
4067                continue;
4068            }
4069
4070            let prefix = format!("_graph/{}/edges/{}/", ns, current);
4071            if let Ok(edges) = db.scan(txn, prefix.as_bytes()) {
4072                for (_key, value) in edges {
4073                    if let Ok(edge_str) = std::str::from_utf8(&value) {
4074                        if let Some(to_pos) = edge_str.find(r#""to_id":""#) {
4075                            let start = to_pos + r#""to_id":""#.len();
4076                            if let Some(end) = edge_str[start..].find('"') {
4077                                let next = &edge_str[start..start + end];
4078                                if !visited.contains_key(next) {
4079                                    visited.insert(
4080                                        next.to_string(),
4081                                        (current.clone(), edge_str.to_string()),
4082                                    );
4083                                    queue.push_back((next.to_string(), depth + 1));
4084                                }
4085                            }
4086                        }
4087                    }
4088                }
4089            }
4090        }
4091
4092        let _ = db.commit(txn);
4093
4094        if !found {
4095            return ptr::null_mut();
4096        }
4097
4098        // Reconstruct path
4099        let mut path = Vec::new();
4100        let mut edges = Vec::new();
4101        let mut current = to.to_string();
4102        while !current.is_empty() {
4103            path.push(format!(r#""{}""#, current));
4104            if let Some((parent, edge)) = visited.get(&current) {
4105                if !edge.is_empty() {
4106                    edges.push(edge.clone());
4107                }
4108                current = parent.clone();
4109            } else {
4110                break;
4111            }
4112        }
4113        path.reverse();
4114        edges.reverse();
4115
4116        let json = format!(
4117            r#"{{"path":[{}],"edges":[{}]}}"#,
4118            path.join(","),
4119            edges.join(",")
4120        );
4121        let c_string = match std::ffi::CString::new(json) {
4122            Ok(s) => s,
4123            Err(_) => return ptr::null_mut(),
4124        };
4125        unsafe { *out_len = c_string.as_bytes().len() };
4126        c_string.into_raw()
4127    })
4128}
4129
4130// ============================================================================
4131// NEW FFI: End Temporal Edge (set valid_until)
4132// ============================================================================
4133
4134/// End a temporal edge by setting its valid_until to the current time.
4135/// Returns 0 on success, -1 on error, 1 if edge not found.
4136/// # Safety
4137/// All pointers must be valid C strings.
4138#[unsafe(no_mangle)]
4139pub unsafe extern "C" fn sochdb_end_temporal_edge(
4140    ptr: *mut DatabasePtr,
4141    namespace: *const c_char,
4142    from_id: *const c_char,
4143    edge_type: *const c_char,
4144    to_id: *const c_char,
4145) -> c_int {
4146    ffi_guard!({
4147        if ptr.is_null()
4148            || namespace.is_null()
4149            || from_id.is_null()
4150            || edge_type.is_null()
4151            || to_id.is_null()
4152        {
4153            return -1;
4154        }
4155        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4156            Ok(s) => s,
4157            Err(_) => return -1,
4158        };
4159        let from = match unsafe { CStr::from_ptr(from_id) }.to_str() {
4160            Ok(s) => s,
4161            Err(_) => return -1,
4162        };
4163        let etype = match unsafe { CStr::from_ptr(edge_type) }.to_str() {
4164            Ok(s) => s,
4165            Err(_) => return -1,
4166        };
4167        let to = match unsafe { CStr::from_ptr(to_id) }.to_str() {
4168            Ok(s) => s,
4169            Err(_) => return -1,
4170        };
4171        let db = unsafe { &(*ptr).0 };
4172
4173        let txn = match db.begin_transaction() {
4174            Ok(t) => t,
4175            Err(_) => return -1,
4176        };
4177
4178        // Scan for temporal edges matching from/type/to
4179        let prefix = format!("_graph/{}/temporal/{}/{}/{}/", ns, from, etype, to);
4180        let edges = match db.scan(txn, prefix.as_bytes()) {
4181            Ok(e) => e,
4182            Err(_) => {
4183                let _ = db.abort(txn);
4184                return -1;
4185            }
4186        };
4187
4188        let now = std::time::SystemTime::now()
4189            .duration_since(std::time::UNIX_EPOCH)
4190            .unwrap()
4191            .as_millis() as u64;
4192
4193        let mut found = false;
4194        for (key, value) in edges {
4195            if let Ok(val_str) = std::str::from_utf8(&value) {
4196                // Only end edges that are currently active (valid_until == 0)
4197                if val_str.contains(r#""valid_until":0"#) {
4198                    let new_val =
4199                        val_str.replace(r#""valid_until":0"#, &format!(r#""valid_until":{}"#, now));
4200                    if db.put(txn, &key, new_val.as_bytes()).is_ok() {
4201                        found = true;
4202                    }
4203                }
4204            }
4205        }
4206
4207        match db.commit(txn) {
4208            Ok(_) => {
4209                if found {
4210                    0
4211                } else {
4212                    1
4213                }
4214            }
4215            Err(_) => -1,
4216        }
4217    })
4218}
4219
4220// ============================================================================
4221// NEW FFI: Cache Management (delete, clear, stats)
4222// ============================================================================
4223
4224/// Delete a specific entry from the semantic cache.
4225/// Returns 0 on success, 1 if not found, -1 on error.
4226/// # Safety
4227/// All pointers must be valid C strings.
4228#[unsafe(no_mangle)]
4229pub unsafe extern "C" fn sochdb_cache_delete(
4230    ptr: *mut DatabasePtr,
4231    cache_name: *const c_char,
4232    key: *const c_char,
4233) -> c_int {
4234    ffi_guard!({
4235        if ptr.is_null() || cache_name.is_null() || key.is_null() {
4236            return -1;
4237        }
4238        let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
4239            Ok(s) => s,
4240            Err(_) => return -1,
4241        };
4242        let k = match unsafe { CStr::from_ptr(key) }.to_str() {
4243            Ok(s) => s,
4244            Err(_) => return -1,
4245        };
4246        let db = unsafe { &(*ptr).0 };
4247
4248        let txn = match db.begin_transaction() {
4249            Ok(t) => t,
4250            Err(_) => return -1,
4251        };
4252
4253        let key_hash = format!("{:016x}", twox_hash::xxh3::hash64(k.as_bytes()));
4254        let cache_key = format!("_cache/{}/{}", cache, key_hash);
4255
4256        match db.get(txn, cache_key.as_bytes()) {
4257            Ok(Some(_)) => match db.delete(txn, cache_key.as_bytes()) {
4258                Ok(_) => match db.commit(txn) {
4259                    Ok(_) => 0,
4260                    Err(_) => -1,
4261                },
4262                Err(_) => {
4263                    let _ = db.abort(txn);
4264                    -1
4265                }
4266            },
4267            Ok(None) => {
4268                let _ = db.commit(txn);
4269                1
4270            }
4271            Err(_) => {
4272                let _ = db.abort(txn);
4273                -1
4274            }
4275        }
4276    })
4277}
4278
4279/// Clear all entries from a semantic cache.
4280/// Returns number of entries deleted, or -1 on error.
4281/// # Safety
4282/// All pointers must be valid C strings.
4283#[unsafe(no_mangle)]
4284pub unsafe extern "C" fn sochdb_cache_clear(
4285    ptr: *mut DatabasePtr,
4286    cache_name: *const c_char,
4287) -> i64 {
4288    ffi_guard!({
4289        if ptr.is_null() || cache_name.is_null() {
4290            return -1;
4291        }
4292        let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
4293            Ok(s) => s,
4294            Err(_) => return -1,
4295        };
4296        let db = unsafe { &(*ptr).0 };
4297
4298        let txn = match db.begin_transaction() {
4299            Ok(t) => t,
4300            Err(_) => return -1,
4301        };
4302
4303        let prefix = format!("_cache/{}/", cache);
4304        let entries = match db.scan(txn, prefix.as_bytes()) {
4305            Ok(e) => e,
4306            Err(_) => {
4307                let _ = db.abort(txn);
4308                return -1;
4309            }
4310        };
4311
4312        let mut count = 0i64;
4313        for (key, _) in &entries {
4314            if db.delete(txn, key).is_ok() {
4315                count += 1;
4316            }
4317        }
4318
4319        match db.commit(txn) {
4320            Ok(_) => count,
4321            Err(_) => -1,
4322        }
4323    })
4324}
4325
4326/// Get cache statistics. Returns JSON string.
4327/// Caller must free with sochdb_free_string.
4328/// # Safety
4329/// All pointers must be valid.
4330#[unsafe(no_mangle)]
4331pub unsafe extern "C" fn sochdb_cache_stats(
4332    ptr: *mut DatabasePtr,
4333    cache_name: *const c_char,
4334    out_len: *mut usize,
4335) -> *mut c_char {
4336    ffi_guard!({
4337        if ptr.is_null() || cache_name.is_null() || out_len.is_null() {
4338            return ptr::null_mut();
4339        }
4340        let cache = match unsafe { CStr::from_ptr(cache_name) }.to_str() {
4341            Ok(s) => s,
4342            Err(_) => return ptr::null_mut(),
4343        };
4344        let db = unsafe { &(*ptr).0 };
4345
4346        let txn = match db.begin_transaction() {
4347            Ok(t) => t,
4348            Err(_) => return ptr::null_mut(),
4349        };
4350
4351        let prefix = format!("_cache/{}/", cache);
4352        let entries = match db.scan(txn, prefix.as_bytes()) {
4353            Ok(e) => e,
4354            Err(_) => {
4355                let _ = db.abort(txn);
4356                return ptr::null_mut();
4357            }
4358        };
4359        let _ = db.commit(txn);
4360
4361        let now = std::time::SystemTime::now()
4362            .duration_since(std::time::UNIX_EPOCH)
4363            .unwrap()
4364            .as_secs();
4365
4366        let total = entries.len();
4367        let mut expired = 0usize;
4368        let mut total_bytes = 0usize;
4369
4370        for (_key, value) in &entries {
4371            total_bytes += value.len();
4372            if let Ok(val_str) = std::str::from_utf8(value) {
4373                if let Some(exp_pos) = val_str.find(r#""expires_at":"#) {
4374                    let exp_start = exp_pos + r#""expires_at":"#.len();
4375                    if let Some(exp_end) = val_str[exp_start..].find('}') {
4376                        let expires_at: u64 =
4377                            val_str[exp_start..exp_start + exp_end].parse().unwrap_or(0);
4378                        if expires_at > 0 && now > expires_at {
4379                            expired += 1;
4380                        }
4381                    }
4382                }
4383            }
4384        }
4385
4386        let json = format!(
4387            r#"{{"cache_name":"{}","total_entries":{},"expired_entries":{},"active_entries":{},"total_bytes":{}}}"#,
4388            cache,
4389            total,
4390            expired,
4391            total - expired,
4392            total_bytes
4393        );
4394        let c_string = match std::ffi::CString::new(json) {
4395            Ok(s) => s,
4396            Err(_) => return ptr::null_mut(),
4397        };
4398        unsafe { *out_len = c_string.as_bytes().len() };
4399        c_string.into_raw()
4400    })
4401}
4402
4403// ============================================================================
4404// NEW FFI: Collection Management (delete, count, list)
4405// ============================================================================
4406
4407/// Delete a vector collection and all its data.
4408/// Returns 0 on success, -1 on error.
4409/// # Safety
4410/// All pointers must be valid C strings.
4411#[unsafe(no_mangle)]
4412pub unsafe extern "C" fn sochdb_collection_delete(
4413    ptr: *mut DatabasePtr,
4414    namespace: *const c_char,
4415    collection: *const c_char,
4416) -> c_int {
4417    ffi_guard!({
4418        if ptr.is_null() || namespace.is_null() || collection.is_null() {
4419            return -1;
4420        }
4421        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4422            Ok(s) => s,
4423            Err(_) => return -1,
4424        };
4425        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
4426            Ok(s) => s,
4427            Err(_) => return -1,
4428        };
4429        let db = unsafe { &(*ptr).0 };
4430
4431        let txn = match db.begin_transaction() {
4432            Ok(t) => t,
4433            Err(_) => return -1,
4434        };
4435
4436        // Delete config
4437        let config_key = format!("{}/_collections/{}", ns, col);
4438        let _ = db.delete(txn, config_key.as_bytes());
4439
4440        // Delete all vectors
4441        let vec_prefix = format!("{}/collections/{}/vectors_bin/", ns, col);
4442        if let Ok(entries) = db.scan(txn, vec_prefix.as_bytes()) {
4443            for (key, _) in entries {
4444                let _ = db.delete(txn, &key);
4445            }
4446        }
4447
4448        // Delete all metadata
4449        let meta_prefix = format!("{}/collections/{}/meta/", ns, col);
4450        if let Ok(entries) = db.scan(txn, meta_prefix.as_bytes()) {
4451            for (key, _) in entries {
4452                let _ = db.delete(txn, &key);
4453            }
4454        }
4455
4456        // Remove from in-memory index registry
4457        let registry = COLLECTION_INDEXES.get_or_init(|| Mutex::new(HashMap::new()));
4458        let key = collection_key(ns, col);
4459        registry.lock().remove(&key);
4460
4461        match db.commit(txn) {
4462            Ok(_) => 0,
4463            Err(_) => -1,
4464        }
4465    })
4466}
4467
4468/// Count vectors in a collection.
4469/// Returns count on success, -1 on error.
4470/// # Safety
4471/// All pointers must be valid C strings.
4472#[unsafe(no_mangle)]
4473pub unsafe extern "C" fn sochdb_collection_count(
4474    ptr: *mut DatabasePtr,
4475    namespace: *const c_char,
4476    collection: *const c_char,
4477) -> i64 {
4478    ffi_guard!({
4479        if ptr.is_null() || namespace.is_null() || collection.is_null() {
4480            return -1;
4481        }
4482        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4483            Ok(s) => s,
4484            Err(_) => return -1,
4485        };
4486        let col = match unsafe { CStr::from_ptr(collection) }.to_str() {
4487            Ok(s) => s,
4488            Err(_) => return -1,
4489        };
4490        let db = unsafe { &(*ptr).0 };
4491
4492        let txn = match db.begin_transaction() {
4493            Ok(t) => t,
4494            Err(_) => return -1,
4495        };
4496
4497        let meta_prefix = format!("{}/collections/{}/meta/", ns, col);
4498        match db.scan(txn, meta_prefix.as_bytes()) {
4499            Ok(entries) => {
4500                let count = entries.len() as i64;
4501                let _ = db.commit(txn);
4502                count
4503            }
4504            Err(_) => {
4505                let _ = db.abort(txn);
4506                -1
4507            }
4508        }
4509    })
4510}
4511
4512/// List all collections in a namespace. Returns JSON array of collection names.
4513/// Caller must free the returned string with sochdb_free_string.
4514/// # Safety
4515/// All pointers must be valid.
4516#[unsafe(no_mangle)]
4517pub unsafe extern "C" fn sochdb_collection_list(
4518    ptr: *mut DatabasePtr,
4519    namespace: *const c_char,
4520    out_len: *mut usize,
4521) -> *mut c_char {
4522    ffi_guard!({
4523        if ptr.is_null() || namespace.is_null() || out_len.is_null() {
4524            return ptr::null_mut();
4525        }
4526        let ns = match unsafe { CStr::from_ptr(namespace) }.to_str() {
4527            Ok(s) => s,
4528            Err(_) => return ptr::null_mut(),
4529        };
4530        let db = unsafe { &(*ptr).0 };
4531
4532        let txn = match db.begin_transaction() {
4533            Ok(t) => t,
4534            Err(_) => return ptr::null_mut(),
4535        };
4536
4537        let prefix = format!("{}/_collections/", ns);
4538        let entries = match db.scan(txn, prefix.as_bytes()) {
4539            Ok(e) => e,
4540            Err(_) => {
4541                let _ = db.abort(txn);
4542                return ptr::null_mut();
4543            }
4544        };
4545        let _ = db.commit(txn);
4546
4547        let names: Vec<String> = entries
4548            .iter()
4549            .filter_map(|(key, _)| {
4550                let key_str = std::str::from_utf8(key).ok()?;
4551                let name = key_str.strip_prefix(&prefix)?;
4552                Some(format!(r#""{}""#, name))
4553            })
4554            .collect();
4555
4556        let json = format!("[{}]", names.join(","));
4557        let c_string = match std::ffi::CString::new(json) {
4558            Ok(s) => s,
4559            Err(_) => return ptr::null_mut(),
4560        };
4561        unsafe { *out_len = c_string.as_bytes().len() };
4562        c_string.into_raw()
4563    })
4564}
4565
4566// ============================================================================
4567// NEW FFI: Schema / Table Operations
4568// ============================================================================
4569
4570/// List all registered tables. Returns JSON array of table names.
4571/// Caller must free the returned string with sochdb_free_string.
4572/// # Safety
4573/// ptr must be a valid pointer returned by sochdb_open.
4574#[unsafe(no_mangle)]
4575pub unsafe extern "C" fn sochdb_list_tables(
4576    ptr: *mut DatabasePtr,
4577    out_len: *mut usize,
4578) -> *mut c_char {
4579    ffi_guard!({
4580        if ptr.is_null() || out_len.is_null() {
4581            return ptr::null_mut();
4582        }
4583        let db = unsafe { &(*ptr).0 };
4584        let tables = db.list_tables();
4585
4586        let names: Vec<String> = tables.iter().map(|t| format!(r#""{}""#, t)).collect();
4587        let json = format!("[{}]", names.join(","));
4588        let c_string = match std::ffi::CString::new(json) {
4589            Ok(s) => s,
4590            Err(_) => return ptr::null_mut(),
4591        };
4592        unsafe { *out_len = c_string.as_bytes().len() };
4593        c_string.into_raw()
4594    })
4595}
4596
4597/// Get a table schema as JSON. Returns null if table not found.
4598/// Caller must free the returned string with sochdb_free_string.
4599/// # Safety
4600/// ptr and table_name must be valid pointers.
4601#[unsafe(no_mangle)]
4602pub unsafe extern "C" fn sochdb_get_table_schema(
4603    ptr: *mut DatabasePtr,
4604    table_name: *const c_char,
4605    out_len: *mut usize,
4606) -> *mut c_char {
4607    ffi_guard!({
4608        if ptr.is_null() || table_name.is_null() || out_len.is_null() {
4609            return ptr::null_mut();
4610        }
4611        let db = unsafe { &(*ptr).0 };
4612        let name = match unsafe { CStr::from_ptr(table_name) }.to_str() {
4613            Ok(s) => s,
4614            Err(_) => return ptr::null_mut(),
4615        };
4616
4617        match db.get_table_schema(name) {
4618            Some(schema) => {
4619                // Serialize schema to JSON
4620                let columns: Vec<String> = schema
4621                    .columns
4622                    .iter()
4623                    .map(|col| {
4624                        format!(
4625                            r#"{{"name":"{}","type":"{}","nullable":{}}}"#,
4626                            col.name,
4627                            format!("{:?}", col.col_type),
4628                            col.nullable
4629                        )
4630                    })
4631                    .collect();
4632                let json = format!(
4633                    r#"{{"table":"{}","columns":[{}]}}"#,
4634                    schema.name,
4635                    columns.join(",")
4636                );
4637                let c_string = match std::ffi::CString::new(json) {
4638                    Ok(s) => s,
4639                    Err(_) => return ptr::null_mut(),
4640                };
4641                unsafe { *out_len = c_string.as_bytes().len() };
4642                c_string.into_raw()
4643            }
4644            None => ptr::null_mut(),
4645        }
4646    })
4647}
4648
4649// ============================================================================
4650// NEW FFI: Compression Configuration
4651// ============================================================================
4652
4653/// Set compression type for the database.
4654/// compression: 0=None, 1=Lz4, 2=ZstdFast, 3=ZstdMax
4655/// Returns 0 on success, -1 on error.
4656/// # Safety
4657/// ptr must be a valid pointer returned by sochdb_open.
4658#[unsafe(no_mangle)]
4659pub unsafe extern "C" fn sochdb_set_compression(ptr: *mut DatabasePtr, compression: u8) -> c_int {
4660    ffi_guard!({
4661        if ptr.is_null() {
4662            return -1;
4663        }
4664        let db = unsafe { &(*ptr).0 };
4665        let _comp_type = crate::compression::CompressionType::from_u8(compression);
4666
4667        // Store compression preference via KV
4668        let txn = match db.begin_transaction() {
4669            Ok(t) => t,
4670            Err(_) => return -1,
4671        };
4672        let key = b"_config/compression";
4673        let val = format!("{}", compression);
4674        if db.put(txn, key, val.as_bytes()).is_err() {
4675            let _ = db.abort(txn);
4676            return -1;
4677        }
4678        match db.commit(txn) {
4679            Ok(_) => 0,
4680            Err(_) => -1,
4681        }
4682    })
4683}
4684
4685/// Get current compression type.
4686/// Returns 0=None, 1=Lz4, 2=ZstdFast, 3=ZstdMax, 255 on error.
4687/// # Safety
4688/// ptr must be a valid pointer returned by sochdb_open.
4689#[unsafe(no_mangle)]
4690pub unsafe extern "C" fn sochdb_get_compression(ptr: *mut DatabasePtr) -> u8 {
4691    ffi_guard!({
4692        if ptr.is_null() {
4693            return 255;
4694        }
4695        let db = unsafe { &(*ptr).0 };
4696
4697        let txn = match db.begin_transaction() {
4698            Ok(t) => t,
4699            Err(_) => return 255,
4700        };
4701        let key = b"_config/compression";
4702        match db.get(txn, key) {
4703            Ok(Some(val)) => {
4704                let _ = db.commit(txn);
4705                std::str::from_utf8(&val)
4706                    .ok()
4707                    .and_then(|s| s.parse::<u8>().ok())
4708                    .unwrap_or(0)
4709            }
4710            _ => {
4711                let _ = db.commit(txn);
4712                0
4713            }
4714        }
4715    })
4716}
4717
4718// ============================================================================
4719// NEW FFI: SQL Execute
4720// ============================================================================
4721
4722/// Execute a SQL query and return results as JSON.
4723/// Returns JSON: {"columns": [...], "rows": [[...], ...]}
4724/// Caller must free the returned string with sochdb_free_string.
4725/// # Safety
4726/// All pointers must be valid.
4727#[unsafe(no_mangle)]
4728pub unsafe extern "C" fn sochdb_execute_sql(
4729    ptr: *mut DatabasePtr,
4730    handle: C_TxnHandle,
4731    sql_ptr: *const c_char,
4732    out_len: *mut usize,
4733) -> *mut c_char {
4734    ffi_guard!({
4735        if ptr.is_null() || sql_ptr.is_null() || out_len.is_null() {
4736            return ptr::null_mut();
4737        }
4738        let db = unsafe { &(*ptr).0 };
4739        let _sql = match unsafe { CStr::from_ptr(sql_ptr) }.to_str() {
4740            Ok(s) => s,
4741            Err(_) => return ptr::null_mut(),
4742        };
4743        let txn = TxnHandle {
4744            txn_id: handle.txn_id,
4745            snapshot_ts: handle.snapshot_ts,
4746        };
4747
4748        // Use the query builder's path-based interface for SQL
4749        // The Database has an execute method or we can use the query builder
4750        let result = db.query(txn, "").execute();
4751
4752        // For now, return the scan results as JSON
4753        // This is a simplified SQL handler - the real implementation
4754        // should parse SQL and map to appropriate operations
4755        match result {
4756            Ok(qr) => {
4757                let toon = qr.to_toon();
4758                let c_string = match std::ffi::CString::new(toon) {
4759                    Ok(s) => s,
4760                    Err(_) => return ptr::null_mut(),
4761                };
4762                unsafe { *out_len = c_string.as_bytes().len() };
4763                c_string.into_raw()
4764            }
4765            Err(_) => ptr::null_mut(),
4766        }
4767    })
4768}
4769
4770// ============================================================================
4771// NEW FFI: Namespace Management
4772// ============================================================================
4773
4774/// Create a namespace. Returns 0 on success, -1 on error.
4775/// # Safety
4776/// All pointer arguments must be valid C strings.
4777#[unsafe(no_mangle)]
4778pub unsafe extern "C" fn sochdb_namespace_create(
4779    ptr: *mut DatabasePtr,
4780    name: *const c_char,
4781) -> c_int {
4782    ffi_guard!({
4783        if ptr.is_null() || name.is_null() {
4784            return -1;
4785        }
4786        let ns = match unsafe { CStr::from_ptr(name) }.to_str() {
4787            Ok(s) => s,
4788            Err(_) => return -1,
4789        };
4790        let db = unsafe { &(*ptr).0 };
4791
4792        let txn = match db.begin_transaction() {
4793            Ok(t) => t,
4794            Err(_) => return -1,
4795        };
4796
4797        let key = format!("_namespaces/{}", ns);
4798        let now = std::time::SystemTime::now()
4799            .duration_since(std::time::UNIX_EPOCH)
4800            .unwrap()
4801            .as_secs();
4802        let value = format!(r#"{{"name":"{}","created_at":{}}}"#, ns, now);
4803
4804        if db.put(txn, key.as_bytes(), value.as_bytes()).is_err() {
4805            let _ = db.abort(txn);
4806            return -1;
4807        }
4808        match db.commit(txn) {
4809            Ok(_) => 0,
4810            Err(_) => -1,
4811        }
4812    })
4813}
4814
4815/// Delete a namespace and all its data. Returns 0 on success, -1 on error.
4816/// # Safety
4817/// All pointer arguments must be valid C strings.
4818#[unsafe(no_mangle)]
4819pub unsafe extern "C" fn sochdb_namespace_delete(
4820    ptr: *mut DatabasePtr,
4821    name: *const c_char,
4822) -> c_int {
4823    ffi_guard!({
4824        if ptr.is_null() || name.is_null() {
4825            return -1;
4826        }
4827        let ns = match unsafe { CStr::from_ptr(name) }.to_str() {
4828            Ok(s) => s,
4829            Err(_) => return -1,
4830        };
4831        let db = unsafe { &(*ptr).0 };
4832
4833        let txn = match db.begin_transaction() {
4834            Ok(t) => t,
4835            Err(_) => return -1,
4836        };
4837
4838        // Delete namespace metadata
4839        let ns_key = format!("_namespaces/{}", ns);
4840        let _ = db.delete(txn, ns_key.as_bytes());
4841
4842        // Delete all data under namespace prefix
4843        let ns_prefix = format!("{}/", ns);
4844        if let Ok(entries) = db.scan(txn, ns_prefix.as_bytes()) {
4845            for (key, _) in entries {
4846                let _ = db.delete(txn, &key);
4847            }
4848        }
4849
4850        match db.commit(txn) {
4851            Ok(_) => 0,
4852            Err(_) => -1,
4853        }
4854    })
4855}
4856
4857/// List all namespaces. Returns JSON array of namespace names.
4858/// Caller must free the returned string with sochdb_free_string.
4859/// # Safety
4860/// All pointers must be valid.
4861#[unsafe(no_mangle)]
4862pub unsafe extern "C" fn sochdb_namespace_list(
4863    ptr: *mut DatabasePtr,
4864    out_len: *mut usize,
4865) -> *mut c_char {
4866    ffi_guard!({
4867        if ptr.is_null() || out_len.is_null() {
4868            return ptr::null_mut();
4869        }
4870        let db = unsafe { &(*ptr).0 };
4871
4872        let txn = match db.begin_transaction() {
4873            Ok(t) => t,
4874            Err(_) => return ptr::null_mut(),
4875        };
4876
4877        let prefix = b"_namespaces/";
4878        let entries = match db.scan(txn, prefix) {
4879            Ok(e) => e,
4880            Err(_) => {
4881                let _ = db.abort(txn);
4882                return ptr::null_mut();
4883            }
4884        };
4885        let _ = db.commit(txn);
4886
4887        let names: Vec<String> = entries
4888            .iter()
4889            .filter_map(|(key, _)| {
4890                let key_str = std::str::from_utf8(key).ok()?;
4891                let name = key_str.strip_prefix("_namespaces/")?;
4892                Some(format!(r#""{}""#, name))
4893            })
4894            .collect();
4895
4896        let json = format!("[{}]", names.join(","));
4897        let c_string = match std::ffi::CString::new(json) {
4898            Ok(s) => s,
4899            Err(_) => return ptr::null_mut(),
4900        };
4901        unsafe { *out_len = c_string.as_bytes().len() };
4902        c_string.into_raw()
4903    })
4904}