Skip to main content

sochdb_storage/
ffi.rs

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