use super::*;
pub(crate) fn compare(prop: &Option<PropertyValue>, op: CompareOp, lit: &Literal) -> Option<bool> {
let Some(prop) = prop else { return None };
if matches!(prop, PropertyValue::Null) || matches!(lit, Literal::Null) {
return None;
}
compare_property_pair(prop, op, &literal_to_value(lit))
}
pub(crate) fn compare_property_pair_opt(
a: &Option<PropertyValue>,
op: CompareOp,
b: &Option<PropertyValue>,
) -> Option<bool> {
let (Some(a), Some(b)) = (a, b) else {
return None;
};
if matches!(a, PropertyValue::Null) || matches!(b, PropertyValue::Null) {
return None;
}
compare_property_pair(a, op, b)
}
pub(crate) fn compare_property_pair(
a: &PropertyValue,
op: CompareOp,
b: &PropertyValue,
) -> Option<bool> {
match (a, b) {
(PropertyValue::Int(a), PropertyValue::Int(b)) => Some(cmp_ord(op, *a, *b)),
(PropertyValue::Int(a), PropertyValue::Float(b)) => Some(cmp_f64(op, *a as f64, *b)),
(PropertyValue::Float(a), PropertyValue::Float(b)) => Some(cmp_f64(op, *a, *b)),
(PropertyValue::Float(a), PropertyValue::Int(b)) => Some(cmp_f64(op, *a, *b as f64)),
(PropertyValue::String(a), PropertyValue::String(b)) => Some(match op {
CompareOp::StartsWith => a.starts_with(b.as_str()),
CompareOp::EndsWith => a.ends_with(b.as_str()),
CompareOp::Contains => a.contains(b.as_str()),
_ => cmp_ord(op, a.as_str(), b.as_str()),
}),
(PropertyValue::Bool(a), PropertyValue::Bool(b)) => Some(cmp_ord(op, *a, *b)),
(PropertyValue::Date(a), PropertyValue::Date(b)) => Some(cmp_ord(op, *a, *b)),
(PropertyValue::LocalTime(a), PropertyValue::LocalTime(b)) => Some(cmp_ord(op, *a, *b)),
(
PropertyValue::Time {
nanos_of_day: na,
offset_seconds: oa,
},
PropertyValue::Time {
nanos_of_day: nb,
offset_seconds: ob,
},
) => Some(cmp_ord(
op,
na - *oa as i64 * 1_000_000_000,
nb - *ob as i64 * 1_000_000_000,
)),
(
PropertyValue::LocalDateTime {
epoch_seconds: sa,
nanos: na,
},
PropertyValue::LocalDateTime {
epoch_seconds: sb,
nanos: nb,
},
) => Some(cmp_ord(op, (*sa, *na), (*sb, *nb))),
(
PropertyValue::DateTime {
epoch_seconds: sa,
nanos: na,
..
},
PropertyValue::DateTime {
epoch_seconds: sb,
nanos: nb,
..
},
) => Some(cmp_ord(op, (*sa, *na), (*sb, *nb))),
(PropertyValue::Duration { .. }, PropertyValue::Duration { .. }) => match op {
CompareOp::Eq => Some(a == b),
CompareOp::Ne => Some(a != b),
_ => None,
},
_ => match op {
CompareOp::Eq => Some(false),
CompareOp::Ne => Some(true),
CompareOp::StartsWith
| CompareOp::EndsWith
| CompareOp::Contains
| CompareOp::Lt
| CompareOp::Le
| CompareOp::Gt
| CompareOp::Ge => None,
},
}
}
pub(crate) fn value_to_bool3(v: &Value) -> Result<Option<bool>, QueryError> {
match v {
Value::Null => Ok(None),
Value::Literal(Literal::Bool(b)) | Value::Property(PropertyValue::Bool(b)) => Ok(Some(*b)),
other => Err(QueryError::Type(format!(
"expected a boolean, got {other:?}"
))),
}
}
pub(crate) fn bool3_to_value(b: Option<bool>) -> Value {
match b {
Some(b) => Value::Literal(Literal::Bool(b)),
None => Value::Null,
}
}
pub(crate) fn and3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
match (a, b) {
(Some(false), _) | (_, Some(false)) => Some(false),
(Some(true), Some(true)) => Some(true),
_ => None,
}
}
pub(crate) fn or3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
match (a, b) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
_ => None,
}
}
pub(crate) fn xor3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
match (a, b) {
(Some(a), Some(b)) => Some(a != b),
_ => None,
}
}
pub(crate) fn cmp_f64(op: CompareOp, a: f64, b: f64) -> bool {
match op {
CompareOp::Eq => a == b,
CompareOp::Ne => a != b,
CompareOp::Lt => a < b,
CompareOp::Le => a <= b,
CompareOp::Gt => a > b,
CompareOp::Ge => a >= b,
CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => false,
}
}
pub(crate) fn cmp_ord<T: PartialOrd>(op: CompareOp, a: T, b: T) -> bool {
match op {
CompareOp::Eq => a == b,
CompareOp::Ne => a != b,
CompareOp::Lt => a < b,
CompareOp::Le => a <= b,
CompareOp::Gt => a > b,
CompareOp::Ge => a >= b,
CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => false,
}
}
pub(crate) fn value_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
(Value::Property(pa), Value::Property(pb)) => property_value_eq(pa, pb),
(Value::Literal(la), Value::Literal(lb)) => la == lb,
(Value::Property(pa), Value::Literal(lb)) => *pa == literal_to_value(lb),
(Value::Literal(la), Value::Property(pb)) => literal_to_value(la) == *pb,
(Value::Node(na), Value::Node(nb)) => na.id == nb.id,
(Value::Edge(ea), Value::Edge(eb)) => ea.id == eb.id,
(Value::List(la), Value::List(lb)) => {
la.len() == lb.len() && la.iter().zip(lb).all(|(x, y)| value_eq(x, y))
}
(Value::Path(pa), Value::Path(pb)) => {
pa.len() == pb.len()
&& pa.iter().zip(pb).all(|(x, y)| match (x, y) {
(PathElem::Node(na), PathElem::Node(nb)) => na.id == nb.id,
(PathElem::Edge(ea), PathElem::Edge(eb)) => ea.id == eb.id,
_ => false,
})
}
_ => false,
}
}
pub(crate) fn property_value_eq(a: &PropertyValue, b: &PropertyValue) -> bool {
match (a, b) {
(
PropertyValue::Time {
nanos_of_day: na,
offset_seconds: oa,
},
PropertyValue::Time {
nanos_of_day: nb,
offset_seconds: ob,
},
) => na - *oa as i64 * 1_000_000_000 == nb - *ob as i64 * 1_000_000_000,
(
PropertyValue::DateTime {
epoch_seconds: sa,
nanos: na,
..
},
PropertyValue::DateTime {
epoch_seconds: sb,
nanos: nb,
..
},
) => sa == sb && na == nb,
_ => a == b,
}
}
pub(crate) fn apply_order_by(
rows: Vec<Vec<Value>>,
columns: &[String],
order_by: &[(ReturnExpr, SortDir)],
items: Option<&[ReturnItem]>,
skip: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Vec<Value>>, QueryError> {
let order_by_col: Vec<Option<usize>> = order_by
.iter()
.map(|(expr, _)| {
columns
.iter()
.position(|c| *c == default_column_name(expr, 0))
.or_else(|| {
items.and_then(|items| items.iter().position(|item| item.expr == *expr))
})
})
.collect();
let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(rows.len());
for row in rows {
let row_map: HashMap<String, Value> =
columns.iter().cloned().zip(row.iter().cloned()).collect();
let keys = order_by
.iter()
.zip(&order_by_col)
.map(|((expr, _), col)| match col {
Some(i) => Ok(row[*i].clone()),
None => eval_projected_expr(expr, &row_map),
})
.collect::<Result<Vec<_>, _>>()?;
keyed.push((keys, row));
}
Ok(top_k_by(keyed, order_by, skip, limit)
.into_iter()
.map(|(_, row)| row)
.collect())
}
pub(crate) fn eval_projected_expr(
expr: &ReturnExpr,
row: &HashMap<String, Value>,
) -> Result<Value, QueryError> {
match expr {
ReturnExpr::Var(name) => row
.get(name)
.cloned()
.ok_or_else(|| QueryError::UnboundVariable(name.clone())),
ReturnExpr::Prop(pa) => {
let base = row
.get(&pa.var)
.ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
match base {
Value::Map(m) => Ok(m.get(&pa.prop).cloned().unwrap_or(Value::Null)),
Value::Node(n) => Ok(match n.props.get(&pa.prop).cloned() {
Some(PropertyValue::Null) | None => Value::Null,
Some(v) => property_value_to_value(v),
}),
Value::Edge(e) => Ok(match e.props.get(&pa.prop).cloned() {
Some(PropertyValue::Null) | None => Value::Null,
Some(v) => property_value_to_value(v),
}),
Value::Property(pv) => Ok(match temporal_component(pv, &pa.prop) {
Some(component) => Value::Property(component),
None => Value::Null,
}),
_ => Ok(Value::Null),
}
}
ReturnExpr::PropOf(base, prop) => {
let v = eval_projected_expr(base, row)?;
property_of_value(&v, prop)
}
ReturnExpr::Lit(lit) => Ok(match lit {
Literal::Null => Value::Null,
other => Value::Literal(other.clone()),
}),
ReturnExpr::Call { name, args, .. } => {
if is_aggregate_name(name) {
return Err(QueryError::Semantic(format!(
"aggregate function '{name}' can only be used as a return item's top-level expression"
)));
}
let arg_values = args
.iter()
.map(|a| eval_projected_expr(a, row))
.collect::<Result<Vec<_>, _>>()?;
call_builtin(name, &arg_values, temporal::capture_now())
}
ReturnExpr::CountStar => Err(QueryError::Semantic(
"count(*) can only be used as a return item's top-level expression".into(),
)),
ReturnExpr::Case { test, whens, else_ } => {
let test_value = match test {
Some(t) => Some(eval_projected_expr(t, row)?),
None => None,
};
for (when, then) in whens {
let when_value = eval_projected_expr(when, row)?;
let matched = match &test_value {
Some(tv) => value_eq(tv, &when_value),
None => matches!(when_value, Value::Literal(Literal::Bool(true))),
};
if matched {
return eval_projected_expr(then, row);
}
}
match else_ {
Some(e) => eval_projected_expr(e, row),
None => Ok(Value::Null),
}
}
ReturnExpr::Arith(l, op, r) => {
let lv = eval_projected_expr(l, row)?;
let rv = eval_projected_expr(r, row)?;
apply_arith(*op, &lv, &rv)
}
ReturnExpr::Neg(e) => {
let v = eval_projected_expr(e, row)?;
apply_neg(&v)
}
ReturnExpr::ListLit(items) => Ok(Value::List(
items
.iter()
.map(|item| eval_projected_expr(item, row))
.collect::<Result<Vec<_>, _>>()?,
)),
ReturnExpr::Index(base, index) => {
let base_v = eval_projected_expr(base, row)?;
let index_v = eval_projected_expr(index, row)?;
apply_index(&base_v, &index_v)
}
ReturnExpr::Slice(base, start, end) => {
let base_v = eval_projected_expr(base, row)?;
let start_v = start
.as_deref()
.map(|s| eval_projected_expr(s, row))
.transpose()?;
let end_v = end
.as_deref()
.map(|e| eval_projected_expr(e, row))
.transpose()?;
apply_slice(&base_v, start_v.as_ref(), end_v.as_ref())
}
ReturnExpr::ListComp {
var,
source,
where_clause,
project,
} => {
let source_v = eval_projected_expr(source, row)?;
let items = match source_v {
Value::List(items) => items,
Value::Null => return Ok(Value::Null),
other => {
return Err(QueryError::Type(format!(
"list comprehension source must be a list, got {other:?}"
)))
}
};
let mut result = Vec::with_capacity(items.len());
for item in items {
let mut scoped_row = row.clone();
scoped_row.insert(var.clone(), item.clone());
let keep = match where_clause {
Some(w) => value_to_bool3(&eval_projected_expr(w, &scoped_row)?)? == Some(true),
None => true,
};
if !keep {
continue;
}
result.push(match project {
Some(p) => eval_projected_expr(p, &scoped_row)?,
None => item,
});
}
Ok(Value::List(result))
}
ReturnExpr::Quantifier {
kind,
var,
source,
where_clause,
} => {
let source_v = eval_projected_expr(source, row)?;
let items = match source_v {
Value::List(items) => items,
Value::Null => return Ok(Value::Null),
other => {
return Err(QueryError::Type(format!(
"quantifier source must be a list, got {other:?}"
)))
}
};
let mut preds = Vec::with_capacity(items.len());
for item in &items {
let mut scoped_row = row.clone();
scoped_row.insert(var.clone(), item.clone());
preds.push(match where_clause {
Some(w) => value_to_bool3(&eval_projected_expr(w, &scoped_row)?)?,
None => item_truthy(item),
});
}
Ok(match eval_quantifier(*kind, &preds) {
Some(b) => Value::Literal(Literal::Bool(b)),
None => Value::Null,
})
}
ReturnExpr::MapLit(entries) => {
let mut map = BTreeMap::new();
for (k, v) in entries {
map.insert(k.clone(), eval_projected_expr(v, row)?);
}
Ok(Value::Map(map))
}
ReturnExpr::And(l, r) => Ok(bool3_to_value(and3(
value_to_bool3(&eval_projected_expr(l, row)?)?,
value_to_bool3(&eval_projected_expr(r, row)?)?,
))),
ReturnExpr::Or(l, r) => Ok(bool3_to_value(or3(
value_to_bool3(&eval_projected_expr(l, row)?)?,
value_to_bool3(&eval_projected_expr(r, row)?)?,
))),
ReturnExpr::Xor(l, r) => Ok(bool3_to_value(xor3(
value_to_bool3(&eval_projected_expr(l, row)?)?,
value_to_bool3(&eval_projected_expr(r, row)?)?,
))),
ReturnExpr::Not(e) => Ok(bool3_to_value(
value_to_bool3(&eval_projected_expr(e, row)?)?.map(|b| !b),
)),
ReturnExpr::Compare(l, op, r) => {
let lv = eval_projected_expr(l, row)?;
let rv = eval_projected_expr(r, row)?;
Ok(bool3_to_value(compare_values(&lv, *op, &rv)))
}
ReturnExpr::IsNull(e) => Ok(Value::Literal(Literal::Bool(matches!(
eval_projected_expr(e, row)?,
Value::Null
)))),
ReturnExpr::In(needle, haystack) => {
let nv = eval_projected_expr(needle, row)?;
let hv = eval_projected_expr(haystack, row)?;
Ok(bool3_to_value(list_membership_ternary(&nv, &hv)?))
}
ReturnExpr::HasLabel(var, labels) => {
let binding = row
.get(var)
.ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
match binding {
Value::Node(n) => Ok(Value::Literal(Literal::Bool(
labels.iter().all(|l| n.labels.contains(l)),
))),
Value::Null => Ok(Value::Null),
other => Err(QueryError::Type(format!(
"'{var}' isn't a node — (n:Label) needs a node binding, got {other:?}"
))),
}
}
ReturnExpr::PatternPredicate(_) => Err(QueryError::Semantic(
"a pattern predicate (`(n)-->()` etc) can only be used inside WHERE".into(),
)),
ReturnExpr::PatternComprehension { .. } => Err(QueryError::Semantic(
"a pattern comprehension can only be used in RETURN/WITH position, or as an ORDER BY \
key that repeats one of their items verbatim"
.into(),
)),
ReturnExpr::ExistsPattern { .. } | ReturnExpr::ExistsSubquery(_) => Err(
QueryError::Semantic("an exists {} subquery can only be used inside WHERE".into()),
),
}
}
pub(crate) fn dedup_rows(rows: Vec<Vec<Value>>) -> Result<Vec<Vec<Value>>, QueryError> {
let mut seen: HashSet<Vec<HashKey>> = HashSet::with_capacity(rows.len());
let mut out = Vec::with_capacity(rows.len());
for row in rows {
let key = row
.iter()
.map(value_hash_key)
.collect::<Result<Vec<_>, _>>()?;
if seen.insert(key) {
out.push(row);
}
}
Ok(out)
}
pub(crate) fn dedup_binding_rows(
items: &[ReturnItem],
rows: Vec<BindingRow>,
) -> Result<Vec<BindingRow>, QueryError> {
let names: Vec<String> = items
.iter()
.enumerate()
.map(with_item_output_name)
.collect();
let mut seen: HashSet<Vec<HashKey>> = HashSet::with_capacity(rows.len());
let mut out = Vec::with_capacity(rows.len());
for row in rows {
let key = names
.iter()
.map(|name| {
binding_hash_key(row.get(name).unwrap_or_else(|| {
panic!("DISTINCT row missing its own projected column '{name}'")
}))
})
.collect::<Result<Vec<_>, _>>()?;
if seen.insert(key) {
out.push(row);
}
}
Ok(out)
}
pub(crate) fn top_k_by<T>(
mut keyed: Vec<(Vec<Value>, T)>,
order_by: &[(ReturnExpr, SortDir)],
skip: Option<i64>,
limit: Option<i64>,
) -> Vec<(Vec<Value>, T)> {
let cmp = |a: &(Vec<Value>, T), b: &(Vec<Value>, T)| -> std::cmp::Ordering {
for (i, (_, dir)) in order_by.iter().enumerate() {
let ord = compare_with_dir(&a.0[i], &b.0[i], *dir);
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
std::cmp::Ordering::Equal
};
let skip_n = skip.unwrap_or(0).max(0) as usize;
match limit {
Some(n) => {
let k = skip_n + n.max(0) as usize;
if k == 0 {
keyed.clear();
} else if k < keyed.len() {
keyed.select_nth_unstable_by(k - 1, cmp);
keyed.truncate(k);
keyed.sort_by(cmp);
} else {
keyed.sort_by(cmp);
}
}
None => keyed.sort_by(cmp),
}
if skip_n > 0 {
keyed.drain(0..skip_n.min(keyed.len()));
}
keyed
}
pub(crate) fn compare_with_dir(a: &Value, b: &Value, dir: SortDir) -> std::cmp::Ordering {
let ord = compare_non_null(a, b);
if dir == SortDir::Desc {
ord.reverse()
} else {
ord
}
}
pub(crate) fn cmp_f64_nan_greatest(x: f64, y: f64) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (x.is_nan(), y.is_nan()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
}
}
pub(crate) fn compare_non_null(a: &Value, b: &Value) -> std::cmp::Ordering {
use std::cmp::Ordering;
if let (Value::List(_), Value::List(_)) = (a, b) {
return list_cmp_asc(a, b);
}
let pa = value_to_comparable(a);
let pb = value_to_comparable(b);
match (pa, pb) {
(Some(PropertyValue::Int(x)), Some(PropertyValue::Int(y))) => x.cmp(&y),
(Some(PropertyValue::Int(x)), Some(PropertyValue::Float(y))) => {
cmp_f64_nan_greatest(x as f64, y)
}
(Some(PropertyValue::Float(x)), Some(PropertyValue::Int(y))) => {
cmp_f64_nan_greatest(x, y as f64)
}
(Some(PropertyValue::Float(x)), Some(PropertyValue::Float(y))) => {
cmp_f64_nan_greatest(x, y)
}
(Some(PropertyValue::String(x)), Some(PropertyValue::String(y))) => x.cmp(&y),
(Some(PropertyValue::Bool(x)), Some(PropertyValue::Bool(y))) => x.cmp(&y),
(Some(PropertyValue::Date(x)), Some(PropertyValue::Date(y))) => x.cmp(&y),
(Some(PropertyValue::LocalTime(x)), Some(PropertyValue::LocalTime(y))) => x.cmp(&y),
(
Some(PropertyValue::Time {
nanos_of_day: x,
offset_seconds: ox,
}),
Some(PropertyValue::Time {
nanos_of_day: y,
offset_seconds: oy,
}),
) => (x - ox as i64 * 1_000_000_000).cmp(&(y - oy as i64 * 1_000_000_000)),
(
Some(PropertyValue::LocalDateTime {
epoch_seconds: xs,
nanos: xn,
}),
Some(PropertyValue::LocalDateTime {
epoch_seconds: ys,
nanos: yn,
}),
) => (xs, xn).cmp(&(ys, yn)),
(
Some(PropertyValue::DateTime {
epoch_seconds: xs,
nanos: xn,
..
}),
Some(PropertyValue::DateTime {
epoch_seconds: ys,
nanos: yn,
..
}),
) => (xs, xn).cmp(&(ys, yn)),
_ => match (type_rank(a), type_rank(b)) {
(Some(ra), Some(rb)) if ra != rb => ra.cmp(&rb),
_ => Ordering::Equal,
},
}
}
pub(crate) fn type_rank(v: &Value) -> Option<u8> {
match v {
Value::Map(_) => Some(0),
Value::Node(_) => Some(1),
Value::Edge(_) => Some(2),
Value::List(_) => Some(3),
Value::Path(_) => Some(4),
Value::Literal(Literal::String(_)) | Value::Property(PropertyValue::String(_)) => Some(5),
Value::Literal(Literal::Bool(_)) | Value::Property(PropertyValue::Bool(_)) => Some(6),
Value::Literal(Literal::Int(_))
| Value::Property(PropertyValue::Int(_))
| Value::Literal(Literal::Float(_))
| Value::Property(PropertyValue::Float(_)) => Some(7),
Value::Property(PropertyValue::Date(_)) => Some(8),
Value::Property(PropertyValue::LocalTime(_)) => Some(9),
Value::Property(PropertyValue::Time { .. }) => Some(10),
Value::Property(PropertyValue::LocalDateTime { .. }) => Some(11),
Value::Property(PropertyValue::DateTime { .. }) => Some(12),
Value::Null | Value::Literal(Literal::Null) | Value::Property(PropertyValue::Null) => {
Some(13)
}
_ => None,
}
}
pub(crate) fn list_cmp_asc(a: &Value, b: &Value) -> std::cmp::Ordering {
use std::cmp::Ordering;
let a_null = matches!(a, Value::Null);
let b_null = matches!(b, Value::Null);
match (a_null, b_null) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Greater,
(false, true) => return Ordering::Less,
(false, false) => {}
}
if let (Value::List(xs), Value::List(ys)) = (a, b) {
for (x, y) in xs.iter().zip(ys) {
match list_cmp_asc(x, y) {
Ordering::Equal => continue,
other => return other,
}
}
return xs.len().cmp(&ys.len());
}
compare_non_null(a, b)
}
pub(crate) fn value_to_comparable(v: &Value) -> Option<PropertyValue> {
match v {
Value::Property(pv) => Some(pv.clone()),
Value::Literal(lit) => Some(literal_to_value(lit)),
_ => None,
}
}
pub(crate) fn comparable_ordering(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
if let (Value::List(_), Value::List(_)) = (a, b) {
return Some(list_cmp_asc(a, b));
}
let (pa, pb) = match (value_to_comparable(a), value_to_comparable(b)) {
(Some(pa), Some(pb)) => (pa, pb),
_ => {
return match (type_rank(a), type_rank(b)) {
(Some(ra), Some(rb)) if ra != rb => Some(ra.cmp(&rb)),
_ => None,
};
}
};
Some(match (pa, pb) {
(PropertyValue::Int(x), PropertyValue::Int(y)) => x.cmp(&y),
(PropertyValue::Int(x), PropertyValue::Float(y)) => cmp_f64_nan_greatest(x as f64, y),
(PropertyValue::Float(x), PropertyValue::Int(y)) => cmp_f64_nan_greatest(x, y as f64),
(PropertyValue::Float(x), PropertyValue::Float(y)) => cmp_f64_nan_greatest(x, y),
(PropertyValue::String(x), PropertyValue::String(y)) => x.cmp(&y),
(PropertyValue::Bool(x), PropertyValue::Bool(y)) => x.cmp(&y),
(PropertyValue::Date(x), PropertyValue::Date(y)) => x.cmp(&y),
(PropertyValue::LocalTime(x), PropertyValue::LocalTime(y)) => x.cmp(&y),
(
PropertyValue::Time {
nanos_of_day: x,
offset_seconds: ox,
},
PropertyValue::Time {
nanos_of_day: y,
offset_seconds: oy,
},
) => (x - ox as i64 * 1_000_000_000).cmp(&(y - oy as i64 * 1_000_000_000)),
(
PropertyValue::LocalDateTime {
epoch_seconds: xs,
nanos: xn,
},
PropertyValue::LocalDateTime {
epoch_seconds: ys,
nanos: yn,
},
) => (xs, xn).cmp(&(ys, yn)),
(
PropertyValue::DateTime {
epoch_seconds: xs,
nanos: xn,
..
},
PropertyValue::DateTime {
epoch_seconds: ys,
nanos: yn,
..
},
) => (xs, xn).cmp(&(ys, yn)),
_ => return None,
})
}
pub(crate) fn compare_values(a: &Value, op: CompareOp, b: &Value) -> Option<bool> {
if matches!(a, Value::Null) || matches!(b, Value::Null) {
return None;
}
match op {
CompareOp::Eq => value_equal_ternary(a, b),
CompareOp::Ne => value_equal_ternary(a, b).map(|eq| !eq),
CompareOp::Lt => ordered_compare(a, b, |o| o == std::cmp::Ordering::Less),
CompareOp::Le => ordered_compare(a, b, |o| o != std::cmp::Ordering::Greater),
CompareOp::Gt => ordered_compare(a, b, |o| o == std::cmp::Ordering::Greater),
CompareOp::Ge => ordered_compare(a, b, |o| o != std::cmp::Ordering::Less),
CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => {
let (Some(s), Some(p)) = (as_arith_str(a), as_arith_str(b)) else {
return None;
};
Some(match op {
CompareOp::StartsWith => s.starts_with(p),
CompareOp::EndsWith => s.ends_with(p),
CompareOp::Contains => s.contains(p),
_ => unreachable!("only StartsWith/EndsWith/Contains reach this arm"),
})
}
}
}
pub(crate) fn ordered_compare(
a: &Value,
b: &Value,
pred: impl Fn(std::cmp::Ordering) -> bool,
) -> Option<bool> {
if let (Some(x), Some(y)) = (value_as_f64(a), value_as_f64(b)) {
return Some(x.partial_cmp(&y).map(pred).unwrap_or(false));
}
value_partial_cmp(a, b).map(pred)
}
pub(crate) fn value_partial_cmp(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
use std::cmp::Ordering;
if matches!(a, Value::Null) || matches!(b, Value::Null) {
return None;
}
if let (Value::List(xs), Value::List(ys)) = (a, b) {
for (x, y) in xs.iter().zip(ys) {
match value_partial_cmp(x, y) {
Some(Ordering::Equal) => continue,
other => return other,
}
}
return Some(xs.len().cmp(&ys.len()));
}
if value_to_comparable(a).is_none() || value_to_comparable(b).is_none() {
return None;
}
comparable_ordering(a, b)
}
pub(crate) fn value_equal_ternary(a: &Value, b: &Value) -> Option<bool> {
match (a, b) {
(Value::Null, _) | (_, Value::Null) => None,
(Value::List(xs), Value::List(ys)) => {
if xs.len() != ys.len() {
return Some(false);
}
fold_ternary_eq(xs.iter().zip(ys).map(|(x, y)| value_equal_ternary(x, y)))
}
(Value::Map(x), Value::Map(y)) => {
if !x.keys().eq(y.keys()) {
return Some(false);
}
fold_ternary_eq(x.iter().map(|(k, xv)| value_equal_ternary(xv, &y[k])))
}
_ => Some(values_equal_numeric_aware(a, b)),
}
}
pub(crate) fn list_membership_ternary(
needle: &Value,
haystack: &Value,
) -> Result<Option<bool>, QueryError> {
match haystack {
Value::Null => Ok(None),
Value::List(items) => {
let mut saw_unknown = false;
for item in items {
match value_equal_ternary(needle, item) {
Some(true) => return Ok(Some(true)),
Some(false) => {}
None => saw_unknown = true,
}
}
Ok(if saw_unknown { None } else { Some(false) })
}
other => Err(QueryError::Type(format!(
"IN requires a list on the right-hand side, got {other:?}"
))),
}
}
pub(crate) fn fold_ternary_eq(mut results: impl Iterator<Item = Option<bool>>) -> Option<bool> {
let mut saw_unknown = false;
for r in results.by_ref() {
match r {
Some(false) => return Some(false),
Some(true) => {}
None => saw_unknown = true,
}
}
if saw_unknown {
None
} else {
Some(true)
}
}
pub(crate) fn values_equal_numeric_aware(a: &Value, b: &Value) -> bool {
match (as_arith_num(a), as_arith_num(b)) {
(Some(ArithNum::Int(x)), Some(ArithNum::Int(y))) => x == y,
(Some(ArithNum::Int(x)), Some(ArithNum::Float(y)))
| (Some(ArithNum::Float(y)), Some(ArithNum::Int(x))) => x as f64 == y,
(Some(ArithNum::Float(x)), Some(ArithNum::Float(y))) => x == y,
_ => value_eq(a, b),
}
}