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}