use crate::util::usage;
use anyhow::{Context, Result};
use cv_core::query::{FieldId, SessionQuery};
use std::collections::HashSet;
pub(crate) use cv_core::query::{filter_refs, TextSets};
pub(crate) fn build(query: Option<String>) -> Result<Option<SessionQuery>> {
match query {
Some(q) => match SessionQuery::parse(&q) {
Ok(parsed) => Ok(Some(parsed)),
Err(e) => usage(e),
},
None => Ok(None),
}
}
pub(crate) fn text_sets(query: &SessionQuery) -> Result<TextSets> {
let needles = query.needles(FieldId::Text);
if needles.is_empty() {
return Ok(TextSets::empty());
}
if !cv_search::default_tantivy_dir().exists() {
eprintln!("cv: text: needs a full-text index — run `cv index` (treating text: as no match)");
return Ok(TextSets::resolve(query, |_| HashSet::new()));
}
let borrowed: Vec<_> = needles.iter().map(String::as_str).collect();
let matches = cv_search::text_search_ids_batch(None, &borrowed).context("opening full-text query index")?;
let mut sets = std::collections::HashMap::new();
for (needle, result) in needles.into_iter().zip(matches) {
sets.insert(
needle.clone(),
result.with_context(|| format!("resolving text:{needle:?}"))?,
);
}
Ok(TextSets::resolve(query, |needle| {
sets.remove(needle).unwrap_or_default()
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unavailable_index_and_missing_index_have_distinct_query_outcomes() {
let _world = crate::test_world("query-text-index-errors");
let query = SessionQuery::parse("NOT text:needle").unwrap();
let absent = text_sets(&query).unwrap();
assert!(!absent.contains("needle", "session"));
std::fs::create_dir_all(cv_search::default_tantivy_dir()).unwrap();
assert!(
text_sets(&query).is_err(),
"existing unreadable index became false membership"
);
}
#[test]
fn text_sets_batch_distinct_needles_without_losing_membership() {
let world = crate::test_world("query_text_sets_batch");
let refs: Vec<_> = [("first", "alpha"), ("second", "beta")]
.into_iter()
.map(|(id, content)| {
let path = world.write_session(
id,
&[serde_json::json!({
"type":"user", "message":{"role":"user", "content":content}
})],
);
cv_core::SessionRef::new(id, cv_core::Harness::Claude, path)
})
.collect();
cv_search::index_sessions(None, refs, true, false).unwrap();
let query = SessionQuery::parse("text:alpha OR text:beta OR text:alpha").unwrap();
let sets = text_sets(&query).unwrap();
assert!(sets.contains("alpha", "first"));
assert!(!sets.contains("alpha", "second"));
assert!(sets.contains("beta", "second"));
assert!(!sets.contains("beta", "first"));
let invalid = SessionQuery::parse("NOT text:\"(\"").unwrap();
assert!(
text_sets(&invalid).is_err(),
"invalid full-text syntax became an empty false fact"
);
}
}