Skip to main content

overdrive/
shared.rs

1//! Thread-safe wrapper for OverDriveDB
2//!
3//! `OverDriveDB` is not `Send + Sync` by design. Use `SharedDB` when you need
4//! to share a single database across multiple threads (e.g., a web server).
5//!
6//! ## Quick Example
7//!
8//! ```no_run
9//! use overdrive::shared::SharedDB;
10//! use std::thread;
11//!
12//! let db = SharedDB::open("app.odb").unwrap();
13//!
14//! let db2 = db.clone(); // cheaply cloned — same underlying Mutex
15//! let handle = thread::spawn(move || {
16//!     db2.with(|d| {
17//!         d.query("SELECT * FROM users").unwrap()
18//!     }).unwrap()
19//! });
20//!
21//! let result = handle.join().unwrap();
22//! println!("{:?}", result.rows);
23//! ```
24
25use crate::{OverDriveDB, QueryResult};
26use crate::result::{SdkResult, SdkError};
27use serde_json::Value;
28use std::sync::{Arc, Mutex};
29
30/// A thread-safe, cheaply-cloneable handle to an `OverDriveDB`.
31///
32/// Internally wraps the database in an `Arc<Mutex<OverDriveDB>>`.
33/// Multiple `SharedDB` instances pointing to the same database can be
34/// safely sent across threads.
35///
36/// # Locking
37/// Each `.with()` call acquires the mutex for the duration of the closure.
38/// Keep closures short to avoid blocking other threads.
39#[derive(Clone)]
40pub struct SharedDB {
41    inner: Arc<Mutex<OverDriveDB>>,
42}
43
44impl SharedDB {
45    /// Open (or create) a database wrapped in a thread-safe handle.
46    ///
47    /// File permissions are hardened automatically on open.
48    pub fn open(path: &str) -> SdkResult<Self> {
49        let db = OverDriveDB::open(path)?;
50        Ok(Self {
51            inner: Arc::new(Mutex::new(db)),
52        })
53    }
54
55    /// Open with an encryption key from an environment variable.
56    ///
57    /// ```no_run
58    /// use overdrive::shared::SharedDB;
59    /// // $env:ODB_KEY="my-aes-256-key"
60    /// let db = SharedDB::open_encrypted("app.odb", "ODB_KEY").unwrap();
61    /// ```
62    pub fn open_encrypted(path: &str, key_env_var: &str) -> SdkResult<Self> {
63        let db = OverDriveDB::open_encrypted(path, key_env_var)?;
64        Ok(Self {
65            inner: Arc::new(Mutex::new(db)),
66        })
67    }
68
69    /// Execute a closure with exclusive access to the database.
70    ///
71    /// The mutex is acquired for the duration of `f` and released when it returns.
72    /// Returns `SecurityError` if the mutex has been poisoned by a panicking thread.
73    ///
74    /// ```ignore
75    /// let count = db.with(|d| d.count("users")).unwrap();
76    /// ```
77    pub fn with<F, T>(&self, f: F) -> SdkResult<T>
78    where
79        F: FnOnce(&mut OverDriveDB) -> T,
80    {
81        let mut guard = self.inner.lock().map_err(|_| {
82            SdkError::SecurityError(
83                "SharedDB mutex is poisoned — a thread panicked while holding the lock. \
84                 Create a new SharedDB instance to recover.".to_string()
85            )
86        })?;
87        Ok(f(&mut guard))
88    }
89
90    /// Convenience: execute an SQL query.
91    pub fn query(&self, sql: &str) -> SdkResult<QueryResult> {
92        self.with(|db| db.query(sql))?
93    }
94
95    /// Convenience: execute a safe parameterized SQL query.
96    ///
97    /// See `OverDriveDB::query_safe()` for full documentation.
98    pub fn query_safe(&self, sql_template: &str, params: &[&str]) -> SdkResult<QueryResult> {
99        self.with(|db| db.query_safe(sql_template, params))?
100    }
101
102    /// Convenience: insert a document into a table.
103    pub fn insert(&self, table: &str, doc: &Value) -> SdkResult<String> {
104        self.with(|db| db.insert(table, doc))?
105    }
106
107    /// Convenience: get a document by `_id`.
108    pub fn get(&self, table: &str, id: &str) -> SdkResult<Option<Value>> {
109        self.with(|db| db.get(table, id))?
110    }
111
112    /// Convenience: create a backup at `dest_path`.
113    pub fn backup(&self, dest_path: &str) -> SdkResult<()> {
114        self.with(|db| db.backup(dest_path))?
115    }
116
117    /// Convenience: sync to disk.
118    pub fn sync(&self) -> SdkResult<()> {
119        self.with(|db| db.sync())?
120    }
121
122    /// Number of `SharedDB` handles pointing to this same database (Arc strong count).
123    pub fn handle_count(&self) -> usize {
124        Arc::strong_count(&self.inner)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_shared_db_clone_count() {
134        // We can't open a real DB in unit tests without the native lib,
135        // but we can verify the Arc count logic compiles and works with a mock.
136        // Real integration tests go in ../examples/
137        let _ = std::marker::PhantomData::<SharedDB>;
138    }
139}