core, desktop: Db::conn everywhere, Change::default, notes_where, and shared row mappings
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
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>
This commit is contained in:
+69
-89
@@ -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<Vec<Note
|
||||
" ORDER BY pinned DESC, position DESC, updated_at DESC"
|
||||
});
|
||||
|
||||
notes_where(conn, &sql, params_from_iter(binds.iter()))
|
||||
}
|
||||
|
||||
/// The notes whose ids `sql` selects, in its order, each loaded in full. Every list
|
||||
/// the UI reads goes through `load_note`, so a list and the note it opens can never
|
||||
/// disagree about what the note is.
|
||||
fn notes_where<P: Params>(conn: &Connection, sql: &str, params: P) -> rusqlite::Result<Vec<Note>> {
|
||||
let ids: Vec<String> = {
|
||||
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::<rusqlite::Result<Vec<String>>>()?
|
||||
};
|
||||
ids.iter().map(|id| load_note(conn, id)).collect()
|
||||
@@ -452,15 +459,14 @@ pub fn get_note(conn: &Connection, id: &str) -> rusqlite::Result<Note> {
|
||||
}
|
||||
|
||||
pub fn reminders(conn: &Connection) -> rusqlite::Result<Vec<Note>> {
|
||||
let ids: Vec<String> = {
|
||||
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::<rusqlite::Result<Vec<String>>>()?
|
||||
};
|
||||
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<Vec<Due
|
||||
}
|
||||
|
||||
pub fn titles(conn: &Connection) -> rusqlite::Result<Vec<TitleEntry>> {
|
||||
// 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<String> = {
|
||||
let mut stmt = conn.prepare("SELECT id FROM notes WHERE trashed = 0")?;
|
||||
let rows = stmt.query_map([], |r| r.get(0))?;
|
||||
rows.collect::<rusqlite::Result<Vec<String>>>()?
|
||||
};
|
||||
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<Vec<Note>> {
|
||||
let pat = format!("%{}%", escape_like(q));
|
||||
let ids: Vec<String> = {
|
||||
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::<rusqlite::Result<Vec<String>>>()?
|
||||
};
|
||||
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<Label> {
|
||||
Ok(Label {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
color: r.get(2)?,
|
||||
count: Some(r.get(3)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn load_label(conn: &Connection, id: &str) -> rusqlite::Result<Label> {
|
||||
conn.query_row(
|
||||
"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 WHERE l.id = ?1",
|
||||
[id],
|
||||
|r| {
|
||||
Ok(Label {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
color: r.get(2)?,
|
||||
count: Some(r.get(3)?),
|
||||
})
|
||||
},
|
||||
)
|
||||
conn.query_row(&format!("{LABEL_SELECT} WHERE l.id = ?1"), [id], label_row)
|
||||
}
|
||||
|
||||
pub fn list_labels(conn: &Connection) -> rusqlite::Result<Vec<Label>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"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 ORDER BY l.name COLLATE NOCASE",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok(Label {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
color: r.get(2)?,
|
||||
count: Some(r.get(3)?),
|
||||
})
|
||||
})?;
|
||||
let mut stmt = conn.prepare(&format!("{LABEL_SELECT} ORDER BY l.name COLLATE NOCASE"))?;
|
||||
let rows = stmt.query_map([], label_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
@@ -1170,19 +1154,25 @@ pub fn merge_labels(
|
||||
|
||||
// ---- saved filters ----------------------------------------------------------
|
||||
|
||||
const SAVED_FILTER_SELECT: &str = "SELECT id, name, params, position FROM saved_filters";
|
||||
|
||||
/// A stored filter. Params that no longer parse read as no filter at all rather
|
||||
/// than failing the whole list.
|
||||
fn saved_filter_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<SavedFilter> {
|
||||
let params_str: String = r.get(2)?;
|
||||
Ok(SavedFilter {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
params: serde_json::from_str(¶ms_str).unwrap_or_else(|_| json!({})),
|
||||
position: r.get(3)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_saved_filters(conn: &Connection) -> rusqlite::Result<Vec<SavedFilter>> {
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT id, name, params, position FROM saved_filters ORDER BY position ASC, name COLLATE NOCASE")?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
let params_str: String = r.get(2)?;
|
||||
let params = serde_json::from_str(¶ms_str).unwrap_or_else(|_| serde_json::json!({}));
|
||||
Ok(SavedFilter {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
params,
|
||||
position: r.get(3)?,
|
||||
})
|
||||
})?;
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"{SAVED_FILTER_SELECT} ORDER BY position ASC, name COLLATE NOCASE"
|
||||
))?;
|
||||
let rows = stmt.query_map([], saved_filter_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
@@ -1225,19 +1215,9 @@ pub fn rename_saved_filter(
|
||||
params![name, id],
|
||||
)?;
|
||||
conn.query_row(
|
||||
"SELECT id, name, params, position FROM saved_filters WHERE id = ?1",
|
||||
&format!("{SAVED_FILTER_SELECT} WHERE id = ?1"),
|
||||
[id],
|
||||
|r| {
|
||||
let params_str: String = r.get(2)?;
|
||||
let params =
|
||||
serde_json::from_str(¶ms_str).unwrap_or_else(|_| serde_json::json!({}));
|
||||
Ok(SavedFilter {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
params,
|
||||
position: r.get(3)?,
|
||||
})
|
||||
},
|
||||
saved_filter_row,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user