Add location filter to library search

LibraryFilter.location does a case-insensitive substring match against
the stored location (e.g. "Box 1"), mirroring the existing text filter.
Tested for substring/case-insensitivity and the empty-string no-op case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 21:34:58 -05:00
parent e57c3dec51
commit 436975f64f
2 changed files with 42 additions and 0 deletions

View File

@@ -81,6 +81,10 @@ impl Catalog {
);
vals.push(Value::Integer(deck_id));
}
if let Some(loc) = filter.location.as_ref().map(|l| l.trim()).filter(|l| !l.is_empty()) {
sql.push_str(" AND LOWER(lc.location) LIKE ?");
vals.push(Value::Text(format!("%{}%", loc.to_lowercase())));
}
sql.push_str(" ORDER BY oc.name COLLATE NOCASE");
@@ -226,4 +230,39 @@ mod tests {
}).unwrap());
assert_eq!(in_deck, vec!["Lightning Bolt"]);
}
#[test]
fn location_filter_is_case_insensitive_substring() {
let fx = owned_fixture();
let cat = &fx.catalog;
cat.set_location("lightning-bolt", "Box 1").unwrap();
cat.set_location("sol-ring", "Box 12").unwrap();
cat.set_location("serra-angel", "Binder A").unwrap();
// llanowar-elves and deathrite-shaman are left with no location.
let box_matches = names(&cat.search_library(&LibraryFilter {
location: Some("box".into()),
..Default::default()
}).unwrap());
assert_eq!(box_matches, vec!["Lightning Bolt", "Sol Ring"]);
let exact = names(&cat.search_library(&LibraryFilter {
location: Some("Box 1".into()),
..Default::default()
}).unwrap());
assert_eq!(exact, vec!["Lightning Bolt", "Sol Ring"]); // substring, so "Box 1" ⊆ "Box 12" too
let none = cat.search_library(&LibraryFilter {
location: Some("nonexistent".into()),
..Default::default()
}).unwrap();
assert!(none.is_empty());
// Empty/whitespace location text is treated as "no filter".
let all = cat.search_library(&LibraryFilter {
location: Some(" ".into()),
..Default::default()
}).unwrap();
assert_eq!(all.len(), 5);
}
}

View File

@@ -181,4 +181,7 @@ pub struct LibraryFilter {
pub keywords: Vec<String>,
/// Restrict to cards assigned to this deck id.
pub in_deck: Option<i64>,
/// Case-insensitive substring match against the card's stored location
/// (e.g. "Box 1"). Cards with no location set never match a non-empty value.
pub location: Option<String>,
}