use std::cmp::Ordering;
use anyhow::{Result, anyhow};
use crate::{Properties, Value};
pub fn evaluate(expression: &str, properties: &Properties) -> Result<bool> {
let parts: Vec<&str> = expression.split_whitespace().collect();
if parts.len() != 3 {
tracing::warn!(
"Complex CHECK constraint expression '{}' not fully supported yet; allowing write.",
expression
);
return Ok(true);
}
let prop_part = parts[0].trim_start_matches('(');
let prop_name = match prop_part.find('.') {
Some(idx) => &prop_part[idx + 1..],
None => prop_part,
};
let op = parts[1];
let val_str = parts[2].trim_end_matches(')');
let prop_val = match properties.get(prop_name) {
Some(v) => v,
None => return Ok(true),
};
let target_val = parse_target(val_str);
match op {
"=" | "==" => Ok(if prop_val.is_number() && target_val.is_number() {
compare_values(prop_val, &target_val)?.is_eq()
} else {
prop_val == &target_val
}),
"!=" | "<>" => Ok(if prop_val.is_number() && target_val.is_number() {
!compare_values(prop_val, &target_val)?.is_eq()
} else {
prop_val != &target_val
}),
">" => Ok(compare_values(prop_val, &target_val)?.is_gt()),
"<" => Ok(compare_values(prop_val, &target_val)?.is_lt()),
">=" => Ok(compare_values(prop_val, &target_val)?.is_ge()),
"<=" => Ok(compare_values(prop_val, &target_val)?.is_le()),
_ => {
tracing::warn!("Unsupported operator '{}' in CHECK constraint", op);
Ok(true)
}
}
}
fn parse_target(val_str: &str) -> Value {
if (val_str.starts_with('\'') && val_str.ends_with('\''))
|| (val_str.starts_with('"') && val_str.ends_with('"'))
{
return Value::String(val_str[1..val_str.len() - 1].to_string());
}
if let Ok(n) = val_str.parse::<i64>() {
return Value::Int(n);
}
if let Ok(n) = val_str.parse::<f64>() {
return Value::Float(n);
}
if let Ok(b) = val_str.parse::<bool>() {
return Value::Bool(b);
}
Value::String(val_str.to_string())
}
fn compare_values(a: &Value, b: &Value) -> Result<Ordering> {
match (a, b) {
(Value::Int(n1), Value::Int(n2)) => Ok(n1.cmp(n2)),
(Value::Float(f1), Value::Float(f2)) => Ok(f1.partial_cmp(f2).unwrap_or(Ordering::Equal)),
(Value::Int(n), Value::Float(f)) => Ok(if f.is_nan() {
Ordering::Equal
} else {
crate::cmp_i64_f64(*n, *f)
}),
(Value::Float(f), Value::Int(n)) => Ok(if f.is_nan() {
Ordering::Equal
} else {
crate::cmp_i64_f64(*n, *f).reverse()
}),
(Value::String(s1), Value::String(s2)) => Ok(s1.cmp(s2)),
_ => Err(anyhow!(
"Cannot compare incompatible types: {:?} vs {:?}",
a,
b
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn props(pairs: &[(&str, Value)]) -> Properties {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), v.clone()))
.collect()
}
#[test]
fn numeric_equality_coerces_across_int_and_float() {
let p = props(&[("score", Value::Float(5.0))]);
assert!(evaluate("(n.score = 5)", &p).unwrap());
assert!(!evaluate("(n.score != 5)", &p).unwrap());
let p = props(&[("score", Value::Int(5))]);
assert!(evaluate("(n.score = 5.0)", &p).unwrap());
}
#[test]
fn non_numeric_equality_stays_strict() {
let p = props(&[("name", Value::String("a".into()))]);
assert!(evaluate("(n.name = 'a')", &p).unwrap());
assert!(!evaluate("(n.name = 'b')", &p).unwrap());
}
#[test]
fn large_integers_compare_exactly() {
let p = props(&[("v", Value::Int(9_007_199_254_740_993))]);
assert!(evaluate("(n.v > 9007199254740992.0)", &p).unwrap());
}
#[test]
fn number_wrapper_target_is_not_special_cased() {
let p = props(&[("v", Value::Int(7))]);
assert!(evaluate("(n.v < Number(8.5))", &p).is_err());
}
#[test]
fn ordering_operators() {
let p = props(&[("v", Value::Int(5))]);
assert!(evaluate("(n.v > 4)", &p).unwrap());
assert!(evaluate("(n.v >= 5)", &p).unwrap());
assert!(evaluate("(n.v < 6)", &p).unwrap());
assert!(evaluate("(n.v <= 5)", &p).unwrap());
assert!(!evaluate("(n.v > 5)", &p).unwrap());
}
#[test]
fn unsupported_shapes_allow_the_write() {
let p = props(&[("v", Value::Int(5))]);
assert!(evaluate("(n.other = 1)", &p).unwrap());
assert!(evaluate("(n.v > 1 AND n.v < 9)", &p).unwrap());
assert!(evaluate("(n.v ~~ 1)", &p).unwrap());
}
#[test]
fn incomparable_operands_error() {
let p = props(&[("name", Value::String("a".into()))]);
assert!(evaluate("(n.name > 5)", &p).is_err());
}
#[test]
fn bare_property_name_is_accepted() {
let p = props(&[("v", Value::Int(5))]);
assert!(evaluate("v = 5", &p).unwrap());
}
}