use crate::mutation::PropertyFilter;
use hawdb_core::Value;
use std::collections::BTreeMap;
pub fn property_filter_matches(
filter: &PropertyFilter,
id: u64,
properties: &BTreeMap<String, Value>,
) -> bool {
match filter {
PropertyFilter::And(filters) => filters
.iter()
.all(|filter| property_filter_matches(filter, id, properties)),
PropertyFilter::Or(filters) => filters
.iter()
.any(|filter| property_filter_matches(filter, id, properties)),
PropertyFilter::Not(filter) => !property_filter_matches(filter, id, properties),
PropertyFilter::IdEq { value } => &Value::Int(id as i64) == value,
PropertyFilter::IdNotEq { value } => &Value::Int(id as i64) != value,
PropertyFilter::IdRange { lower, upper } => {
range_bounds_match(&Value::Int(id as i64), lower.as_ref(), upper.as_ref())
}
PropertyFilter::IdIn { values } => {
values.iter().any(|value| value == &Value::Int(id as i64))
}
PropertyFilter::Eq { property, value } => properties
.get(property)
.map(|actual| actual == value)
.unwrap_or(false),
PropertyFilter::NotEq { property, value } => properties
.get(property)
.map(|actual| actual != value)
.unwrap_or(false),
PropertyFilter::IsNull { property } => properties
.get(property)
.map(|actual| actual == &Value::Null)
.unwrap_or(true),
PropertyFilter::IsNotNull { property } => properties
.get(property)
.map(|actual| actual != &Value::Null)
.unwrap_or(false),
PropertyFilter::In { property, values } => properties
.get(property)
.map(|actual| values.iter().any(|value| value == actual))
.unwrap_or(false),
PropertyFilter::ListContains { property, value } => properties
.get(property)
.and_then(|actual| match actual {
Value::List(values) => Some(values.iter().any(|actual| actual == value)),
_ => None,
})
.unwrap_or(false),
PropertyFilter::ListContainsLower { property, value } => properties
.get(property)
.and_then(|actual| match actual {
Value::List(values) => Some(values.iter().any(|actual| match actual {
Value::String(actual) => actual.to_lowercase().contains(value),
_ => false,
})),
_ => None,
})
.unwrap_or(false),
PropertyFilter::Contains { property, value } => properties
.get(property)
.and_then(|actual| match actual {
Value::String(actual) => Some(actual.contains(value)),
_ => None,
})
.unwrap_or(false),
PropertyFilter::StartsWith { property, value } => properties
.get(property)
.and_then(|actual| match actual {
Value::String(actual) => Some(actual.starts_with(value)),
_ => None,
})
.unwrap_or(false),
PropertyFilter::EndsWith { property, value } => properties
.get(property)
.and_then(|actual| match actual {
Value::String(actual) => Some(actual.ends_with(value)),
_ => None,
})
.unwrap_or(false),
PropertyFilter::RegexMatch { property, pattern } => properties
.get(property)
.and_then(|actual| match actual {
Value::String(actual) => Some(pattern.is_match(actual)),
_ => None,
})
.unwrap_or(false),
PropertyFilter::DefaultIfNullOrEq {
property,
empty,
default,
value,
negated,
} => {
let actual = properties.get(property).unwrap_or(&Value::Null);
let normalized = if actual == &Value::Null || actual == empty {
default
} else {
actual
};
let matches = normalized == value;
if *negated {
!matches
} else {
matches
}
}
PropertyFilter::Range {
property,
lower,
upper,
} => properties
.get(property)
.map(|actual| range_bounds_match(actual, lower.as_ref(), upper.as_ref()))
.unwrap_or(false),
}
}
pub fn properties_contain_all(
properties: &BTreeMap<String, Value>,
required: &BTreeMap<String, Value>,
) -> bool {
required
.iter()
.all(|(property, value)| properties.get(property) == Some(value))
}
pub fn range_bounds_match(
value: &Value,
lower: Option<&(Value, bool)>,
upper: Option<&(Value, bool)>,
) -> bool {
if let Some((bound, inclusive)) = lower {
let Some(ordering) = comparable_value_ordering(value, bound) else {
return false;
};
if ordering == std::cmp::Ordering::Less
|| (ordering == std::cmp::Ordering::Equal && !inclusive)
{
return false;
}
}
if let Some((bound, inclusive)) = upper {
let Some(ordering) = comparable_value_ordering(value, bound) else {
return false;
};
if ordering == std::cmp::Ordering::Greater
|| (ordering == std::cmp::Ordering::Equal && !inclusive)
{
return false;
}
}
true
}
pub fn comparable_value_ordering(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
match (left, right) {
(Value::Int(left), Value::Int(right)) => Some(left.cmp(right)),
(Value::Float(left), Value::Float(right)) => Some(left.total_cmp(right)),
(Value::Int(left), Value::Float(right)) => Some((*left as f64).total_cmp(right)),
(Value::Float(left), Value::Int(right)) => Some(left.total_cmp(&(*right as f64))),
(Value::String(left), Value::String(right)) => Some(left.cmp(right)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn property_filters_preserve_null_and_numeric_range_semantics() {
let properties = BTreeMap::from([
("score".to_string(), Value::Int(7)),
("optional".to_string(), Value::Null),
]);
assert!(property_filter_matches(
&PropertyFilter::Range {
property: "score".to_string(),
lower: Some((Value::Float(6.5), false)),
upper: Some((Value::Int(7), true)),
},
11,
&properties,
));
assert!(property_filter_matches(
&PropertyFilter::IsNull {
property: "missing".to_string(),
},
11,
&properties,
));
}
}