use super::{contains_ordered_phrase, parse_fts_query, FtsExprKind, FtsModifier};
use crate::query::{Fts, FtsQueryParams, SearchQuery};
use crate::schema::IndexParams;
use crate::text::Tokenizer;
fn parse(expression: &str, operator: Option<&str>) -> super::ParsedFtsQuery {
let params =
IndexParams::fts(Some("whitespace"), None, None).expect("FTS params must be valid");
let tokenizer = Tokenizer::from_index_params(Some(¶ms)).expect("tokenizer must be valid");
let mut fts = Fts::new().expect("FTS payload must be valid");
fts.set_query_string(expression)
.expect("query string must be valid");
let mut query = SearchQuery::fts("body", &fts, 10).expect("query must be valid");
if let Some(operator) = operator {
query
.set_fts_params(FtsQueryParams::new(Some(operator)).expect("operator must be valid"))
.expect("operator must be accepted");
}
parse_fts_query(&query, &tokenizer).expect("expression must parse")
}
#[test]
fn boolean_precedence_and_modifiers_are_structural() {
let parsed = parse("+Rust database OR python AND NOT legacy", None);
let FtsExprKind::Or(children) = &parsed.root.kind else {
panic!("root must be OR");
};
assert_eq!(children.len(), 2);
let FtsExprKind::Or(left) = &children[0].kind else {
panic!("left branch must use the implicit OR shape");
};
assert_eq!(left[0].modifier, FtsModifier::Must);
let FtsExprKind::And(right) = &children[1].kind else {
panic!("right branch must be AND");
};
assert_eq!(right[1].modifier, FtsModifier::MustNot);
}
#[test]
fn explicit_operator_overrides_the_default_operator() {
let parsed = parse("rust OR python database", Some("AND"));
assert!(matches!(parsed.root.kind, FtsExprKind::Or(_)));
assert!(parsed.simple().is_none());
}
#[test]
fn advanced_leaves_expand_against_one_vocabulary() {
let mut parsed = parse("rust* OR rust~1 OR [mango TO rust]", None);
parsed.expand_terms(["trust", "rust", "mango", "rustacean", "python", "rust"]);
assert_eq!(
parsed.all_terms(),
["mango", "python", "rust", "rustacean", "trust"]
);
assert!(parsed.simple().is_none());
}
#[test]
fn exact_queries_do_not_consume_a_dynamic_vocabulary() {
let mut parsed = parse("rust AND database", None);
parsed.expand_terms(std::iter::once_with(|| {
panic!("exact queries must keep the specialized path")
}));
assert_eq!(parsed.all_terms(), ["database", "rust"]);
assert!(parsed.simple().is_some());
}
#[test]
fn boosts_and_phrase_slop_are_attached_to_the_target_atom() {
let parsed = parse("body:\"vector engine\"~2^3", None);
assert!((parsed.root.boost - 3.0).abs() < f64::EPSILON);
assert!(matches!(
parsed.root.kind,
FtsExprKind::Phrase { slop: 2, .. }
));
}
#[test]
fn ordered_phrase_slop_counts_total_intervening_tokens() {
let tokens = ["vector", "fast", "search", "engine"].map(str::to_string);
let phrase = ["vector", "search", "engine"].map(str::to_string);
assert!(!contains_ordered_phrase(&tokens, &phrase, 0));
assert!(contains_ordered_phrase(&tokens, &phrase, 1));
let reversed = ["search", "vector"].map(str::to_string);
assert!(!contains_ordered_phrase(&tokens, &reversed, 4));
}
fn try_parse(expression: &str) -> crate::error::Result<super::ParsedFtsQuery> {
let params =
IndexParams::fts(Some("whitespace"), None, None).expect("FTS params must be valid");
let tokenizer = Tokenizer::from_index_params(Some(¶ms)).expect("tokenizer must be valid");
let mut fts = Fts::new().expect("FTS payload must be valid");
fts.set_query_string(expression)?;
let query = SearchQuery::fts("body", &fts, 10)?;
parse_fts_query(&query, &tokenizer)
}
#[test]
fn parser_rejects_adversarial_and_oversized_constructs() {
for expression in [
"",
"+++",
"+-rust",
"rust~~",
"\"unterminated",
"other:rust",
"[mango rust]",
"rust^",
"((((((",
"rust~999",
"\"a b\"~9999",
"~~~~",
"+ +rust",
"body:",
"{a TO}",
"[ TO b]",
"AND OR",
"NOT",
"+AND",
"body:other:rust",
"rust~0",
"rust~3",
"rust~~1",
"{a TO b TO c}",
"[a TO]",
"[TO b]",
"()",
"(rust",
"rust)",
"\"\"~2",
&format!("{}~1", "a".repeat(300)),
"w?ld~1",
"rust^0",
"rust^-1",
"rust^1000001",
"title:rust",
"-only",
"\"a b\"~1025",
"rust AND NOT +legacy",
"[a TO b",
"field:",
"body:",
"rust^1e400",
"rust~",
"+-",
"AND rust",
"rust AND",
"{a\\ TO b}",
"[a TO\\ b]",
"{ TO }",
] {
assert!(
try_parse(expression).is_err(),
"expected parse failure for {expression:?}"
);
}
}
#[test]
fn parser_accepts_boundary_valid_constructs_for_contrast() {
assert!(try_parse("rust~1").is_ok());
assert!(try_parse("rust~2").is_ok());
assert!(try_parse("\"vector engine\"~2").is_ok());
assert!(try_parse("body:rust^2").is_ok());
assert!(try_parse("[mango TO rust]").is_ok());
assert!(try_parse("{mango TO rust}").is_ok());
assert!(try_parse("[* TO *]").is_ok());
assert!(try_parse("rust*").is_ok());
assert!(try_parse("+rust -legacy").is_ok());
}