From 05c82c2362b564940e1dfb8d667b905e31071382 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 7 Oct 2026 19:57:24 -0400 Subject: [PATCH] core, desktop: Db::conn everywhere, Change::default, notes_where, and shared row mappings 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 --- core/src/local/mod.rs | 9 +- core/src/local/store.rs | 158 +++++++++++------------- core/src/sync/engine.rs | 4 +- core/src/sync/pull.rs | 6 +- core/src/sync/push.rs | 47 ++----- core/src/sync/sharing.rs | 4 +- desktop/src-tauri/src/autosync.rs | 4 +- desktop/src-tauri/src/capture.rs | 4 +- desktop/src-tauri/src/commands/local.rs | 62 +++++----- desktop/src-tauri/src/commands/sync.rs | 10 +- desktop/src-tauri/src/reminders.rs | 2 +- desktop/src-tauri/src/update.rs | 6 +- 12 files changed, 132 insertions(+), 184 deletions(-) diff --git a/core/src/local/mod.rs b/core/src/local/mod.rs index e5c71e7..bd1ce4e 100644 --- a/core/src/local/mod.rs +++ b/core/src/local/mod.rs @@ -26,12 +26,9 @@ 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. + /// 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 diff --git a/core/src/local/store.rs b/core/src/local/store.rs index 456fe15..ef1e9c3 100644 --- a/core/src/local/store.rs +++ b/core/src/local/store.rs @@ -8,7 +8,7 @@ //! up with the values the frontend sends. use chrono::{DateTime, Duration, SecondsFormat, Utc}; -use rusqlite::{params, params_from_iter, Connection, OptionalExtension}; +use rusqlite::{params, params_from_iter, Connection, OptionalExtension, Params}; use serde_json::{json, Value}; use uuid::Uuid; @@ -439,9 +439,16 @@ pub fn list_notes(conn: &Connection, q: &ListQuery) -> rusqlite::Result(conn: &Connection, sql: &str, params: P) -> rusqlite::Result> { let ids: Vec = { - let mut stmt = conn.prepare(&sql)?; - let rows = stmt.query_map(params_from_iter(binds.iter()), |r| r.get::<_, String>(0))?; + let mut stmt = conn.prepare(sql)?; + let rows = stmt.query_map(params, |r| r.get::<_, String>(0))?; rows.collect::>>()? }; ids.iter().map(|id| load_note(conn, id)).collect() @@ -452,15 +459,14 @@ pub fn get_note(conn: &Connection, id: &str) -> rusqlite::Result { } pub fn reminders(conn: &Connection) -> rusqlite::Result> { - let ids: Vec = { - let mut stmt = - // The owner's reminders: a note shared with us neither alerts us nor is ours - // to clear, the same as on the server. - conn.prepare("SELECT id FROM notes WHERE trashed = 0 AND remind_at IS NOT NULL AND permission = 'owner' ORDER BY remind_at ASC")?; - let rows = stmt.query_map([], |r| r.get::<_, String>(0))?; - rows.collect::>>()? - }; - ids.iter().map(|id| load_note(conn, id)).collect() + // The owner's reminders: a note shared with us neither alerts us nor is ours to + // clear, the same as on the server. + notes_where( + conn, + "SELECT id FROM notes WHERE trashed = 0 AND remind_at IS NOT NULL AND permission = 'owner' + ORDER BY remind_at ASC", + [], + ) } /// Reminders due at or before `now_ms`, soonest first, trash excluded. @@ -501,35 +507,24 @@ pub fn due_reminders(conn: &Connection, now_ms: i64) -> rusqlite::Result rusqlite::Result> { - // Names come from `load_note` so the palette and the card can never disagree - // about what a note is called. The command palette reads this; correctness - // beats one query per note at personal scale. - let ids: Vec = { - let mut stmt = conn.prepare("SELECT id FROM notes WHERE trashed = 0")?; - let rows = stmt.query_map([], |r| r.get(0))?; - rows.collect::>>()? - }; - ids.iter() - .map(|id| { - let note = load_note(conn, id)?; - Ok(TitleEntry { - id: note.id, - title: note.display_title, - }) + // The command palette reads this; one query per note is fine at personal scale. + let notes = notes_where(conn, "SELECT id FROM notes WHERE trashed = 0", [])?; + Ok(notes + .into_iter() + .map(|note| TitleEntry { + id: note.id, + title: note.display_title, }) - .collect() + .collect()) } pub fn search(conn: &Connection, q: &str) -> rusqlite::Result> { let pat = format!("%{}%", escape_like(q)); - let ids: Vec = { - let mut stmt = conn.prepare( - "SELECT id FROM notes WHERE trashed = 0 AND body LIKE ?1 ESCAPE '\\' ORDER BY updated_at DESC", - )?; - let rows = stmt.query_map([&pat], |r| r.get::<_, String>(0))?; - rows.collect::>>()? - }; - ids.iter().map(|id| load_note(conn, id)).collect() + notes_where( + conn, + "SELECT id FROM notes WHERE trashed = 0 AND body LIKE ?1 ESCAPE '\\' ORDER BY updated_at DESC", + [&pat], + ) } // ---- notes: write ----------------------------------------------------------- @@ -1031,39 +1026,28 @@ pub fn restore_revision(conn: &Connection, id: &str, rev_id: &str) -> rusqlite:: // ---- labels ----------------------------------------------------------------- +/// A label with how many live notes carry it; `label_row` reads it. +const LABEL_SELECT: &str = "SELECT l.id, l.name, l.color, + (SELECT COUNT(*) FROM note_labels nl JOIN notes n ON n.id = nl.note_id + WHERE nl.label_id = l.id AND n.trashed = 0) + FROM labels l"; + +fn label_row(r: &rusqlite::Row<'_>) -> rusqlite::Result