Core: tag names fold every letter, as the server does (#5385)
SQLite's lower() folds ASCII only, so a device could hold "Café" and "CAFÉ" as two tags while the server held one. derive::fold (to_lowercase) is now the one comparison. It is registered as the deterministic SQL function fold() on every connection (schema::migrate), and find_or_create, the rename clash, the pull clash and the unique index all use it. derive's push_unique used eq_ignore_ascii_case and now folds the same way. v14 merges pairs a device already holds before rebuilding the index. The older row survives, as in rename_label. Memberships move with via_tag kept, affected notes go dirty, and the merged-away row is a pending delete. It is written out rather than calling store::merge_labels so the migration doesn't depend on store code that later versions may change. rusqlite gains its "functions" feature (operator-approved, 2026-10-08). grammar.json gains "#café and #CAFÉ" -> ["café"], which all three implementations run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+14
-2
@@ -248,7 +248,7 @@ fn touch(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
fn find_or_create_label(conn: &Connection, name: &str) -> rusqlite::Result<String> {
|
||||
let existing: Option<String> = conn
|
||||
.query_row(
|
||||
"SELECT id FROM labels WHERE lower(name) = lower(?1)",
|
||||
"SELECT id FROM labels WHERE fold(name) = fold(?1)",
|
||||
[name],
|
||||
|r| r.get(0),
|
||||
)
|
||||
@@ -1039,7 +1039,7 @@ pub fn create_label(conn: &Connection, name: &str) -> rusqlite::Result<Label> {
|
||||
pub fn rename_label(conn: &Connection, id: &str, name: &str) -> rusqlite::Result<Label> {
|
||||
let clash: Option<(String, String)> = conn
|
||||
.query_row(
|
||||
"SELECT id, created_at FROM labels WHERE lower(name) = lower(?1) AND id <> ?2",
|
||||
"SELECT id, created_at FROM labels WHERE fold(name) = fold(?1) AND id <> ?2",
|
||||
params![name, id],
|
||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||
)
|
||||
@@ -1580,6 +1580,18 @@ mod tests {
|
||||
assert!(edited.labels.is_empty());
|
||||
}
|
||||
|
||||
/// Every letter folds, accented ones included, as on the server (#5385). With
|
||||
/// SQLite's lower() the second note minted a "CAFÉ" tag of its own.
|
||||
#[test]
|
||||
fn a_tag_spelled_with_different_accented_capitals_is_the_same_tag() {
|
||||
let conn = db();
|
||||
let first = note(&conn, "coffee at #Café");
|
||||
let second = note(&conn, "coffee at #CAFÉ");
|
||||
assert_eq!(second.labels.len(), 1);
|
||||
assert_eq!(second.labels[0].id, first.labels[0].id);
|
||||
assert_eq!(list_labels(&conn).expect("labels").len(), 1);
|
||||
}
|
||||
|
||||
// ---- writes ----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user