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}