Fix FTS search for path punctuation like /idobkin.
Sanitize MATCH tokens so slash/dot queries no longer error and leave a stale full history list; clear the UI on search failure.
This commit is contained in:
+57
-2
@@ -58,6 +58,26 @@ pub struct Database {
|
||||
pub conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
/// Turn free-form user input into a safe FTS5 MATCH expression.
|
||||
///
|
||||
/// FTS5 treats `/`, `.`, `"`, and boolean keywords (`AND`/`OR`/`NOT`) as
|
||||
/// syntax — raw `query + "*"` blows up on paths like `/idobkin` and leaves
|
||||
/// the UI stuck on the previous unfiltered list. We keep alphanumeric
|
||||
/// (plus `_`) tokens, quote them, and apply a prefix `*` to each so typing
|
||||
/// mid-phrase still works.
|
||||
fn build_fts_query(raw: &str) -> Option<String> {
|
||||
let tokens: Vec<String> = raw
|
||||
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
|
||||
.filter(|t| !t.is_empty())
|
||||
.map(|t| format!("\"{}\"*", t.replace('"', "\"\"")))
|
||||
.collect();
|
||||
if tokens.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tokens.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new() -> SqlResult<Self> {
|
||||
let db_path = Self::db_path();
|
||||
@@ -332,10 +352,11 @@ impl Database {
|
||||
}
|
||||
|
||||
pub fn search_entries(&self, query: &str, limit: i64) -> SqlResult<Vec<ClipboardEntry>> {
|
||||
let Some(fts_query) = build_fts_query(query) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let limit = limit.clamp(1, DISPLAY_LIMIT);
|
||||
// FTS5 match query — prefix search with *
|
||||
let fts_query = format!("{}*", query.replace('"', "\"\""));
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT e.id, e.content, e.content_type, e.created_at, e.pinned, e.content_hash, e.thumbnail
|
||||
FROM clipboard_entries e
|
||||
@@ -643,6 +664,40 @@ mod tests {
|
||||
assert_eq!(db.search_entries("searchable", 100).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_fts_query_strips_path_punctuation() {
|
||||
assert_eq!(build_fts_query("/idobkin").as_deref(), Some("\"idobkin\"*"));
|
||||
assert_eq!(
|
||||
build_fts_query("github.com/idobkin").as_deref(),
|
||||
Some("\"github\"* \"com\"* \"idobkin\"*")
|
||||
);
|
||||
assert_eq!(build_fts_query(" /// ").as_deref(), None);
|
||||
assert_eq!(build_fts_query("AND OR").as_deref(), Some("\"AND\"* \"OR\"*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_accepts_leading_slash_and_dots() {
|
||||
let db = test_db();
|
||||
db.insert_entry("user /idobkin on github", "text", "h1", None)
|
||||
.unwrap();
|
||||
db.insert_entry("unrelated Synonyms", "text", "h2", None)
|
||||
.unwrap();
|
||||
db.insert_entry("visit https://github.com/idobkin/repo", "text", "h3", None)
|
||||
.unwrap();
|
||||
|
||||
let slash = db.search_entries("/idobkin", 100).unwrap();
|
||||
assert_eq!(slash.len(), 2);
|
||||
assert!(slash.iter().all(|e| e.content.contains("idobkin")));
|
||||
|
||||
let dotted = db.search_entries("github.com", 100).unwrap();
|
||||
assert_eq!(dotted.len(), 1);
|
||||
assert!(dotted[0].content.contains("github.com"));
|
||||
|
||||
// Must not error (FTS syntax) — punctuation-only → empty.
|
||||
assert!(db.search_entries("///", 100).unwrap().is_empty());
|
||||
assert!(db.search_entries("AND", 100).unwrap().is_empty());
|
||||
}
|
||||
|
||||
// ── Trim ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user