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