diff --git a/milli/tests/search/filters.rs b/milli/tests/search/filters.rs index 18de24ac3..0b5296b82 100644 --- a/milli/tests/search/filters.rs +++ b/milli/tests/search/filters.rs @@ -82,10 +82,17 @@ test_filter!( vec![Left(vec!["tag=red", "tag=green"]), Left(vec!["asc_desc_rank<3", "asc_desc_rank<1"])] ); test_filter!(exists_filter_1, vec![Right("opt1 EXISTS")]); +test_filter!(exists_filter_2, vec![Right("opt1.opt2 EXISTS")]); test_filter!(exists_filter_1_not, vec![Right("opt1 NOT EXISTS")]); test_filter!(exists_filter_1_not_alt, vec![Right("NOT opt1 EXISTS")]); test_filter!(exists_filter_1_double_not, vec![Right("NOT opt1 NOT EXISTS")]); +test_filter!(null_filter_1, vec![Right("opt1 NULL")]); +test_filter!(null_filter_2, vec![Right("opt1.opt2 NULL")]); +test_filter!(null_filter_1_not, vec![Right("opt1 NOT NULL")]); +test_filter!(null_filter_1_not_alt, vec![Right("NOT opt1 NULL")]); +test_filter!(null_filter_1_double_not, vec![Right("NOT opt1 NOT NULL")]); + test_filter!(in_filter, vec![Right("tag_in IN[1, 2, 3, four, five]")]); test_filter!(not_in_filter, vec![Right("tag_in NOT IN[1, 2, 3, four, five]")]); test_filter!(not_not_in_filter, vec![Right("NOT tag_in NOT IN[1, 2, 3, four, five]")]); diff --git a/milli/tests/search/mod.rs b/milli/tests/search/mod.rs index 18c74e344..e67c1bc64 100644 --- a/milli/tests/search/mod.rs +++ b/milli/tests/search/mod.rs @@ -205,6 +205,18 @@ fn execute_filter(filter: &str, document: &TestDocument) -> Option { } else if let Some(opt1) = &document.opt1 { id = contains_key_rec(opt1, "opt2").then(|| document.id.clone()); } + } else if matches!(filter, "opt1 NULL" | "NOT opt1 NOT NULL") { + id = document.opt1.as_ref().map_or(false, |v| v.is_null()).then(|| document.id.clone()); + } else if matches!(filter, "NOT opt1 NULL" | "opt1 NOT NULL") { + id = document.opt1.as_ref().map_or(true, |v| !v.is_null()).then(|| document.id.clone()); + } else if matches!(filter, "opt1.opt2 NULL") { + if document.opt1opt2.as_ref().map_or(false, |v| v.is_null()) { + id = Some(document.id.clone()); + } else if let Some(opt1) = &document.opt1 { + if !opt1.is_null() { + id = contains_null_rec(opt1, "opt2").then(|| document.id.clone()); + } + } } else if matches!( filter, "tag_in IN[1, 2, 3, four, five]" | "NOT tag_in NOT IN[1, 2, 3, four, five]" @@ -240,6 +252,28 @@ pub fn contains_key_rec(v: &serde_json::Value, key: &str) -> bool { } } +pub fn contains_null_rec(v: &serde_json::Value, key: &str) -> bool { + match v { + serde_json::Value::Object(v) => { + for (k, v) in v.iter() { + if k == key && v.is_null() || contains_null_rec(v, key) { + return true; + } + } + false + } + serde_json::Value::Array(v) => { + for v in v.iter() { + if contains_null_rec(v, key) { + return true; + } + } + false + } + _ => false, + } +} + pub fn expected_filtered_ids(filters: Vec, &str>>) -> HashSet { let dataset: Vec = serde_json::Deserializer::from_str(CONTENT).into_iter().map(|r| r.unwrap()).collect();