Skip to main content

cypherlite_query/executor/
eval.rs

1// Expression evaluator: eval(), eval_cmp() with typed comparison
2
3use super::{ExecutionError, Params, Record, ScalarFnLookup, Value};
4use crate::parser::ast::*;
5use cypherlite_core::LabelRegistry;
6use cypherlite_storage::StorageEngine;
7
8// @MX:ANCHOR: [AUTO] Expression evaluator — called by Filter, Project, Create, Set, and Sort operators
9// @MX:REASON: fan_in >= 5; core evaluation logic for all WHERE/RETURN/SET expressions
10/// Evaluate an expression against a record (current row bindings).
11pub fn eval(
12    expr: &Expression,
13    record: &Record,
14    engine: &StorageEngine,
15    params: &Params,
16    scalar_fns: &dyn ScalarFnLookup,
17) -> Result<Value, ExecutionError> {
18    match expr {
19        Expression::Literal(lit) => Ok(eval_literal(lit)),
20        Expression::Variable(name) => Ok(record.get(name).cloned().unwrap_or(Value::Null)),
21        Expression::Property(inner_expr, prop_name) => {
22            // Check for temporal property override (from AT TIME / BETWEEN TIME queries)
23            if let Expression::Variable(var_name) = inner_expr.as_ref() {
24                const TEMPORAL_PREFIX: &str = "__temporal_props__";
25                let mut temporal_key =
26                    String::with_capacity(TEMPORAL_PREFIX.len() + var_name.len());
27                temporal_key.push_str(TEMPORAL_PREFIX);
28                temporal_key.push_str(var_name);
29                if let Some(Value::List(props_list)) = record.get(&temporal_key) {
30                    return eval_temporal_property_access(props_list, prop_name, engine);
31                }
32            }
33            let inner = eval(inner_expr, record, engine, params, scalar_fns)?;
34            eval_property_access(&inner, prop_name, engine)
35        }
36        Expression::Parameter(name) => Ok(params.get(name).cloned().unwrap_or(Value::Null)),
37        Expression::BinaryOp(op, lhs, rhs) => match op {
38            // Short-circuit evaluation for AND: false AND x => false
39            BinaryOp::And => {
40                let left = eval(lhs, record, engine, params, scalar_fns)?;
41                if let Value::Bool(false) = &left {
42                    return Ok(Value::Bool(false));
43                }
44                let right = eval(rhs, record, engine, params, scalar_fns)?;
45                eval_boolean_op(BinaryOp::And, &left, &right)
46            }
47            // Short-circuit evaluation for OR: true OR x => true
48            BinaryOp::Or => {
49                let left = eval(lhs, record, engine, params, scalar_fns)?;
50                if let Value::Bool(true) = &left {
51                    return Ok(Value::Bool(true));
52                }
53                let right = eval(rhs, record, engine, params, scalar_fns)?;
54                eval_boolean_op(BinaryOp::Or, &left, &right)
55            }
56            _ => {
57                let left = eval(lhs, record, engine, params, scalar_fns)?;
58                let right = eval(rhs, record, engine, params, scalar_fns)?;
59                eval_binary_op(*op, &left, &right)
60            }
61        },
62        Expression::UnaryOp(op, inner) => {
63            let val = eval(inner, record, engine, params, scalar_fns)?;
64            eval_unary_op(*op, &val)
65        }
66        Expression::IsNull(inner, negated) => {
67            let val = eval(inner, record, engine, params, scalar_fns)?;
68            let is_null = val == Value::Null;
69            if *negated {
70                Ok(Value::Bool(!is_null))
71            } else {
72                Ok(Value::Bool(is_null))
73            }
74        }
75        Expression::ListLiteral(elements) => {
76            let mut values = Vec::with_capacity(elements.len());
77            for elem in elements {
78                values.push(eval(elem, record, engine, params, scalar_fns)?);
79            }
80            Ok(Value::List(values))
81        }
82        // CountStar and FunctionCall with aggregates are handled at the aggregate level.
83        // When encountered in non-aggregate context, return Null.
84        Expression::CountStar => Ok(Value::Null),
85        Expression::FunctionCall { name, args, .. } => {
86            // Non-aggregate function calls: evaluate if known
87            let func_name = name.to_lowercase();
88            match func_name.as_str() {
89                "count" | "sum" | "avg" | "min" | "max" | "collect" => {
90                    // Aggregates are handled by AggregateOp, return Null here.
91                    Ok(Value::Null)
92                }
93                "id" => {
94                    if args.len() != 1 {
95                        return Err(ExecutionError {
96                            message: "id() requires exactly one argument".to_string(),
97                        });
98                    }
99                    let val = eval(&args[0], record, engine, params, scalar_fns)?;
100                    match val {
101                        Value::Node(nid) => Ok(Value::Int64(nid.0 as i64)),
102                        Value::Edge(eid) => Ok(Value::Int64(eid.0 as i64)),
103                        _ => Err(ExecutionError {
104                            message: "id() requires a node or edge argument".to_string(),
105                        }),
106                    }
107                }
108                "type" => {
109                    if args.len() != 1 {
110                        return Err(ExecutionError {
111                            message: "type() requires exactly one argument".to_string(),
112                        });
113                    }
114                    let val = eval(&args[0], record, engine, params, scalar_fns)?;
115                    match val {
116                        Value::Edge(eid) => {
117                            if let Some(edge) = engine.get_edge(eid) {
118                                let type_name = engine
119                                    .catalog()
120                                    .rel_type_name(edge.rel_type_id)
121                                    .unwrap_or("")
122                                    .to_string();
123                                Ok(Value::String(type_name))
124                            } else {
125                                Ok(Value::Null)
126                            }
127                        }
128                        _ => Err(ExecutionError {
129                            message: "type() requires an edge argument".to_string(),
130                        }),
131                    }
132                }
133                "labels" => {
134                    if args.len() != 1 {
135                        return Err(ExecutionError {
136                            message: "labels() requires exactly one argument".to_string(),
137                        });
138                    }
139                    let val = eval(&args[0], record, engine, params, scalar_fns)?;
140                    match val {
141                        Value::Node(nid) => {
142                            if let Some(node) = engine.get_node(nid) {
143                                let label_names: Vec<Value> = node
144                                    .labels
145                                    .iter()
146                                    .filter_map(|lid| {
147                                        engine
148                                            .catalog()
149                                            .label_name(*lid)
150                                            .map(|n| Value::String(n.to_string()))
151                                    })
152                                    .collect();
153                                Ok(Value::List(label_names))
154                            } else {
155                                Ok(Value::Null)
156                            }
157                        }
158                        _ => Err(ExecutionError {
159                            message: "labels() requires a node argument".to_string(),
160                        }),
161                    }
162                }
163                "datetime" => {
164                    if args.len() != 1 {
165                        return Err(ExecutionError {
166                            message: "datetime() requires exactly one string argument".to_string(),
167                        });
168                    }
169                    let val = eval(&args[0], record, engine, params, scalar_fns)?;
170                    match val {
171                        Value::String(s) => {
172                            let millis = parse_iso8601_to_millis(&s)
173                                .map_err(|e| ExecutionError { message: e })?;
174                            Ok(Value::DateTime(millis))
175                        }
176                        _ => Err(ExecutionError {
177                            message: "datetime() requires a string argument".to_string(),
178                        }),
179                    }
180                }
181                "now" => {
182                    if !args.is_empty() {
183                        return Err(ExecutionError {
184                            message: "now() takes no arguments".to_string(),
185                        });
186                    }
187                    // Read query start time from params
188                    match params.get("__query_start_ms__") {
189                        Some(Value::Int64(ms)) => Ok(Value::DateTime(*ms)),
190                        _ => {
191                            // Fallback: use current system time
192                            let ms = std::time::SystemTime::now()
193                                .duration_since(std::time::UNIX_EPOCH)
194                                .map(|d| d.as_millis() as i64)
195                                .unwrap_or(0);
196                            Ok(Value::DateTime(ms))
197                        }
198                    }
199                }
200                _ => {
201                    // Evaluate arguments, then try plugin scalar function lookup.
202                    let evaluated_args: Result<Vec<_>, _> = args
203                        .iter()
204                        .map(|a| eval(a, record, engine, params, scalar_fns))
205                        .collect();
206                    let evaluated_args = evaluated_args?;
207                    match scalar_fns.call_scalar(&func_name, &evaluated_args) {
208                        Some(result) => result,
209                        None => Err(ExecutionError {
210                            message: format!("unknown function: {}", name),
211                        }),
212                    }
213                }
214            }
215        }
216        #[cfg(feature = "hypergraph")]
217        Expression::TemporalRef { node, timestamp } => {
218            // Evaluate both sub-expressions and return a placeholder.
219            // The actual interpretation is done in the executor during hyperedge creation.
220            let _node_val = eval(node, record, engine, params, scalar_fns)?;
221            let _ts_val = eval(timestamp, record, engine, params, scalar_fns)?;
222            // For expression evaluation context, return the node value
223            // (temporal resolution happens at the hyperedge executor level).
224            eval(node, record, engine, params, scalar_fns)
225        }
226    }
227}
228
229/// Convert a literal AST node to a Value.
230fn eval_literal(lit: &Literal) -> Value {
231    match lit {
232        Literal::Integer(i) => Value::Int64(*i),
233        Literal::Float(f) => Value::Float64(*f),
234        Literal::String(s) => Value::String(s.clone()),
235        Literal::Bool(b) => Value::Bool(*b),
236        Literal::Null => Value::Null,
237    }
238}
239
240/// Access a property on a Value. For Node/Edge, look up from engine.
241/// Access a property from temporal version properties.
242/// The props_list is a List of [prop_key_id, value] pairs.
243fn eval_temporal_property_access(
244    props_list: &[Value],
245    prop_name: &str,
246    engine: &StorageEngine,
247) -> Result<Value, ExecutionError> {
248    let prop_key_id = engine.catalog().prop_key_id(prop_name);
249    match prop_key_id {
250        Some(kid) => {
251            for item in props_list {
252                if let Value::List(pair) = item {
253                    if pair.len() == 2 {
254                        if let Value::Int64(k) = &pair[0] {
255                            if *k as u32 == kid {
256                                return Ok(pair[1].clone());
257                            }
258                        }
259                    }
260                }
261            }
262            Ok(Value::Null)
263        }
264        None => Ok(Value::Null),
265    }
266}
267
268/// Access a property on a Value. For Node/Edge, look up from engine.
269fn eval_property_access(
270    val: &Value,
271    prop_name: &str,
272    engine: &StorageEngine,
273) -> Result<Value, ExecutionError> {
274    match val {
275        Value::Node(nid) => {
276            let node = engine.get_node(*nid).ok_or_else(|| ExecutionError {
277                message: format!("node {} not found", nid.0),
278            })?;
279            let prop_key_id = engine.catalog().prop_key_id(prop_name);
280            match prop_key_id {
281                Some(kid) => {
282                    for (k, v) in &node.properties {
283                        if *k == kid {
284                            return Ok(Value::from(v.clone()));
285                        }
286                    }
287                    Ok(Value::Null)
288                }
289                None => Ok(Value::Null),
290            }
291        }
292        Value::Edge(eid) => {
293            let edge = engine.get_edge(*eid).ok_or_else(|| ExecutionError {
294                message: format!("edge {} not found", eid.0),
295            })?;
296            let prop_key_id = engine.catalog().prop_key_id(prop_name);
297            match prop_key_id {
298                Some(kid) => {
299                    for (k, v) in &edge.properties {
300                        if *k == kid {
301                            return Ok(Value::from(v.clone()));
302                        }
303                    }
304                    Ok(Value::Null)
305                }
306                None => Ok(Value::Null),
307            }
308        }
309        #[cfg(feature = "subgraph")]
310        Value::Subgraph(sg_id) => {
311            // Special property: _temporal_anchor maps to SubgraphRecord.temporal_anchor
312            if prop_name == "_temporal_anchor" {
313                let sg = engine.get_subgraph(*sg_id).ok_or_else(|| ExecutionError {
314                    message: format!("subgraph {} not found", sg_id.0),
315                })?;
316                return match sg.temporal_anchor {
317                    Some(ms) => Ok(Value::Int64(ms)),
318                    None => Ok(Value::Null),
319                };
320            }
321            // Regular property access on SubgraphRecord.properties
322            let sg = engine.get_subgraph(*sg_id).ok_or_else(|| ExecutionError {
323                message: format!("subgraph {} not found", sg_id.0),
324            })?;
325            let prop_key_id = engine.catalog().prop_key_id(prop_name);
326            match prop_key_id {
327                Some(kid) => {
328                    for (k, v) in &sg.properties {
329                        if *k == kid {
330                            return Ok(Value::from(v.clone()));
331                        }
332                    }
333                    Ok(Value::Null)
334                }
335                None => Ok(Value::Null),
336            }
337        }
338        #[cfg(feature = "hypergraph")]
339        Value::Hyperedge(he_id) => {
340            let he = engine.get_hyperedge(*he_id).ok_or_else(|| ExecutionError {
341                message: format!("hyperedge {} not found", he_id.0),
342            })?;
343            let prop_key_id = engine.catalog().prop_key_id(prop_name);
344            match prop_key_id {
345                Some(kid) => {
346                    for (k, v) in &he.properties {
347                        if *k == kid {
348                            return Ok(Value::from(v.clone()));
349                        }
350                    }
351                    Ok(Value::Null)
352                }
353                None => Ok(Value::Null),
354            }
355        }
356        // NN-001, NN-002: Lazy TemporalRef resolution via VersionStore.
357        // When a TemporalNode's properties are accessed, resolve the node state
358        // at the referenced timestamp by walking the version chain.
359        #[cfg(feature = "hypergraph")]
360        Value::TemporalNode(nid, timestamp) => {
361            resolve_temporal_node_property(*nid, *timestamp, prop_name, engine)
362        }
363        Value::Null => Ok(Value::Null),
364        _ => Err(ExecutionError {
365            message: format!("cannot access property '{}' on non-entity value", prop_name),
366        }),
367    }
368}
369
370/// NN-001: Resolve a property on a node at a specific point in time.
371///
372/// Walk the VersionStore chain for the given node. Each version is a pre-update
373/// snapshot. Find the version whose `_updated_at` is closest to but not after
374/// `timestamp`. If no suitable version is found, fall back to the current node.
375#[cfg(feature = "hypergraph")]
376fn resolve_temporal_node_property(
377    nid: cypherlite_core::NodeId,
378    timestamp: i64,
379    prop_name: &str,
380    engine: &StorageEngine,
381) -> Result<Value, ExecutionError> {
382    use cypherlite_storage::version::VersionRecord;
383
384    let updated_at_key = engine.catalog().prop_key_id("_updated_at");
385
386    // Get version chain (oldest to newest).
387    let chain = engine.version_store().get_version_chain(nid.0);
388
389    // Find the best version: the latest version whose _updated_at <= timestamp.
390    let mut best_version: Option<&cypherlite_core::NodeRecord> = None;
391    for (_seq, record) in &chain {
392        if let VersionRecord::Node(node_rec) = record {
393            if let Some(ua_key) = updated_at_key {
394                for (k, v) in &node_rec.properties {
395                    if *k == ua_key {
396                        let ua_ms = match v {
397                            cypherlite_core::PropertyValue::DateTime(ms) => *ms,
398                            cypherlite_core::PropertyValue::Int64(ms) => *ms,
399                            _ => continue,
400                        };
401                        if ua_ms <= timestamp {
402                            best_version = Some(node_rec);
403                        }
404                        break;
405                    }
406                }
407            } else {
408                // No _updated_at key registered; use latest version as best guess.
409                best_version = Some(node_rec);
410            }
411        }
412    }
413
414    // Look up the property from the resolved version (or current node as fallback).
415    let prop_key_id = engine.catalog().prop_key_id(prop_name);
416    match prop_key_id {
417        Some(kid) => {
418            if let Some(node_rec) = best_version {
419                // Read property from versioned node record.
420                for (k, v) in &node_rec.properties {
421                    if *k == kid {
422                        return Ok(Value::from(v.clone()));
423                    }
424                }
425                Ok(Value::Null)
426            } else {
427                // Fallback: no matching version, use current node state.
428                let node = engine.get_node(nid).ok_or_else(|| ExecutionError {
429                    message: format!("node {} not found", nid.0),
430                })?;
431                for (k, v) in &node.properties {
432                    if *k == kid {
433                        return Ok(Value::from(v.clone()));
434                    }
435                }
436                Ok(Value::Null)
437            }
438        }
439        None => Ok(Value::Null),
440    }
441}
442
443/// Evaluate a binary operation.
444fn eval_binary_op(op: BinaryOp, left: &Value, right: &Value) -> Result<Value, ExecutionError> {
445    match op {
446        BinaryOp::And => eval_boolean_op(op, left, right),
447        BinaryOp::Or => eval_boolean_op(op, left, right),
448        BinaryOp::Eq
449        | BinaryOp::Neq
450        | BinaryOp::Lt
451        | BinaryOp::Lte
452        | BinaryOp::Gt
453        | BinaryOp::Gte => eval_cmp(left, right, op),
454        BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
455            eval_arithmetic(left, right, op)
456        }
457    }
458}
459
460/// Compare two values with the given comparison operator.
461pub fn eval_cmp(left: &Value, right: &Value, op: BinaryOp) -> Result<Value, ExecutionError> {
462    // Null semantics: any comparison with Null yields false (Cypher three-valued logic)
463    if *left == Value::Null || *right == Value::Null {
464        return Ok(Value::Bool(false));
465    }
466
467    match (left, right) {
468        (Value::Int64(a), Value::Int64(b)) => Ok(Value::Bool(cmp_ord(a, b, op))),
469        (Value::Float64(a), Value::Float64(b)) => Ok(Value::Bool(cmp_f64(*a, *b, op))),
470        // Int64 vs Float64 promotion
471        (Value::Int64(a), Value::Float64(b)) => Ok(Value::Bool(cmp_f64(*a as f64, *b, op))),
472        (Value::Float64(a), Value::Int64(b)) => Ok(Value::Bool(cmp_f64(*a, *b as f64, op))),
473        (Value::String(a), Value::String(b)) => Ok(Value::Bool(cmp_ord(a, b, op))),
474        (Value::Bool(a), Value::Bool(b)) => {
475            // Only Eq and Neq make sense for booleans
476            match op {
477                BinaryOp::Eq => Ok(Value::Bool(a == b)),
478                BinaryOp::Neq => Ok(Value::Bool(a != b)),
479                _ => Err(ExecutionError {
480                    message: "cannot order boolean values".to_string(),
481                }),
482            }
483        }
484        // Node/Edge ID equality
485        (Value::Node(a), Value::Node(b)) => match op {
486            BinaryOp::Eq => Ok(Value::Bool(a == b)),
487            BinaryOp::Neq => Ok(Value::Bool(a != b)),
488            _ => Err(ExecutionError {
489                message: "cannot order node values".to_string(),
490            }),
491        },
492        (Value::Edge(a), Value::Edge(b)) => match op {
493            BinaryOp::Eq => Ok(Value::Bool(a == b)),
494            BinaryOp::Neq => Ok(Value::Bool(a != b)),
495            _ => Err(ExecutionError {
496                message: "cannot order edge values".to_string(),
497            }),
498        },
499        // DateTime comparison: follows numeric ordering of underlying i64
500        (Value::DateTime(a), Value::DateTime(b)) => Ok(Value::Bool(cmp_ord(a, b, op))),
501        _ => Err(ExecutionError {
502            message: "type mismatch in comparison".to_string(),
503        }),
504    }
505}
506
507/// Compare two Ord values with the given operation.
508fn cmp_ord<T: Ord>(a: &T, b: &T, op: BinaryOp) -> bool {
509    match op {
510        BinaryOp::Eq => a == b,
511        BinaryOp::Neq => a != b,
512        BinaryOp::Lt => a < b,
513        BinaryOp::Lte => a <= b,
514        BinaryOp::Gt => a > b,
515        BinaryOp::Gte => a >= b,
516        _ => false,
517    }
518}
519
520/// Compare two f64 values with the given operation.
521fn cmp_f64(a: f64, b: f64, op: BinaryOp) -> bool {
522    match op {
523        BinaryOp::Eq => (a - b).abs() < f64::EPSILON,
524        BinaryOp::Neq => (a - b).abs() >= f64::EPSILON,
525        BinaryOp::Lt => a < b,
526        BinaryOp::Lte => a <= b,
527        BinaryOp::Gt => a > b,
528        BinaryOp::Gte => a >= b,
529        _ => false,
530    }
531}
532
533/// Evaluate arithmetic operations.
534fn eval_arithmetic(left: &Value, right: &Value, op: BinaryOp) -> Result<Value, ExecutionError> {
535    match (left, right) {
536        (Value::Int64(a), Value::Int64(b)) => match op {
537            BinaryOp::Add => Ok(Value::Int64(a.wrapping_add(*b))),
538            BinaryOp::Sub => Ok(Value::Int64(a.wrapping_sub(*b))),
539            BinaryOp::Mul => Ok(Value::Int64(a.wrapping_mul(*b))),
540            BinaryOp::Div => {
541                if *b == 0 {
542                    return Err(ExecutionError {
543                        message: "division by zero".to_string(),
544                    });
545                }
546                Ok(Value::Int64(a / b))
547            }
548            BinaryOp::Mod => {
549                if *b == 0 {
550                    return Err(ExecutionError {
551                        message: "division by zero".to_string(),
552                    });
553                }
554                Ok(Value::Int64(a % b))
555            }
556            _ => Err(ExecutionError {
557                message: "unexpected arithmetic op".to_string(),
558            }),
559        },
560        (Value::Float64(a), Value::Float64(b)) => eval_float_arithmetic(*a, *b, op),
561        (Value::Int64(a), Value::Float64(b)) => eval_float_arithmetic(*a as f64, *b, op),
562        (Value::Float64(a), Value::Int64(b)) => eval_float_arithmetic(*a, *b as f64, op),
563        (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
564        _ => Err(ExecutionError {
565            message: "type mismatch in arithmetic operation".to_string(),
566        }),
567    }
568}
569
570/// Evaluate float arithmetic.
571fn eval_float_arithmetic(a: f64, b: f64, op: BinaryOp) -> Result<Value, ExecutionError> {
572    match op {
573        BinaryOp::Add => Ok(Value::Float64(a + b)),
574        BinaryOp::Sub => Ok(Value::Float64(a - b)),
575        BinaryOp::Mul => Ok(Value::Float64(a * b)),
576        BinaryOp::Div => {
577            if b == 0.0 {
578                return Err(ExecutionError {
579                    message: "division by zero".to_string(),
580                });
581            }
582            Ok(Value::Float64(a / b))
583        }
584        BinaryOp::Mod => {
585            if b == 0.0 {
586                return Err(ExecutionError {
587                    message: "division by zero".to_string(),
588                });
589            }
590            Ok(Value::Float64(a % b))
591        }
592        _ => Err(ExecutionError {
593            message: "unexpected arithmetic op".to_string(),
594        }),
595    }
596}
597
598/// Evaluate boolean operations (AND, OR).
599fn eval_boolean_op(op: BinaryOp, left: &Value, right: &Value) -> Result<Value, ExecutionError> {
600    // Null propagation for boolean ops
601    match (left, right) {
602        (Value::Bool(a), Value::Bool(b)) => match op {
603            BinaryOp::And => Ok(Value::Bool(*a && *b)),
604            BinaryOp::Or => Ok(Value::Bool(*a || *b)),
605            _ => Err(ExecutionError {
606                message: "unexpected boolean op".to_string(),
607            }),
608        },
609        (Value::Null, Value::Bool(b)) => match op {
610            BinaryOp::And => {
611                if !b {
612                    Ok(Value::Bool(false))
613                } else {
614                    Ok(Value::Null)
615                }
616            }
617            BinaryOp::Or => {
618                if *b {
619                    Ok(Value::Bool(true))
620                } else {
621                    Ok(Value::Null)
622                }
623            }
624            _ => Err(ExecutionError {
625                message: "unexpected boolean op".to_string(),
626            }),
627        },
628        (Value::Bool(a), Value::Null) => match op {
629            BinaryOp::And => {
630                if !a {
631                    Ok(Value::Bool(false))
632                } else {
633                    Ok(Value::Null)
634                }
635            }
636            BinaryOp::Or => {
637                if *a {
638                    Ok(Value::Bool(true))
639                } else {
640                    Ok(Value::Null)
641                }
642            }
643            _ => Err(ExecutionError {
644                message: "unexpected boolean op".to_string(),
645            }),
646        },
647        (Value::Null, Value::Null) => Ok(Value::Null),
648        _ => Err(ExecutionError {
649            message: "non-boolean operand in boolean operation".to_string(),
650        }),
651    }
652}
653
654/// Evaluate a unary operation.
655fn eval_unary_op(op: UnaryOp, val: &Value) -> Result<Value, ExecutionError> {
656    match op {
657        UnaryOp::Not => match val {
658            Value::Bool(b) => Ok(Value::Bool(!b)),
659            Value::Null => Ok(Value::Null),
660            _ => Err(ExecutionError {
661                message: "NOT requires a boolean operand".to_string(),
662            }),
663        },
664        UnaryOp::Neg => match val {
665            Value::Int64(i) => Ok(Value::Int64(-i)),
666            Value::Float64(f) => Ok(Value::Float64(-f)),
667            Value::Null => Ok(Value::Null),
668            _ => Err(ExecutionError {
669                message: "unary minus requires a numeric operand".to_string(),
670            }),
671        },
672    }
673}
674
675/// Parse an ISO 8601 string to milliseconds since Unix epoch.
676/// Supports: YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DDTHH:MM:SSZ, YYYY-MM-DDTHH:MM:SS+HH:MM
677fn parse_iso8601_to_millis(s: &str) -> Result<i64, String> {
678    let s = s.trim();
679    if s.len() < 10 {
680        return Err(format!("invalid datetime: '{}'", s));
681    }
682
683    // Parse date part: YYYY-MM-DD
684    let year: i64 = s[0..4]
685        .parse()
686        .map_err(|_| format!("invalid year in '{}'", s))?;
687    if s.as_bytes()[4] != b'-' {
688        return Err(format!("invalid datetime: '{}'", s));
689    }
690    let month: u32 = s[5..7]
691        .parse()
692        .map_err(|_| format!("invalid month in '{}'", s))?;
693    if s.as_bytes()[7] != b'-' {
694        return Err(format!("invalid datetime: '{}'", s));
695    }
696    let day: u32 = s[8..10]
697        .parse()
698        .map_err(|_| format!("invalid day in '{}'", s))?;
699
700    if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
701        return Err(format!("invalid date values in '{}'", s));
702    }
703
704    let mut hour: u32 = 0;
705    let mut minute: u32 = 0;
706    let mut second: u32 = 0;
707    let mut tz_offset_minutes: i64 = 0;
708
709    let rest = &s[10..];
710    if !rest.is_empty() {
711        // Expect 'T' separator
712        if rest.as_bytes()[0] != b'T' {
713            return Err(format!("expected 'T' separator in '{}'", s));
714        }
715        let time_str = &rest[1..];
716        if time_str.len() < 8 {
717            return Err(format!("incomplete time in '{}'", s));
718        }
719        hour = time_str[0..2]
720            .parse()
721            .map_err(|_| format!("invalid hour in '{}'", s))?;
722        if time_str.as_bytes()[2] != b':' {
723            return Err(format!("invalid time format in '{}'", s));
724        }
725        minute = time_str[3..5]
726            .parse()
727            .map_err(|_| format!("invalid minute in '{}'", s))?;
728        if time_str.as_bytes()[5] != b':' {
729            return Err(format!("invalid time format in '{}'", s));
730        }
731        second = time_str[6..8]
732            .parse()
733            .map_err(|_| format!("invalid second in '{}'", s))?;
734
735        // Parse timezone suffix
736        let tz_part = &time_str[8..];
737        if !tz_part.is_empty() {
738            if tz_part == "Z" {
739                // UTC
740            } else if tz_part.len() == 6
741                && (tz_part.as_bytes()[0] == b'+' || tz_part.as_bytes()[0] == b'-')
742            {
743                let sign: i64 = if tz_part.as_bytes()[0] == b'+' { 1 } else { -1 };
744                let tz_hour: i64 = tz_part[1..3]
745                    .parse()
746                    .map_err(|_| format!("invalid timezone hour in '{}'", s))?;
747                let tz_min: i64 = tz_part[4..6]
748                    .parse()
749                    .map_err(|_| format!("invalid timezone minute in '{}'", s))?;
750                tz_offset_minutes = sign * (tz_hour * 60 + tz_min);
751            } else {
752                return Err(format!("invalid timezone in '{}'", s));
753            }
754        }
755    }
756
757    // Convert to days since epoch using Howard Hinnant's algorithm
758    let days = days_from_civil(year, month, day);
759    let total_seconds = days * 86400 + hour as i64 * 3600 + minute as i64 * 60 + second as i64
760        - tz_offset_minutes * 60;
761
762    Ok(total_seconds * 1000)
763}
764
765/// Convert (year, month, day) to days since 1970-01-01.
766/// Based on Howard Hinnant's `days_from_civil` algorithm.
767fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
768    let y = if month <= 2 { year - 1 } else { year };
769    let m = if month <= 2 { month + 9 } else { month - 3 };
770    let era = if y >= 0 { y } else { y - 399 } / 400;
771    let yoe = (y - era * 400) as u32; // year of era [0, 399]
772    let doy = (153 * m + 2) / 5 + day - 1; // day of year [0, 365]
773    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // day of era [0, 146096]
774    era * 146097 + doe as i64 - 719468
775}
776
777/// Utility: compare two Values for sorting. Returns Ordering.
778/// Used by SortOp.
779pub fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
780    use std::cmp::Ordering;
781    match (a, b) {
782        (Value::Null, Value::Null) => Ordering::Equal,
783        (Value::Null, _) => Ordering::Less,
784        (_, Value::Null) => Ordering::Greater,
785        (Value::Int64(x), Value::Int64(y)) => x.cmp(y),
786        (Value::Float64(x), Value::Float64(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
787        (Value::Int64(x), Value::Float64(y)) => {
788            (*x as f64).partial_cmp(y).unwrap_or(Ordering::Equal)
789        }
790        (Value::Float64(x), Value::Int64(y)) => {
791            x.partial_cmp(&(*y as f64)).unwrap_or(Ordering::Equal)
792        }
793        (Value::String(x), Value::String(y)) => x.cmp(y),
794        (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
795        (Value::DateTime(x), Value::DateTime(y)) => x.cmp(y),
796        _ => Ordering::Equal,
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use cypherlite_core::SyncMode;
804    use cypherlite_storage::StorageEngine;
805    use tempfile::tempdir;
806
807    fn test_engine(dir: &std::path::Path) -> StorageEngine {
808        let config = cypherlite_core::DatabaseConfig {
809            path: dir.join("test.cyl"),
810            wal_sync_mode: SyncMode::Normal,
811            ..Default::default()
812        };
813        StorageEngine::open(config).expect("open")
814    }
815
816    // ======================================================================
817    // TASK-045: eval() and eval_cmp() tests
818    // ======================================================================
819
820    // EXEC-T007: eval_cmp Int64 vs Float64 (promotion)
821    #[test]
822    fn test_eval_cmp_int_vs_float_promotion() {
823        let result = eval_cmp(&Value::Int64(5), &Value::Float64(5.0), BinaryOp::Eq);
824        assert_eq!(result, Ok(Value::Bool(true)));
825
826        let result = eval_cmp(&Value::Int64(5), &Value::Float64(6.0), BinaryOp::Lt);
827        assert_eq!(result, Ok(Value::Bool(true)));
828
829        let result = eval_cmp(&Value::Float64(3.0), &Value::Int64(3), BinaryOp::Eq);
830        assert_eq!(result, Ok(Value::Bool(true)));
831    }
832
833    // EXEC-T008: eval_cmp Null vs anything -> false
834    #[test]
835    fn test_eval_cmp_null_always_false() {
836        assert_eq!(
837            eval_cmp(&Value::Null, &Value::Int64(1), BinaryOp::Eq),
838            Ok(Value::Bool(false))
839        );
840        assert_eq!(
841            eval_cmp(&Value::Int64(1), &Value::Null, BinaryOp::Eq),
842            Ok(Value::Bool(false))
843        );
844        assert_eq!(
845            eval_cmp(&Value::Null, &Value::Null, BinaryOp::Eq),
846            Ok(Value::Bool(false))
847        );
848        assert_eq!(
849            eval_cmp(&Value::Null, &Value::String("x".into()), BinaryOp::Lt),
850            Ok(Value::Bool(false))
851        );
852    }
853
854    // EXEC-T009: eval_cmp type mismatch -> ExecutionError
855    #[test]
856    fn test_eval_cmp_type_mismatch() {
857        let result = eval_cmp(&Value::Int64(1), &Value::String("x".into()), BinaryOp::Eq);
858        assert!(result.is_err());
859        assert!(result
860            .expect_err("should error")
861            .message
862            .contains("type mismatch"));
863    }
864
865    #[test]
866    fn test_eval_cmp_int_int() {
867        assert_eq!(
868            eval_cmp(&Value::Int64(3), &Value::Int64(5), BinaryOp::Lt),
869            Ok(Value::Bool(true))
870        );
871        assert_eq!(
872            eval_cmp(&Value::Int64(5), &Value::Int64(5), BinaryOp::Lte),
873            Ok(Value::Bool(true))
874        );
875        assert_eq!(
876            eval_cmp(&Value::Int64(5), &Value::Int64(3), BinaryOp::Gt),
877            Ok(Value::Bool(true))
878        );
879        assert_eq!(
880            eval_cmp(&Value::Int64(5), &Value::Int64(5), BinaryOp::Gte),
881            Ok(Value::Bool(true))
882        );
883        assert_eq!(
884            eval_cmp(&Value::Int64(3), &Value::Int64(5), BinaryOp::Neq),
885            Ok(Value::Bool(true))
886        );
887    }
888
889    #[test]
890    fn test_eval_cmp_string_string() {
891        assert_eq!(
892            eval_cmp(
893                &Value::String("abc".into()),
894                &Value::String("def".into()),
895                BinaryOp::Lt
896            ),
897            Ok(Value::Bool(true))
898        );
899        assert_eq!(
900            eval_cmp(
901                &Value::String("abc".into()),
902                &Value::String("abc".into()),
903                BinaryOp::Eq
904            ),
905            Ok(Value::Bool(true))
906        );
907    }
908
909    #[test]
910    fn test_eval_literal() {
911        let dir = tempdir().expect("tempdir");
912        let engine = test_engine(dir.path());
913        let record = Record::new();
914        let params = Params::new();
915
916        let result = eval(
917            &Expression::Literal(Literal::Integer(42)),
918            &record,
919            &engine,
920            &params,
921            &(),
922        );
923        assert_eq!(result, Ok(Value::Int64(42)));
924
925        let result = eval(
926            &Expression::Literal(Literal::Float(3.15)),
927            &record,
928            &engine,
929            &params,
930            &(),
931        );
932        assert_eq!(result, Ok(Value::Float64(3.15)));
933
934        let result = eval(
935            &Expression::Literal(Literal::String("hello".into())),
936            &record,
937            &engine,
938            &params,
939            &(),
940        );
941        assert_eq!(result, Ok(Value::String("hello".into())));
942
943        let result = eval(
944            &Expression::Literal(Literal::Bool(true)),
945            &record,
946            &engine,
947            &params,
948            &(),
949        );
950        assert_eq!(result, Ok(Value::Bool(true)));
951
952        let result = eval(
953            &Expression::Literal(Literal::Null),
954            &record,
955            &engine,
956            &params,
957            &(),
958        );
959        assert_eq!(result, Ok(Value::Null));
960    }
961
962    #[test]
963    fn test_eval_variable_lookup() {
964        let dir = tempdir().expect("tempdir");
965        let engine = test_engine(dir.path());
966        let mut record = Record::new();
967        record.insert("x".to_string(), Value::Int64(99));
968        let params = Params::new();
969
970        let result = eval(
971            &Expression::Variable("x".to_string()),
972            &record,
973            &engine,
974            &params,
975            &(),
976        );
977        assert_eq!(result, Ok(Value::Int64(99)));
978
979        // Missing variable returns Null
980        let result = eval(
981            &Expression::Variable("missing".to_string()),
982            &record,
983            &engine,
984            &params,
985            &(),
986        );
987        assert_eq!(result, Ok(Value::Null));
988    }
989
990    #[test]
991    fn test_eval_parameter_lookup() {
992        let dir = tempdir().expect("tempdir");
993        let engine = test_engine(dir.path());
994        let record = Record::new();
995        let mut params = Params::new();
996        params.insert("name".to_string(), Value::String("Alice".into()));
997
998        let result = eval(
999            &Expression::Parameter("name".to_string()),
1000            &record,
1001            &engine,
1002            &params,
1003            &(),
1004        );
1005        assert_eq!(result, Ok(Value::String("Alice".into())));
1006
1007        // Missing parameter returns Null
1008        let result = eval(
1009            &Expression::Parameter("missing".to_string()),
1010            &record,
1011            &engine,
1012            &params,
1013            &(),
1014        );
1015        assert_eq!(result, Ok(Value::Null));
1016    }
1017
1018    #[test]
1019    fn test_eval_property_access_on_node() {
1020        let dir = tempdir().expect("tempdir");
1021        let mut engine = test_engine(dir.path());
1022
1023        // Register property key and create node
1024        let name_key = engine.get_or_create_prop_key("name");
1025        let nid = engine.create_node(
1026            vec![],
1027            vec![(
1028                name_key,
1029                cypherlite_core::PropertyValue::String("Alice".into()),
1030            )],
1031        );
1032
1033        let mut record = Record::new();
1034        record.insert("n".to_string(), Value::Node(nid));
1035        let params = Params::new();
1036
1037        let result = eval(
1038            &Expression::Property(
1039                Box::new(Expression::Variable("n".to_string())),
1040                "name".to_string(),
1041            ),
1042            &record,
1043            &engine,
1044            &params,
1045            &(),
1046        );
1047        assert_eq!(result, Ok(Value::String("Alice".into())));
1048
1049        // Non-existent property returns Null
1050        let result = eval(
1051            &Expression::Property(
1052                Box::new(Expression::Variable("n".to_string())),
1053                "age".to_string(),
1054            ),
1055            &record,
1056            &engine,
1057            &params,
1058            &(),
1059        );
1060        assert_eq!(result, Ok(Value::Null));
1061    }
1062
1063    #[test]
1064    fn test_eval_arithmetic_int() {
1065        assert_eq!(
1066            eval_arithmetic(&Value::Int64(10), &Value::Int64(3), BinaryOp::Add),
1067            Ok(Value::Int64(13))
1068        );
1069        assert_eq!(
1070            eval_arithmetic(&Value::Int64(10), &Value::Int64(3), BinaryOp::Sub),
1071            Ok(Value::Int64(7))
1072        );
1073        assert_eq!(
1074            eval_arithmetic(&Value::Int64(10), &Value::Int64(3), BinaryOp::Mul),
1075            Ok(Value::Int64(30))
1076        );
1077        assert_eq!(
1078            eval_arithmetic(&Value::Int64(10), &Value::Int64(3), BinaryOp::Div),
1079            Ok(Value::Int64(3))
1080        );
1081        assert_eq!(
1082            eval_arithmetic(&Value::Int64(10), &Value::Int64(3), BinaryOp::Mod),
1083            Ok(Value::Int64(1))
1084        );
1085    }
1086
1087    #[test]
1088    fn test_eval_arithmetic_division_by_zero() {
1089        assert!(eval_arithmetic(&Value::Int64(10), &Value::Int64(0), BinaryOp::Div).is_err());
1090        assert!(
1091            eval_arithmetic(&Value::Float64(10.0), &Value::Float64(0.0), BinaryOp::Div).is_err()
1092        );
1093    }
1094
1095    #[test]
1096    fn test_eval_arithmetic_mixed_types() {
1097        let result = eval_arithmetic(&Value::Int64(10), &Value::Float64(2.5), BinaryOp::Add);
1098        assert_eq!(result, Ok(Value::Float64(12.5)));
1099    }
1100
1101    #[test]
1102    fn test_eval_arithmetic_null_propagation() {
1103        assert_eq!(
1104            eval_arithmetic(&Value::Null, &Value::Int64(5), BinaryOp::Add),
1105            Ok(Value::Null)
1106        );
1107    }
1108
1109    #[test]
1110    fn test_eval_arithmetic_type_mismatch() {
1111        assert!(
1112            eval_arithmetic(&Value::String("x".into()), &Value::Int64(1), BinaryOp::Add).is_err()
1113        );
1114    }
1115
1116    #[test]
1117    fn test_eval_boolean_and_or() {
1118        assert_eq!(
1119            eval_boolean_op(BinaryOp::And, &Value::Bool(true), &Value::Bool(false)),
1120            Ok(Value::Bool(false))
1121        );
1122        assert_eq!(
1123            eval_boolean_op(BinaryOp::And, &Value::Bool(true), &Value::Bool(true)),
1124            Ok(Value::Bool(true))
1125        );
1126        assert_eq!(
1127            eval_boolean_op(BinaryOp::Or, &Value::Bool(false), &Value::Bool(true)),
1128            Ok(Value::Bool(true))
1129        );
1130        assert_eq!(
1131            eval_boolean_op(BinaryOp::Or, &Value::Bool(false), &Value::Bool(false)),
1132            Ok(Value::Bool(false))
1133        );
1134    }
1135
1136    #[test]
1137    fn test_eval_boolean_non_bool_error() {
1138        assert!(eval_boolean_op(BinaryOp::And, &Value::Int64(1), &Value::Bool(true)).is_err());
1139    }
1140
1141    #[test]
1142    fn test_eval_is_null() {
1143        let dir = tempdir().expect("tempdir");
1144        let engine = test_engine(dir.path());
1145        let mut record = Record::new();
1146        record.insert("x".to_string(), Value::Null);
1147        record.insert("y".to_string(), Value::Int64(1));
1148        let params = Params::new();
1149
1150        // IS NULL
1151        let result = eval(
1152            &Expression::IsNull(Box::new(Expression::Variable("x".to_string())), false),
1153            &record,
1154            &engine,
1155            &params,
1156            &(),
1157        );
1158        assert_eq!(result, Ok(Value::Bool(true)));
1159
1160        // IS NOT NULL
1161        let result = eval(
1162            &Expression::IsNull(Box::new(Expression::Variable("y".to_string())), true),
1163            &record,
1164            &engine,
1165            &params,
1166            &(),
1167        );
1168        assert_eq!(result, Ok(Value::Bool(true)));
1169    }
1170
1171    #[test]
1172    fn test_eval_unary_not() {
1173        let dir = tempdir().expect("tempdir");
1174        let engine = test_engine(dir.path());
1175        let record = Record::new();
1176        let params = Params::new();
1177
1178        let result = eval(
1179            &Expression::UnaryOp(
1180                UnaryOp::Not,
1181                Box::new(Expression::Literal(Literal::Bool(true))),
1182            ),
1183            &record,
1184            &engine,
1185            &params,
1186            &(),
1187        );
1188        assert_eq!(result, Ok(Value::Bool(false)));
1189    }
1190
1191    #[test]
1192    fn test_eval_unary_neg() {
1193        let dir = tempdir().expect("tempdir");
1194        let engine = test_engine(dir.path());
1195        let record = Record::new();
1196        let params = Params::new();
1197
1198        let result = eval(
1199            &Expression::UnaryOp(
1200                UnaryOp::Neg,
1201                Box::new(Expression::Literal(Literal::Integer(42))),
1202            ),
1203            &record,
1204            &engine,
1205            &params,
1206            &(),
1207        );
1208        assert_eq!(result, Ok(Value::Int64(-42)));
1209    }
1210
1211    #[test]
1212    fn test_compare_values_ordering() {
1213        use std::cmp::Ordering;
1214        assert_eq!(
1215            compare_values(&Value::Int64(1), &Value::Int64(2)),
1216            Ordering::Less
1217        );
1218        assert_eq!(
1219            compare_values(&Value::Null, &Value::Int64(1)),
1220            Ordering::Less
1221        );
1222        assert_eq!(
1223            compare_values(&Value::Int64(1), &Value::Null),
1224            Ordering::Greater
1225        );
1226        assert_eq!(
1227            compare_values(&Value::String("a".into()), &Value::String("b".into())),
1228            Ordering::Less
1229        );
1230    }
1231
1232    // ======================================================================
1233    // U-002: datetime() built-in function
1234    // ======================================================================
1235
1236    #[test]
1237    fn test_eval_datetime_date_only() {
1238        let dir = tempdir().expect("tempdir");
1239        let engine = test_engine(dir.path());
1240        let record = Record::new();
1241        let params = Params::new();
1242
1243        // datetime('2024-01-15') -> 2024-01-15T00:00:00.000Z
1244        let result = eval(
1245            &Expression::FunctionCall {
1246                name: "datetime".to_string(),
1247                distinct: false,
1248                args: vec![Expression::Literal(Literal::String(
1249                    "2024-01-15".to_string(),
1250                ))],
1251            },
1252            &record,
1253            &engine,
1254            &params,
1255            &(),
1256        );
1257        assert_eq!(result, Ok(Value::DateTime(1_705_276_800_000)));
1258    }
1259
1260    #[test]
1261    fn test_eval_datetime_with_time() {
1262        let dir = tempdir().expect("tempdir");
1263        let engine = test_engine(dir.path());
1264        let record = Record::new();
1265        let params = Params::new();
1266
1267        // datetime('2024-01-15T10:30:00')
1268        let result = eval(
1269            &Expression::FunctionCall {
1270                name: "datetime".to_string(),
1271                distinct: false,
1272                args: vec![Expression::Literal(Literal::String(
1273                    "2024-01-15T10:30:00".to_string(),
1274                ))],
1275            },
1276            &record,
1277            &engine,
1278            &params,
1279            &(),
1280        );
1281        assert_eq!(
1282            result,
1283            Ok(Value::DateTime(
1284                1_705_276_800_000 + 10 * 3_600_000 + 30 * 60_000
1285            ))
1286        );
1287    }
1288
1289    #[test]
1290    fn test_eval_datetime_with_z_suffix() {
1291        let dir = tempdir().expect("tempdir");
1292        let engine = test_engine(dir.path());
1293        let record = Record::new();
1294        let params = Params::new();
1295
1296        let result = eval(
1297            &Expression::FunctionCall {
1298                name: "datetime".to_string(),
1299                distinct: false,
1300                args: vec![Expression::Literal(Literal::String(
1301                    "2024-01-15T10:30:00Z".to_string(),
1302                ))],
1303            },
1304            &record,
1305            &engine,
1306            &params,
1307            &(),
1308        );
1309        assert_eq!(
1310            result,
1311            Ok(Value::DateTime(
1312                1_705_276_800_000 + 10 * 3_600_000 + 30 * 60_000
1313            ))
1314        );
1315    }
1316
1317    #[test]
1318    fn test_eval_datetime_with_timezone_offset() {
1319        let dir = tempdir().expect("tempdir");
1320        let engine = test_engine(dir.path());
1321        let record = Record::new();
1322        let params = Params::new();
1323
1324        // datetime('2024-01-15T10:30:00+09:00') -> UTC 01:30:00
1325        let result = eval(
1326            &Expression::FunctionCall {
1327                name: "datetime".to_string(),
1328                distinct: false,
1329                args: vec![Expression::Literal(Literal::String(
1330                    "2024-01-15T10:30:00+09:00".to_string(),
1331                ))],
1332            },
1333            &record,
1334            &engine,
1335            &params,
1336            &(),
1337        );
1338        assert_eq!(
1339            result,
1340            Ok(Value::DateTime(1_705_276_800_000 + 3_600_000 + 30 * 60_000))
1341        );
1342    }
1343
1344    #[test]
1345    fn test_eval_datetime_invalid_format() {
1346        let dir = tempdir().expect("tempdir");
1347        let engine = test_engine(dir.path());
1348        let record = Record::new();
1349        let params = Params::new();
1350
1351        let result = eval(
1352            &Expression::FunctionCall {
1353                name: "datetime".to_string(),
1354                distinct: false,
1355                args: vec![Expression::Literal(Literal::String(
1356                    "not-a-date".to_string(),
1357                ))],
1358            },
1359            &record,
1360            &engine,
1361            &params,
1362            &(),
1363        );
1364        assert!(result.is_err());
1365    }
1366
1367    #[test]
1368    fn test_eval_datetime_wrong_arg_count() {
1369        let dir = tempdir().expect("tempdir");
1370        let engine = test_engine(dir.path());
1371        let record = Record::new();
1372        let params = Params::new();
1373
1374        let result = eval(
1375            &Expression::FunctionCall {
1376                name: "datetime".to_string(),
1377                distinct: false,
1378                args: vec![],
1379            },
1380            &record,
1381            &engine,
1382            &params,
1383            &(),
1384        );
1385        assert!(result.is_err());
1386    }
1387
1388    #[test]
1389    fn test_eval_datetime_non_string_arg() {
1390        let dir = tempdir().expect("tempdir");
1391        let engine = test_engine(dir.path());
1392        let record = Record::new();
1393        let params = Params::new();
1394
1395        let result = eval(
1396            &Expression::FunctionCall {
1397                name: "datetime".to_string(),
1398                distinct: false,
1399                args: vec![Expression::Literal(Literal::Integer(42))],
1400            },
1401            &record,
1402            &engine,
1403            &params,
1404            &(),
1405        );
1406        assert!(result.is_err());
1407    }
1408
1409    // ======================================================================
1410    // U-003: now() function
1411    // ======================================================================
1412
1413    #[test]
1414    fn test_eval_now_returns_datetime() {
1415        let dir = tempdir().expect("tempdir");
1416        let engine = test_engine(dir.path());
1417        let record = Record::new();
1418        let mut params = Params::new();
1419        params.insert(
1420            "__query_start_ms__".to_string(),
1421            Value::Int64(1_700_000_000_000),
1422        );
1423
1424        let result = eval(
1425            &Expression::FunctionCall {
1426                name: "now".to_string(),
1427                distinct: false,
1428                args: vec![],
1429            },
1430            &record,
1431            &engine,
1432            &params,
1433            &(),
1434        );
1435        assert_eq!(result, Ok(Value::DateTime(1_700_000_000_000)));
1436    }
1437
1438    #[test]
1439    fn test_eval_now_no_args() {
1440        let dir = tempdir().expect("tempdir");
1441        let engine = test_engine(dir.path());
1442        let record = Record::new();
1443        let mut params = Params::new();
1444        params.insert(
1445            "__query_start_ms__".to_string(),
1446            Value::Int64(1_700_000_000_000),
1447        );
1448
1449        // now() with args should fail
1450        let result = eval(
1451            &Expression::FunctionCall {
1452                name: "now".to_string(),
1453                distinct: false,
1454                args: vec![Expression::Literal(Literal::Integer(1))],
1455            },
1456            &record,
1457            &engine,
1458            &params,
1459            &(),
1460        );
1461        assert!(result.is_err());
1462    }
1463
1464    // ======================================================================
1465    // U-004: DateTime comparison operators
1466    // ======================================================================
1467
1468    #[test]
1469    fn test_eval_cmp_datetime_eq() {
1470        assert_eq!(
1471            eval_cmp(
1472                &Value::DateTime(1_700_000_000_000),
1473                &Value::DateTime(1_700_000_000_000),
1474                BinaryOp::Eq
1475            ),
1476            Ok(Value::Bool(true))
1477        );
1478        assert_eq!(
1479            eval_cmp(
1480                &Value::DateTime(1_700_000_000_000),
1481                &Value::DateTime(1_700_000_000_001),
1482                BinaryOp::Eq
1483            ),
1484            Ok(Value::Bool(false))
1485        );
1486    }
1487
1488    #[test]
1489    fn test_eval_cmp_datetime_lt_gt() {
1490        assert_eq!(
1491            eval_cmp(
1492                &Value::DateTime(1_000),
1493                &Value::DateTime(2_000),
1494                BinaryOp::Lt
1495            ),
1496            Ok(Value::Bool(true))
1497        );
1498        assert_eq!(
1499            eval_cmp(
1500                &Value::DateTime(2_000),
1501                &Value::DateTime(1_000),
1502                BinaryOp::Gt
1503            ),
1504            Ok(Value::Bool(true))
1505        );
1506        assert_eq!(
1507            eval_cmp(
1508                &Value::DateTime(1_000),
1509                &Value::DateTime(1_000),
1510                BinaryOp::Lte
1511            ),
1512            Ok(Value::Bool(true))
1513        );
1514        assert_eq!(
1515            eval_cmp(
1516                &Value::DateTime(1_000),
1517                &Value::DateTime(1_000),
1518                BinaryOp::Gte
1519            ),
1520            Ok(Value::Bool(true))
1521        );
1522    }
1523
1524    #[test]
1525    fn test_eval_cmp_datetime_neq() {
1526        assert_eq!(
1527            eval_cmp(
1528                &Value::DateTime(1_000),
1529                &Value::DateTime(2_000),
1530                BinaryOp::Neq
1531            ),
1532            Ok(Value::Bool(true))
1533        );
1534    }
1535
1536    #[test]
1537    fn test_eval_cmp_datetime_vs_non_datetime_error() {
1538        let result = eval_cmp(&Value::DateTime(1_000), &Value::Int64(1_000), BinaryOp::Eq);
1539        assert!(result.is_err());
1540    }
1541
1542    #[test]
1543    fn test_eval_cmp_datetime_vs_null() {
1544        assert_eq!(
1545            eval_cmp(&Value::DateTime(1_000), &Value::Null, BinaryOp::Eq),
1546            Ok(Value::Bool(false))
1547        );
1548    }
1549
1550    #[test]
1551    fn test_compare_values_datetime_ordering() {
1552        use std::cmp::Ordering;
1553        assert_eq!(
1554            compare_values(&Value::DateTime(1_000), &Value::DateTime(2_000)),
1555            Ordering::Less
1556        );
1557        assert_eq!(
1558            compare_values(&Value::DateTime(2_000), &Value::DateTime(1_000)),
1559            Ordering::Greater
1560        );
1561        assert_eq!(
1562            compare_values(&Value::DateTime(1_000), &Value::DateTime(1_000)),
1563            Ordering::Equal
1564        );
1565    }
1566
1567    // U-005: DateTime epoch
1568    #[test]
1569    fn test_eval_datetime_epoch() {
1570        let dir = tempdir().expect("tempdir");
1571        let engine = test_engine(dir.path());
1572        let record = Record::new();
1573        let params = Params::new();
1574
1575        let result = eval(
1576            &Expression::FunctionCall {
1577                name: "datetime".to_string(),
1578                distinct: false,
1579                args: vec![Expression::Literal(Literal::String(
1580                    "1970-01-01".to_string(),
1581                ))],
1582            },
1583            &record,
1584            &engine,
1585            &params,
1586            &(),
1587        );
1588        assert_eq!(result, Ok(Value::DateTime(0)));
1589    }
1590
1591    // ── Hypergraph property access tests ───────────────────────────────
1592    #[cfg(feature = "hypergraph")]
1593    mod hyperedge_property_tests {
1594        use super::*;
1595
1596        #[test]
1597        fn test_property_access_on_hyperedge() {
1598            let dir = tempdir().expect("tempdir");
1599            let mut engine = test_engine(dir.path());
1600
1601            let rel_type = engine.get_or_create_rel_type("INVOLVES");
1602            let prop_key = engine.get_or_create_prop_key("weight");
1603
1604            use cypherlite_core::{GraphEntity, PropertyValue};
1605            let n1 = engine.create_node(vec![], vec![]);
1606            let he_id = engine.create_hyperedge(
1607                rel_type,
1608                vec![GraphEntity::Node(n1)],
1609                vec![],
1610                vec![(prop_key, PropertyValue::Int64(42))],
1611            );
1612
1613            let mut record = Record::new();
1614            record.insert("he".to_string(), Value::Hyperedge(he_id));
1615
1616            let expr = Expression::Property(
1617                Box::new(Expression::Variable("he".to_string())),
1618                "weight".to_string(),
1619            );
1620            let result = eval(&expr, &record, &engine, &Params::new(), &());
1621            assert_eq!(result, Ok(Value::Int64(42)));
1622        }
1623
1624        #[test]
1625        fn test_property_access_on_hyperedge_missing_prop() {
1626            let dir = tempdir().expect("tempdir");
1627            let mut engine = test_engine(dir.path());
1628
1629            let rel_type = engine.get_or_create_rel_type("INVOLVES");
1630
1631            let he_id = engine.create_hyperedge(rel_type, vec![], vec![], vec![]);
1632
1633            let mut record = Record::new();
1634            record.insert("he".to_string(), Value::Hyperedge(he_id));
1635
1636            let expr = Expression::Property(
1637                Box::new(Expression::Variable("he".to_string())),
1638                "nonexistent".to_string(),
1639            );
1640            let result = eval(&expr, &record, &engine, &Params::new(), &());
1641            assert_eq!(result, Ok(Value::Null));
1642        }
1643
1644        #[test]
1645        fn test_property_access_on_hyperedge_not_found() {
1646            let dir = tempdir().expect("tempdir");
1647            let engine = test_engine(dir.path());
1648
1649            let fake_id = cypherlite_core::HyperEdgeId(999);
1650            let mut record = Record::new();
1651            record.insert("he".to_string(), Value::Hyperedge(fake_id));
1652
1653            let expr = Expression::Property(
1654                Box::new(Expression::Variable("he".to_string())),
1655                "weight".to_string(),
1656            );
1657            let result = eval(&expr, &record, &engine, &Params::new(), &());
1658            assert!(result.is_err());
1659        }
1660
1661        // NN-001: TemporalNode property access falls back to current node
1662        // when no versions exist.
1663        #[test]
1664        fn test_temporal_node_no_versions_falls_back_to_current() {
1665            let dir = tempdir().expect("tempdir");
1666            let mut engine = test_engine(dir.path());
1667
1668            let name_key = engine.get_or_create_prop_key("name");
1669            let nid = engine.create_node(
1670                vec![],
1671                vec![(
1672                    name_key,
1673                    cypherlite_core::PropertyValue::String("Alice".into()),
1674                )],
1675            );
1676
1677            let mut record = Record::new();
1678            record.insert("n".to_string(), Value::TemporalNode(nid, 999_999));
1679            let params = Params::new();
1680
1681            let result = eval(
1682                &Expression::Property(
1683                    Box::new(Expression::Variable("n".to_string())),
1684                    "name".to_string(),
1685                ),
1686                &record,
1687                &engine,
1688                &params,
1689                &(),
1690            );
1691            assert_eq!(result, Ok(Value::String("Alice".into())));
1692        }
1693
1694        // NN-001: TemporalNode property access resolves from VersionStore
1695        // when versions exist.
1696        #[test]
1697        fn test_temporal_node_resolves_versioned_properties() {
1698            let dir = tempdir().expect("tempdir");
1699            let mut engine = test_engine(dir.path());
1700
1701            let name_key = engine.get_or_create_prop_key("name");
1702            let updated_at_key = engine.get_or_create_prop_key("_updated_at");
1703
1704            // Create node with name='Alice' and _updated_at=100
1705            let nid = engine.create_node(
1706                vec![],
1707                vec![
1708                    (
1709                        name_key,
1710                        cypherlite_core::PropertyValue::String("Alice".into()),
1711                    ),
1712                    (
1713                        updated_at_key,
1714                        cypherlite_core::PropertyValue::DateTime(100),
1715                    ),
1716                ],
1717            );
1718
1719            // Update node to name='Bob' and _updated_at=200
1720            // This should snapshot the old state (Alice, _updated_at=100)
1721            engine
1722                .update_node(
1723                    nid,
1724                    vec![
1725                        (
1726                            name_key,
1727                            cypherlite_core::PropertyValue::String("Bob".into()),
1728                        ),
1729                        (
1730                            updated_at_key,
1731                            cypherlite_core::PropertyValue::DateTime(200),
1732                        ),
1733                    ],
1734                )
1735                .expect("update");
1736
1737            // TemporalNode with timestamp=150 should resolve to the version
1738            // where _updated_at=100 (Alice), because 100 <= 150.
1739            let mut record = Record::new();
1740            record.insert("n".to_string(), Value::TemporalNode(nid, 150));
1741            let params = Params::new();
1742
1743            let result = eval(
1744                &Expression::Property(
1745                    Box::new(Expression::Variable("n".to_string())),
1746                    "name".to_string(),
1747                ),
1748                &record,
1749                &engine,
1750                &params,
1751                &(),
1752            );
1753            assert_eq!(result, Ok(Value::String("Alice".into())));
1754        }
1755
1756        // NN-002: TemporalNode resolves to latest matching version
1757        // when multiple versions exist.
1758        #[test]
1759        fn test_temporal_node_multiple_versions_picks_latest_match() {
1760            let dir = tempdir().expect("tempdir");
1761            let mut engine = test_engine(dir.path());
1762
1763            let name_key = engine.get_or_create_prop_key("name");
1764            let updated_at_key = engine.get_or_create_prop_key("_updated_at");
1765
1766            // Create: name='v1', _updated_at=100
1767            let nid = engine.create_node(
1768                vec![],
1769                vec![
1770                    (
1771                        name_key,
1772                        cypherlite_core::PropertyValue::String("v1".into()),
1773                    ),
1774                    (
1775                        updated_at_key,
1776                        cypherlite_core::PropertyValue::DateTime(100),
1777                    ),
1778                ],
1779            );
1780
1781            // Update to v2 at time 200 (snapshots v1)
1782            engine
1783                .update_node(
1784                    nid,
1785                    vec![
1786                        (
1787                            name_key,
1788                            cypherlite_core::PropertyValue::String("v2".into()),
1789                        ),
1790                        (
1791                            updated_at_key,
1792                            cypherlite_core::PropertyValue::DateTime(200),
1793                        ),
1794                    ],
1795                )
1796                .expect("update 1");
1797
1798            // Update to v3 at time 300 (snapshots v2)
1799            engine
1800                .update_node(
1801                    nid,
1802                    vec![
1803                        (
1804                            name_key,
1805                            cypherlite_core::PropertyValue::String("v3".into()),
1806                        ),
1807                        (
1808                            updated_at_key,
1809                            cypherlite_core::PropertyValue::DateTime(300),
1810                        ),
1811                    ],
1812                )
1813                .expect("update 2");
1814
1815            // At timestamp 250: should resolve to v2 (version with _updated_at=200)
1816            let mut record = Record::new();
1817            record.insert("n".to_string(), Value::TemporalNode(nid, 250));
1818            let params = Params::new();
1819
1820            let result = eval(
1821                &Expression::Property(
1822                    Box::new(Expression::Variable("n".to_string())),
1823                    "name".to_string(),
1824                ),
1825                &record,
1826                &engine,
1827                &params,
1828                &(),
1829            );
1830            assert_eq!(result, Ok(Value::String("v2".into())));
1831        }
1832
1833        // NN-002: TemporalNode with timestamp before all versions
1834        // falls back to current node.
1835        #[test]
1836        fn test_temporal_node_timestamp_before_all_versions() {
1837            let dir = tempdir().expect("tempdir");
1838            let mut engine = test_engine(dir.path());
1839
1840            let name_key = engine.get_or_create_prop_key("name");
1841            let updated_at_key = engine.get_or_create_prop_key("_updated_at");
1842
1843            // Create: name='Alice', _updated_at=200
1844            let nid = engine.create_node(
1845                vec![],
1846                vec![
1847                    (
1848                        name_key,
1849                        cypherlite_core::PropertyValue::String("Alice".into()),
1850                    ),
1851                    (
1852                        updated_at_key,
1853                        cypherlite_core::PropertyValue::DateTime(200),
1854                    ),
1855                ],
1856            );
1857
1858            // Update to name='Bob', _updated_at=300 (snapshots Alice at 200)
1859            engine
1860                .update_node(
1861                    nid,
1862                    vec![
1863                        (
1864                            name_key,
1865                            cypherlite_core::PropertyValue::String("Bob".into()),
1866                        ),
1867                        (
1868                            updated_at_key,
1869                            cypherlite_core::PropertyValue::DateTime(300),
1870                        ),
1871                    ],
1872                )
1873                .expect("update");
1874
1875            // Timestamp=50 is before the earliest version (_updated_at=200).
1876            // No version has _updated_at <= 50, so falls back to current node (Bob).
1877            let mut record = Record::new();
1878            record.insert("n".to_string(), Value::TemporalNode(nid, 50));
1879            let params = Params::new();
1880
1881            let result = eval(
1882                &Expression::Property(
1883                    Box::new(Expression::Variable("n".to_string())),
1884                    "name".to_string(),
1885                ),
1886                &record,
1887                &engine,
1888                &params,
1889                &(),
1890            );
1891            assert_eq!(result, Ok(Value::String("Bob".into())));
1892        }
1893
1894        // NN-003: TemporalNode non-existent property returns Null.
1895        #[test]
1896        fn test_temporal_node_nonexistent_property() {
1897            let dir = tempdir().expect("tempdir");
1898            let mut engine = test_engine(dir.path());
1899
1900            let name_key = engine.get_or_create_prop_key("name");
1901            let nid = engine.create_node(
1902                vec![],
1903                vec![(
1904                    name_key,
1905                    cypherlite_core::PropertyValue::String("Alice".into()),
1906                )],
1907            );
1908
1909            let mut record = Record::new();
1910            record.insert("n".to_string(), Value::TemporalNode(nid, 999));
1911            let params = Params::new();
1912
1913            let result = eval(
1914                &Expression::Property(
1915                    Box::new(Expression::Variable("n".to_string())),
1916                    "nonexistent".to_string(),
1917                ),
1918                &record,
1919                &engine,
1920                &params,
1921                &(),
1922            );
1923            assert_eq!(result, Ok(Value::Null));
1924        }
1925    }
1926
1927    // ======================================================================
1928    // M2-1: Temporal key allocation optimization -- functional verification
1929    // ======================================================================
1930
1931    #[test]
1932    fn test_temporal_property_access_with_optimized_key() {
1933        // Verify that temporal property access works correctly after
1934        // replacing format!() with pre-allocated String::with_capacity
1935        let dir = tempdir().expect("tempdir");
1936        let mut engine = test_engine(dir.path());
1937
1938        let name_key = engine.get_or_create_prop_key("name");
1939
1940        let mut record = Record::new();
1941        // Simulate temporal properties override as injected by temporal operators
1942        // Format: Value::List(vec![Value::List(vec![Value::Int64(key_id), value]), ...])
1943        record.insert(
1944            "__temporal_props__n".to_string(),
1945            Value::List(vec![Value::List(vec![
1946                Value::Int64(name_key as i64),
1947                Value::String("TemporalAlice".into()),
1948            ])]),
1949        );
1950        record.insert("n".to_string(), Value::Null);
1951        let params = Params::new();
1952
1953        // Access n.name should resolve from temporal override, not from storage
1954        let result = eval(
1955            &Expression::Property(
1956                Box::new(Expression::Variable("n".to_string())),
1957                "name".to_string(),
1958            ),
1959            &record,
1960            &engine,
1961            &params,
1962            &(),
1963        );
1964        assert_eq!(
1965            result,
1966            Ok(Value::String("TemporalAlice".into())),
1967            "Temporal property override should resolve correctly"
1968        );
1969    }
1970
1971    #[test]
1972    fn test_temporal_property_access_empty_var_name() {
1973        // Edge case: empty variable name with temporal prefix
1974        let dir = tempdir().expect("tempdir");
1975        let mut engine = test_engine(dir.path());
1976
1977        let name_key = engine.get_or_create_prop_key("name");
1978
1979        let mut record = Record::new();
1980        record.insert(
1981            "__temporal_props__".to_string(),
1982            Value::List(vec![Value::List(vec![
1983                Value::Int64(name_key as i64),
1984                Value::String("Empty".into()),
1985            ])]),
1986        );
1987        record.insert("".to_string(), Value::Null);
1988        let params = Params::new();
1989
1990        let result = eval(
1991            &Expression::Property(
1992                Box::new(Expression::Variable("".to_string())),
1993                "name".to_string(),
1994            ),
1995            &record,
1996            &engine,
1997            &params,
1998            &(),
1999        );
2000        assert_eq!(
2001            result,
2002            Ok(Value::String("Empty".into())),
2003            "Empty var name temporal access should work"
2004        );
2005    }
2006
2007    // ======================================================================
2008    // M2-2: AND/OR short-circuit evaluation
2009    // ======================================================================
2010
2011    /// Helper: expression that divides 1/0 to force an error if evaluated
2012    fn div_by_zero_expr() -> Expression {
2013        Expression::BinaryOp(
2014            BinaryOp::Div,
2015            Box::new(Expression::Literal(Literal::Integer(1))),
2016            Box::new(Expression::Literal(Literal::Integer(0))),
2017        )
2018    }
2019
2020    fn bool_expr(val: bool) -> Expression {
2021        Expression::Literal(Literal::Bool(val))
2022    }
2023
2024    fn null_expr() -> Expression {
2025        Expression::Literal(Literal::Null)
2026    }
2027
2028    #[test]
2029    fn test_and_short_circuit_false() {
2030        // AND(false, 1/0) should return false without evaluating right side
2031        let dir = tempdir().expect("tempdir");
2032        let engine = test_engine(dir.path());
2033        let record = Record::new();
2034        let params = Params::new();
2035
2036        let expr = Expression::BinaryOp(
2037            BinaryOp::And,
2038            Box::new(bool_expr(false)),
2039            Box::new(div_by_zero_expr()),
2040        );
2041        let result = eval(&expr, &record, &engine, &params, &());
2042        assert_eq!(
2043            result,
2044            Ok(Value::Bool(false)),
2045            "AND(false, error) should short-circuit to false"
2046        );
2047    }
2048
2049    #[test]
2050    fn test_or_short_circuit_true() {
2051        // OR(true, 1/0) should return true without evaluating right side
2052        let dir = tempdir().expect("tempdir");
2053        let engine = test_engine(dir.path());
2054        let record = Record::new();
2055        let params = Params::new();
2056
2057        let expr = Expression::BinaryOp(
2058            BinaryOp::Or,
2059            Box::new(bool_expr(true)),
2060            Box::new(div_by_zero_expr()),
2061        );
2062        let result = eval(&expr, &record, &engine, &params, &());
2063        assert_eq!(
2064            result,
2065            Ok(Value::Bool(true)),
2066            "OR(true, error) should short-circuit to true"
2067        );
2068    }
2069
2070    #[test]
2071    fn test_and_true_evaluates_right() {
2072        // AND(true, 1/0) should evaluate right side and produce error
2073        let dir = tempdir().expect("tempdir");
2074        let engine = test_engine(dir.path());
2075        let record = Record::new();
2076        let params = Params::new();
2077
2078        let expr = Expression::BinaryOp(
2079            BinaryOp::And,
2080            Box::new(bool_expr(true)),
2081            Box::new(div_by_zero_expr()),
2082        );
2083        let result = eval(&expr, &record, &engine, &params, &());
2084        assert!(
2085            result.is_err(),
2086            "AND(true, error) should evaluate right side and error"
2087        );
2088    }
2089
2090    #[test]
2091    fn test_or_false_evaluates_right() {
2092        // OR(false, 1/0) should evaluate right side and produce error
2093        let dir = tempdir().expect("tempdir");
2094        let engine = test_engine(dir.path());
2095        let record = Record::new();
2096        let params = Params::new();
2097
2098        let expr = Expression::BinaryOp(
2099            BinaryOp::Or,
2100            Box::new(bool_expr(false)),
2101            Box::new(div_by_zero_expr()),
2102        );
2103        let result = eval(&expr, &record, &engine, &params, &());
2104        assert!(
2105            result.is_err(),
2106            "OR(false, error) should evaluate right side and error"
2107        );
2108    }
2109
2110    #[test]
2111    fn test_and_null_evaluates_right() {
2112        // AND(NULL, false) = false, AND(NULL, true) = NULL (3-valued logic)
2113        let dir = tempdir().expect("tempdir");
2114        let engine = test_engine(dir.path());
2115        let record = Record::new();
2116        let params = Params::new();
2117
2118        let expr_null_and_false = Expression::BinaryOp(
2119            BinaryOp::And,
2120            Box::new(null_expr()),
2121            Box::new(bool_expr(false)),
2122        );
2123        assert_eq!(
2124            eval(&expr_null_and_false, &record, &engine, &params, &()),
2125            Ok(Value::Bool(false)),
2126            "NULL AND false = false"
2127        );
2128
2129        let expr_null_and_true = Expression::BinaryOp(
2130            BinaryOp::And,
2131            Box::new(null_expr()),
2132            Box::new(bool_expr(true)),
2133        );
2134        assert_eq!(
2135            eval(&expr_null_and_true, &record, &engine, &params, &()),
2136            Ok(Value::Null),
2137            "NULL AND true = NULL"
2138        );
2139    }
2140
2141    #[test]
2142    fn test_or_null_evaluates_right() {
2143        // OR(NULL, true) = true, OR(NULL, false) = NULL (3-valued logic)
2144        let dir = tempdir().expect("tempdir");
2145        let engine = test_engine(dir.path());
2146        let record = Record::new();
2147        let params = Params::new();
2148
2149        let expr_null_or_true = Expression::BinaryOp(
2150            BinaryOp::Or,
2151            Box::new(null_expr()),
2152            Box::new(bool_expr(true)),
2153        );
2154        assert_eq!(
2155            eval(&expr_null_or_true, &record, &engine, &params, &()),
2156            Ok(Value::Bool(true)),
2157            "NULL OR true = true"
2158        );
2159
2160        let expr_null_or_false = Expression::BinaryOp(
2161            BinaryOp::Or,
2162            Box::new(null_expr()),
2163            Box::new(bool_expr(false)),
2164        );
2165        assert_eq!(
2166            eval(&expr_null_or_false, &record, &engine, &params, &()),
2167            Ok(Value::Null),
2168            "NULL OR false = NULL"
2169        );
2170    }
2171
2172    #[test]
2173    fn test_nested_short_circuit() {
2174        // (false AND (true OR 1/0)) should short-circuit the outer AND
2175        let dir = tempdir().expect("tempdir");
2176        let engine = test_engine(dir.path());
2177        let record = Record::new();
2178        let params = Params::new();
2179
2180        let inner_or = Expression::BinaryOp(
2181            BinaryOp::Or,
2182            Box::new(bool_expr(true)),
2183            Box::new(div_by_zero_expr()),
2184        );
2185        let outer_and = Expression::BinaryOp(
2186            BinaryOp::And,
2187            Box::new(bool_expr(false)),
2188            Box::new(inner_or),
2189        );
2190        assert_eq!(
2191            eval(&outer_and, &record, &engine, &params, &()),
2192            Ok(Value::Bool(false)),
2193            "false AND (...) should short-circuit without evaluating inner"
2194        );
2195
2196        // (true OR (false AND 1/0)) should short-circuit the outer OR
2197        let inner_and = Expression::BinaryOp(
2198            BinaryOp::And,
2199            Box::new(bool_expr(false)),
2200            Box::new(div_by_zero_expr()),
2201        );
2202        let outer_or =
2203            Expression::BinaryOp(BinaryOp::Or, Box::new(bool_expr(true)), Box::new(inner_and));
2204        assert_eq!(
2205            eval(&outer_or, &record, &engine, &params, &()),
2206            Ok(Value::Bool(true)),
2207            "true OR (...) should short-circuit without evaluating inner"
2208        );
2209    }
2210
2211    #[test]
2212    fn test_and_error_on_left_propagates() {
2213        // AND(1/0, false) should propagate the left-side error
2214        let dir = tempdir().expect("tempdir");
2215        let engine = test_engine(dir.path());
2216        let record = Record::new();
2217        let params = Params::new();
2218
2219        let expr = Expression::BinaryOp(
2220            BinaryOp::And,
2221            Box::new(div_by_zero_expr()),
2222            Box::new(bool_expr(false)),
2223        );
2224        let result = eval(&expr, &record, &engine, &params, &());
2225        assert!(
2226            result.is_err(),
2227            "AND(error, ...) should propagate left-side error"
2228        );
2229    }
2230
2231    #[test]
2232    fn test_or_error_on_left_propagates() {
2233        // OR(1/0, true) should propagate the left-side error
2234        let dir = tempdir().expect("tempdir");
2235        let engine = test_engine(dir.path());
2236        let record = Record::new();
2237        let params = Params::new();
2238
2239        let expr = Expression::BinaryOp(
2240            BinaryOp::Or,
2241            Box::new(div_by_zero_expr()),
2242            Box::new(bool_expr(true)),
2243        );
2244        let result = eval(&expr, &record, &engine, &params, &());
2245        assert!(
2246            result.is_err(),
2247            "OR(error, ...) should propagate left-side error"
2248        );
2249    }
2250
2251    #[test]
2252    fn test_and_null_null() {
2253        // NULL AND NULL = NULL
2254        let dir = tempdir().expect("tempdir");
2255        let engine = test_engine(dir.path());
2256        let record = Record::new();
2257        let params = Params::new();
2258
2259        let expr =
2260            Expression::BinaryOp(BinaryOp::And, Box::new(null_expr()), Box::new(null_expr()));
2261        assert_eq!(
2262            eval(&expr, &record, &engine, &params, &()),
2263            Ok(Value::Null),
2264            "NULL AND NULL = NULL"
2265        );
2266    }
2267
2268    #[test]
2269    fn test_or_null_null() {
2270        // NULL OR NULL = NULL
2271        let dir = tempdir().expect("tempdir");
2272        let engine = test_engine(dir.path());
2273        let record = Record::new();
2274        let params = Params::new();
2275
2276        let expr = Expression::BinaryOp(BinaryOp::Or, Box::new(null_expr()), Box::new(null_expr()));
2277        assert_eq!(
2278            eval(&expr, &record, &engine, &params, &()),
2279            Ok(Value::Null),
2280            "NULL OR NULL = NULL"
2281        );
2282    }
2283}