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