Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Web typecheck and unit tests (push) Successful in 16s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 15s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Successful in 1m22s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m6s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m20s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m21s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Canceled after 12m20s
From the audit (#5179, core and desktop half). - Every `db.0.lock().map_err(|e| e.to_string())?` (about 50 sites in core and the desktop) is now `db.conn()?`. The few sites that deliberately handle a poisoned lock differently, and the tests, keep their own spelling. - push::Change derives Default, so its four constructors name only the fields they set. - store: list_notes, reminders, titles and search share notes_where (ids from a query, each loaded through load_note). Labels share LABEL_SELECT/label_row, and saved filters share SAVED_FILTER_SELECT/saved_filter_row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.8 KiB
Rust
78 lines
2.8 KiB
Rust
//! 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 portable;
|
|
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<Connection>);
|
|
|
|
impl Db {
|
|
/// Lock the store, reporting a poisoned lock as a message rather than a panic.
|
|
///
|
|
/// The one way every caller takes the lock. Returning the guard from here lets a
|
|
/// caller bind it by inference without naming `rusqlite::Connection`, so layers
|
|
/// above the store need not depend on what it is made of.
|
|
///
|
|
/// 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<std::sync::MutexGuard<'_, Connection>, 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<Db> {
|
|
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<Db> {
|
|
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"),
|
|
)
|
|
}
|