Skip to main content

cratestack_rusqlite/
runtime.rs

1//! `RusqliteRuntime` — the on-device storage handle.
2//!
3//! Owns a single `rusqlite::Connection` behind a `Mutex`. Mobile apps are
4//! single-user; pooling adds binary size and a concurrency story we don't
5//! need. If a future use case wants a pool, swap the `Mutex<Connection>`
6//! for a connection-pool wrapper without touching the delegate code.
7
8use std::path::Path;
9use std::sync::Mutex;
10
11use rusqlite::Connection;
12
13/// Errors surfaced by the on-device runtime. Stays close to `rusqlite::Error`
14/// for now — wrapping in a cratestack-specific variant only when we cross
15/// the FFI boundary (Phase 5).
16#[derive(Debug)]
17pub enum RusqliteError {
18    /// Underlying SQLite error.
19    Sqlite(rusqlite::Error),
20    /// Operation expected exactly one row but got a different count.
21    NotFound,
22    /// Locked or poisoned mutex around the connection.
23    Locked,
24    /// Batch request exceeded the documented per-call item cap.
25    BatchTooLarge { actual: usize, maximum: usize },
26    /// Batch request contained the same primary key twice. The first/duplicate
27    /// indices are surfaced so callers can immediately pinpoint the offender
28    /// in their input list.
29    DuplicateBatchKey { first: usize, duplicate: usize },
30    /// Caller-side input rejected before any SQL ran (e.g. `update_many`
31    /// without a filter, an empty patch set). Distinct from a SQLite-level
32    /// error so callers can surface a fast-fail validation message rather
33    /// than a generic database error.
34    Validation(String),
35}
36
37impl std::fmt::Display for RusqliteError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Sqlite(error) => write!(f, "sqlite error: {error}"),
41            Self::NotFound => write!(f, "not found"),
42            Self::Locked => write!(f, "connection mutex poisoned"),
43            Self::BatchTooLarge { actual, maximum } => {
44                write!(f, "batch size {actual} exceeds maximum of {maximum}",)
45            }
46            Self::DuplicateBatchKey { first, duplicate } => write!(
47                f,
48                "duplicate primary key in batch at positions {first} and {duplicate}",
49            ),
50            Self::Validation(message) => write!(f, "validation error: {message}"),
51        }
52    }
53}
54
55impl std::error::Error for RusqliteError {
56    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57        match self {
58            Self::Sqlite(error) => Some(error),
59            _ => None,
60        }
61    }
62}
63
64impl From<rusqlite::Error> for RusqliteError {
65    fn from(value: rusqlite::Error) -> Self {
66        Self::Sqlite(value)
67    }
68}
69
70/// The on-device storage handle. Cheap to clone via `Arc` at the call site;
71/// the runtime itself is not `Clone` because the underlying connection
72/// shouldn't be silently duplicated.
73pub struct RusqliteRuntime {
74    conn: Mutex<Connection>,
75}
76
77impl RusqliteRuntime {
78    /// Open an in-memory database. Intended for tests; mobile apps will
79    /// almost always use [`Self::open`].
80    pub fn open_in_memory() -> Result<Self, RusqliteError> {
81        let conn = Connection::open_in_memory()?;
82        configure(&conn)?;
83        Ok(Self {
84            conn: Mutex::new(conn),
85        })
86    }
87
88    /// Open or create a database at the given path. Applies the default
89    /// pragmas (foreign keys on, journal mode WAL) appropriate for a
90    /// mobile app's storage characteristics.
91    pub fn open(path: impl AsRef<Path>) -> Result<Self, RusqliteError> {
92        let conn = Connection::open(path)?;
93        configure(&conn)?;
94        Ok(Self {
95            conn: Mutex::new(conn),
96        })
97    }
98
99    /// Run a closure with exclusive access to the underlying connection.
100    /// Use for migrations, multi-statement transactions, and anywhere the
101    /// ORM delegate isn't expressive enough.
102    pub fn with_connection<F, T>(&self, f: F) -> Result<T, RusqliteError>
103    where
104        F: FnOnce(&mut Connection) -> Result<T, RusqliteError>,
105    {
106        let mut guard = self.conn.lock().map_err(|_| RusqliteError::Locked)?;
107        f(&mut guard)
108    }
109}
110
111fn configure(conn: &Connection) -> Result<(), rusqlite::Error> {
112    conn.pragma_update(None, "foreign_keys", "ON")?;
113    // WAL is the right default for an app holding the file open across the
114    // session — better read concurrency, fewer fsync stalls.
115    // pragma_update returns an error if the journal mode pragma isn't
116    // recognised (in-memory connections), so swallow that silently.
117    let _ = conn.pragma_update(None, "journal_mode", "WAL");
118    Ok(())
119}