1use super::{ExecutionError, Params, Record, ScalarFnLookup, Value};
4use crate::parser::ast::*;
5use cypherlite_core::LabelRegistry;
6use cypherlite_storage::StorageEngine;
7
8pub 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 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 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 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 Expression::CountStar => Ok(Value::Null),
85 Expression::FunctionCall { name, args, .. } => {
86 let func_name = name.to_lowercase();
88 match func_name.as_str() {
89 "count" | "sum" | "avg" | "min" | "max" | "collect" => {
90 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 match params.get("__query_start_ms__") {
189 Some(Value::Int64(ms)) => Ok(Value::DateTime(*ms)),
190 _ => {
191 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 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 let _node_val = eval(node, record, engine, params, scalar_fns)?;
221 let _ts_val = eval(timestamp, record, engine, params, scalar_fns)?;
222 eval(node, record, engine, params, scalar_fns)
225 }
226 }
227}
228
229fn 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
240fn 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
268fn 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 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 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 #[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#[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 let chain = engine.version_store().get_version_chain(nid.0);
388
389 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 best_version = Some(node_rec);
410 }
411 }
412 }
413
414 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 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 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
443fn 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
460pub fn eval_cmp(left: &Value, right: &Value, op: BinaryOp) -> Result<Value, ExecutionError> {
462 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 (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 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 (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 (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
507fn 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
520fn 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
533fn 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
570fn 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
598fn eval_boolean_op(op: BinaryOp, left: &Value, right: &Value) -> Result<Value, ExecutionError> {
600 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
654fn 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
675fn 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 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 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 let tz_part = &time_str[8..];
737 if !tz_part.is_empty() {
738 if tz_part == "Z" {
739 } 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 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
765fn 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; let doy = (153 * m + 2) / 5 + day - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe as i64 - 719468
775}
776
777pub 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 #[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 #[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 #[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 ¶ms,
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 ¶ms,
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 ¶ms,
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 ¶ms,
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 ¶ms,
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 ¶ms,
975 &(),
976 );
977 assert_eq!(result, Ok(Value::Int64(99)));
978
979 let result = eval(
981 &Expression::Variable("missing".to_string()),
982 &record,
983 &engine,
984 ¶ms,
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 ¶ms,
1003 &(),
1004 );
1005 assert_eq!(result, Ok(Value::String("Alice".into())));
1006
1007 let result = eval(
1009 &Expression::Parameter("missing".to_string()),
1010 &record,
1011 &engine,
1012 ¶ms,
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 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 ¶ms,
1045 &(),
1046 );
1047 assert_eq!(result, Ok(Value::String("Alice".into())));
1048
1049 let result = eval(
1051 &Expression::Property(
1052 Box::new(Expression::Variable("n".to_string())),
1053 "age".to_string(),
1054 ),
1055 &record,
1056 &engine,
1057 ¶ms,
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 let result = eval(
1152 &Expression::IsNull(Box::new(Expression::Variable("x".to_string())), false),
1153 &record,
1154 &engine,
1155 ¶ms,
1156 &(),
1157 );
1158 assert_eq!(result, Ok(Value::Bool(true)));
1159
1160 let result = eval(
1162 &Expression::IsNull(Box::new(Expression::Variable("y".to_string())), true),
1163 &record,
1164 &engine,
1165 ¶ms,
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 ¶ms,
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 ¶ms,
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 #[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 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 ¶ms,
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 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 ¶ms,
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 ¶ms,
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 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 ¶ms,
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 ¶ms,
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 ¶ms,
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 ¶ms,
1404 &(),
1405 );
1406 assert!(result.is_err());
1407 }
1408
1409 #[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 ¶ms,
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 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 ¶ms,
1459 &(),
1460 );
1461 assert!(result.is_err());
1462 }
1463
1464 #[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 #[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 ¶ms,
1586 &(),
1587 );
1588 assert_eq!(result, Ok(Value::DateTime(0)));
1589 }
1590
1591 #[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 #[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 ¶ms,
1689 &(),
1690 );
1691 assert_eq!(result, Ok(Value::String("Alice".into())));
1692 }
1693
1694 #[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 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 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 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 ¶ms,
1751 &(),
1752 );
1753 assert_eq!(result, Ok(Value::String("Alice".into())));
1754 }
1755
1756 #[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 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 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 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 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 ¶ms,
1828 &(),
1829 );
1830 assert_eq!(result, Ok(Value::String("v2".into())));
1831 }
1832
1833 #[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 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 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 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 ¶ms,
1889 &(),
1890 );
1891 assert_eq!(result, Ok(Value::String("Bob".into())));
1892 }
1893
1894 #[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 ¶ms,
1921 &(),
1922 );
1923 assert_eq!(result, Ok(Value::Null));
1924 }
1925 }
1926
1927 #[test]
1932 fn test_temporal_property_access_with_optimized_key() {
1933 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 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 let result = eval(
1955 &Expression::Property(
1956 Box::new(Expression::Variable("n".to_string())),
1957 "name".to_string(),
1958 ),
1959 &record,
1960 &engine,
1961 ¶ms,
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 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 ¶ms,
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 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 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, ¶ms, &());
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 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, ¶ms, &());
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 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, ¶ms, &());
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 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, ¶ms, &());
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 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, ¶ms, &()),
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, ¶ms, &()),
2136 Ok(Value::Null),
2137 "NULL AND true = NULL"
2138 );
2139 }
2140
2141 #[test]
2142 fn test_or_null_evaluates_right() {
2143 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, ¶ms, &()),
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, ¶ms, &()),
2167 Ok(Value::Null),
2168 "NULL OR false = NULL"
2169 );
2170 }
2171
2172 #[test]
2173 fn test_nested_short_circuit() {
2174 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, ¶ms, &()),
2192 Ok(Value::Bool(false)),
2193 "false AND (...) should short-circuit without evaluating inner"
2194 );
2195
2196 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, ¶ms, &()),
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 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, ¶ms, &());
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 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, ¶ms, &());
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 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, ¶ms, &()),
2263 Ok(Value::Null),
2264 "NULL AND NULL = NULL"
2265 );
2266 }
2267
2268 #[test]
2269 fn test_or_null_null() {
2270 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, ¶ms, &()),
2279 Ok(Value::Null),
2280 "NULL OR NULL = NULL"
2281 );
2282 }
2283}