Skip to main content

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