Skip to main content

akar_function/scalar/
comparison.rs

1use crate::registry::*;
2use akar_common::types::Value;
3
4// ==================== Comparison ====================
5
6pub(crate) fn evaluate_comparison(op: ComparisonOp, args: &[Value]) -> Result<Value, String> {
7    if args.len() < 2 && !matches!(op, ComparisonOp::IsNull | ComparisonOp::IsNotNull) {
8        return Err("Comparison requires 2 arguments".into());
9    }
10
11    match op {
12        ComparisonOp::Eq => Ok(Value::Bool(values_equal(&args[0], &args[1]))),
13        ComparisonOp::NotEq => Ok(Value::Bool(!values_equal(&args[0], &args[1]))),
14        ComparisonOp::Lt => Ok(Value::Bool(compare_values(&args[0], &args[1])?.is_lt())),
15        ComparisonOp::Lte => Ok(Value::Bool(!compare_values(&args[0], &args[1])?.is_gt())),
16        ComparisonOp::Gt => Ok(Value::Bool(compare_values(&args[0], &args[1])?.is_gt())),
17        ComparisonOp::Gte => Ok(Value::Bool(!compare_values(&args[0], &args[1])?.is_lt())),
18        ComparisonOp::IsNull => Ok(Value::Bool(matches!(args[0], Value::Null))),
19        ComparisonOp::IsNotNull => Ok(Value::Bool(!matches!(args[0], Value::Null))),
20    }
21}
22
23/// Exact cross-type numeric equality.
24///
25/// `Value::UInt64` and `Value::Int64` derive-distinct `PartialEq` instances
26/// (e.g. `UInt64(5) == Int64(5)` is `false`), so `WHERE uint64_col = 5` would
27/// silently return zero rows. Mixed integer operands are compared via `i128`
28/// promotion instead. Floats follow the NaN convention: NaN = NaN is true.
29fn values_equal(a: &Value, b: &Value) -> bool {
30    if a == b {
31        return true;
32    }
33    if let (Value::Double(x), Value::Double(y)) = (a, b) {
34        return x.is_nan() && y.is_nan();
35    }
36    if let (Value::Float(x), Value::Float(y)) = (a, b) {
37        return x.is_nan() && y.is_nan();
38    }
39    if let (Some(x), Some(y)) = (integer_to_i128(a), integer_to_i128(b)) {
40        return x == y;
41    }
42    // Cross-type float promotion: compare a float against any numeric via f64
43    // (e.g. a FLOAT column value against an integer/Double literal).
44    if let (Ok(x), Ok(y)) = (
45        super::arithmetic::numeric_to_f64(a),
46        super::arithmetic::numeric_to_f64(b),
47    ) {
48        return x.to_bits() == y.to_bits() || (x.is_nan() && y.is_nan());
49    }
50    false
51}
52
53/// Total-order comparison for floats with a NaN convention: NaN sorts greater
54/// than every finite value, and NaN == NaN.
55#[inline]
56pub(crate) fn double_cmp(a: f64, b: f64) -> std::cmp::Ordering {
57    if a.is_nan() {
58        if b.is_nan() {
59            std::cmp::Ordering::Equal
60        } else {
61            std::cmp::Ordering::Greater
62        }
63    } else if b.is_nan() {
64        std::cmp::Ordering::Less
65    } else {
66        a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)
67    }
68}
69
70/// Widened representation of any integer `Value` variant (exact, no overflow).
71fn integer_to_i128(v: &Value) -> Option<i128> {
72    match v {
73        Value::Int64(x) => Some(*x as i128),
74        Value::Int32(x) => Some(*x as i128),
75        Value::Int16(x) => Some(*x as i128),
76        Value::Int8(x) => Some(*x as i128),
77        Value::UInt64(x) => Some(*x as i128),
78        Value::UInt32(x) => Some(*x as i128),
79        Value::UInt16(x) => Some(*x as i128),
80        Value::UInt8(x) => Some(*x as i128),
81        _ => None,
82    }
83}
84
85pub(crate) fn compare_values(a: &Value, b: &Value) -> Result<std::cmp::Ordering, String> {
86    match (a, b) {
87        (Value::Int64(x), Value::Int64(y)) => Ok(x.cmp(y)),
88        (Value::Int32(x), Value::Int32(y)) => Ok(x.cmp(y)),
89        (Value::Int16(x), Value::Int16(y)) => Ok(x.cmp(y)),
90        (Value::Int8(x), Value::Int8(y)) => Ok(x.cmp(y)),
91        (Value::UInt64(x), Value::UInt64(y)) => Ok(x.cmp(y)),
92        (Value::UInt32(x), Value::UInt32(y)) => Ok(x.cmp(y)),
93        (Value::UInt16(x), Value::UInt16(y)) => Ok(x.cmp(y)),
94        (Value::UInt8(x), Value::UInt8(y)) => Ok(x.cmp(y)),
95        (Value::Double(x), Value::Double(y)) => Ok(double_cmp(*x, *y)),
96        (Value::Float(x), Value::Float(y)) => Ok(double_cmp(*x as f64, *y as f64)),
97        (Value::String(x), Value::String(y)) => Ok(x.cmp(y)),
98        (Value::Bool(x), Value::Bool(y)) => Ok(x.cmp(y)),
99        (Value::Date(x), Value::Date(y)) => Ok(x.cmp(y)),
100        (Value::Timestamp(x), Value::Timestamp(y)) => Ok(x.cmp(y)),
101        // Cross-type numeric promotion (int ↔ float)
102        (Value::Int64(x), Value::Double(y)) => Ok(double_cmp(*x as f64, *y)),
103        (Value::Double(x), Value::Int64(y)) => Ok(double_cmp(*x, *y as f64)),
104        (Value::Float(x), Value::Double(y)) => Ok(double_cmp(*x as f64, *y)),
105        (Value::Double(x), Value::Float(y)) => Ok(double_cmp(*x, *y as f64)),
106        (Value::Float(x), Value::Int64(y)) => Ok(double_cmp(*x as f64, *y as f64)),
107        (Value::Int64(x), Value::Float(y)) => Ok(double_cmp(*x as f64, *y as f64)),
108        // Mixed signed/unsigned integer promotion (exact via i128). A UInt64
109        // column compared against an Int64 literal (e.g. `WHERE id > 5`) would
110        // otherwise hit the generic "Cannot compare types" error below.
111        _ => {
112            if let (Some(x), Some(y)) = (integer_to_i128(a), integer_to_i128(b)) {
113                Ok(x.cmp(&y))
114            } else {
115                Err("Cannot compare types".into())
116            }
117        }
118    }
119}