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