//! The local-first store: on-device SQLite, and the source of truth for every //! client. A client built on this is fully usable with no server and no account. //! //! Framework-free on purpose. The desktop reaches it through Tauri commands and //! Android through uniffi, but neither of those concerns appears in here. pub mod derive; pub mod models; pub mod recur; pub mod retention; pub mod schema; pub mod store; use std::path::Path; use std::sync::Mutex; use rusqlite::Connection; /// The shared database handle. rusqlite connections aren't `Sync`, so a `Mutex` /// serializes access — fine, since operations are quick and a client is single-user. /// How it is held is the caller's business: Tauri manages it as state, Android holds /// it in the uniffi object. pub struct Db(pub Mutex); impl Db { /// Lock the store, reporting a poisoned lock as a message rather than a panic. /// /// Every consumer was writing `db.0.lock().map_err(|e| e.to_string())?` at each /// call site. Beyond the repetition, that spelling forces the caller to NAME /// `rusqlite::Connection` in any helper that returns the guard — which would make /// rusqlite a dependency of a layer whose whole point is not to know what the /// store is made of. Returning it from here means callers can bind the guard by /// inference and never name the type. /// /// A poisoned lock means some earlier call panicked while holding it. The store /// is not necessarily corrupt, but this connection can't be trusted blind, so it /// surfaces as an error the UI can show instead of a second panic. pub fn conn(&self) -> Result, String> { self.0 .lock() .map_err(|_| "the local store lock was poisoned by an earlier panic".to_string()) } } /// Open (creating if needed) the database at `path` and bring it to the latest schema. pub fn open(path: &Path) -> rusqlite::Result { let conn = Connection::open(path)?; schema::migrate(&conn)?; Ok(Db(Mutex::new(conn))) } /// Open a migrated, in-memory store. /// /// Exists so a CONSUMER can test against a real schema without taking a rusqlite /// dependency of its own just to build a `Db` — which is exactly what the desktop /// crate was doing before the core was extracted. The Android bindings will want the /// same thing. pub fn open_in_memory() -> rusqlite::Result { let conn = Connection::open_in_memory()?; schema::migrate(&conn)?; Ok(Db(Mutex::new(conn))) } /// A one-line count summary of the store, for the startup log. pub fn summary(db: &Db) -> String { let conn = match db.0.lock() { Ok(c) => c, Err(_) => return "counts unavailable (lock poisoned)".to_string(), }; let count = |sql: &str| { conn.query_row(sql, [], |r| r.get::<_, i64>(0)) .unwrap_or(-1) }; format!( "{} notes, {} labels", count("SELECT COUNT(*) FROM notes"), count("SELECT COUNT(*) FROM labels"), ) }