Skip to main content

uni_query/query/
expr_eval.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Expression evaluation helper functions.
5//!
6//! This module extracts high-complexity expression evaluation logic from the main executor
7//! to reduce cognitive complexity and improve maintainability.
8
9use anyhow::{Result, anyhow};
10use std::cmp::Ordering;
11use uni_common::{TemporalValue, Value};
12
13use crate::query::datetime::{
14    CypherDuration, TemporalType, add_cypher_duration_to_date, add_cypher_duration_to_datetime,
15    add_cypher_duration_to_localdatetime, add_cypher_duration_to_localtime,
16    add_cypher_duration_to_time, classify_temporal, eval_datetime_function, is_duration_value,
17    parse_datetime_utc, parse_duration_from_value, parse_duration_to_cypher,
18};
19use crate::query::spatial::eval_spatial_function;
20use uni_cypher::ast::BinaryOp;
21
22/// Evaluate a binary operation on two already-evaluated values.
23///
24/// This function handles all binary operators (Eq, NotEq, And, Or, Gt, Lt, etc.)
25/// and returns the result of the operation.
26pub fn eval_binary_op(left: &Value, op: &BinaryOp, right: &Value) -> Result<Value> {
27    // Null propagation for most operators (except AND/OR which have three-valued logic)
28    if !matches!(op, BinaryOp::And | BinaryOp::Or) && (left.is_null() || right.is_null()) {
29        return Ok(Value::Null);
30    }
31
32    match op {
33        BinaryOp::Eq => Ok(match cypher_eq(left, right) {
34            Some(b) => Value::Bool(b),
35            None => Value::Null,
36        }),
37        BinaryOp::NotEq => Ok(match cypher_eq(left, right) {
38            Some(b) => Value::Bool(!b),
39            None => Value::Null,
40        }),
41        BinaryOp::And => {
42            // Three-valued logic: false dominates, null propagates with true
43            match (left.as_bool(), right.as_bool()) {
44                (Some(false), _) | (_, Some(false)) => Ok(Value::Bool(false)),
45                (Some(true), Some(true)) => Ok(Value::Bool(true)),
46                _ if left.is_null() || right.is_null() => Ok(Value::Null),
47                _ => Err(anyhow!(
48                    "InvalidArgumentType: Expected bool for AND operands"
49                )),
50            }
51        }
52        BinaryOp::Or => {
53            // Three-valued logic: true dominates, null propagates with false
54            match (left.as_bool(), right.as_bool()) {
55                (Some(true), _) | (_, Some(true)) => Ok(Value::Bool(true)),
56                (Some(false), Some(false)) => Ok(Value::Bool(false)),
57                _ if left.is_null() || right.is_null() => Ok(Value::Null),
58                _ => Err(anyhow!(
59                    "InvalidArgumentType: Expected bool for OR operands"
60                )),
61            }
62        }
63        BinaryOp::Xor => {
64            // Three-valued logic: any null operand returns null
65            match (left.as_bool(), right.as_bool()) {
66                (Some(l), Some(r)) => Ok(Value::Bool(l ^ r)),
67                _ if left.is_null() || right.is_null() => Ok(Value::Null),
68                _ => Err(anyhow!(
69                    "InvalidArgumentType: Expected bool for XOR operands"
70                )),
71            }
72        }
73        BinaryOp::Gt => eval_comparison(left, right, |ordering| ordering.is_gt()),
74        BinaryOp::Lt => eval_comparison(left, right, |ordering| ordering.is_lt()),
75        BinaryOp::GtEq => eval_comparison(left, right, |ordering| ordering.is_ge()),
76        BinaryOp::LtEq => eval_comparison(left, right, |ordering| ordering.is_le()),
77        BinaryOp::Contains => eval_string_predicate(left, right, "CONTAINS", |l, r| l.contains(r)),
78        BinaryOp::StartsWith => {
79            eval_string_predicate(left, right, "STARTS WITH", |l, r| l.starts_with(r))
80        }
81        BinaryOp::EndsWith => {
82            eval_string_predicate(left, right, "ENDS WITH", |l, r| l.ends_with(r))
83        }
84        BinaryOp::Add => eval_add(left, right),
85        BinaryOp::Sub => eval_sub(left, right),
86        BinaryOp::Mul => eval_mul(left, right),
87        BinaryOp::Div => eval_div(left, right),
88        BinaryOp::Mod => eval_numeric_op(left, right, |a, b| a % b),
89        BinaryOp::Pow => eval_numeric_op(left, right, |a, b| a.powf(b)),
90        BinaryOp::Regex => {
91            let l = left
92                .as_str()
93                .ok_or_else(|| anyhow!("Left operand of =~ must be a string"))?;
94            let pattern = right
95                .as_str()
96                .ok_or_else(|| anyhow!("Right operand of =~ must be a regex pattern string"))?;
97            let re = regex::Regex::new(pattern)
98                .map_err(|e| anyhow!("Invalid regex pattern '{}': {}", pattern, e))?;
99            Ok(Value::Bool(re.is_match(l)))
100        }
101        BinaryOp::ApproxEq => eval_vector_similarity(left, right),
102    }
103}
104
105/// Deep equality comparison with Cypher-compliant numeric coercion and 3-valued logic.
106/// Returns Some(bool) for True/False, and None for Null/Unknown.
107pub fn cypher_eq(left: &Value, right: &Value) -> Option<bool> {
108    if left.is_null() || right.is_null() {
109        return None;
110    }
111
112    // Exact integer equality — avoid f64 precision loss for large i64 values
113    if let (Some(l), Some(r)) = (left.as_i64(), right.as_i64()) {
114        return Some(l == r);
115    }
116
117    // Mixed numeric equality (1 = 1.0)
118    if let (Some(l), Some(r)) = (left.as_f64(), right.as_f64()) {
119        if l.is_nan() || r.is_nan() {
120            return Some(false);
121        }
122        return Some(l == r);
123    }
124
125    // Structural equality for Lists
126    if let (Value::List(l), Value::List(r)) = (left, right) {
127        if l.len() != r.len() {
128            return Some(false);
129        }
130        let mut has_null = false;
131        for (lv, rv) in l.iter().zip(r.iter()) {
132            match cypher_eq(lv, rv) {
133                Some(false) => return Some(false),
134                None => has_null = true,
135                Some(true) => {}
136            }
137        }
138        return if has_null { None } else { Some(true) };
139    }
140
141    // Structural equality for Maps
142    if let (Value::Map(l), Value::Map(r)) = (left, right) {
143        // If both are nodes (have _vid), compare by _vid ONLY
144        if let (Some(vid_l), Some(vid_r)) = (l.get("_vid"), r.get("_vid")) {
145            return Some(vid_l == vid_r);
146        }
147        // If both are edges (have _eid), compare by _eid ONLY
148        if let (Some(eid_l), Some(eid_r)) = (l.get("_eid"), r.get("_eid")) {
149            return Some(eid_l == eid_r);
150        }
151
152        if l.len() != r.len() {
153            return Some(false);
154        }
155
156        let mut has_null = false;
157        for (k, lv) in l {
158            if let Some(rv) = r.get(k) {
159                match cypher_eq(lv, rv) {
160                    Some(false) => return Some(false),
161                    None => has_null = true,
162                    Some(true) => {}
163                }
164            } else {
165                return Some(false);
166            }
167        }
168        return if has_null { None } else { Some(true) };
169    }
170
171    // Fallback to standard equality for other types (String, Bool)
172    Some(left == right)
173}
174
175/// Evaluate IN operator.
176pub fn eval_in_op(left: &Value, right: &Value) -> Result<Value> {
177    if let Value::List(arr) = right {
178        let mut has_null = false;
179        // Check exact match using cypher_eq (handles numeric coercion and node identity)
180        for item in arr {
181            match cypher_eq(left, item) {
182                Some(true) => return Ok(Value::Bool(true)),
183                None => has_null = true,
184                _ => {}
185            }
186        }
187
188        // Fallback: Check for Node Object vs VID mismatch.
189        // When left is a node map, compare its _vid against list items that may
190        // be raw VID integers or "label:offset" strings.
191        if let Value::Map(map) = left
192            && let Some(vid_val) = map.get("_vid")
193            && let Some(vid_u64) = vid_val.as_u64()
194        {
195            let vid = uni_common::core::id::Vid::from(vid_u64);
196            let vid_str = vid.to_string();
197            for item in arr {
198                match item {
199                    Value::String(s) if s == &vid_str => return Ok(Value::Bool(true)),
200                    Value::Int(n) if *n as u64 == vid_u64 => return Ok(Value::Bool(true)),
201                    _ => {}
202                }
203            }
204        }
205
206        if has_null {
207            Ok(Value::Null)
208        } else {
209            Ok(Value::Bool(false))
210        }
211    } else {
212        Err(anyhow!("Right side of IN must be a list"))
213    }
214}
215
216fn eval_string_predicate(
217    left: &Value,
218    right: &Value,
219    op_name: &str,
220    check: fn(&str, &str) -> bool,
221) -> Result<Value> {
222    let l = left
223        .as_str()
224        .ok_or_else(|| anyhow!("Left side of {} must be a string", op_name))?;
225    let r = right
226        .as_str()
227        .ok_or_else(|| anyhow!("Right side of {} must be a string", op_name))?;
228    Ok(Value::Bool(check(l, r)))
229}
230
231fn eval_numeric_op<F>(left: &Value, right: &Value, op: F) -> Result<Value>
232where
233    F: Fn(f64, f64) -> f64,
234{
235    // Cypher null propagation: null op anything = null
236    if left.is_null() || right.is_null() {
237        return Ok(Value::Null);
238    }
239    let (l, r) = match (left.as_f64(), right.as_f64()) {
240        (Some(l), Some(r)) => (l, r),
241        _ => return Err(anyhow!("Arithmetic operation requires numbers")),
242    };
243    let result = op(l, r);
244    // Return integer if result has no fractional part and both inputs were integers
245    if !result.is_nan()
246        && !result.is_infinite()
247        && result.fract() == 0.0
248        && left.is_i64()
249        && right.is_i64()
250    {
251        Ok(Value::Int(result as i64))
252    } else {
253        Ok(Value::Float(result))
254    }
255}
256
257// ============================================================================
258// Temporal-aware arithmetic operations
259// ============================================================================
260
261/// Add a duration to a temporal value, dispatching by temporal type.
262/// Accepts both Value::Temporal and Value::String temporal values.
263fn add_temporal_duration_to_value(val: &Value, dur: &CypherDuration) -> Result<Value> {
264    match val {
265        Value::Temporal(tv) => add_temporal_duration_typed(tv, dur),
266        Value::Map(map) => {
267            if let Some(tv) = temporal_from_map_wrapper(map) {
268                add_temporal_duration_typed(&tv, dur)
269            } else {
270                Err(anyhow!("Expected temporal value for duration arithmetic"))
271            }
272        }
273        Value::String(s) => {
274            if let Some(tv) = temporal_from_json_wrapper_str(s) {
275                return add_temporal_duration_typed(&tv, dur);
276            }
277            let ttype = classify_temporal(s)
278                .ok_or_else(|| anyhow!("Cannot classify temporal value: {}", s))?;
279            let result_str = match ttype {
280                TemporalType::Date => add_cypher_duration_to_date(s, dur)?,
281                TemporalType::LocalTime => add_cypher_duration_to_localtime(s, dur)?,
282                TemporalType::Time => add_cypher_duration_to_time(s, dur)?,
283                TemporalType::LocalDateTime => add_cypher_duration_to_localdatetime(s, dur)?,
284                TemporalType::DateTime => add_cypher_duration_to_datetime(s, dur)?,
285                TemporalType::Duration => {
286                    return Err(anyhow!("Cannot add duration to duration this way"));
287                }
288            };
289            Ok(Value::String(result_str))
290        }
291        _ => Err(anyhow!("Expected temporal value for duration arithmetic")),
292    }
293}
294
295/// Add a CypherDuration to a typed TemporalValue, returning a new Value::Temporal.
296fn add_temporal_duration_typed(tv: &TemporalValue, dur: &CypherDuration) -> Result<Value> {
297    // Convert to string, perform the operation, and re-parse the result.
298    // This reuses the existing well-tested string-based arithmetic.
299    let s = tv.to_string();
300    let ttype = tv.temporal_type();
301    let result_str = match ttype {
302        TemporalType::Date => add_cypher_duration_to_date(&s, dur)?,
303        TemporalType::LocalTime => add_cypher_duration_to_localtime(&s, dur)?,
304        TemporalType::Time => add_cypher_duration_to_time(&s, dur)?,
305        TemporalType::LocalDateTime => add_cypher_duration_to_localdatetime(&s, dur)?,
306        TemporalType::DateTime => add_cypher_duration_to_datetime(&s, dur)?,
307        TemporalType::Duration => return Err(anyhow!("Cannot add duration to duration this way")),
308    };
309    // Re-parse through the datetime constructor to get a Value::Temporal
310    let args = [Value::String(result_str)];
311    match ttype {
312        TemporalType::Date => eval_datetime_function("DATE", &args),
313        TemporalType::LocalTime => eval_datetime_function("LOCALTIME", &args),
314        TemporalType::Time => eval_datetime_function("TIME", &args),
315        TemporalType::LocalDateTime => eval_datetime_function("LOCALDATETIME", &args),
316        TemporalType::DateTime => eval_datetime_function("DATETIME", &args),
317        TemporalType::Duration => unreachable!(),
318    }
319}
320
321/// Evaluate addition with temporal-aware dispatch.
322fn eval_add(left: &Value, right: &Value) -> Result<Value> {
323    // Null propagation
324    if left.is_null() || right.is_null() {
325        return Ok(Value::Null);
326    }
327
328    // List concatenation: list + list, list + scalar, scalar + list
329    match (left, right) {
330        (Value::List(l), Value::List(r)) => {
331            let mut result = l.clone();
332            result.extend(r.iter().cloned());
333            return Ok(Value::List(result));
334        }
335        (Value::List(l), _) => {
336            let mut result = l.clone();
337            result.push(right.clone());
338            return Ok(Value::List(result));
339        }
340        (_, Value::List(r)) => {
341            let mut result = vec![left.clone()];
342            result.extend(r.iter().cloned());
343            return Ok(Value::List(result));
344        }
345        _ => {}
346    }
347
348    // Numeric addition
349    if let (Some(l), Some(r)) = (left.as_f64(), right.as_f64()) {
350        if left.is_i64() && right.is_i64() {
351            return Ok(Value::Int(left.as_i64().unwrap() + right.as_i64().unwrap()));
352        }
353        return Ok(Value::Float(l + r));
354    }
355
356    // Temporal string + Duration / Duration + Temporal string
357    if let Value::String(s) = left
358        && classify_temporal(s).is_some_and(|t| t != TemporalType::Duration)
359        && let Ok(dur) = parse_duration_from_value(right)
360    {
361        return add_temporal_duration_to_value(left, &dur);
362    }
363    if let Value::String(s) = right
364        && classify_temporal(s).is_some_and(|t| t != TemporalType::Duration)
365        && let Ok(dur) = parse_duration_from_value(left)
366    {
367        return add_temporal_duration_to_value(right, &dur);
368    }
369
370    // Temporal + Duration (supports typed temporals and map-wrapped temporals)
371    if let Some(tv) = temporal_from_value(left)
372        && !matches!(tv, TemporalValue::Duration { .. })
373        && (is_duration_value(right) || right.is_number())
374    {
375        let dur = parse_duration_from_value(right)?;
376        return add_temporal_duration_typed(&tv, &dur);
377    }
378    // Duration + Temporal
379    if let Some(tv) = temporal_from_value(right)
380        && !matches!(tv, TemporalValue::Duration { .. })
381        && (is_duration_value(left) || left.is_number())
382    {
383        let dur = parse_duration_from_value(left)?;
384        return add_temporal_duration_typed(&tv, &dur);
385    }
386    // Duration + Duration
387    if let (
388        Some(TemporalValue::Duration {
389            months: m1,
390            days: d1,
391            nanos: n1,
392        }),
393        Some(TemporalValue::Duration {
394            months: m2,
395            days: d2,
396            nanos: n2,
397        }),
398    ) = (temporal_from_value(left), temporal_from_value(right))
399    {
400        return Ok(Value::Temporal(TemporalValue::Duration {
401            months: m1 + m2,
402            days: d1 + d2,
403            nanos: n1 + n2,
404        }));
405    }
406
407    // String concatenation (with temporal awareness for backward compat)
408    if let (Value::String(l), Value::String(r)) = (left, right) {
409        let l_type = classify_temporal(l);
410        let r_type = classify_temporal(r);
411
412        match (l_type, r_type) {
413            // temporal + duration
414            (Some(lt), Some(TemporalType::Duration)) if lt != TemporalType::Duration => {
415                let dur = parse_duration_to_cypher(r)?;
416                return add_temporal_duration_to_value(left, &dur);
417            }
418            // duration + temporal
419            (Some(TemporalType::Duration), Some(rt)) if rt != TemporalType::Duration => {
420                let dur = parse_duration_to_cypher(l)?;
421                return add_temporal_duration_to_value(right, &dur);
422            }
423            // duration + duration (component-wise)
424            (Some(TemporalType::Duration), Some(TemporalType::Duration)) => {
425                let d1 = parse_duration_to_cypher(l)?;
426                let d2 = parse_duration_to_cypher(r)?;
427                return Ok(Value::String(d1.add(&d2).to_iso8601()));
428            }
429            // Not temporal: string concatenation
430            _ => return Ok(Value::String(format!("{}{}", l, r))),
431        }
432    }
433
434    // temporal string + integer microseconds
435    if let Value::String(_) = left
436        && right.is_number()
437        && classify_value_temporal(left).is_some_and(|t| t != TemporalType::Duration)
438    {
439        let dur = parse_duration_from_value(right)?;
440        return add_temporal_duration_to_value(left, &dur);
441    }
442    // integer microseconds + temporal string
443    if let Value::String(_) = right
444        && left.is_number()
445        && classify_value_temporal(right).is_some_and(|t| t != TemporalType::Duration)
446    {
447        let dur = parse_duration_from_value(left)?;
448        return add_temporal_duration_to_value(right, &dur);
449    }
450
451    Err(anyhow!(
452        "Invalid types for addition: left={:?}, right={:?}",
453        left,
454        right
455    ))
456}
457
458/// Classify a Value's temporal type (works for both Temporal and String).
459fn classify_value_temporal(val: &Value) -> Option<TemporalType> {
460    match val {
461        Value::Temporal(tv) => Some(tv.temporal_type()),
462        Value::String(s) => classify_temporal(s),
463        _ => None,
464    }
465}
466
467/// Evaluate subtraction with temporal-aware dispatch.
468fn eval_sub(left: &Value, right: &Value) -> Result<Value> {
469    // Null propagation
470    if left.is_null() || right.is_null() {
471        return Ok(Value::Null);
472    }
473
474    // Temporal - Duration (Value::Temporal)
475    if let Value::Temporal(tv) = left
476        && !matches!(tv, TemporalValue::Duration { .. })
477    {
478        if let Value::Temporal(TemporalValue::Duration {
479            months,
480            days,
481            nanos,
482        }) = right
483        {
484            let dur = CypherDuration::new(-months, -days, -nanos);
485            return add_temporal_duration_typed(tv, &dur);
486        }
487        if is_duration_value(right) || right.is_number() {
488            let dur = parse_duration_from_value(right)?.negate();
489            return add_temporal_duration_typed(tv, &dur);
490        }
491    }
492    // Duration - Duration (Value::Temporal)
493    if let (
494        Value::Temporal(TemporalValue::Duration {
495            months: m1,
496            days: d1,
497            nanos: n1,
498        }),
499        Value::Temporal(TemporalValue::Duration {
500            months: m2,
501            days: d2,
502            nanos: n2,
503        }),
504    ) = (left, right)
505    {
506        return Ok(Value::Temporal(TemporalValue::Duration {
507            months: m1 - m2,
508            days: d1 - d2,
509            nanos: n1 - n2,
510        }));
511    }
512    // Same temporal type - temporal difference
513    if let (Value::Temporal(l), Value::Temporal(r)) = (left, right)
514        && l.temporal_type() == r.temporal_type()
515        && l.temporal_type() != TemporalType::Duration
516    {
517        let args = [left.clone(), right.clone()];
518        return crate::query::datetime::eval_datetime_function("DURATION.BETWEEN", &args);
519    }
520
521    // String temporal - duration (backward compat)
522    if let (Value::String(l), Value::String(r)) = (left, right) {
523        let l_type = classify_temporal(l);
524        let r_type = classify_temporal(r);
525
526        match (l_type, r_type) {
527            (Some(lt), Some(TemporalType::Duration)) if lt != TemporalType::Duration => {
528                let dur = parse_duration_to_cypher(r)?.negate();
529                return add_temporal_duration_to_value(left, &dur);
530            }
531            (Some(TemporalType::Duration), Some(TemporalType::Duration)) => {
532                let d1 = parse_duration_to_cypher(l)?;
533                let d2 = parse_duration_to_cypher(r)?;
534                return Ok(Value::String(d1.sub(&d2).to_iso8601()));
535            }
536            (Some(lt), Some(rt))
537                if lt != TemporalType::Duration && rt != TemporalType::Duration && lt == rt =>
538            {
539                let args = [left.clone(), right.clone()];
540                return crate::query::datetime::eval_datetime_function("DURATION.BETWEEN", &args);
541            }
542            _ => {}
543        }
544    }
545
546    // temporal string - integer microseconds
547    if let Value::String(_) = left
548        && right.is_number()
549        && classify_value_temporal(left).is_some_and(|t| t != TemporalType::Duration)
550    {
551        let dur = parse_duration_from_value(right)?.negate();
552        return add_temporal_duration_to_value(left, &dur);
553    }
554
555    eval_numeric_op(left, right, |a, b| a - b)
556}
557
558/// Extract a CypherDuration from a Value, if it is a duration type.
559///
560/// Handles both `Value::Temporal(Duration { .. })` and duration strings.
561fn extract_cypher_duration(val: &Value) -> Option<Result<(CypherDuration, bool)>> {
562    match val {
563        Value::Temporal(TemporalValue::Duration {
564            months,
565            days,
566            nanos,
567        }) => Some(Ok((CypherDuration::new(*months, *days, *nanos), true))),
568        Value::String(s) if is_duration_value(val) => {
569            Some(parse_duration_to_cypher(s).map(|d| (d, false)))
570        }
571        _ => None,
572    }
573}
574
575/// Convert a `CypherDuration` result back to the appropriate `Value` type.
576///
577/// `is_temporal` indicates whether the source was a `Value::Temporal` (returns temporal)
578/// or a `Value::String` (returns ISO 8601 string).
579fn duration_to_value(result: CypherDuration, is_temporal: bool) -> Value {
580    if is_temporal {
581        result.to_temporal_value()
582    } else {
583        Value::String(result.to_iso8601())
584    }
585}
586
587/// Evaluate multiplication with duration support.
588fn eval_mul(left: &Value, right: &Value) -> Result<Value> {
589    if left.is_null() || right.is_null() {
590        return Ok(Value::Null);
591    }
592
593    // duration * number (either side)
594    if let Some(dur_result) = extract_cypher_duration(left)
595        && let Some(factor) = right.as_f64()
596    {
597        let (dur, is_temporal) = dur_result?;
598        return Ok(duration_to_value(dur.multiply(factor), is_temporal));
599    }
600    if let Some(dur_result) = extract_cypher_duration(right)
601        && let Some(factor) = left.as_f64()
602    {
603        let (dur, is_temporal) = dur_result?;
604        return Ok(duration_to_value(dur.multiply(factor), is_temporal));
605    }
606
607    eval_numeric_op(left, right, |a, b| a * b)
608}
609
610/// Evaluate division with duration support.
611fn eval_div(left: &Value, right: &Value) -> Result<Value> {
612    if left.is_null() || right.is_null() {
613        return Ok(Value::Null);
614    }
615
616    // duration / number (left side only -- division is not commutative)
617    if let Some(dur_result) = extract_cypher_duration(left)
618        && let Some(divisor) = right.as_f64()
619    {
620        let (dur, is_temporal) = dur_result?;
621        return Ok(duration_to_value(dur.divide(divisor), is_temporal));
622    }
623
624    // OpenCypher: integer / integer = integer (truncated toward zero)
625    if let (Value::Int(l), Value::Int(r)) = (left, right) {
626        return if *r == 0 {
627            Err(anyhow!("Division by zero"))
628        } else {
629            Ok(Value::Int(l / r))
630        };
631    }
632
633    eval_numeric_op(left, right, |a, b| a / b)
634}
635
636/// Helper for comparisons between two values with temporal awareness and structural support.
637///
638/// Per Cypher semantics:
639/// - NULL compared with anything returns NULL
640/// - Incompatible types (e.g., string vs int) return NULL, not an error
641fn eval_comparison<F>(left: &Value, right: &Value, check: F) -> Result<Value>
642where
643    F: Fn(Ordering) -> bool,
644{
645    // Handle NULL inputs - any comparison with NULL returns NULL
646    if left.is_null() || right.is_null() {
647        return Ok(Value::Null);
648    }
649
650    // Handle NaN - NaN vs number returns false, NaN vs non-number returns null (cross-type)
651    let left_nan = left.as_f64().is_some_and(|f| f.is_nan());
652    let right_nan = right.as_f64().is_some_and(|f| f.is_nan());
653    if left_nan || right_nan {
654        if left_nan && right_nan {
655            return Ok(Value::Bool(false));
656        }
657        let other = if left_nan { right } else { left };
658        if other.as_f64().is_some() {
659            return Ok(Value::Bool(false)); // NaN vs number
660        }
661        return Ok(Value::Null); // NaN vs non-number (cross-type)
662    }
663
664    let ord = cypher_partial_cmp(left, right);
665    match ord {
666        Some(o) => Ok(Value::Bool(check(o))),
667        None => Ok(Value::Null),
668    }
669}
670
671/// Deep partial comparison with Cypher-compliant numeric coercion and structural support.
672fn cypher_partial_cmp(left: &Value, right: &Value) -> Option<Ordering> {
673    if left.is_null() || right.is_null() {
674        return None;
675    }
676
677    let left_temporal = temporal_from_value(left);
678    let right_temporal = temporal_from_value(right);
679    if let (Some(l), Some(r)) = (&left_temporal, &right_temporal) {
680        return temporal_partial_cmp(l, r);
681    }
682    if let (Some(_), Value::String(rs)) = (&left_temporal, right) {
683        let ls = left.to_string();
684        if let (Some(lt), Some(rt)) = (classify_temporal(&ls), classify_temporal(rs))
685            && lt == rt
686        {
687            return temporal_string_cmp(&ls, rs, lt);
688        }
689        return None;
690    }
691    if let (Value::String(ls), Some(_)) = (left, &right_temporal) {
692        let rs = right.to_string();
693        if let (Some(lt), Some(rt)) = (classify_temporal(ls), classify_temporal(&rs))
694            && lt == rt
695        {
696            return temporal_string_cmp(ls, &rs, lt);
697        }
698        return None;
699    }
700
701    // Exact integer ordering — avoid f64 precision loss for large i64 values
702    if let (Some(l), Some(r)) = (left.as_i64(), right.as_i64()) {
703        return Some(l.cmp(&r));
704    }
705
706    // Number vs Number
707    if let (Some(l), Some(r)) = (left.as_f64(), right.as_f64()) {
708        return l.partial_cmp(&r);
709    }
710
711    // String vs String (includes temporal string comparison for ISO-format strings)
712    if let (Some(l), Some(r)) = (left.as_str(), right.as_str()) {
713        // Temporal-aware comparison
714        if let (Some(lt), Some(rt)) = (classify_temporal(l), classify_temporal(r))
715            && lt == rt
716        {
717            let res = temporal_string_cmp(l, r, lt);
718            if res.is_some() {
719                return res;
720            }
721        }
722        return l.partial_cmp(r);
723    }
724
725    // Boolean vs Boolean
726    if let (Some(l), Some(r)) = (left.as_bool(), right.as_bool()) {
727        return l.partial_cmp(&r);
728    }
729
730    // Array vs Array (Lexicographic)
731    if let (Value::List(l), Value::List(r)) = (left, right) {
732        for (lv, rv) in l.iter().zip(r.iter()) {
733            match cypher_partial_cmp(lv, rv) {
734                Some(Ordering::Equal) => continue,
735                other => return other,
736            }
737        }
738        return l.len().partial_cmp(&r.len());
739    }
740
741    // Maps are not orderable in Cypher, only comparable for equality
742    None
743}
744
745/// Compare two TemporalValues directly using numeric representation.
746fn temporal_partial_cmp(left: &TemporalValue, right: &TemporalValue) -> Option<Ordering> {
747    match (left, right) {
748        (
749            TemporalValue::Date {
750                days_since_epoch: l,
751            },
752            TemporalValue::Date {
753                days_since_epoch: r,
754            },
755        ) => Some(l.cmp(r)),
756        (
757            TemporalValue::LocalTime {
758                nanos_since_midnight: l,
759            },
760            TemporalValue::LocalTime {
761                nanos_since_midnight: r,
762            },
763        ) => Some(l.cmp(r)),
764        (
765            TemporalValue::Time {
766                nanos_since_midnight: lm,
767                offset_seconds: lo,
768            },
769            TemporalValue::Time {
770                nanos_since_midnight: rm,
771                offset_seconds: ro,
772            },
773        ) => {
774            // Compare in UTC: local_nanos - offset
775            let l_utc = *lm as i128 - (*lo as i128) * 1_000_000_000;
776            let r_utc = *rm as i128 - (*ro as i128) * 1_000_000_000;
777            Some(l_utc.cmp(&r_utc))
778        }
779        (
780            TemporalValue::LocalDateTime {
781                nanos_since_epoch: l,
782            },
783            TemporalValue::LocalDateTime {
784                nanos_since_epoch: r,
785            },
786        ) => Some(l.cmp(r)),
787        (
788            TemporalValue::DateTime {
789                nanos_since_epoch: l,
790                ..
791            },
792            TemporalValue::DateTime {
793                nanos_since_epoch: r,
794                ..
795            },
796        ) => {
797            // Both are in UTC, so direct comparison
798            Some(l.cmp(r))
799        }
800        // Durations are not orderable
801        (TemporalValue::Duration { .. }, TemporalValue::Duration { .. }) => None,
802        // Different temporal types are not comparable
803        _ => None,
804    }
805}
806
807/// Extract a `TemporalValue` from any `Value` variant that can represent one.
808///
809/// Handles `Value::Temporal`, `Value::Map` (JSON-serialized temporal wrappers),
810/// and `Value::String` — first tries JSON wrapper format
811/// (`{"Date":{"days_since_epoch":0}}`), then falls back to human-readable
812/// ISO 8601 strings like `"2024-01-15"` or `"12:35:15+05:00"`.
813pub(crate) fn temporal_from_value(v: &Value) -> Option<TemporalValue> {
814    match v {
815        Value::Temporal(tv) => Some(tv.clone()),
816        Value::Map(map) => temporal_from_map_wrapper(map),
817        Value::String(s) => {
818            temporal_from_json_wrapper_str(s).or_else(|| temporal_from_human_readable_str(s))
819        }
820        _ => None,
821    }
822}
823
824/// Parse a human-readable ISO 8601 temporal string (e.g. `"12:35:15+05:00"`,
825/// `"2024-01-15"`) into a `TemporalValue` by classifying and evaluating it.
826pub(crate) fn temporal_from_human_readable_str(s: &str) -> Option<TemporalValue> {
827    let fn_name = match classify_temporal(s)? {
828        TemporalType::Date => "DATE",
829        TemporalType::LocalTime => "LOCALTIME",
830        TemporalType::Time => "TIME",
831        TemporalType::LocalDateTime => "LOCALDATETIME",
832        TemporalType::DateTime => "DATETIME",
833        TemporalType::Duration => "DURATION",
834    };
835    match eval_datetime_function(fn_name, &[Value::String(s.to_string())]).ok()? {
836        Value::Temporal(tv) => Some(tv),
837        _ => None,
838    }
839}
840
841/// Try to interpret a map as a temporal value.
842///
843/// Recognizes single-entry maps with a temporal type key (`Date`, `Time`, etc.)
844/// whose value is a map of the appropriate fields. Returns `None` if the map
845/// does not match any temporal pattern.
846pub(crate) fn temporal_from_map_wrapper(
847    map: &std::collections::HashMap<String, Value>,
848) -> Option<TemporalValue> {
849    if map.len() != 1 {
850        return None;
851    }
852
853    let as_i32 = |v: &Value| v.as_i64().and_then(|n| i32::try_from(n).ok());
854    let as_i64 = |v: &Value| v.as_i64();
855
856    if let Some(Value::Map(inner)) = map.get("Date") {
857        let days = inner.get("days_since_epoch").and_then(as_i32)?;
858        return Some(TemporalValue::Date {
859            days_since_epoch: days,
860        });
861    }
862    if let Some(Value::Map(inner)) = map.get("LocalTime") {
863        let nanos = inner.get("nanos_since_midnight").and_then(as_i64)?;
864        return Some(TemporalValue::LocalTime {
865            nanos_since_midnight: nanos,
866        });
867    }
868    if let Some(Value::Map(inner)) = map.get("Time") {
869        let nanos = inner.get("nanos_since_midnight").and_then(as_i64)?;
870        let offset = inner.get("offset_seconds").and_then(as_i32)?;
871        return Some(TemporalValue::Time {
872            nanos_since_midnight: nanos,
873            offset_seconds: offset,
874        });
875    }
876    if let Some(Value::Map(inner)) = map.get("LocalDateTime") {
877        let nanos = inner.get("nanos_since_epoch").and_then(as_i64)?;
878        return Some(TemporalValue::LocalDateTime {
879            nanos_since_epoch: nanos,
880        });
881    }
882    if let Some(Value::Map(inner)) = map.get("DateTime") {
883        let nanos = inner.get("nanos_since_epoch").and_then(as_i64)?;
884        let offset = inner.get("offset_seconds").and_then(as_i32)?;
885        let timezone_name = match inner.get("timezone_name") {
886            Some(Value::String(s)) => Some(s.clone()),
887            _ => None,
888        };
889        return Some(TemporalValue::DateTime {
890            nanos_since_epoch: nanos,
891            offset_seconds: offset,
892            timezone_name,
893        });
894    }
895    if let Some(Value::Map(inner)) = map.get("Duration") {
896        let months = inner.get("months").and_then(as_i64)?;
897        let days = inner.get("days").and_then(as_i64)?;
898        let nanos = inner.get("nanos").and_then(as_i64)?;
899        return Some(TemporalValue::Duration {
900            months,
901            days,
902            nanos,
903        });
904    }
905    None
906}
907
908fn temporal_from_json_wrapper_str(s: &str) -> Option<TemporalValue> {
909    let parsed: serde_json::Value = serde_json::from_str(s).ok()?;
910    let obj = parsed.as_object()?;
911    if obj.len() != 1 {
912        return None;
913    }
914
915    let as_i32 = |o: &serde_json::Map<String, serde_json::Value>, key: &str| {
916        o.get(key)
917            .and_then(serde_json::Value::as_i64)
918            .and_then(|n| i32::try_from(n).ok())
919    };
920    let as_i64 = |o: &serde_json::Map<String, serde_json::Value>, key: &str| {
921        o.get(key).and_then(serde_json::Value::as_i64)
922    };
923
924    if let Some(inner) = obj.get("Date").and_then(serde_json::Value::as_object) {
925        return Some(TemporalValue::Date {
926            days_since_epoch: as_i32(inner, "days_since_epoch")?,
927        });
928    }
929    if let Some(inner) = obj.get("LocalTime").and_then(serde_json::Value::as_object) {
930        return Some(TemporalValue::LocalTime {
931            nanos_since_midnight: as_i64(inner, "nanos_since_midnight")?,
932        });
933    }
934    if let Some(inner) = obj.get("Time").and_then(serde_json::Value::as_object) {
935        return Some(TemporalValue::Time {
936            nanos_since_midnight: as_i64(inner, "nanos_since_midnight")?,
937            offset_seconds: as_i32(inner, "offset_seconds")?,
938        });
939    }
940    if let Some(inner) = obj
941        .get("LocalDateTime")
942        .and_then(serde_json::Value::as_object)
943    {
944        return Some(TemporalValue::LocalDateTime {
945            nanos_since_epoch: as_i64(inner, "nanos_since_epoch")?,
946        });
947    }
948    if let Some(inner) = obj.get("DateTime").and_then(serde_json::Value::as_object) {
949        return Some(TemporalValue::DateTime {
950            nanos_since_epoch: as_i64(inner, "nanos_since_epoch")?,
951            offset_seconds: as_i32(inner, "offset_seconds")?,
952            timezone_name: inner
953                .get("timezone_name")
954                .and_then(serde_json::Value::as_str)
955                .map(str::to_string),
956        });
957    }
958    if let Some(inner) = obj.get("Duration").and_then(serde_json::Value::as_object) {
959        return Some(TemporalValue::Duration {
960            months: as_i64(inner, "months")?,
961            days: as_i64(inner, "days")?,
962            nanos: as_i64(inner, "nanos")?,
963        });
964    }
965    None
966}
967
968/// Compare two temporal strings of the same type.
969fn temporal_string_cmp(l: &str, r: &str, ttype: TemporalType) -> Option<Ordering> {
970    match ttype {
971        TemporalType::Date => {
972            let ld = chrono::NaiveDate::parse_from_str(l, "%Y-%m-%d").ok();
973            let rd = chrono::NaiveDate::parse_from_str(r, "%Y-%m-%d").ok();
974            ld.and_then(|l| rd.map(|r| l.cmp(&r)))
975        }
976        TemporalType::LocalTime => {
977            let lt = parse_time_for_cmp(l).ok();
978            let rt = parse_time_for_cmp(r).ok();
979            lt.and_then(|l| rt.map(|r| l.cmp(&r)))
980        }
981        TemporalType::Time => {
982            let ln = time_with_tz_to_utc_nanos(l).ok();
983            let rn = time_with_tz_to_utc_nanos(r).ok();
984            ln.and_then(|l| rn.map(|r| l.cmp(&r)))
985        }
986        TemporalType::LocalDateTime => {
987            let ldt = parse_local_datetime_for_cmp(l).ok();
988            let rdt = parse_local_datetime_for_cmp(r).ok();
989            ldt.and_then(|l| rdt.map(|r| l.cmp(&r)))
990        }
991        TemporalType::DateTime => {
992            let ldt = parse_datetime_utc(l).ok();
993            let rdt = parse_datetime_utc(r).ok();
994            ldt.and_then(|l| rdt.map(|r| l.cmp(&r)))
995        }
996        TemporalType::Duration => None, // Durations are not orderable
997    }
998}
999
1000/// Parse a time string for comparison.
1001fn parse_time_for_cmp(s: &str) -> Result<chrono::NaiveTime> {
1002    chrono::NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
1003        .or_else(|_| chrono::NaiveTime::parse_from_str(s, "%H:%M:%S"))
1004        .or_else(|_| chrono::NaiveTime::parse_from_str(s, "%H:%M"))
1005        .map_err(|_| anyhow!("Cannot parse time: {}", s))
1006}
1007
1008/// Parse a local datetime string for comparison.
1009fn parse_local_datetime_for_cmp(s: &str) -> Result<chrono::NaiveDateTime> {
1010    chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f")
1011        .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"))
1012        .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M"))
1013        .map_err(|_| anyhow!("Cannot parse localdatetime: {}", s))
1014}
1015
1016const NANOS_PER_SECOND_CMP: i64 = 1_000_000_000;
1017
1018/// Normalize a time-with-timezone string to UTC nanoseconds for comparison.
1019fn time_with_tz_to_utc_nanos(s: &str) -> Result<i64> {
1020    use chrono::Timelike;
1021    let (_, time, tz_info) = crate::query::datetime::parse_datetime_with_tz(s)?;
1022    let local_nanos = time.hour() as i64 * 3_600 * NANOS_PER_SECOND_CMP
1023        + time.minute() as i64 * 60 * NANOS_PER_SECOND_CMP
1024        + time.second() as i64 * NANOS_PER_SECOND_CMP
1025        + time.nanosecond() as i64;
1026
1027    // Subtract timezone offset to get UTC
1028    let offset_secs: i64 = match tz_info {
1029        Some(ref tz) => {
1030            let today = chrono::NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
1031            let ndt = chrono::NaiveDateTime::new(today, time);
1032            tz.offset_for_local(&ndt)?.local_minus_utc() as i64
1033        }
1034        None => 0,
1035    };
1036
1037    Ok(local_nanos - offset_secs * NANOS_PER_SECOND_CMP)
1038}
1039
1040// ============================================================================
1041// List/Collection function helpers
1042// ============================================================================
1043
1044fn eval_size(arg: &Value) -> Result<Value> {
1045    match arg {
1046        Value::List(arr) => Ok(Value::Int(arr.len() as i64)),
1047        Value::Map(map) => Ok(Value::Int(map.len() as i64)),
1048        Value::String(s) => Ok(Value::Int(s.len() as i64)),
1049        Value::Null => Ok(Value::Null),
1050        _ => Err(anyhow!("size() expects a List, Map, or String")),
1051    }
1052}
1053
1054fn eval_keys(arg: &Value) -> Result<Value> {
1055    match arg {
1056        Value::Map(map) => {
1057            // Entities (nodes/edges) are detected by internal fields (_vid, _eid).
1058            // For entities, null-valued properties don't exist (REMOVE sets them to Null).
1059            // For plain maps, null-valued keys are valid and must be included.
1060            let is_entity =
1061                map.contains_key("_vid") || map.contains_key("_eid") || map.contains_key("_labels");
1062            let mut keys: Vec<&String> = map
1063                .iter()
1064                .filter(|(k, v)| !k.starts_with('_') && (!is_entity || !v.is_null()))
1065                .map(|(k, _)| k)
1066                .collect();
1067            keys.sort();
1068            Ok(Value::List(
1069                keys.into_iter().map(|k| Value::String(k.clone())).collect(),
1070            ))
1071        }
1072        Value::Null => Ok(Value::Null),
1073        _ => Err(anyhow!("keys() expects a Map")),
1074    }
1075}
1076
1077fn eval_head(arg: &Value) -> Result<Value> {
1078    match arg {
1079        Value::List(arr) => Ok(arr.first().cloned().unwrap_or(Value::Null)),
1080        Value::Null => Ok(Value::Null),
1081        _ => Err(anyhow!("head() expects a List")),
1082    }
1083}
1084
1085fn eval_tail(arg: &Value) -> Result<Value> {
1086    match arg {
1087        Value::List(arr) => Ok(Value::List(arr.get(1..).unwrap_or_default().to_vec())),
1088        Value::Null => Ok(Value::Null),
1089        _ => Err(anyhow!("tail() expects a List")),
1090    }
1091}
1092
1093fn eval_last(arg: &Value) -> Result<Value> {
1094    match arg {
1095        Value::List(arr) => Ok(arr.last().cloned().unwrap_or(Value::Null)),
1096        Value::Null => Ok(Value::Null),
1097        _ => Err(anyhow!("last() expects a List")),
1098    }
1099}
1100
1101fn eval_length(arg: &Value) -> Result<Value> {
1102    match arg {
1103        Value::List(arr) => Ok(Value::Int(arr.len() as i64)),
1104        Value::String(s) => Ok(Value::Int(s.len() as i64)),
1105        Value::Path(p) => Ok(Value::Int(p.edges.len() as i64)),
1106        Value::Map(map) => {
1107            // Path object encoded as map (legacy fallback)
1108            if map.contains_key("nodes")
1109                && map.contains_key("relationships")
1110                && let Some(Value::List(rels)) = map.get("relationships")
1111            {
1112                return Ok(Value::Int(rels.len() as i64));
1113            }
1114            Ok(Value::Null)
1115        }
1116        Value::Null => Ok(Value::Null),
1117        _ => Err(anyhow!("length() expects a List, String, or Path")),
1118    }
1119}
1120
1121fn eval_nodes(arg: &Value) -> Result<Value> {
1122    match arg {
1123        Value::Path(p) => Ok(Value::List(
1124            p.nodes.iter().map(|n| Value::Node(n.clone())).collect(),
1125        )),
1126        Value::Map(map) => {
1127            if let Some(nodes) = map.get("nodes") {
1128                Ok(nodes.clone())
1129            } else {
1130                Ok(Value::Null)
1131            }
1132        }
1133        Value::Null => Ok(Value::Null),
1134        _ => Err(anyhow!("nodes() expects a Path")),
1135    }
1136}
1137
1138fn eval_relationships(arg: &Value) -> Result<Value> {
1139    match arg {
1140        Value::Path(p) => Ok(Value::List(
1141            p.edges.iter().map(|e| Value::Edge(e.clone())).collect(),
1142        )),
1143        Value::Map(map) => {
1144            if let Some(rels) = map.get("relationships") {
1145                Ok(rels.clone())
1146            } else {
1147                Ok(Value::Null)
1148            }
1149        }
1150        Value::Null => Ok(Value::Null),
1151        _ => Err(anyhow!("relationships() expects a Path")),
1152    }
1153}
1154
1155/// Evaluate list/collection functions: SIZE, KEYS, HEAD, TAIL, LAST, LENGTH, NODES, RELATIONSHIPS
1156fn eval_list_function(name: &str, args: &[Value]) -> Result<Value> {
1157    if args.len() != 1 {
1158        return Err(anyhow!("{}() requires 1 argument", name));
1159    }
1160    match name {
1161        "SIZE" => eval_size(&args[0]),
1162        "KEYS" => eval_keys(&args[0]),
1163        "HEAD" => eval_head(&args[0]),
1164        "TAIL" => eval_tail(&args[0]),
1165        "LAST" => eval_last(&args[0]),
1166        "LENGTH" => eval_length(&args[0]),
1167        "NODES" => eval_nodes(&args[0]),
1168        "RELATIONSHIPS" => eval_relationships(&args[0]),
1169        _ => Err(anyhow!("Unknown list function: {}", name)),
1170    }
1171}
1172
1173// ============================================================================
1174// Type conversion function helpers
1175// ============================================================================
1176
1177fn eval_tointeger(arg: &Value) -> Result<Value> {
1178    match arg {
1179        Value::Int(i) => Ok(Value::Int(*i)),
1180        Value::Float(f) => Ok(Value::Int(*f as i64)),
1181        Value::String(s) => Ok(s.parse::<i64>().map(Value::Int).unwrap_or(Value::Null)),
1182        Value::Null => Ok(Value::Null),
1183        _ => Err(anyhow!(
1184            "InvalidArgumentValue: toInteger() cannot convert type"
1185        )),
1186    }
1187}
1188
1189fn eval_tofloat(arg: &Value) -> Result<Value> {
1190    match arg {
1191        Value::Int(i) => Ok(Value::Float(*i as f64)),
1192        Value::Float(f) => Ok(Value::Float(*f)),
1193        Value::String(s) => Ok(s.parse::<f64>().map(Value::Float).unwrap_or(Value::Null)),
1194        Value::Null => Ok(Value::Null),
1195        _ => Err(anyhow!(
1196            "InvalidArgumentValue: toFloat() cannot convert type"
1197        )),
1198    }
1199}
1200
1201fn eval_tostring(arg: &Value) -> Result<Value> {
1202    match arg {
1203        Value::String(s) => Ok(Value::String(s.clone())),
1204        Value::Int(i) => Ok(Value::String(i.to_string())),
1205        Value::Float(f) => {
1206            // Match Cypher convention: whole floats display with ".0"
1207            if f.fract() == 0.0 && f.is_finite() {
1208                Ok(Value::String(format!("{f:.1}")))
1209            } else {
1210                Ok(Value::String(f.to_string()))
1211            }
1212        }
1213        Value::Bool(b) => Ok(Value::String(b.to_string())),
1214        Value::Null => Ok(Value::Null),
1215        other => Ok(Value::String(other.to_string())),
1216    }
1217}
1218
1219fn eval_toboolean(arg: &Value) -> Result<Value> {
1220    match arg {
1221        Value::Bool(b) => Ok(Value::Bool(*b)),
1222        Value::String(s) => {
1223            let lower = s.to_lowercase();
1224            if lower == "true" {
1225                Ok(Value::Bool(true))
1226            } else if lower == "false" {
1227                Ok(Value::Bool(false))
1228            } else {
1229                Ok(Value::Null)
1230            }
1231        }
1232        Value::Null => Ok(Value::Null),
1233        _ => Err(anyhow!(
1234            "InvalidArgumentValue: toBoolean() cannot convert type"
1235        )),
1236    }
1237}
1238
1239/// Evaluate type conversion functions: TOINTEGER, TOFLOAT, TOSTRING, TOBOOLEAN
1240fn eval_type_function(name: &str, args: &[Value]) -> Result<Value> {
1241    if args.len() != 1 {
1242        return Err(anyhow!("{}() requires 1 argument", name));
1243    }
1244    match name {
1245        "TOINTEGER" | "TOINT" => eval_tointeger(&args[0]),
1246        "TOFLOAT" => eval_tofloat(&args[0]),
1247        "TOSTRING" => eval_tostring(&args[0]),
1248        "TOBOOLEAN" | "TOBOOL" => eval_toboolean(&args[0]),
1249        _ => Err(anyhow!("Unknown type function: {}", name)),
1250    }
1251}
1252
1253// ============================================================================
1254// Math function helpers
1255// ============================================================================
1256
1257fn eval_abs(arg: &Value) -> Result<Value> {
1258    match arg {
1259        Value::Int(i) => Ok(Value::Int(i.abs())),
1260        Value::Float(f) => Ok(Value::Float(f.abs())),
1261        Value::Null => Ok(Value::Null),
1262        _ => Err(anyhow!("abs() expects a number")),
1263    }
1264}
1265
1266fn eval_sqrt(arg: &Value) -> Result<Value> {
1267    match arg {
1268        v if v.is_number() => {
1269            let f = v.as_f64().unwrap();
1270            if f < 0.0 {
1271                Ok(Value::Null)
1272            } else {
1273                Ok(Value::Float(f.sqrt()))
1274            }
1275        }
1276        Value::Null => Ok(Value::Null),
1277        _ => Err(anyhow!("sqrt() expects a number")),
1278    }
1279}
1280
1281fn eval_sign(arg: &Value) -> Result<Value> {
1282    match arg {
1283        Value::Int(i) => Ok(Value::Int(i.signum())),
1284        Value::Float(f) => Ok(Value::Int(f.signum() as i64)),
1285        Value::Null => Ok(Value::Null),
1286        _ => Err(anyhow!("sign() expects a number")),
1287    }
1288}
1289
1290fn eval_power(args: &[Value]) -> Result<Value> {
1291    if args.len() != 2 {
1292        return Err(anyhow!("power() requires 2 arguments"));
1293    }
1294    match (&args[0], &args[1]) {
1295        (a, b) if a.is_number() && b.is_number() => {
1296            let base = a.as_f64().unwrap();
1297            let exp = b.as_f64().unwrap();
1298            Ok(Value::Float(base.powf(exp)))
1299        }
1300        (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1301        _ => Err(anyhow!("power() expects numeric arguments")),
1302    }
1303}
1304
1305/// Apply a unary numeric operation, handling null and type checking.
1306fn eval_unary_numeric_op<F>(arg: &Value, func_name: &str, op: F) -> Result<Value>
1307where
1308    F: Fn(f64) -> f64,
1309{
1310    match arg {
1311        Value::Int(i) => Ok(Value::Float(op(*i as f64))),
1312        Value::Float(f) => Ok(Value::Float(op(*f))),
1313        Value::Null => Ok(Value::Null),
1314        _ => Err(anyhow!("{}() expects a number", func_name)),
1315    }
1316}
1317
1318fn eval_atan2(args: &[Value]) -> Result<Value> {
1319    if args.len() != 2 {
1320        return Err(anyhow!("atan2() requires 2 arguments"));
1321    }
1322    match (&args[0], &args[1]) {
1323        (a, b) if a.is_number() && b.is_number() => {
1324            let y_val = a.as_f64().unwrap();
1325            let x_val = b.as_f64().unwrap();
1326            Ok(Value::Float(y_val.atan2(x_val)))
1327        }
1328        (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1329        _ => Err(anyhow!("atan2() expects numeric arguments")),
1330    }
1331}
1332
1333/// Helper to require exactly one argument for a function.
1334fn require_one_arg<'a>(name: &str, args: &'a [Value]) -> Result<&'a Value> {
1335    if args.len() != 1 {
1336        return Err(anyhow!("{} requires 1 argument", name));
1337    }
1338    Ok(&args[0])
1339}
1340
1341/// Evaluate math functions: ABS, CEIL, FLOOR, ROUND, SQRT, SIGN, LOG, LOG10, EXP, POWER, SIN, COS, TAN, etc.
1342///
1343/// Single-argument trig/math functions that simply delegate to `eval_unary_numeric_op`
1344/// are inlined here to reduce unnecessary indirection.
1345fn eval_math_function(name: &str, args: &[Value]) -> Result<Value> {
1346    match name {
1347        // Single-argument functions with dedicated implementations
1348        "ABS" => eval_abs(require_one_arg(name, args)?),
1349        "CEIL" => eval_unary_numeric_op(require_one_arg(name, args)?, "ceil", f64::ceil),
1350        "FLOOR" => eval_unary_numeric_op(require_one_arg(name, args)?, "floor", f64::floor),
1351        "ROUND" => eval_unary_numeric_op(require_one_arg(name, args)?, "round", f64::round),
1352        "SQRT" => eval_sqrt(require_one_arg(name, args)?),
1353        "SIGN" => eval_sign(require_one_arg(name, args)?),
1354        "LOG" => eval_unary_numeric_op(require_one_arg(name, args)?, "log", f64::ln),
1355        "LOG10" => eval_unary_numeric_op(require_one_arg(name, args)?, "log10", f64::log10),
1356        "EXP" => eval_unary_numeric_op(require_one_arg(name, args)?, "exp", f64::exp),
1357        "SIN" => eval_unary_numeric_op(require_one_arg(name, args)?, "sin", f64::sin),
1358        "COS" => eval_unary_numeric_op(require_one_arg(name, args)?, "cos", f64::cos),
1359        "TAN" => eval_unary_numeric_op(require_one_arg(name, args)?, "tan", f64::tan),
1360        "ASIN" => eval_unary_numeric_op(require_one_arg(name, args)?, "asin", f64::asin),
1361        "ACOS" => eval_unary_numeric_op(require_one_arg(name, args)?, "acos", f64::acos),
1362        "ATAN" => eval_unary_numeric_op(require_one_arg(name, args)?, "atan", f64::atan),
1363        "DEGREES" => {
1364            eval_unary_numeric_op(require_one_arg(name, args)?, "degrees", f64::to_degrees)
1365        }
1366        "RADIANS" => {
1367            eval_unary_numeric_op(require_one_arg(name, args)?, "radians", f64::to_radians)
1368        }
1369        "HAVERSIN" => eval_unary_numeric_op(require_one_arg(name, args)?, "haversin", |f| {
1370            (1.0 - f.cos()) / 2.0
1371        }),
1372        // Two-argument functions
1373        "POWER" | "POW" => eval_power(args),
1374        "ATAN2" => eval_atan2(args),
1375        // Zero-argument constants
1376        "PI" => {
1377            if !args.is_empty() {
1378                return Err(anyhow!("PI takes no arguments"));
1379            }
1380            Ok(Value::Float(std::f64::consts::PI))
1381        }
1382        "E" => {
1383            if !args.is_empty() {
1384                return Err(anyhow!("E takes no arguments"));
1385            }
1386            Ok(Value::Float(std::f64::consts::E))
1387        }
1388        "RAND" => {
1389            if !args.is_empty() {
1390                return Err(anyhow!("RAND takes no arguments"));
1391            }
1392            use rand::Rng;
1393            let mut rng = rand::thread_rng();
1394            Ok(Value::Float(rng.gen_range(0.0..1.0)))
1395        }
1396        _ => Err(anyhow!("Unknown math function: {}", name)),
1397    }
1398}
1399
1400// ============================================================================
1401// String function helpers
1402// ============================================================================
1403
1404/// Apply a unary string operation, handling null and type checking.
1405fn eval_unary_string_op<F>(arg: &Value, func_name: &str, op: F) -> Result<Value>
1406where
1407    F: FnOnce(&str) -> String,
1408{
1409    match arg {
1410        Value::String(s) => Ok(Value::String(op(s))),
1411        Value::Null => Ok(Value::Null),
1412        _ => Err(anyhow!("{}() expects a string", func_name)),
1413    }
1414}
1415
1416fn eval_toupper(args: &[Value]) -> Result<Value> {
1417    let arg = require_one_arg("toUpper", args)?;
1418    eval_unary_string_op(arg, "toUpper", |s| s.to_uppercase())
1419}
1420
1421fn eval_tolower(args: &[Value]) -> Result<Value> {
1422    let arg = require_one_arg("toLower", args)?;
1423    eval_unary_string_op(arg, "toLower", |s| s.to_lowercase())
1424}
1425
1426fn eval_trim(args: &[Value]) -> Result<Value> {
1427    let arg = require_one_arg("trim", args)?;
1428    eval_unary_string_op(arg, "trim", |s| s.trim().to_string())
1429}
1430
1431fn eval_ltrim(args: &[Value]) -> Result<Value> {
1432    let arg = require_one_arg("ltrim", args)?;
1433    eval_unary_string_op(arg, "ltrim", |s| s.trim_start().to_string())
1434}
1435
1436fn eval_rtrim(args: &[Value]) -> Result<Value> {
1437    let arg = require_one_arg("rtrim", args)?;
1438    eval_unary_string_op(arg, "rtrim", |s| s.trim_end().to_string())
1439}
1440
1441fn eval_reverse(args: &[Value]) -> Result<Value> {
1442    let arg = require_one_arg("reverse", args)?;
1443    match arg {
1444        Value::String(s) => Ok(Value::String(s.chars().rev().collect())),
1445        Value::List(arr) => Ok(Value::List(arr.iter().rev().cloned().collect())),
1446        Value::Null => Ok(Value::Null),
1447        _ => Err(anyhow!("reverse() expects a string or list")),
1448    }
1449}
1450
1451fn eval_replace(args: &[Value]) -> Result<Value> {
1452    if args.len() != 3 {
1453        return Err(anyhow!("replace() requires 3 arguments"));
1454    }
1455    match (&args[0], &args[1], &args[2]) {
1456        (Value::String(s), Value::String(search), Value::String(replacement)) => Ok(Value::String(
1457            s.replace(search.as_str(), replacement.as_str()),
1458        )),
1459        (Value::Null, _, _) => Ok(Value::Null),
1460        _ => Err(anyhow!("replace() expects string arguments")),
1461    }
1462}
1463
1464pub(crate) fn eval_split(args: &[Value]) -> Result<Value> {
1465    if args.len() != 2 {
1466        return Err(anyhow!("split() requires 2 arguments"));
1467    }
1468    match (&args[0], &args[1]) {
1469        (Value::String(s), Value::String(delimiter)) => {
1470            let parts: Vec<Value> = s
1471                .split(delimiter.as_str())
1472                .map(|p| Value::String(p.to_string()))
1473                .collect();
1474            Ok(Value::List(parts))
1475        }
1476        (Value::Null, _) => Ok(Value::Null),
1477        _ => Err(anyhow!("split() expects string arguments")),
1478    }
1479}
1480
1481fn eval_substring(args: &[Value]) -> Result<Value> {
1482    if args.len() < 2 || args.len() > 3 {
1483        return Err(anyhow!("substring() requires 2 or 3 arguments"));
1484    }
1485    match &args[0] {
1486        Value::String(s) => {
1487            let start = args[1]
1488                .as_i64()
1489                .ok_or_else(|| anyhow!("substring() start must be an integer"))?
1490                as usize;
1491            let len = if args.len() == 3 {
1492                args[2]
1493                    .as_i64()
1494                    .ok_or_else(|| anyhow!("substring() length must be an integer"))?
1495                    as usize
1496            } else {
1497                s.len().saturating_sub(start)
1498            };
1499            let chars: Vec<char> = s.chars().collect();
1500            let end = (start + len).min(chars.len());
1501            let result: String = chars[start.min(chars.len())..end].iter().collect();
1502            Ok(Value::String(result))
1503        }
1504        Value::Null => Ok(Value::Null),
1505        _ => Err(anyhow!("substring() expects a string")),
1506    }
1507}
1508
1509fn eval_left(args: &[Value]) -> Result<Value> {
1510    if args.len() != 2 {
1511        return Err(anyhow!("left() requires 2 arguments"));
1512    }
1513    match (&args[0], &args[1]) {
1514        (Value::String(s), n) if n.is_number() => {
1515            let len = n.as_i64().unwrap_or(0) as usize;
1516            Ok(Value::String(s.chars().take(len).collect()))
1517        }
1518        (Value::Null, _) => Ok(Value::Null),
1519        _ => Err(anyhow!("left() expects a string and integer")),
1520    }
1521}
1522
1523fn eval_right(args: &[Value]) -> Result<Value> {
1524    if args.len() != 2 {
1525        return Err(anyhow!("right() requires 2 arguments"));
1526    }
1527    match (&args[0], &args[1]) {
1528        (Value::String(s), n) if n.is_number() => {
1529            let len = n.as_i64().unwrap_or(0) as usize;
1530            let chars: Vec<char> = s.chars().collect();
1531            let start = chars.len().saturating_sub(len);
1532            Ok(Value::String(chars[start..].iter().collect()))
1533        }
1534        (Value::Null, _) => Ok(Value::Null),
1535        _ => Err(anyhow!("right() expects a string and integer")),
1536    }
1537}
1538
1539/// Shared implementation for lpad/rpad. `pad_left` controls direction.
1540fn eval_pad(func_name: &str, args: &[Value], pad_left: bool) -> Result<Value> {
1541    if args.len() < 2 || args.len() > 3 {
1542        return Err(anyhow!("{}() requires 2 or 3 arguments", func_name));
1543    }
1544    let s = match &args[0] {
1545        Value::String(s) => s,
1546        Value::Null => return Ok(Value::Null),
1547        _ => {
1548            return Err(anyhow!(
1549                "{}() expects a string as first argument",
1550                func_name
1551            ));
1552        }
1553    };
1554    let len = match &args[1] {
1555        Value::Int(n) => *n as usize,
1556        Value::Float(f) => *f as i64 as usize,
1557        Value::Null => return Ok(Value::Null),
1558        _ => {
1559            return Err(anyhow!(
1560                "{}() expects an integer as second argument",
1561                func_name
1562            ));
1563        }
1564    };
1565    if len > 1_000_000 {
1566        return Err(anyhow!(
1567            "{}() length exceeds maximum limit of 1,000,000",
1568            func_name
1569        ));
1570    }
1571    let pad_str = if args.len() == 3 {
1572        match &args[2] {
1573            Value::String(p) => p.as_str(),
1574            Value::Null => return Ok(Value::Null),
1575            _ => {
1576                return Err(anyhow!(
1577                    "{}() expects a string as third argument",
1578                    func_name
1579                ));
1580            }
1581        }
1582    } else {
1583        " "
1584    };
1585
1586    let s_chars: Vec<char> = s.chars().collect();
1587    if s_chars.len() >= len {
1588        return Ok(Value::String(s_chars.into_iter().take(len).collect()));
1589    }
1590
1591    let pad_chars: Vec<char> = pad_str.chars().collect();
1592    if pad_chars.is_empty() {
1593        return Ok(Value::String(s.clone()));
1594    }
1595
1596    let needed = len - s_chars.len();
1597    let full_pads = needed / pad_chars.len();
1598    let partial_pad = needed % pad_chars.len();
1599
1600    let mut padding = String::with_capacity(needed);
1601    for _ in 0..full_pads {
1602        padding.push_str(pad_str);
1603    }
1604    padding.extend(pad_chars.into_iter().take(partial_pad));
1605
1606    let result = if pad_left {
1607        format!("{}{}", padding, s)
1608    } else {
1609        format!("{}{}", s, padding)
1610    };
1611    Ok(Value::String(result))
1612}
1613
1614fn eval_lpad(args: &[Value]) -> Result<Value> {
1615    eval_pad("lpad", args, true)
1616}
1617
1618fn eval_rpad(args: &[Value]) -> Result<Value> {
1619    eval_pad("rpad", args, false)
1620}
1621
1622/// Evaluate string functions: TOUPPER, TOLOWER, TRIM, LTRIM, RTRIM, REVERSE, REPLACE, SPLIT, SUBSTRING, LEFT, RIGHT, LPAD, RPAD
1623fn eval_string_function(name: &str, args: &[Value]) -> Result<Value> {
1624    match name {
1625        "TOUPPER" | "UPPER" => eval_toupper(args),
1626        "TOLOWER" | "LOWER" => eval_tolower(args),
1627        "TRIM" => eval_trim(args),
1628        "LTRIM" => eval_ltrim(args),
1629        "RTRIM" => eval_rtrim(args),
1630        "REVERSE" => eval_reverse(args),
1631        "REPLACE" => eval_replace(args),
1632        "SPLIT" => eval_split(args),
1633        "SUBSTRING" => eval_substring(args),
1634        "LEFT" => eval_left(args),
1635        "RIGHT" => eval_right(args),
1636        "LPAD" => eval_lpad(args),
1637        "RPAD" => eval_rpad(args),
1638        _ => Err(anyhow!("Unknown string function: {}", name)),
1639    }
1640}
1641
1642/// Evaluate the RANGE function
1643fn eval_range_function(args: &[Value]) -> Result<Value> {
1644    if args.len() < 2 || args.len() > 3 {
1645        return Err(anyhow!("range() requires 2 or 3 arguments"));
1646    }
1647    let start = args[0]
1648        .as_i64()
1649        .ok_or_else(|| anyhow!("range() start must be an integer"))?;
1650    let end = args[1]
1651        .as_i64()
1652        .ok_or_else(|| anyhow!("range() end must be an integer"))?;
1653    let step = if args.len() == 3 {
1654        args[2]
1655            .as_i64()
1656            .ok_or_else(|| anyhow!("range() step must be an integer"))?
1657    } else {
1658        1
1659    };
1660    if step == 0 {
1661        return Err(anyhow!("range() step cannot be zero"));
1662    }
1663    let mut result = Vec::new();
1664    let mut i = start;
1665    if step > 0 {
1666        while i <= end {
1667            result.push(Value::Int(i));
1668            i += step;
1669        }
1670    } else {
1671        while i >= end {
1672            result.push(Value::Int(i));
1673            i += step;
1674        }
1675    }
1676    Ok(Value::List(result))
1677}
1678
1679/// Evaluate a built-in scalar function.
1680///
1681/// This handles functions like COALESCE, NULLIF, SIZE, KEYS, HEAD, TAIL, etc.
1682/// Functions that require argument evaluation (like COALESCE) take pre-evaluated args.
1683pub fn eval_scalar_function(
1684    name: &str,
1685    args: &[Value],
1686    custom_fns: Option<&super::executor::custom_functions::CustomFunctionRegistry>,
1687) -> Result<Value> {
1688    let name_upper = name.to_uppercase();
1689
1690    match name_upper.as_str() {
1691        // Null-handling functions
1692        "COALESCE" => {
1693            for arg in args {
1694                if !arg.is_null() {
1695                    return Ok(arg.clone());
1696                }
1697            }
1698            Ok(Value::Null)
1699        }
1700        "NULLIF" => {
1701            if args.len() != 2 {
1702                return Err(anyhow!("NULLIF requires 2 arguments"));
1703            }
1704            if args[0] == args[1] {
1705                Ok(Value::Null)
1706            } else {
1707                Ok(args[0].clone())
1708            }
1709        }
1710
1711        // List/Collection functions
1712        "SIZE" | "KEYS" | "HEAD" | "TAIL" | "LAST" | "LENGTH" | "NODES" | "RELATIONSHIPS" => {
1713            eval_list_function(&name_upper, args)
1714        }
1715
1716        // Type conversion functions
1717        "TOINTEGER" | "TOINT" | "TOFLOAT" | "TOSTRING" | "TOBOOLEAN" | "TOBOOL" => {
1718            eval_type_function(&name_upper, args)
1719        }
1720
1721        // Math functions
1722        "ABS" | "CEIL" | "FLOOR" | "ROUND" | "SQRT" | "SIGN" | "LOG" | "LOG10" | "EXP"
1723        | "POWER" | "POW" | "SIN" | "COS" | "TAN" | "ASIN" | "ACOS" | "ATAN" | "ATAN2"
1724        | "DEGREES" | "RADIANS" | "HAVERSIN" | "PI" | "E" | "RAND" => {
1725            eval_math_function(&name_upper, args)
1726        }
1727
1728        // String functions
1729        "TOUPPER" | "UPPER" | "TOLOWER" | "LOWER" | "TRIM" | "LTRIM" | "RTRIM" | "REVERSE"
1730        | "REPLACE" | "SPLIT" | "SUBSTRING" | "LEFT" | "RIGHT" | "LPAD" | "RPAD" => {
1731            eval_string_function(&name_upper, args)
1732        }
1733
1734        // Date/Time functions
1735        "DATE"
1736        | "TIME"
1737        | "DATETIME"
1738        | "LOCALDATETIME"
1739        | "LOCALTIME"
1740        | "DURATION"
1741        | "YEAR"
1742        | "MONTH"
1743        | "DAY"
1744        | "HOUR"
1745        | "MINUTE"
1746        | "SECOND"
1747        | "DATETIME.FROMEPOCH"
1748        | "DATETIME.FROMEPOCHMILLIS"
1749        | "DATE.TRUNCATE"
1750        | "TIME.TRUNCATE"
1751        | "DATETIME.TRUNCATE"
1752        | "LOCALDATETIME.TRUNCATE"
1753        | "LOCALTIME.TRUNCATE"
1754        | "DATETIME.TRANSACTION"
1755        | "DATETIME.STATEMENT"
1756        | "DATETIME.REALTIME"
1757        | "DATE.TRANSACTION"
1758        | "DATE.STATEMENT"
1759        | "DATE.REALTIME"
1760        | "TIME.TRANSACTION"
1761        | "TIME.STATEMENT"
1762        | "TIME.REALTIME"
1763        | "LOCALTIME.TRANSACTION"
1764        | "LOCALTIME.STATEMENT"
1765        | "LOCALTIME.REALTIME"
1766        | "LOCALDATETIME.TRANSACTION"
1767        | "LOCALDATETIME.STATEMENT"
1768        | "LOCALDATETIME.REALTIME"
1769        | "DURATION.BETWEEN"
1770        | "DURATION.INMONTHS"
1771        | "DURATION.INDAYS"
1772        | "DURATION.INSECONDS" => eval_datetime_function(&name_upper, args),
1773
1774        // Spatial functions
1775        "POINT" | "DISTANCE" | "POINT.WITHINBBOX" => eval_spatial_function(&name_upper, args),
1776
1777        "RANGE" => eval_range_function(args),
1778
1779        "UNI.TEMPORAL.VALIDAT" => eval_valid_at(args),
1780
1781        "VECTOR_DISTANCE" => {
1782            if args.len() < 2 || args.len() > 3 {
1783                return Err(anyhow!("vector_distance requires 2 or 3 arguments"));
1784            }
1785            let metric = if args.len() == 3 {
1786                args[2].as_str().ok_or(anyhow!("metric must be string"))?
1787            } else {
1788                "cosine"
1789            };
1790            eval_vector_distance(&args[0], &args[1], metric)
1791        }
1792
1793        // Bitwise functions
1794        "UNI_BITWISE_OR"
1795        | "UNI_BITWISE_AND"
1796        | "UNI_BITWISE_XOR"
1797        | "UNI_BITWISE_NOT"
1798        | "UNI_BITWISE_SHIFTLEFT"
1799        | "UNI_BITWISE_SHIFTRIGHT" => eval_bitwise_function(&name_upper, args),
1800
1801        // Similarity functions — pure vector-vector case only (no storage access).
1802        // Storage-dependent cases (auto-embed, FTS) are handled in read.rs.
1803        "SIMILAR_TO" => {
1804            if args.len() < 2 {
1805                return Err(anyhow!("similar_to requires at least 2 arguments"));
1806            }
1807            crate::query::similar_to::eval_similar_to_pure(&args[0], &args[1])
1808        }
1809
1810        "VECTOR_SIMILARITY" => {
1811            if args.len() != 2 {
1812                return Err(anyhow!("vector_similarity takes 2 arguments"));
1813            }
1814            eval_vector_similarity(&args[0], &args[1])
1815        }
1816
1817        _ => {
1818            // Fall back to custom function registry before reporting an error.
1819            if let Some(func) = custom_fns.and_then(|r| r.get(name)) {
1820                return func(args).map_err(|e| anyhow!("{}", e));
1821            }
1822            Err(anyhow!("Function {} not implemented or is aggregate", name))
1823        }
1824    }
1825}
1826
1827/// Evaluate uni.temporal.validAt(node, start_prop, end_prop, time)
1828///
1829/// Checks if a node/edge was valid at a given point in time using half-open interval
1830/// semantics: `[valid_from, valid_to)` where `valid_from <= time < valid_to`.
1831///
1832/// If `valid_to` is NULL or missing, the interval is open-ended (valid indefinitely).
1833/// If `valid_from` is NULL or missing, the entity is considered invalid.
1834fn eval_valid_at(args: &[Value]) -> Result<Value> {
1835    if args.len() != 4 {
1836        return Err(anyhow!(
1837            "validAt requires 4 arguments: node, start_prop, end_prop, time"
1838        ));
1839    }
1840
1841    let node_map = match &args[0] {
1842        Value::Map(map) => map,
1843        Value::Null => return Ok(Value::Bool(false)),
1844        _ => {
1845            return Err(anyhow!(
1846                "validAt expects a Node or Edge (Object) as first argument"
1847            ));
1848        }
1849    };
1850
1851    let start_prop = args[1]
1852        .as_str()
1853        .ok_or_else(|| anyhow!("start_prop must be a string"))?;
1854    let end_prop = args[2]
1855        .as_str()
1856        .ok_or_else(|| anyhow!("end_prop must be a string"))?;
1857
1858    let time_str = match &args[3] {
1859        Value::String(s) => s,
1860        _ => return Err(anyhow!("time argument must be a datetime string")),
1861    };
1862
1863    let query_time = parse_datetime_utc(time_str)
1864        .map_err(|_| anyhow!("Invalid query time format: {}", time_str))?;
1865
1866    let valid_from_val = node_map.get(start_prop);
1867    let valid_from = match valid_from_val {
1868        Some(Value::String(s)) => parse_datetime_utc(s)
1869            .map_err(|_| anyhow!("Invalid datetime in property {}: {}", start_prop, s))?,
1870        Some(Value::Null) | None => return Ok(Value::Bool(false)),
1871        _ => return Err(anyhow!("Property {} must be a datetime string", start_prop)),
1872    };
1873
1874    let valid_to_val = node_map.get(end_prop);
1875    let valid_to = match valid_to_val {
1876        Some(Value::String(s)) => Some(
1877            parse_datetime_utc(s)
1878                .map_err(|_| anyhow!("Invalid datetime in property {}: {}", end_prop, s))?,
1879        ),
1880        Some(Value::Null) | None => None,
1881        _ => {
1882            return Err(anyhow!(
1883                "Property {} must be a datetime string or null",
1884                end_prop
1885            ));
1886        }
1887    };
1888
1889    // Half-open interval: [valid_from, valid_to)
1890    let is_valid = valid_from <= query_time && valid_to.map(|vt| query_time < vt).unwrap_or(true);
1891
1892    Ok(Value::Bool(is_valid))
1893}
1894
1895/// Evaluate vector similarity between two vectors (cosine similarity).
1896pub fn eval_vector_similarity(v1: &Value, v2: &Value) -> Result<Value> {
1897    let (arr1, arr2) = match (v1, v2) {
1898        (Value::List(a1), Value::List(a2)) => (a1, a2),
1899        _ => return Err(anyhow!("vector_similarity arguments must be arrays")),
1900    };
1901
1902    if arr1.len() != arr2.len() {
1903        return Err(anyhow!(
1904            "Vector dimensions mismatch: {} vs {}",
1905            arr1.len(),
1906            arr2.len()
1907        ));
1908    }
1909
1910    let mut dot = 0.0;
1911    let mut norm1_sq = 0.0;
1912    let mut norm2_sq = 0.0;
1913
1914    for (v1_elem, v2_elem) in arr1.iter().zip(arr2.iter()) {
1915        let f1 = v1_elem
1916            .as_f64()
1917            .ok_or_else(|| anyhow!("Vector element not a number"))?;
1918        let f2 = v2_elem
1919            .as_f64()
1920            .ok_or_else(|| anyhow!("Vector element not a number"))?;
1921        dot += f1 * f2;
1922        norm1_sq += f1 * f1;
1923        norm2_sq += f2 * f2;
1924    }
1925
1926    let mag1 = norm1_sq.sqrt();
1927    let mag2 = norm2_sq.sqrt();
1928
1929    let sim = if mag1 == 0.0 || mag2 == 0.0 {
1930        0.0
1931    } else {
1932        dot / (mag1 * mag2)
1933    };
1934
1935    Ok(Value::Float(sim))
1936}
1937
1938/// Evaluate vector distance between two vectors.
1939pub fn eval_vector_distance(v1: &Value, v2: &Value, metric: &str) -> Result<Value> {
1940    let (arr1, arr2) = match (v1, v2) {
1941        (Value::List(a1), Value::List(a2)) => (a1, a2),
1942        _ => return Err(anyhow!("vector_distance arguments must be arrays")),
1943    };
1944
1945    if arr1.len() != arr2.len() {
1946        return Err(anyhow!(
1947            "Vector dimensions mismatch: {} vs {}",
1948            arr1.len(),
1949            arr2.len()
1950        ));
1951    }
1952
1953    // Helper to get f64 iterator
1954    let iter1 = arr1
1955        .iter()
1956        .map(|v| v.as_f64().ok_or(anyhow!("Vector element not a number")));
1957    let iter2 = arr2
1958        .iter()
1959        .map(|v| v.as_f64().ok_or(anyhow!("Vector element not a number")));
1960
1961    match metric.to_lowercase().as_str() {
1962        "cosine" => {
1963            // Cosine distance = 1 - cosine similarity
1964            let mut dot = 0.0;
1965            let mut norm1_sq = 0.0;
1966            let mut norm2_sq = 0.0;
1967
1968            for (r1, r2) in iter1.zip(iter2) {
1969                let f1 = r1?;
1970                let f2 = r2?;
1971                dot += f1 * f2;
1972                norm1_sq += f1 * f1;
1973                norm2_sq += f2 * f2;
1974            }
1975
1976            let mag1 = norm1_sq.sqrt();
1977            let mag2 = norm2_sq.sqrt();
1978
1979            if mag1 == 0.0 || mag2 == 0.0 {
1980                Ok(Value::Float(1.0))
1981            } else {
1982                let sim = dot / (mag1 * mag2);
1983                // Clamp to [-1, 1] to avoid numerical errors
1984                let sim = sim.clamp(-1.0, 1.0);
1985                Ok(Value::Float(1.0 - sim))
1986            }
1987        }
1988        "euclidean" | "l2" => {
1989            let mut sum_sq_diff = 0.0;
1990            for (r1, r2) in iter1.zip(iter2) {
1991                let f1 = r1?;
1992                let f2 = r2?;
1993                let diff = f1 - f2;
1994                sum_sq_diff += diff * diff;
1995            }
1996            Ok(Value::Float(sum_sq_diff.sqrt()))
1997        }
1998        "dot" | "inner_product" => {
1999            let mut dot = 0.0;
2000            for (r1, r2) in iter1.zip(iter2) {
2001                let f1 = r1?;
2002                let f2 = r2?;
2003                dot += f1 * f2;
2004            }
2005            Ok(Value::Float(1.0 - dot))
2006        }
2007        _ => Err(anyhow!("Unknown metric: {}", metric)),
2008    }
2009}
2010
2011/// Check if a function name is a known scalar function (not aggregate).
2012pub fn is_scalar_function(name: &str) -> bool {
2013    let name_upper = name.to_uppercase();
2014    matches!(
2015        name_upper.as_str(),
2016        "COALESCE"
2017            | "NULLIF"
2018            | "SIZE"
2019            | "KEYS"
2020            | "HEAD"
2021            | "TAIL"
2022            | "LAST"
2023            | "LENGTH"
2024            | "NODES"
2025            | "RELATIONSHIPS"
2026            | "TOINTEGER"
2027            | "TOINT"
2028            | "TOFLOAT"
2029            | "TOSTRING"
2030            | "TOBOOLEAN"
2031            | "TOBOOL"
2032            | "ABS"
2033            | "CEIL"
2034            | "FLOOR"
2035            | "ROUND"
2036            | "SQRT"
2037            | "SIGN"
2038            | "LOG"
2039            | "LOG10"
2040            | "EXP"
2041            | "POWER"
2042            | "POW"
2043            | "SIN"
2044            | "COS"
2045            | "TAN"
2046            | "ASIN"
2047            | "ACOS"
2048            | "ATAN"
2049            | "ATAN2"
2050            | "DEGREES"
2051            | "RADIANS"
2052            | "HAVERSIN"
2053            | "PI"
2054            | "E"
2055            | "RAND"
2056            | "TOUPPER"
2057            | "UPPER"
2058            | "TOLOWER"
2059            | "LOWER"
2060            | "TRIM"
2061            | "LTRIM"
2062            | "RTRIM"
2063            | "REVERSE"
2064            | "REPLACE"
2065            | "SPLIT"
2066            | "SUBSTRING"
2067            | "LEFT"
2068            | "RIGHT"
2069            | "LPAD"
2070            | "RPAD"
2071            | "RANGE"
2072            | "UNI.VALIDAT"
2073            | "VALIDAT"
2074            | "SIMILAR_TO"
2075            | "VECTOR_SIMILARITY"
2076            | "VECTOR_DISTANCE"
2077            | "DATE"
2078            | "TIME"
2079            | "DATETIME"
2080            | "DURATION"
2081            | "YEAR"
2082            | "MONTH"
2083            | "DAY"
2084            | "HOUR"
2085            | "MINUTE"
2086            | "SECOND"
2087            | "ID"
2088            | "ELEMENTID"
2089            | "TYPE"
2090            | "LABELS"
2091            | "PROPERTIES"
2092            | "STARTNODE"
2093            | "ENDNODE"
2094            | "ANY"
2095            | "ALL"
2096            | "NONE"
2097            | "SINGLE"
2098    )
2099}
2100
2101/// Evaluate bitwise functions (uni_bitwise_*)
2102fn eval_bitwise_function(name: &str, args: &[Value]) -> Result<Value> {
2103    let require_int = |v: &Value, fname: &str| -> Result<i64> {
2104        v.as_i64()
2105            .ok_or_else(|| anyhow!("{} requires integer arguments", fname))
2106    };
2107
2108    let bitwise_binary = |fname: &str, op: fn(i64, i64) -> i64| -> Result<Value> {
2109        if args.len() != 2 {
2110            return Err(anyhow!("{} requires exactly 2 arguments", fname));
2111        }
2112        let l = require_int(&args[0], fname)?;
2113        let r = require_int(&args[1], fname)?;
2114        Ok(Value::Int(op(l, r)))
2115    };
2116
2117    match name {
2118        "UNI_BITWISE_OR" => bitwise_binary("uni_bitwise_or", |l, r| l | r),
2119        "UNI_BITWISE_AND" => bitwise_binary("uni_bitwise_and", |l, r| l & r),
2120        "UNI_BITWISE_XOR" => bitwise_binary("uni_bitwise_xor", |l, r| l ^ r),
2121        "UNI_BITWISE_SHIFTLEFT" => bitwise_binary("uni_bitwise_shiftLeft", |l, r| l << r),
2122        "UNI_BITWISE_SHIFTRIGHT" => bitwise_binary("uni_bitwise_shiftRight", |l, r| l >> r),
2123        "UNI_BITWISE_NOT" => {
2124            if args.len() != 1 {
2125                return Err(anyhow!("uni_bitwise_not requires exactly 1 argument"));
2126            }
2127            Ok(Value::Int(!require_int(&args[0], "uni_bitwise_not")?))
2128        }
2129        _ => Err(anyhow!("Unknown bitwise function: {}", name)),
2130    }
2131}
2132
2133#[cfg(test)]
2134mod tests {
2135    use super::*;
2136    /// Helper to create string values in tests (replaces s("..."))
2137    fn s(v: &str) -> Value {
2138        Value::String(v.into())
2139    }
2140    /// Helper to create int values in tests (replaces json!(i))
2141    fn i(v: i64) -> Value {
2142        Value::Int(v)
2143    }
2144
2145    #[test]
2146    fn test_binary_op_eq() {
2147        assert_eq!(
2148            eval_binary_op(&i(1), &BinaryOp::Eq, &i(1)).unwrap(),
2149            Value::Bool(true)
2150        );
2151        assert_eq!(
2152            eval_binary_op(&i(1), &BinaryOp::Eq, &i(2)).unwrap(),
2153            Value::Bool(false)
2154        );
2155    }
2156
2157    #[test]
2158    fn test_binary_op_comparison() {
2159        assert_eq!(
2160            eval_binary_op(&i(5), &BinaryOp::Gt, &i(3)).unwrap(),
2161            Value::Bool(true)
2162        );
2163        assert_eq!(
2164            eval_binary_op(&i(5), &BinaryOp::Lt, &i(3)).unwrap(),
2165            Value::Bool(false)
2166        );
2167    }
2168
2169    #[test]
2170    fn test_binary_op_xor() {
2171        // true XOR true = false
2172        assert_eq!(
2173            eval_binary_op(&Value::Bool(true), &BinaryOp::Xor, &Value::Bool(true)).unwrap(),
2174            Value::Bool(false)
2175        );
2176        // true XOR false = true
2177        assert_eq!(
2178            eval_binary_op(&Value::Bool(true), &BinaryOp::Xor, &Value::Bool(false)).unwrap(),
2179            Value::Bool(true)
2180        );
2181        // false XOR true = true
2182        assert_eq!(
2183            eval_binary_op(&Value::Bool(false), &BinaryOp::Xor, &Value::Bool(true)).unwrap(),
2184            Value::Bool(true)
2185        );
2186        // false XOR false = false
2187        assert_eq!(
2188            eval_binary_op(&Value::Bool(false), &BinaryOp::Xor, &Value::Bool(false)).unwrap(),
2189            Value::Bool(false)
2190        );
2191    }
2192
2193    #[test]
2194    fn test_binary_op_contains() {
2195        assert_eq!(
2196            eval_binary_op(&s("hello world"), &BinaryOp::Contains, &s("world")).unwrap(),
2197            Value::Bool(true)
2198        );
2199    }
2200
2201    #[test]
2202    fn test_scalar_function_size() {
2203        assert_eq!(
2204            eval_scalar_function("SIZE", &[Value::List(vec![i(1), i(2), i(3)])], None).unwrap(),
2205            Value::Int(3)
2206        );
2207    }
2208
2209    #[test]
2210    fn test_scalar_function_head() {
2211        assert_eq!(
2212            eval_scalar_function("HEAD", &[Value::List(vec![i(1), i(2), i(3)])], None).unwrap(),
2213            Value::Int(1)
2214        );
2215    }
2216
2217    #[test]
2218    fn test_scalar_function_coalesce() {
2219        assert_eq!(
2220            eval_scalar_function(
2221                "COALESCE",
2222                &[Value::Null, Value::Int(1), Value::Int(2)],
2223                None
2224            )
2225            .unwrap(),
2226            Value::Int(1)
2227        );
2228    }
2229
2230    #[test]
2231    fn test_vector_similarity() {
2232        let v1 = Value::List(vec![Value::Float(1.0), Value::Float(0.0)]);
2233        let v2 = Value::List(vec![Value::Float(1.0), Value::Float(0.0)]);
2234        let result = eval_vector_similarity(&v1, &v2).unwrap();
2235        assert_eq!(result.as_f64().unwrap(), 1.0);
2236    }
2237
2238    #[test]
2239    fn test_regex_match() {
2240        // Basic regex match
2241        assert_eq!(
2242            eval_binary_op(&s("hello world"), &BinaryOp::Regex, &s("hello.*")).unwrap(),
2243            Value::Bool(true)
2244        );
2245
2246        // No match
2247        assert_eq!(
2248            eval_binary_op(&s("hello world"), &BinaryOp::Regex, &s("^world")).unwrap(),
2249            Value::Bool(false)
2250        );
2251
2252        // Case sensitive
2253        assert_eq!(
2254            eval_binary_op(&s("Hello"), &BinaryOp::Regex, &s("hello")).unwrap(),
2255            Value::Bool(false)
2256        );
2257
2258        // Case insensitive with flag
2259        assert_eq!(
2260            eval_binary_op(&s("Hello"), &BinaryOp::Regex, &s("(?i)hello")).unwrap(),
2261            Value::Bool(true)
2262        );
2263    }
2264
2265    #[test]
2266    fn test_regex_null_handling() {
2267        // Left operand is null
2268        assert_eq!(
2269            eval_binary_op(&Value::Null, &BinaryOp::Regex, &s(".*")).unwrap(),
2270            Value::Null
2271        );
2272
2273        // Right operand is null
2274        assert_eq!(
2275            eval_binary_op(&s("hello"), &BinaryOp::Regex, &Value::Null).unwrap(),
2276            Value::Null
2277        );
2278    }
2279
2280    #[test]
2281    fn test_regex_invalid_pattern() {
2282        // Invalid regex pattern should return error
2283        let result = eval_binary_op(&s("hello"), &BinaryOp::Regex, &s("[invalid"));
2284        assert!(result.is_err());
2285        assert!(result.unwrap_err().to_string().contains("Invalid regex"));
2286    }
2287
2288    #[test]
2289    fn test_regex_special_characters() {
2290        // Email pattern with escaped dots
2291        assert_eq!(
2292            eval_binary_op(
2293                &s("test@example.com"),
2294                &BinaryOp::Regex,
2295                &s(r"^[\w.-]+@[\w.-]+\.\w+$")
2296            )
2297            .unwrap(),
2298            Value::Bool(true)
2299        );
2300
2301        // Phone number pattern
2302        assert_eq!(
2303            eval_binary_op(
2304                &s("123-456-7890"),
2305                &BinaryOp::Regex,
2306                &s(r"^\d{3}-\d{3}-\d{4}$")
2307            )
2308            .unwrap(),
2309            Value::Bool(true)
2310        );
2311
2312        // Non-matching phone
2313        assert_eq!(
2314            eval_binary_op(
2315                &s("1234567890"),
2316                &BinaryOp::Regex,
2317                &s(r"^\d{3}-\d{3}-\d{4}$")
2318            )
2319            .unwrap(),
2320            Value::Bool(false)
2321        );
2322    }
2323
2324    #[test]
2325    fn test_regex_anchors() {
2326        // Start anchor
2327        assert_eq!(
2328            eval_binary_op(&s("hello world"), &BinaryOp::Regex, &s("^hello")).unwrap(),
2329            Value::Bool(true)
2330        );
2331        assert_eq!(
2332            eval_binary_op(&s("say hello"), &BinaryOp::Regex, &s("^hello")).unwrap(),
2333            Value::Bool(false)
2334        );
2335
2336        // End anchor
2337        assert_eq!(
2338            eval_binary_op(&s("hello world"), &BinaryOp::Regex, &s("world$")).unwrap(),
2339            Value::Bool(true)
2340        );
2341        assert_eq!(
2342            eval_binary_op(&s("world hello"), &BinaryOp::Regex, &s("world$")).unwrap(),
2343            Value::Bool(false)
2344        );
2345
2346        // Full match with both anchors
2347        assert_eq!(
2348            eval_binary_op(&s("hello"), &BinaryOp::Regex, &s("^hello$")).unwrap(),
2349            Value::Bool(true)
2350        );
2351        assert_eq!(
2352            eval_binary_op(&s("hello world"), &BinaryOp::Regex, &s("^hello$")).unwrap(),
2353            Value::Bool(false)
2354        );
2355    }
2356
2357    #[test]
2358    fn test_temporal_arithmetic() {
2359        // datetime + duration (1 hour)
2360        let dt = s("2024-01-15T10:00:00Z");
2361        let dur = Value::Int(3_600_000_000_i64);
2362        let result = eval_binary_op(&dt, &BinaryOp::Add, &dur).unwrap();
2363        assert!(result.to_string().contains("11:00"));
2364
2365        // date + duration (1 day)
2366        let d = s("2024-01-01");
2367        let dur_day = Value::Int(86_400_000_000_i64);
2368        let result = eval_binary_op(&d, &BinaryOp::Add, &dur_day).unwrap();
2369        assert_eq!(result.to_string(), "2024-01-02");
2370
2371        // datetime - datetime (returns ISO 8601 duration)
2372        let dt1 = s("2024-01-02T00:00:00Z");
2373        let dt2 = s("2024-01-01T00:00:00Z");
2374        let result = eval_binary_op(&dt1, &BinaryOp::Sub, &dt2).unwrap();
2375        // Result is now ISO 8601 duration string (1 day = PT24H for datetime types)
2376        let dur_str = result.to_string();
2377        assert!(dur_str.starts_with('P'));
2378        assert!(dur_str.contains("24H")); // 24 hours
2379    }
2380
2381    // Bitwise operator tests removed - bitwise operations now use functions (uni_bitwise_*)
2382    // See bitwise_functions_test.rs for comprehensive bitwise function tests
2383
2384    #[test]
2385    fn test_temporal_arithmetic_edge_cases() {
2386        // Negative duration (subtracting time)
2387        let dt = s("2024-01-15T10:00:00Z");
2388        let neg_dur = Value::Int(-3_600_000_000_i64); // -1 hour
2389        let result = eval_binary_op(&dt, &BinaryOp::Add, &neg_dur).unwrap();
2390        assert!(result.to_string().contains("09:00"));
2391
2392        // Duration subtraction resulting in negative duration
2393        let dur1 = s("PT1H"); // 1 hour as ISO 8601
2394        let dur2 = s("PT2H"); // 2 hours as ISO 8601
2395        let result = eval_binary_op(&dur1, &BinaryOp::Sub, &dur2).unwrap();
2396        // Result is ISO 8601 duration string (negative 1 hour)
2397        let dur_str = result.to_string();
2398        assert!(dur_str.starts_with('P') || dur_str.starts_with("-P"));
2399
2400        // Zero duration addition
2401        let dt = s("2024-01-15T10:00:00Z");
2402        let zero_dur = Value::Int(0_i64);
2403        let result = eval_binary_op(&dt, &BinaryOp::Add, &zero_dur).unwrap();
2404        assert!(result.to_string().contains("10:00"));
2405
2406        // Date crossing year boundary
2407        let d = s("2023-12-31");
2408        let one_day = Value::Int(86_400_000_000_i64);
2409        let result = eval_binary_op(&d, &BinaryOp::Add, &one_day).unwrap();
2410        assert_eq!(result.to_string(), "2024-01-01");
2411
2412        // Same datetime subtraction yields zero duration
2413        let dt1 = s("2024-01-15T10:00:00Z");
2414        let dt2 = s("2024-01-15T10:00:00Z");
2415        let result = eval_binary_op(&dt1, &BinaryOp::Sub, &dt2).unwrap();
2416        // Zero duration should be "PT0S" or similar
2417        let dur_str = result.to_string();
2418        assert!(dur_str.starts_with('P'));
2419
2420        // Leap year handling
2421        let leap_day = s("2024-02-28");
2422        let one_day = Value::Int(86_400_000_000_i64);
2423        let result = eval_binary_op(&leap_day, &BinaryOp::Add, &one_day).unwrap();
2424        assert_eq!(result.to_string(), "2024-02-29");
2425    }
2426
2427    #[test]
2428    fn test_regex_empty_string() {
2429        // Empty string matches empty pattern
2430        assert_eq!(
2431            eval_binary_op(&s(""), &BinaryOp::Regex, &s("^$")).unwrap(),
2432            Value::Bool(true)
2433        );
2434
2435        // Empty string doesn't match non-empty pattern
2436        assert_eq!(
2437            eval_binary_op(&s(""), &BinaryOp::Regex, &s(".+")).unwrap(),
2438            Value::Bool(false)
2439        );
2440
2441        // Non-empty string matches .* (matches anything including empty)
2442        assert_eq!(
2443            eval_binary_op(&s("hello"), &BinaryOp::Regex, &s(".*")).unwrap(),
2444            Value::Bool(true)
2445        );
2446    }
2447
2448    #[test]
2449    fn test_regex_type_errors() {
2450        // Non-string left operand
2451        let result = eval_binary_op(&Value::Int(123), &BinaryOp::Regex, &s("\\d+"));
2452        assert!(result.is_err());
2453        assert!(result.unwrap_err().to_string().contains("must be a string"));
2454
2455        // Non-string right operand (pattern)
2456        let result = eval_binary_op(&s("hello"), &BinaryOp::Regex, &Value::Int(123));
2457        assert!(result.is_err());
2458        assert!(result.unwrap_err().to_string().contains("pattern string"));
2459    }
2460
2461    #[test]
2462    fn test_and_null_handling() {
2463        // Three-valued logic: false dominates, null propagates with true
2464
2465        // false AND null = false (false dominates)
2466        assert_eq!(
2467            eval_binary_op(&Value::Bool(false), &BinaryOp::And, &Value::Null).unwrap(),
2468            Value::Bool(false)
2469        );
2470        assert_eq!(
2471            eval_binary_op(&Value::Null, &BinaryOp::And, &Value::Bool(false)).unwrap(),
2472            Value::Bool(false)
2473        );
2474
2475        // true AND null = null
2476        assert_eq!(
2477            eval_binary_op(&Value::Bool(true), &BinaryOp::And, &Value::Null).unwrap(),
2478            Value::Null
2479        );
2480        assert_eq!(
2481            eval_binary_op(&Value::Null, &BinaryOp::And, &Value::Bool(true)).unwrap(),
2482            Value::Null
2483        );
2484
2485        // null AND null = null
2486        assert_eq!(
2487            eval_binary_op(&Value::Null, &BinaryOp::And, &Value::Null).unwrap(),
2488            Value::Null
2489        );
2490
2491        // Non-null cases still work
2492        assert_eq!(
2493            eval_binary_op(&Value::Bool(true), &BinaryOp::And, &Value::Bool(true)).unwrap(),
2494            Value::Bool(true)
2495        );
2496        assert_eq!(
2497            eval_binary_op(&Value::Bool(true), &BinaryOp::And, &Value::Bool(false)).unwrap(),
2498            Value::Bool(false)
2499        );
2500    }
2501
2502    #[test]
2503    fn test_or_null_handling() {
2504        // Three-valued logic: true dominates, null propagates with false
2505
2506        // true OR null = true (true dominates)
2507        assert_eq!(
2508            eval_binary_op(&Value::Bool(true), &BinaryOp::Or, &Value::Null).unwrap(),
2509            Value::Bool(true)
2510        );
2511        assert_eq!(
2512            eval_binary_op(&Value::Null, &BinaryOp::Or, &Value::Bool(true)).unwrap(),
2513            Value::Bool(true)
2514        );
2515
2516        // false OR null = null
2517        assert_eq!(
2518            eval_binary_op(&Value::Bool(false), &BinaryOp::Or, &Value::Null).unwrap(),
2519            Value::Null
2520        );
2521        assert_eq!(
2522            eval_binary_op(&Value::Null, &BinaryOp::Or, &Value::Bool(false)).unwrap(),
2523            Value::Null
2524        );
2525
2526        // null OR null = null
2527        assert_eq!(
2528            eval_binary_op(&Value::Null, &BinaryOp::Or, &Value::Null).unwrap(),
2529            Value::Null
2530        );
2531
2532        // Non-null cases still work
2533        assert_eq!(
2534            eval_binary_op(&Value::Bool(false), &BinaryOp::Or, &Value::Bool(false)).unwrap(),
2535            Value::Bool(false)
2536        );
2537        assert_eq!(
2538            eval_binary_op(&Value::Bool(true), &BinaryOp::Or, &Value::Bool(false)).unwrap(),
2539            Value::Bool(true)
2540        );
2541    }
2542
2543    #[test]
2544    fn test_nan_comparison_with_non_numeric() {
2545        let nan = Value::Float(f64::NAN);
2546
2547        // NaN > number → false
2548        assert_eq!(
2549            eval_binary_op(&nan, &BinaryOp::Gt, &i(1)).unwrap(),
2550            Value::Bool(false)
2551        );
2552
2553        // NaN > NaN → false
2554        assert_eq!(
2555            eval_binary_op(&nan, &BinaryOp::Gt, &nan).unwrap(),
2556            Value::Bool(false)
2557        );
2558
2559        // NaN > string → null (cross-type)
2560        assert_eq!(
2561            eval_binary_op(&nan, &BinaryOp::Gt, &s("a")).unwrap(),
2562            Value::Null
2563        );
2564
2565        // string < NaN → null (cross-type)
2566        assert_eq!(
2567            eval_binary_op(&s("a"), &BinaryOp::Lt, &nan).unwrap(),
2568            Value::Null
2569        );
2570    }
2571
2572    #[test]
2573    fn test_nan_equality_with_non_numeric() {
2574        let nan = Value::Float(f64::NAN);
2575
2576        // NaN = NaN → false
2577        assert_eq!(
2578            eval_binary_op(&nan, &BinaryOp::Eq, &nan).unwrap(),
2579            Value::Bool(false)
2580        );
2581
2582        // NaN <> NaN → true
2583        assert_eq!(
2584            eval_binary_op(&nan, &BinaryOp::NotEq, &nan).unwrap(),
2585            Value::Bool(true)
2586        );
2587
2588        // NaN = 'a' → false (structural mismatch at cypher_eq fallback)
2589        assert_eq!(
2590            eval_binary_op(&nan, &BinaryOp::Eq, &s("a")).unwrap(),
2591            Value::Bool(false)
2592        );
2593
2594        // NaN <> 'a' → true
2595        assert_eq!(
2596            eval_binary_op(&nan, &BinaryOp::NotEq, &s("a")).unwrap(),
2597            Value::Bool(true)
2598        );
2599    }
2600
2601    #[test]
2602    fn test_large_integer_equality() {
2603        // These two values are distinct as i64 but collide when cast to f64
2604        let a = Value::Int(4611686018427387905_i64);
2605        let b = Value::Int(4611686018427387900_i64);
2606
2607        assert_eq!(
2608            eval_binary_op(&a, &BinaryOp::Eq, &b).unwrap(),
2609            Value::Bool(false)
2610        );
2611        assert_eq!(
2612            eval_binary_op(&a, &BinaryOp::Eq, &a).unwrap(),
2613            Value::Bool(true)
2614        );
2615    }
2616
2617    #[test]
2618    fn test_large_integer_ordering() {
2619        let a = Value::Int(4611686018427387905_i64);
2620        let b = Value::Int(4611686018427387900_i64);
2621
2622        assert_eq!(
2623            eval_binary_op(&a, &BinaryOp::Gt, &b).unwrap(),
2624            Value::Bool(true)
2625        );
2626        assert_eq!(
2627            eval_binary_op(&b, &BinaryOp::Lt, &a).unwrap(),
2628            Value::Bool(true)
2629        );
2630    }
2631
2632    #[test]
2633    fn test_int_float_equality_still_works() {
2634        // Regression: 1 = 1.0 must still be true
2635        assert_eq!(
2636            eval_binary_op(&i(1), &BinaryOp::Eq, &Value::Float(1.0)).unwrap(),
2637            Value::Bool(true)
2638        );
2639        assert_eq!(
2640            eval_binary_op(&i(1), &BinaryOp::NotEq, &Value::Float(1.0)).unwrap(),
2641            Value::Bool(false)
2642        );
2643    }
2644
2645    #[test]
2646    fn test_xor_null_handling() {
2647        // Three-valued logic: any null operand returns null
2648
2649        assert_eq!(
2650            eval_binary_op(&Value::Bool(true), &BinaryOp::Xor, &Value::Null).unwrap(),
2651            Value::Null
2652        );
2653        assert_eq!(
2654            eval_binary_op(&Value::Bool(false), &BinaryOp::Xor, &Value::Null).unwrap(),
2655            Value::Null
2656        );
2657        assert_eq!(
2658            eval_binary_op(&Value::Null, &BinaryOp::Xor, &Value::Bool(true)).unwrap(),
2659            Value::Null
2660        );
2661        assert_eq!(
2662            eval_binary_op(&Value::Null, &BinaryOp::Xor, &Value::Null).unwrap(),
2663            Value::Null
2664        );
2665
2666        // Non-null cases still work
2667        assert_eq!(
2668            eval_binary_op(&Value::Bool(true), &BinaryOp::Xor, &Value::Bool(false)).unwrap(),
2669            Value::Bool(true)
2670        );
2671        assert_eq!(
2672            eval_binary_op(&Value::Bool(true), &BinaryOp::Xor, &Value::Bool(true)).unwrap(),
2673            Value::Bool(false)
2674        );
2675    }
2676}