Skip to main content

oxistore_core/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3
4//! `oxistore-core` — Pure Rust storage primitives for OxiStore.
5//!
6//! This crate provides the foundational traits and error types shared across
7//! all OxiStore backends. It is intentionally dependency-free.
8//!
9//! # Key Traits
10//!
11//! - [`KvStore`] — key-value store with reads, writes, range scans, transactions, and snapshots.
12//! - [`KvTxn`] — explicit write transaction (commit / rollback).
13//! - [`KvSnapshot`] — point-in-time read-only view.
14//! - [`ColumnarStore`] — stub for M2+ columnar storage.
15//! - [`BlobStore`] — stub for M4+ blob storage.
16
17use std::path::Path;
18use std::sync::Arc;
19use std::time::Duration;
20
21/// Typed KV adapter with configurable codec (available with `serde-typed` feature).
22#[cfg(feature = "serde-typed")]
23pub mod typed;
24
25#[cfg(feature = "serde-typed")]
26pub use typed::{JsonCodec, TypedCodec, TypedKvError, TypedKvStore};
27
28/// Errors that can be returned by any OxiStore backend.
29#[derive(Debug, Clone)]
30pub enum StoreError {
31    /// An I/O error occurred at the file-system level.
32    Io(Arc<std::io::Error>),
33    /// The database file is corrupt or in an unrecognized format.
34    Corruption(String),
35    /// The requested key was not found (used when absence is treated as error).
36    NotFound,
37    /// A key was inserted but already exists (reserved for unique-insert APIs).
38    AlreadyExists,
39    /// A write transaction conflicted with a concurrent transaction and must be retried.
40    TxnConflict,
41    /// The store is open in read-only mode and does not accept writes.
42    ReadOnly,
43    /// An operation timed out.
44    Timeout,
45    /// A bounded store or cache has exceeded its capacity limit.
46    CapacityExceeded,
47    /// A compare-and-swap operation failed because the expected value did not
48    /// match the current stored value.
49    CasMismatch,
50    /// The requested key was not found in the store.
51    KeyNotFound,
52    /// The operation is not supported by this backend or configuration.
53    Unsupported(String),
54    /// Any other backend-specific error.
55    Other(String),
56}
57
58impl std::fmt::Display for StoreError {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            StoreError::Io(e) => write!(f, "I/O error: {e}"),
62            StoreError::Corruption(s) => write!(f, "corruption: {s}"),
63            StoreError::NotFound => write!(f, "not found"),
64            StoreError::AlreadyExists => write!(f, "already exists"),
65            StoreError::TxnConflict => write!(f, "transaction conflict"),
66            StoreError::ReadOnly => write!(f, "store is read-only"),
67            StoreError::Timeout => write!(f, "operation timed out"),
68            StoreError::CapacityExceeded => write!(f, "capacity exceeded"),
69            StoreError::CasMismatch => write!(f, "compare-and-swap mismatch"),
70            StoreError::KeyNotFound => write!(f, "key not found"),
71            StoreError::Unsupported(s) => write!(f, "unsupported: {s}"),
72            StoreError::Other(s) => write!(f, "error: {s}"),
73        }
74    }
75}
76
77impl std::error::Error for StoreError {}
78
79impl From<std::io::Error> for StoreError {
80    fn from(e: std::io::Error) -> Self {
81        StoreError::Io(Arc::new(e))
82    }
83}
84
85impl From<String> for StoreError {
86    fn from(s: String) -> Self {
87        StoreError::Other(s)
88    }
89}
90
91/// Compute the exclusive upper-bound key for a prefix scan.
92///
93/// Given a prefix like `b"foo"`, returns `Some(b"fop")` — the first key
94/// that would sort after all keys sharing the prefix.
95///
96/// Returns `None` if the prefix is empty or consists entirely of `0xFF`
97/// bytes (i.e. every key in the store matches the prefix).
98///
99/// # Examples
100///
101/// ```
102/// use oxistore_core::prefix_upper_bound;
103///
104/// assert_eq!(prefix_upper_bound(b"foo"), Some(b"fop".to_vec()));
105/// assert_eq!(prefix_upper_bound(b"ab\xff"), Some(b"ac".to_vec()));
106/// assert_eq!(prefix_upper_bound(b"\xff\xff"), None);
107/// assert_eq!(prefix_upper_bound(b""), None);
108/// ```
109pub fn prefix_upper_bound(prefix: &[u8]) -> Option<Vec<u8>> {
110    if prefix.is_empty() {
111        return None;
112    }
113    // Walk backwards to find the last byte that is not 0xFF.
114    let mut upper = prefix.to_vec();
115    while let Some(&last) = upper.last() {
116        if last == 0xFF {
117            upper.pop();
118        } else {
119            // Increment the last non-0xFF byte.
120            if let Some(b) = upper.last_mut() {
121                *b += 1;
122            }
123            return Some(upper);
124        }
125    }
126    // All bytes were 0xFF — no upper bound.
127    None
128}
129
130/// Encode a TTL as an expiry unix-epoch-milliseconds `u64`.
131///
132/// Adds `ttl` to the current [`std::time::SystemTime`] and returns the
133/// resulting point in time as milliseconds since the Unix epoch.
134///
135/// # Errors
136///
137/// Returns [`StoreError::Other`] if the system clock is before the Unix epoch.
138pub fn expiry_epoch_millis(ttl: Duration) -> Result<u64, StoreError> {
139    std::time::SystemTime::now()
140        .duration_since(std::time::UNIX_EPOCH)
141        .map_err(|e| StoreError::Other(e.to_string()))
142        .map(|d| d.checked_add(ttl).unwrap_or(d).as_millis() as u64)
143}
144
145/// Return `true` if an epoch-milliseconds timestamp is in the past.
146///
147/// A timestamp is considered expired when the current time equals or exceeds
148/// `expiry_millis`.
149#[must_use]
150pub fn is_expired(expiry_millis: u64) -> bool {
151    let now = std::time::SystemTime::now()
152        .duration_since(std::time::UNIX_EPOCH)
153        .map(|d| d.as_millis() as u64)
154        .unwrap_or(0);
155    expiry_millis <= now
156}
157
158/// Core key-value store trait.
159///
160/// All backend implementations (`redb`, `sled`, ...) implement this trait so that
161/// callers can depend only on `oxistore-core` and swap backends via the facade.
162///
163/// # Thread Safety
164///
165/// Implementations are required to be `Send + Sync`; interior mutability
166/// (e.g. via `Mutex`) is the backend's responsibility.
167///
168/// # Construction convention (`open_or_create`)
169///
170/// Each backend is expected to provide a method with the following signature
171/// convention (though it is not enforced by this trait because associated
172/// functions cannot be used through trait objects):
173///
174/// ```text
175/// impl BackendStore {
176///     pub fn open(path: impl AsRef<Path>) -> Result<Self, BackendError>;
177///     pub fn open_in_memory() -> Result<Self, BackendError>;  // for tests
178/// }
179/// ```
180///
181/// Use the `oxistore` facade's `open` and `open_in_memory` functions
182/// for backend-agnostic construction.
183pub trait KvStore: Send + Sync {
184    /// Retrieve the value associated with `key`, or `None` if it is absent.
185    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
186
187    /// Insert or overwrite a key-value pair.
188    fn put(&self, key: &[u8], value: &[u8]) -> Result<(), StoreError>;
189
190    /// Remove a key.  No-op if the key is absent.
191    fn delete(&self, key: &[u8]) -> Result<(), StoreError>;
192
193    /// Retrieve values for multiple keys in a single call.
194    ///
195    /// Returns a `Vec` of `Option<Vec<u8>>` in the same order as `keys`.
196    /// The default implementation calls [`KvStore::get`] for each key
197    /// individually; backends with batch-read support should override for
198    /// better performance.
199    fn get_many(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, StoreError> {
200        keys.iter().map(|k| self.get(k)).collect()
201    }
202
203    /// Retrieve a value as a [`std::borrow::Cow`], avoiding a clone when the
204    /// backend can return a borrowed slice.
205    ///
206    /// The default implementation calls [`KvStore::get`] and wraps the owned
207    /// `Vec<u8>` in `Cow::Owned`.  Backends that can return zero-copy
208    /// references should override this method.
209    fn get_ref<'a>(&'a self, key: &[u8]) -> Result<Option<std::borrow::Cow<'a, [u8]>>, StoreError> {
210        self.get(key).map(|opt| opt.map(std::borrow::Cow::Owned))
211    }
212
213    /// Return `true` if `key` is present in the store.
214    ///
215    /// Default implementation delegates to [`KvStore::get`].
216    fn contains(&self, key: &[u8]) -> Result<bool, StoreError> {
217        Ok(self.get(key)?.is_some())
218    }
219
220    /// Return all key-value pairs whose keys fall within `[lo, hi)`,
221    /// in ascending key order.
222    fn range<'a>(&'a self, lo: &[u8], hi: &[u8]) -> Result<RangeIter<'a>, StoreError>;
223
224    /// Return all key-value pairs whose keys fall within `[lo, hi)`,
225    /// in **descending** key order.
226    ///
227    /// The default implementation delegates to [`KvStore::range`], collects the
228    /// results, and reverses the resulting `Vec`.  Backends that support native
229    /// reverse iteration should override for better performance.
230    fn range_rev<'a>(&'a self, lo: &[u8], hi: &[u8]) -> Result<RangeIter<'a>, StoreError> {
231        let items: Vec<RangeItem> = self.range(lo, hi)?.collect();
232        Ok(Box::new(items.into_iter().rev()))
233    }
234
235    /// Iterate all key-value pairs sharing the given `prefix`, in ascending
236    /// key order.
237    ///
238    /// The default implementation computes the exclusive upper bound from the
239    /// prefix and delegates to [`KvStore::range`].  When the prefix is empty,
240    /// the full store is scanned via [`KvStore::iter`].  When the prefix
241    /// consists entirely of `0xFF` bytes (no upper bound exists), the result
242    /// is obtained via [`KvStore::iter`] filtered to keys that start with the
243    /// prefix.
244    fn prefix_scan<'a>(&'a self, prefix: &[u8]) -> Result<RangeIter<'a>, StoreError> {
245        if prefix.is_empty() {
246            return self.iter();
247        }
248        match prefix_upper_bound(prefix) {
249            Some(hi) => self.range(prefix, &hi),
250            None => {
251                // All-0xFF prefix: no upper bound can be computed.
252                // Collect from iter() and filter to keys that start with the prefix.
253                let prefix_owned = prefix.to_vec();
254                let items: Vec<RangeItem> = self
255                    .iter()?
256                    .filter(|r| {
257                        r.as_ref()
258                            .map(|(k, _)| k.starts_with(&prefix_owned))
259                            .unwrap_or(true) // propagate errors
260                    })
261                    .collect();
262                Ok(Box::new(items.into_iter()))
263            }
264        }
265    }
266
267    /// Insert multiple key-value pairs atomically in a single batch.
268    ///
269    /// The default implementation opens a transaction, inserts all pairs,
270    /// and commits.  Backends may override for better performance.
271    fn batch_write(&self, pairs: &[(&[u8], &[u8])]) -> Result<(), StoreError> {
272        let mut txn = self.transaction()?;
273        for &(k, v) in pairs {
274            txn.put(k, v)?;
275        }
276        txn.commit()
277    }
278
279    /// Delete multiple keys atomically in a single batch.
280    ///
281    /// The default implementation opens a transaction, deletes all keys,
282    /// and commits.  Backends may override for better performance.
283    fn batch_delete(&self, keys: &[&[u8]]) -> Result<(), StoreError> {
284        let mut txn = self.transaction()?;
285        for &k in keys {
286            txn.delete(k)?;
287        }
288        txn.commit()
289    }
290
291    /// Return the total number of keys in the store.
292    ///
293    /// The default implementation performs a full iteration and counts entries.
294    /// Backends that maintain key counts natively should override for O(1).
295    fn count(&self) -> Result<u64, StoreError> {
296        let mut n = 0u64;
297        for item in self.iter()? {
298            let _ = item?;
299            n += 1;
300        }
301        Ok(n)
302    }
303
304    /// Return the approximate byte size of the store on disk.
305    ///
306    /// The default implementation returns 0 (unknown).  Backends should
307    /// override if they can compute the on-disk size cheaply.
308    fn size_on_disk(&self) -> Result<u64, StoreError> {
309        Ok(0)
310    }
311
312    /// Iterate all key-value pairs in the store in ascending key order.
313    ///
314    /// This is a required method -- all backend implementations must provide
315    /// a full-store iteration.
316    fn iter<'a>(&'a self) -> Result<RangeIter<'a>, StoreError>;
317
318    /// Iterate all keys (without loading values) in ascending order.
319    ///
320    /// The default implementation wraps [`KvStore::iter`] and discards values.
321    /// Backends that can iterate keys without reading values should override.
322    fn keys<'a>(&'a self) -> Result<KeysIter<'a>, StoreError> {
323        let it = self.iter()?;
324        Ok(Box::new(it.map(|r| r.map(|(k, _v)| k))))
325    }
326
327    /// Atomic compare-and-swap: if the current value for `key` equals
328    /// `expected`, replace it with `new_value` and return `Ok(true)`.
329    /// If the current value does not match `expected`, return `Ok(false)`.
330    ///
331    /// `expected` is `None` for "key must not exist", `Some(v)` for
332    /// "key must hold value v".
333    ///
334    /// The default implementation uses a transaction for atomicity.
335    fn compare_and_swap(
336        &self,
337        key: &[u8],
338        expected: Option<&[u8]>,
339        new_value: &[u8],
340    ) -> Result<bool, StoreError> {
341        let mut txn = self.transaction()?;
342        let current = txn.get(key)?;
343        let matches = match (current.as_deref(), expected) {
344            (None, None) => true,
345            (Some(cur), Some(exp)) => cur == exp,
346            _ => false,
347        };
348        if matches {
349            txn.put(key, new_value)?;
350            txn.commit()?;
351            Ok(true)
352        } else {
353            txn.rollback()?;
354            Ok(false)
355        }
356    }
357
358    /// Insert a key-value pair with a time-to-live.  After `ttl` has elapsed,
359    /// the key is treated as absent (expired).
360    ///
361    /// Backends that support native TTL should override this method.
362    /// The default implementation returns [`StoreError::Unsupported`].
363    fn put_with_ttl(&self, _key: &[u8], _value: &[u8], _ttl: Duration) -> Result<(), StoreError> {
364        Err(StoreError::Unsupported("TTL not supported".to_string()))
365    }
366
367    /// Set a TTL on an existing key.  The key must already exist.
368    ///
369    /// After `ttl` has elapsed the key is treated as absent.
370    /// The default implementation returns [`StoreError::Unsupported`].
371    fn expire(&self, _key: &[u8], _ttl: Duration) -> Result<(), StoreError> {
372        Err(StoreError::Unsupported("TTL not supported".to_string()))
373    }
374
375    /// Return the remaining TTL for a key.
376    ///
377    /// Returns `Ok(None)` if the key exists but has no TTL attached.
378    /// Returns `Err(StoreError::KeyNotFound)` if the key does not exist.
379    /// Returns `Err(StoreError::Unsupported)` by default.
380    fn ttl(&self, _key: &[u8]) -> Result<Option<Duration>, StoreError> {
381        Err(StoreError::Unsupported("TTL not supported".to_string()))
382    }
383
384    /// Remove the TTL from a key, making it persistent.
385    ///
386    /// Returns `Ok(true)` if the key existed and its TTL was removed,
387    /// `Ok(false)` if the key exists but had no TTL.
388    /// Returns `Err(StoreError::KeyNotFound)` if the key does not exist.
389    /// The default implementation returns [`StoreError::Unsupported`].
390    fn persist(&self, _key: &[u8]) -> Result<bool, StoreError> {
391        Err(StoreError::Unsupported("TTL not supported".to_string()))
392    }
393
394    /// Scan and delete all expired keys eagerly.
395    ///
396    /// Returns the count of keys that were deleted.
397    /// The default implementation is a no-op returning `Ok(0)`.
398    fn purge_expired(&self) -> Result<u64, StoreError> {
399        Ok(0)
400    }
401
402    /// Trigger manual compaction on backends that support it.
403    ///
404    /// The default implementation is a no-op.
405    fn compact(&self) -> Result<(), StoreError> {
406        Ok(())
407    }
408
409    /// Create a point-in-time backup to the given path.
410    ///
411    /// The default implementation returns an error indicating backup is
412    /// not supported.  Backends should override if they support backup.
413    fn backup(&self, _path: &Path) -> Result<(), StoreError> {
414        Err(StoreError::Other(
415            "backup not supported for this backend".to_string(),
416        ))
417    }
418
419    /// Restore from a backup at the given path.
420    ///
421    /// The default implementation returns an error.  Backends should
422    /// override if they support restore.
423    fn restore(&self, _path: &Path) -> Result<(), StoreError> {
424        Err(StoreError::Other(
425            "restore not supported for this backend".to_string(),
426        ))
427    }
428
429    /// Begin an explicit write transaction.
430    ///
431    /// Changes made through [`KvTxn`] are only visible after [`KvTxn::commit`].
432    fn transaction(&self) -> Result<Box<dyn KvTxn + '_>, StoreError>;
433
434    /// Capture a point-in-time read-only snapshot of the store.
435    fn snapshot(&self) -> Result<Box<dyn KvSnapshot + '_>, StoreError>;
436
437    /// Ensure all committed data has been written to durable storage.
438    ///
439    /// The exact semantics depend on the backend; for backends that auto-flush
440    /// (e.g. redb commits), this is a no-op or an advisory hint.
441    fn flush(&self) -> Result<(), StoreError>;
442}
443
444/// An explicit write transaction obtained from [`KvStore::transaction`].
445///
446/// All mutations made through `KvTxn` are buffered until [`KvTxn::commit`] is
447/// called.  Dropping without committing has the same effect as
448/// [`KvTxn::rollback`].
449pub trait KvTxn {
450    /// Read a value from the store within this transaction's view.
451    ///
452    /// Implementations that support read-your-writes should return buffered
453    /// writes that have not yet been committed.
454    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
455
456    /// Stage a key-value insertion in the transaction.
457    fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), StoreError>;
458
459    /// Stage a key deletion in the transaction.
460    fn delete(&mut self, key: &[u8]) -> Result<(), StoreError>;
461
462    /// Check whether `key` exists within this transaction's view.
463    ///
464    /// Default implementation delegates to [`KvTxn::get`].
465    fn contains(&self, key: &[u8]) -> Result<bool, StoreError> {
466        Ok(self.get(key)?.is_some())
467    }
468
469    /// Range scan within the transaction's view.
470    ///
471    /// Implementations supporting read-your-writes should merge buffered
472    /// writes with committed data.  The default implementation returns an
473    /// error indicating range is not supported within transactions.
474    fn range<'a>(&'a self, _lo: &[u8], _hi: &[u8]) -> Result<RangeIter<'a>, StoreError> {
475        Err(StoreError::Other(
476            "range not supported within this transaction type".to_string(),
477        ))
478    }
479
480    /// Commit all staged changes atomically.
481    fn commit(self: Box<Self>) -> Result<(), StoreError>;
482
483    /// Discard all staged changes.
484    fn rollback(self: Box<Self>) -> Result<(), StoreError>;
485}
486
487/// A point-in-time read-only view of the store obtained from [`KvStore::snapshot`].
488pub trait KvSnapshot {
489    /// Read a value from the snapshot.
490    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
491
492    /// Return all key-value pairs whose keys fall within `[lo, hi)`,
493    /// in ascending key order.
494    fn range<'a>(&'a self, lo: &[u8], hi: &[u8]) -> Result<RangeIter<'a>, StoreError>;
495
496    /// Return all key-value pairs sharing the given `prefix`, in ascending
497    /// key order.
498    ///
499    /// Default implementation uses [`prefix_upper_bound`] and delegates to
500    /// [`KvSnapshot::range`].
501    fn prefix_scan<'a>(&'a self, prefix: &[u8]) -> Result<RangeIter<'a>, StoreError> {
502        match prefix_upper_bound(prefix) {
503            Some(hi) => self.range(prefix, &hi),
504            None => {
505                // No upper bound — scan everything.
506                self.range(&[], &[])
507            }
508        }
509    }
510
511    /// Check whether `key` exists in the snapshot.
512    ///
513    /// Default implementation delegates to [`KvSnapshot::get`].
514    fn contains(&self, key: &[u8]) -> Result<bool, StoreError> {
515        Ok(self.get(key)?.is_some())
516    }
517}
518
519/// Stub trait for M2+ columnar store — defined here so facade re-exports remain stable.
520pub trait ColumnarStore: Send + Sync {}
521
522/// Marker trait for blob stores that is compatible with `oxistore-blob::BlobStore`.
523///
524/// This stub is defined here so that the `oxistore` facade can reference it
525/// without depending on the full `oxistore-blob` crate.  The canonical, fully
526/// featured async `BlobStore` trait (with `put`, `get`, `delete`, `head`,
527/// `list`, `exists`, `copy`, `rename`, CAS, streaming, etc.) is defined in
528/// the `oxistore-blob` crate.
529///
530/// Every type that implements `oxistore_blob::BlobStore` automatically
531/// satisfies this marker via a blanket impl in `oxistore-blob`.
532///
533/// # Design note
534///
535/// `oxistore-core` is intentionally dependency-free.  Adding async methods
536/// here would require `bytes` and `tokio`, which contradicts that policy.
537/// The full trait lives in `oxistore-blob`; this marker exists only to keep
538/// facade re-exports stable.
539pub trait BlobStore: Send + Sync {}
540
541// Intentionally no methods here — see `oxistore_blob::BlobStore` for the full API.
542
543/// Convenience alias: a heap-allocated [`KvStore`] with `'static` lifetime.
544///
545/// Returned by `oxistore::open`.
546pub type BoxKvStore = Box<dyn KvStore>;
547
548/// A single item produced by a range scan: a `(key, value)` pair or an error.
549pub type RangeItem = Result<(Vec<u8>, Vec<u8>), StoreError>;
550
551/// A boxed iterator over [`RangeItem`]s with a given lifetime.
552pub type RangeIter<'a> = Box<dyn Iterator<Item = RangeItem> + 'a>;
553
554/// A boxed iterator over keys (without values) with a given lifetime.
555///
556/// Used by [`KvStore::keys`].
557pub type KeysIter<'a> = Box<dyn Iterator<Item = Result<Vec<u8>, StoreError>> + 'a>;
558
559/// Backend-agnostic configuration for opening a store.
560///
561/// Each backend maps the fields it supports and ignores the rest.
562#[derive(Debug, Clone)]
563pub struct StoreConfig {
564    /// Block cache size in bytes (backend-specific interpretation).
565    pub cache_size_bytes: Option<u64>,
566    /// Whether to sync writes to disk on every commit.
567    pub sync_writes: bool,
568    /// Whether to open the store in read-only mode.
569    pub read_only: bool,
570}
571
572impl Default for StoreConfig {
573    fn default() -> Self {
574        StoreConfig {
575            cache_size_bytes: None,
576            sync_writes: true,
577            read_only: false,
578        }
579    }
580}
581
582/// Runtime statistics for a store (reads, writes, cache hits, etc.).
583#[derive(Debug, Clone, Default)]
584pub struct StoreMetrics {
585    /// Total number of `get` calls.
586    pub reads: u64,
587    /// Total number of `put` calls.
588    pub writes: u64,
589    /// Total number of `delete` calls.
590    pub deletes: u64,
591    /// Total bytes read.
592    pub bytes_read: u64,
593    /// Total bytes written.
594    pub bytes_written: u64,
595    /// Cache hit count (if a cache layer is in use).
596    pub cache_hits: u64,
597    /// Cache miss count (if a cache layer is in use).
598    pub cache_misses: u64,
599}
600
601impl StoreMetrics {
602    /// Compute the cache hit rate as a fraction (0.0 to 1.0).
603    ///
604    /// Returns 0.0 if no cache lookups have been performed.
605    #[must_use]
606    pub fn cache_hit_rate(&self) -> f64 {
607        let total = self.cache_hits + self.cache_misses;
608        if total == 0 {
609            0.0
610        } else {
611            self.cache_hits as f64 / total as f64
612        }
613    }
614}
615
616/// Ensure the path's parent directory exists, creating it if necessary.
617///
618/// This helper is used by backend `open` implementations to avoid confusing
619/// "file not found" errors when the parent directory does not exist.
620pub fn ensure_parent_dir(path: &Path) -> Result<(), StoreError> {
621    if let Some(parent) = path.parent() {
622        if !parent.as_os_str().is_empty() {
623            std::fs::create_dir_all(parent)?;
624        }
625    }
626    Ok(())
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632
633    #[test]
634    fn prefix_upper_bound_basic() {
635        assert_eq!(prefix_upper_bound(b"foo"), Some(b"fop".to_vec()));
636    }
637
638    #[test]
639    fn prefix_upper_bound_trailing_ff() {
640        assert_eq!(prefix_upper_bound(b"ab\xff"), Some(b"ac".to_vec()));
641    }
642
643    #[test]
644    fn prefix_upper_bound_all_ff() {
645        assert_eq!(prefix_upper_bound(b"\xff\xff"), None);
646    }
647
648    #[test]
649    fn prefix_upper_bound_empty() {
650        assert_eq!(prefix_upper_bound(b""), None);
651    }
652
653    #[test]
654    fn prefix_upper_bound_single_byte() {
655        assert_eq!(prefix_upper_bound(b"a"), Some(b"b".to_vec()));
656    }
657
658    #[test]
659    fn store_error_display() {
660        assert_eq!(format!("{}", StoreError::NotFound), "not found");
661        assert_eq!(format!("{}", StoreError::ReadOnly), "store is read-only");
662        assert_eq!(format!("{}", StoreError::Timeout), "operation timed out");
663        assert_eq!(
664            format!("{}", StoreError::CapacityExceeded),
665            "capacity exceeded"
666        );
667        assert_eq!(
668            format!("{}", StoreError::CasMismatch),
669            "compare-and-swap mismatch"
670        );
671    }
672
673    #[test]
674    fn store_error_from_string() {
675        let err: StoreError = "test error".to_string().into();
676        assert_eq!(format!("{err}"), "error: test error");
677    }
678
679    #[test]
680    fn store_config_default() {
681        let cfg = StoreConfig::default();
682        assert!(cfg.cache_size_bytes.is_none());
683        assert!(cfg.sync_writes);
684        assert!(!cfg.read_only);
685    }
686
687    #[test]
688    fn store_metrics_hit_rate() {
689        let m = StoreMetrics {
690            cache_hits: 80,
691            cache_misses: 20,
692            ..StoreMetrics::default()
693        };
694        assert!((m.cache_hit_rate() - 0.8).abs() < f64::EPSILON);
695    }
696
697    #[test]
698    fn store_metrics_hit_rate_zero() {
699        let m = StoreMetrics::default();
700        assert!((m.cache_hit_rate()).abs() < f64::EPSILON);
701    }
702
703    // ── core-clone-error ────────────────────────────────────────────────────
704
705    #[test]
706    fn store_error_clone_io() {
707        let original = StoreError::from(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
708        let cloned = original.clone();
709        if let StoreError::Io(arc) = cloned {
710            assert_eq!(arc.kind(), std::io::ErrorKind::NotFound);
711        } else {
712            panic!("expected StoreError::Io after clone");
713        }
714    }
715
716    #[test]
717    fn store_error_clone_non_io_variants() {
718        let variants = [
719            StoreError::NotFound,
720            StoreError::AlreadyExists,
721            StoreError::TxnConflict,
722            StoreError::ReadOnly,
723            StoreError::Timeout,
724            StoreError::CapacityExceeded,
725            StoreError::CasMismatch,
726            StoreError::KeyNotFound,
727            StoreError::Corruption("bad".to_string()),
728            StoreError::Unsupported("nope".to_string()),
729            StoreError::Other("misc".to_string()),
730        ];
731        for v in &variants {
732            let _ = v.clone(); // must not panic
733        }
734    }
735
736    // ── core-range-rev ──────────────────────────────────────────────────────
737
738    /// Minimal in-memory `KvStore` for unit-testing default-method behaviour.
739    struct MemKv(std::sync::Mutex<std::collections::BTreeMap<Vec<u8>, Vec<u8>>>);
740
741    impl MemKv {
742        fn new() -> Self {
743            MemKv(std::sync::Mutex::new(std::collections::BTreeMap::new()))
744        }
745    }
746
747    impl KvStore for MemKv {
748        fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError> {
749            Ok(self.0.lock().unwrap().get(key).cloned())
750        }
751
752        fn put(&self, key: &[u8], value: &[u8]) -> Result<(), StoreError> {
753            self.0.lock().unwrap().insert(key.to_vec(), value.to_vec());
754            Ok(())
755        }
756
757        fn delete(&self, key: &[u8]) -> Result<(), StoreError> {
758            self.0.lock().unwrap().remove(key);
759            Ok(())
760        }
761
762        fn range<'a>(&'a self, lo: &[u8], hi: &[u8]) -> Result<RangeIter<'a>, StoreError> {
763            use std::ops::Bound;
764            let map = self.0.lock().unwrap();
765            let pairs: Vec<RangeItem> = map
766                .range((Bound::Included(lo.to_vec()), Bound::Excluded(hi.to_vec())))
767                .map(|(k, v)| Ok((k.clone(), v.clone())))
768                .collect();
769            Ok(Box::new(pairs.into_iter()))
770        }
771
772        fn iter<'a>(&'a self) -> Result<RangeIter<'a>, StoreError> {
773            let map = self.0.lock().unwrap();
774            let pairs: Vec<RangeItem> = map
775                .iter()
776                .map(|(k, v)| Ok((k.clone(), v.clone())))
777                .collect();
778            Ok(Box::new(pairs.into_iter()))
779        }
780
781        fn transaction(&self) -> Result<Box<dyn KvTxn + '_>, StoreError> {
782            Err(StoreError::Unsupported("no txn in MemKv".to_string()))
783        }
784
785        fn snapshot(&self) -> Result<Box<dyn KvSnapshot + '_>, StoreError> {
786            Err(StoreError::Unsupported("no snapshot in MemKv".to_string()))
787        }
788
789        fn flush(&self) -> Result<(), StoreError> {
790            Ok(())
791        }
792    }
793
794    #[test]
795    fn range_rev_descending_order() {
796        let store = MemKv::new();
797        store.put(b"a", b"1").unwrap();
798        store.put(b"b", b"2").unwrap();
799        store.put(b"c", b"3").unwrap();
800        store.put(b"d", b"4").unwrap();
801
802        // range_rev over [a, e) should yield d, c, b, a in that order.
803        let items: Vec<(Vec<u8>, Vec<u8>)> = store
804            .range_rev(b"a", b"e")
805            .unwrap()
806            .map(|r| r.unwrap())
807            .collect();
808
809        let keys: Vec<&[u8]> = items.iter().map(|(k, _)| k.as_slice()).collect();
810        assert_eq!(keys, vec![b"d", b"c", b"b", b"a"]);
811    }
812
813    #[test]
814    fn range_rev_empty_range() {
815        let store = MemKv::new();
816        store.put(b"x", b"v").unwrap();
817
818        // [z, z) is empty — range_rev should return an empty iterator.
819        let items: Vec<_> = store.range_rev(b"z", b"z").unwrap().collect();
820        assert!(items.is_empty());
821    }
822
823    // ── ensure_parent_dir edge cases ────────────────────────────────────────
824
825    #[test]
826    fn ensure_parent_dir_empty_path() {
827        // Empty path has no parent — should succeed (no-op)
828        let result = ensure_parent_dir(std::path::Path::new("some_file.db"));
829        assert!(result.is_ok());
830    }
831
832    #[test]
833    fn ensure_parent_dir_nested() {
834        use std::process;
835        let tmp = std::env::temp_dir().join(format!("oxistore_ensure_parent_{}", process::id()));
836        let deep = tmp.join("a").join("b").join("file.db");
837        let result = ensure_parent_dir(&deep);
838        assert!(result.is_ok());
839        assert!(deep.parent().expect("has parent").exists());
840        let _ = std::fs::remove_dir_all(&tmp);
841    }
842
843    #[test]
844    fn ensure_parent_dir_already_exists() {
845        let tmp = std::env::temp_dir();
846        let path = tmp.join("existing_check.db");
847        // tmp already exists — should succeed without creating anything new
848        let result = ensure_parent_dir(&path);
849        assert!(result.is_ok());
850    }
851
852    // ── StoreError::from(io::Error) variants ───────────────────────────────
853
854    #[test]
855    fn store_error_from_io_error_variants() {
856        use std::io;
857        let kinds = [
858            io::ErrorKind::NotFound,
859            io::ErrorKind::PermissionDenied,
860            io::ErrorKind::AlreadyExists,
861            io::ErrorKind::WouldBlock,
862            io::ErrorKind::TimedOut,
863        ];
864        for kind in kinds {
865            let io_err = io::Error::new(kind, "test error");
866            let store_err: StoreError = io_err.into();
867            match &store_err {
868                StoreError::Io(arc) => assert_eq!(arc.kind(), kind),
869                other => panic!("expected StoreError::Io, got {other:?}"),
870            }
871        }
872    }
873}