Skip to main content

uni_query/query/
expr_eval.rs

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