1use crate::cypher::ast::{
4 ret_val_label, AggArg, AggFunc, Expr, LimitSkip, Operand, OrderItem, OrderTarget, RetItem,
5 RetVal, UnwindExpr,
6};
7use crate::cypher::plan::PlanOp;
8use crate::cypher::RelDir;
9use crate::filter::eval_cmp;
10use crate::result::ResultSet;
11use crate::traverse::{expand, Dir, EdgeRef};
12use crate::value_ops::{cmp_optional, values_equal};
13use crate::view::GraphView;
14use core_storage::{Value, ValueKey};
15use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
16
17#[cfg(test)]
21static FUSED_SCAN_FIRES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
22
23#[cfg(test)]
27static SCAN_KEY_FIRES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
28
29#[cfg(test)]
32pub static INDEX_SCAN_FIRES: std::sync::atomic::AtomicUsize =
33 std::sync::atomic::AtomicUsize::new(0);
34
35#[cfg(test)]
39pub static INDEX_INTERSECT_FIRES: std::sync::atomic::AtomicUsize =
40 std::sync::atomic::AtomicUsize::new(0);
41
42pub struct Params<'a>(pub &'a BTreeMap<String, Value>);
45
46#[derive(Clone, Debug)]
47enum Cell {
48 Node(u32),
49 Rel(EdgeRef),
50 Path(u8),
53 Scalar(Value),
55}
56
57type Row = Vec<Option<Cell>>;
60
61struct VarTable {
62 names: Vec<String>,
63}
64
65impl VarTable {
66 fn intern(&mut self, name: &str) -> usize {
67 if let Some(i) = self.names.iter().position(|n| n == name) {
68 return i;
69 }
70 self.names.push(name.to_string());
71 self.names.len() - 1
72 }
73
74 fn slot(&self, name: &str) -> Option<usize> {
75 self.names.iter().position(|n| n == name)
76 }
77}
78
79struct Projected {
80 columns: Vec<String>,
81 rows: Vec<Vec<Option<Value>>>,
82}
83
84const MAX_INTERMEDIATE_ROWS: usize = 1_000_000;
87
88const MAX_GROUPS: usize = 1_000_000;
90
91type GroupKey = Vec<Option<ValueKey>>;
94type GroupEntry = (Vec<Option<Value>>, Vec<AggAcc>);
99
100#[cfg(test)]
101thread_local! {
102 static TEST_MAX_INTERMEDIATE_ROWS: std::cell::Cell<Option<usize>> =
103 const { std::cell::Cell::new(None) };
104 static TEST_EXPAND_PRODUCED: std::cell::Cell<Option<usize>> =
107 const { std::cell::Cell::new(None) };
108 static TEST_MAX_GROUPS: std::cell::Cell<Option<usize>> =
110 const { std::cell::Cell::new(None) };
111}
112
113fn max_intermediate_rows() -> usize {
114 #[cfg(test)]
115 {
116 TEST_MAX_INTERMEDIATE_ROWS
117 .with(|c| c.get())
118 .unwrap_or(MAX_INTERMEDIATE_ROWS)
119 }
120 #[cfg(not(test))]
121 {
122 MAX_INTERMEDIATE_ROWS
123 }
124}
125
126fn max_groups() -> usize {
127 #[cfg(test)]
128 {
129 TEST_MAX_GROUPS.with(|c| c.get()).unwrap_or(MAX_GROUPS)
130 }
131 #[cfg(not(test))]
132 {
133 MAX_GROUPS
134 }
135}
136
137#[cfg(test)]
140pub(crate) fn with_max_groups<R>(cap: usize, f: impl FnOnce() -> R) -> R {
141 TEST_MAX_GROUPS.with(|c| {
142 let prev = c.replace(Some(cap));
143 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
144 c.set(prev);
145 match result {
146 Ok(v) => v,
147 Err(p) => std::panic::resume_unwind(p),
148 }
149 })
150}
151
152#[cfg(test)]
156pub(crate) fn with_max_intermediate_rows<R>(cap: usize, f: impl FnOnce() -> R) -> R {
157 TEST_MAX_INTERMEDIATE_ROWS.with(|c| {
158 let prev = c.replace(Some(cap));
159 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
160 c.set(prev);
161 match result {
162 Ok(v) => v,
163 Err(p) => std::panic::resume_unwind(p),
164 }
165 })
166}
167
168#[cfg(test)]
170fn record_expand_row() {
171 TEST_EXPAND_PRODUCED.with(|c| {
172 if let Some(prev) = c.get() {
173 c.set(Some(prev + 1));
174 }
175 });
176}
177
178#[cfg(test)]
184pub(crate) fn with_expand_counter<R>(f: impl FnOnce() -> R) -> (R, usize) {
185 TEST_EXPAND_PRODUCED.with(|c| c.set(Some(0)));
186 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
187 let count = TEST_EXPAND_PRODUCED.with(|c| c.get().unwrap_or(0));
188 TEST_EXPAND_PRODUCED.with(|c| c.set(None));
189 match result {
190 Ok(v) => (v, count),
191 Err(p) => std::panic::resume_unwind(p),
192 }
193}
194
195fn row_cap_err(cap: usize) -> String {
196 format!(
197 "intermediate result exceeds {cap} rows; add a LIMIT or constrain patterns with shared variables"
198 )
199}
200
201fn group_cap_err() -> String {
202 format!(
203 "group count exceeds {} distinct keys; add a WHERE clause or constrain the grouping key",
204 max_groups()
205 )
206}
207
208fn group_key_normalize(v: &Value) -> Option<ValueKey> {
219 match v {
220 Value::Int(n) => Some(ValueKey::FloatBits((*n as f64).to_bits())),
221 Value::Float(f) => Some(ValueKey::FloatBits(f.to_bits())),
222 _ => ValueKey::from_value(v),
223 }
224}
225
226pub fn execute(view: &GraphView, plan: &[PlanOp], params: &Params) -> Result<ResultSet, String> {
238 use crate::cypher::plan::row_bound;
239 execute_inner(view, plan, params, row_bound(plan))
240}
241
242pub fn execute_union(
247 view: &GraphView,
248 union: &crate::cypher::parser::UnionQuery,
249 params: &Params,
250) -> Result<ResultSet, String> {
251 let mut acc: Option<ResultSet> = None;
252 for (i, part) in union.parts.iter().enumerate() {
253 let ops = crate::cypher::plan::plan(part)?;
254 let rs = execute(view, &ops, params)?;
255 acc = Some(match acc {
256 None => rs,
257 Some(prev) => {
258 if prev.columns() != rs.columns() {
259 return Err(format!(
260 "UNION requires matching column names across all parts; \
261 got {:?} then {:?}",
262 prev.columns(),
263 rs.columns()
264 ));
265 }
266 let all = union.all_flags.get(i - 1).copied().unwrap_or(false);
268 combine_result_sets(prev, rs, all)
269 }
270 });
271 }
272 Ok(acc.expect("UNION query has at least one part"))
274}
275
276fn combine_result_sets(mut acc: ResultSet, other: ResultSet, all: bool) -> ResultSet {
277 for i in 0..other.len() {
278 acc.push_row(other.row(i).to_vec());
279 }
280 if !all {
281 let mut seen: Vec<Vec<Option<Value>>> = Vec::new();
283 for i in 0..acc.len() {
284 let r = acc.row(i).to_vec();
285 if !seen.contains(&r) {
286 seen.push(r);
287 }
288 }
289 let mut deduped = ResultSet::new(acc.columns().to_vec());
290 for r in seen {
291 deduped.push_row(r);
292 }
293 return deduped;
294 }
295 acc
296}
297
298#[cfg(test)]
303pub(crate) fn execute_unbounded(
304 view: &GraphView,
305 plan: &[PlanOp],
306 params: &Params,
307) -> Result<ResultSet, String> {
308 execute_inner(view, plan, params, None)
309}
310
311fn execute_inner(
312 view: &GraphView,
313 plan: &[PlanOp],
314 params: &Params,
315 row_bound: Option<usize>,
316) -> Result<ResultSet, String> {
317 check_params(plan, params)?;
318
319 let has_var_expand = plan
325 .iter()
326 .any(|op| matches!(op, PlanOp::VarExpand { .. } | PlanOp::ShortestPath { .. }));
327
328 let is_pipeline = plan.iter().any(|op| {
345 matches!(
346 op,
347 PlanOp::With { .. } | PlanOp::Unwind { .. } | PlanOp::LeftOuterApply { .. }
348 )
349 }) || {
350 let mut saw_gagg = false;
351 plan.iter().any(|op| {
352 if matches!(op, PlanOp::GroupAggregate { .. }) {
353 saw_gagg = true;
354 false
355 } else {
356 saw_gagg && matches!(op, PlanOp::Filter { .. } | PlanOp::Project { .. })
357 }
358 })
359 };
360
361 if !has_var_expand
365 && !is_pipeline
366 && plan
367 .iter()
368 .any(|op| matches!(op, PlanOp::GroupAggregate { .. }))
369 {
370 return execute_group_aggregate(view, plan, params);
371 }
372
373 if !has_var_expand
377 && !is_pipeline
378 && plan.iter().any(|op| matches!(op, PlanOp::Aggregate { .. }))
379 {
380 return execute_aggregate(view, plan, params);
381 }
382
383 if let Some(bound) = row_bound {
387 return execute_pull(view, plan, params, bound);
388 }
389
390 let vars = collect_vars(plan);
392 let mut rows: Vec<Row> = vec![vec![None; vars.names.len()]];
393 let mut projected: Option<Projected> = None;
394 let mut pipeline_group_columns: Option<Vec<String>> = None;
397
398 for op in plan {
399 match op {
400 PlanOp::ScanLabel { var, label } => {
401 rows = scan_label(view, &vars, &rows, var, label.as_deref())?;
402 }
403 PlanOp::ScanKey { var, key, label } => {
404 rows = scan_key(view, &vars, &rows, var, key, label.as_deref(), params)?;
405 }
406 PlanOp::IndexScan {
407 var,
408 label,
409 field,
410 value,
411 } => {
412 rows = scan_index(
413 view,
414 &vars,
415 &rows,
416 var,
417 label.as_deref(),
418 field,
419 value,
420 params,
421 )?;
422 }
423 PlanOp::IndexIntersect {
424 var,
425 label,
426 equalities,
427 } => {
428 rows = scan_intersect(
429 view,
430 &vars,
431 &rows,
432 var,
433 label.as_deref(),
434 equalities,
435 params,
436 )?;
437 }
438 PlanOp::LookupProps { var, props } => {
439 rows = retain_node(view, &vars, &rows, var, None, props, params)?;
440 }
441 PlanOp::JoinBound { var, label, props } => {
442 rows = retain_node(view, &vars, &rows, var, label.as_deref(), props, params)?;
443 }
444 PlanOp::Expand { .. } => {
445 rows = exec_expand(view, &vars, &rows, op, params)?;
446 }
447 PlanOp::VarExpand {
448 from,
449 rel_var,
450 etypes,
451 dir,
452 to,
453 min,
454 max,
455 } => {
456 rows = exec_var_expand(
457 view, &vars, &rows, from, rel_var, etypes, *dir, to, *min, *max,
458 )?;
459 }
460 PlanOp::ShortestPath {
461 from,
462 rel_var,
463 etypes,
464 dir,
465 to,
466 max_hops,
467 } => {
468 rows = exec_shortest_path(
469 view, &vars, &rows, from, rel_var, etypes, *dir, to, *max_hops,
470 )?;
471 }
472 PlanOp::Filter { expr } => {
473 rows = exec_filter(view, &vars, &rows, expr, params)?;
474 }
475 PlanOp::Project { items } => {
476 projected = Some(exec_project(view, &vars, &rows, items, params)?);
477 }
478 PlanOp::Distinct => {
479 if let Some(table) = projected.as_mut() {
480 exec_distinct(table)?;
481 } else {
482 return Err("DISTINCT requires a Project".into());
483 }
484 }
485 PlanOp::OrderBy { items } => {
486 if let Some(table) = projected.as_mut() {
487 exec_order_by(table, items)?;
488 } else {
489 exec_order_by_rows(&vars, &mut rows, items, view);
491 }
492 }
493 PlanOp::Skip(ls) => {
494 let n = resolve_ls(ls, params)?;
495 if let Some(table) = projected.as_mut() {
496 apply_skip(&mut table.rows, n);
497 } else {
498 apply_skip(&mut rows, n);
499 }
500 }
501 PlanOp::Limit(ls) => {
502 let n = resolve_ls(ls, params)?;
503 if let Some(table) = projected.as_mut() {
504 apply_limit(&mut table.rows, n);
505 } else {
506 apply_limit(&mut rows, n);
507 }
508 }
509 PlanOp::GroupAggregate { keys, aggs } => {
514 let mut grp_groups: HashMap<GroupKey, GroupEntry> = HashMap::new();
515 let mut grp_key_order: Vec<GroupKey> = Vec::new();
516 let mut grp_cells: HashMap<GroupKey, Vec<Option<Cell>>> = HashMap::new();
523 for row in &rows {
524 let mut gk: GroupKey = Vec::with_capacity(keys.len());
525 let mut display_vals: Vec<Option<Value>> = Vec::with_capacity(keys.len());
526 for (_, item) in keys {
527 let val = project_item(view, &vars, row, item, params)?;
528 gk.push(val.as_ref().and_then(group_key_normalize));
529 display_vals.push(val);
530 }
531 if !grp_groups.contains_key(&gk) {
532 if grp_groups.len() >= max_groups() {
533 return Err(group_cap_err());
534 }
535 grp_key_order.push(gk.clone());
536 grp_cells.insert(gk.clone(), key_source_cells(&vars, row, keys));
537 grp_groups.insert(
538 gk.clone(),
539 (
540 display_vals,
541 aggs.iter().map(|(f, a, _)| AggAcc::for_arg(f, a)).collect(),
542 ),
543 );
544 }
545 let (_, accs) = grp_groups.get_mut(&gk).unwrap();
546 for (acc, (func, arg, _)) in accs.iter_mut().zip(aggs.iter()) {
547 update_acc(view, &vars, row, func, arg, acc)?;
548 }
549 }
550 if keys.is_empty() && grp_key_order.is_empty() {
553 let empty_key: GroupKey = vec![];
554 grp_key_order.push(empty_key.clone());
555 grp_groups.insert(
556 empty_key,
557 (
558 vec![],
559 aggs.iter().map(|(f, a, _)| AggAcc::for_arg(f, a)).collect(),
560 ),
561 );
562 }
563 if is_pipeline {
564 rows = group_result_to_rows(
567 keys,
568 aggs,
569 grp_key_order,
570 &mut grp_groups,
571 &mut grp_cells,
572 &vars,
573 );
574 projected = None;
575 let mut cols: Vec<String> = keys.iter().map(|(c, _)| c.clone()).collect();
578 cols.extend(aggs.iter().map(|(_, _, c)| c.clone()));
579 pipeline_group_columns = Some(cols);
580 } else {
581 projected = Some(build_group_projected(
582 keys,
583 aggs,
584 grp_key_order,
585 &mut grp_groups,
586 ));
587 pipeline_group_columns = None;
588 }
589 }
590 PlanOp::With {
593 items,
594 where_expr,
595 order_by,
596 skip,
597 limit,
598 } => {
599 let row_len = vars.names.len();
603 let mut new_rows: Vec<Row> = Vec::with_capacity(rows.len());
604 for row in &rows {
605 let mut new_row: Row = vec![None; row_len];
606 for item in items {
607 let col = column_name(item);
608 let Some(dst_slot) = vars.slot(&col) else {
609 continue;
610 };
611 match &item.value {
612 RetVal::Var(v) => {
613 if let Some(src_slot) = vars.slot(v) {
615 new_row[dst_slot] = row.get(src_slot).cloned().flatten();
616 }
617 }
618 RetVal::Prop { var, field } => {
619 let val = resolve_prop(view, &vars, row, var, field)?;
620 new_row[dst_slot] = val.map(Cell::Scalar);
621 }
622 RetVal::Agg { .. } => {} RetVal::FuncCall { name, args } => {
624 let val = eval_func(name, args, view, &vars, row, params)?;
625 new_row[dst_slot] = val.map(Cell::Scalar);
626 }
627 RetVal::ScalarExpr(op) => {
628 let val = resolve_operand(view, &vars, row, op, params)?;
629 new_row[dst_slot] = val.map(Cell::Scalar);
630 }
631 }
632 }
633 new_rows.push(new_row);
634 }
635 rows = new_rows;
636 if let Some(expr) = where_expr {
638 rows = exec_filter(view, &vars, &rows, expr, params)?;
639 }
640 if !order_by.is_empty() {
642 exec_order_by_rows(&vars, &mut rows, order_by, view);
643 }
644 if let Some(ls) = skip {
645 let n = resolve_ls(ls, params)?;
646 apply_skip(&mut rows, n);
647 }
648 if let Some(ls) = limit {
649 let n = resolve_ls(ls, params)?;
650 apply_limit(&mut rows, n);
651 }
652 }
653 PlanOp::Unwind { expr, alias } => {
655 let alias_slot = vars
656 .slot(alias)
657 .ok_or_else(|| format!("UNWIND alias `{alias}` not in VarTable"))?;
658 let cap = max_intermediate_rows();
659 let mut new_rows: Vec<Row> = Vec::new();
660 for row in &rows {
661 let list_val: Option<Value> = match expr {
662 UnwindExpr::Lit(vals) => Some(Value::List(vals.clone())),
663 UnwindExpr::Prop { var, field } => {
664 resolve_prop(view, &vars, row, var, field)?
665 }
666 UnwindExpr::Var(name) => {
667 let slot = vars
668 .slot(name)
669 .ok_or_else(|| format!("UNWIND variable `{name}` is not bound"))?;
670 match row.get(slot).and_then(|c| c.as_ref()) {
671 Some(Cell::Scalar(v)) => Some(v.clone()),
672 Some(Cell::Node(_) | Cell::Rel(_) | Cell::Path(_)) => {
673 return Err(format!(
674 "UNWIND requires a list; `{name}` is bound to a graph element"
675 ));
676 }
677 None => None,
678 }
679 }
680 };
681 match list_val {
682 None => {} Some(Value::List(items_list)) => {
684 for item_val in items_list {
686 if new_rows.len() >= cap {
687 return Err(row_cap_err(cap));
688 }
689 let mut new_row = row.clone();
690 new_row[alias_slot] = Some(Cell::Scalar(item_val));
691 new_rows.push(new_row);
692 }
693 }
694 Some(other) => {
695 let type_name = match &other {
696 Value::Int(_) => "Int",
697 Value::Float(_) => "Float",
698 Value::Str(_) => "Str",
699 Value::Bool(_) => "Bool",
700 Value::List(_) => unreachable!(),
701 Value::Map(_) => "Map",
702 };
703 return Err(format!(
704 "UNWIND requires a list; got {type_name} value for `{alias}`"
705 ));
706 }
707 }
708 }
709 rows = new_rows;
710 }
711 PlanOp::Aggregate { func, arg, column } => {
716 let ctx = AggStreamCtx {
717 view,
718 vars: &vars,
719 params,
720 func,
721 arg,
722 };
723 let mut acc = AggAcc::for_arg(func, arg);
724 for row in &rows {
725 agg_stream(&ctx, &[], row, &mut acc)?;
727 }
728 let value = acc.finish();
729 let mut rs = ResultSet::new(vec![column.clone()]);
730 rs.push_row(vec![value]);
731 return Ok(rs);
732 }
733 PlanOp::LeftOuterApply {
739 inner,
740 optional_vars,
741 } => {
742 let cap = max_intermediate_rows();
743 let mut new_rows: Vec<Row> = Vec::new();
744 for outer_row in &rows {
745 let inner_seed: Vec<Row> = vec![outer_row.clone()];
747 let inner_result =
750 exec_left_outer_inner(view, &vars, inner_seed, inner, params)?;
751 if inner_result.is_empty() {
752 let mut null_row = outer_row.clone();
754 for v in optional_vars {
755 if let Some(slot) = vars.slot(v) {
756 null_row[slot] = None;
757 }
758 }
759 if new_rows.len() >= cap {
760 return Err(row_cap_err(cap));
761 }
762 new_rows.push(null_row);
763 } else {
764 for r in inner_result {
765 if new_rows.len() >= cap {
766 return Err(row_cap_err(cap));
767 }
768 new_rows.push(r);
769 }
770 }
771 }
772 rows = new_rows;
773 }
774 }
775 }
776
777 Ok(match projected {
778 Some(table) => finish(table),
779 None => {
780 if let Some(cols) = pipeline_group_columns {
783 let mut rs = ResultSet::new(cols.clone());
784 for row in rows {
785 let vals: Vec<Option<Value>> = cols
786 .iter()
787 .map(|col| {
788 vars.slot(col)
789 .and_then(|s| row.get(s))
790 .and_then(|c| c.as_ref())
791 .and_then(|c| match c {
792 Cell::Scalar(v) => Some(v.clone()),
793 Cell::Node(id) => {
794 view.ids.key_of(*id).map(|k| Value::Str(k.to_owned()))
795 }
796 Cell::Path(h) => Some(Value::Int(*h as i64)),
797 Cell::Rel(_) => None,
798 })
799 })
800 .collect();
801 rs.push_row(vals);
802 }
803 rs
804 } else {
805 ResultSet::new(vec![])
806 }
807 }
808 })
809}
810
811fn exec_left_outer_inner(
820 view: &GraphView,
821 vars: &VarTable,
822 mut rows: Vec<Row>,
823 inner: &[PlanOp],
824 params: &Params,
825) -> Result<Vec<Row>, String> {
826 for op in inner {
827 match op {
828 PlanOp::ScanLabel { var, label } => {
829 rows = scan_label(view, vars, &rows, var, label.as_deref())?;
830 }
831 PlanOp::ScanKey { var, key, label } => {
832 rows = scan_key(view, vars, &rows, var, key, label.as_deref(), params)?;
833 }
834 PlanOp::LookupProps { var, props } => {
835 rows = retain_node(view, vars, &rows, var, None, props, params)?;
836 }
837 PlanOp::JoinBound { var, label, props } => {
838 rows = retain_node(view, vars, &rows, var, label.as_deref(), props, params)?;
839 }
840 PlanOp::Expand { .. } => {
841 rows = exec_expand(view, vars, &rows, op, params)?;
842 }
843 PlanOp::Filter { expr } => {
844 rows = exec_filter(view, vars, &rows, expr, params)?;
845 }
846 other => {
847 return Err(format!(
848 "unsupported op inside OPTIONAL MATCH inner plan: {other:?}"
849 ));
850 }
851 }
852 }
853 Ok(rows)
854}
855
856fn finish(table: Projected) -> ResultSet {
857 let mut rs = ResultSet::new(table.columns);
858 for row in table.rows {
859 rs.push_row(row);
860 }
861 rs
862}
863
864fn check_params(plan: &[PlanOp], params: &Params) -> Result<(), String> {
865 let mut names = Vec::new();
866 let mut seen = BTreeSet::new();
867 collect_params_from_ops(plan, &mut names, &mut seen)?;
868 for name in names {
869 if !params.0.contains_key(&name) {
870 return Err(format!("missing parameter `{name}`"));
871 }
872 }
873 Ok(())
874}
875
876fn collect_params_from_ops(
877 plan: &[PlanOp],
878 names: &mut Vec<String>,
879 seen: &mut BTreeSet<String>,
880) -> Result<(), String> {
881 for op in plan {
882 match op {
883 PlanOp::ScanKey { key, .. } => collect_operand(key, names, seen),
884 PlanOp::LookupProps { props, .. }
885 | PlanOp::JoinBound { props, .. }
886 | PlanOp::Expand {
887 to_props: props, ..
888 } => {
889 for (_, operand) in props {
890 collect_operand(operand, names, seen);
891 }
892 }
893 PlanOp::Filter { expr } => collect_expr(expr, names, seen, 0)?,
894 PlanOp::LeftOuterApply { inner, .. } => {
895 collect_params_from_ops(inner, names, seen)?;
896 }
897 PlanOp::Project { items } => {
900 for item in items {
901 collect_ret_item_params(item, names, seen);
902 }
903 }
904 PlanOp::With {
905 items, where_expr, ..
906 } => {
907 for item in items {
908 collect_ret_item_params(item, names, seen);
909 }
910 if let Some(expr) = where_expr {
911 let _ = collect_expr(expr, names, seen, 0);
912 }
913 }
914 PlanOp::GroupAggregate { keys, aggs } => {
915 for (_, item) in keys {
916 collect_ret_item_params(item, names, seen);
917 }
918 for (_, arg, _) in aggs {
919 match arg {
920 AggArg::Star => {}
921 AggArg::Var(_) => {}
922 AggArg::Prop { .. } => {}
923 AggArg::Distinct(_) => {}
924 }
925 }
926 }
927 PlanOp::Skip(LimitSkip::Param(n)) | PlanOp::Limit(LimitSkip::Param(n))
930 if seen.insert(n.clone()) =>
931 {
932 names.push(n.clone());
933 }
934 _ => {}
935 }
936 }
937 Ok(())
938}
939
940fn collect_ret_item_params(item: &RetItem, names: &mut Vec<String>, seen: &mut BTreeSet<String>) {
942 match &item.value {
943 RetVal::FuncCall { args, .. } => {
944 for arg in args {
945 collect_operand(arg, names, seen);
946 }
947 }
948 RetVal::ScalarExpr(op) => {
949 collect_operand(op, names, seen);
950 }
951 RetVal::Prop { .. } | RetVal::Var(_) | RetVal::Agg { .. } => {}
952 }
953}
954
955fn collect_operand(op: &Operand, names: &mut Vec<String>, seen: &mut BTreeSet<String>) {
956 match op {
957 Operand::Param(n) => {
958 if seen.insert(n.clone()) {
959 names.push(n.clone());
960 }
961 }
962 Operand::FuncCall { args, .. } => {
963 for arg in args {
964 collect_operand(arg, names, seen);
965 }
966 }
967 Operand::BinArith { left, right, .. } => {
968 collect_operand(left, names, seen);
969 collect_operand(right, names, seen);
970 }
971 _ => {}
972 }
973}
974
975fn collect_expr(
976 expr: &Expr,
977 names: &mut Vec<String>,
978 seen: &mut BTreeSet<String>,
979 depth: u32,
980) -> Result<(), String> {
981 if depth > 256 {
982 return Err("expression nesting too deep".into());
983 }
984 match expr {
985 Expr::And(lhs, rhs) | Expr::Or(lhs, rhs) => {
986 collect_expr(lhs, names, seen, depth + 1)?;
987 collect_expr(rhs, names, seen, depth + 1)
988 }
989 Expr::Not(inner) => collect_expr(inner, names, seen, depth + 1),
990 Expr::Cmp { lhs, rhs, .. } => {
991 collect_operand(lhs, names, seen);
992 collect_operand(rhs, names, seen);
993 Ok(())
994 }
995 Expr::Truthy(op) => {
996 collect_operand(op, names, seen);
997 Ok(())
998 }
999 Expr::IsNull(op) | Expr::IsNotNull(op) => {
1000 collect_operand(op, names, seen);
1001 Ok(())
1002 }
1003 Expr::In { expr, list } => {
1004 collect_operand(expr, names, seen);
1005 for item in list {
1006 collect_operand(item, names, seen);
1007 }
1008 Ok(())
1009 }
1010 }
1011}
1012
1013fn collect_vars(plan: &[PlanOp]) -> VarTable {
1014 let mut vars = VarTable { names: Vec::new() };
1015 for op in plan {
1016 match op {
1017 PlanOp::ScanLabel { var, .. } => {
1018 vars.intern(var);
1019 }
1020 PlanOp::ScanKey { var, key, .. } => {
1021 vars.intern(var);
1022 intern_operand(&mut vars, key);
1023 }
1024 PlanOp::IndexScan { var, value, .. } => {
1025 vars.intern(var);
1026 intern_operand(&mut vars, value);
1027 }
1028 PlanOp::IndexIntersect {
1029 var, equalities, ..
1030 } => {
1031 vars.intern(var);
1032 for (_, operand) in equalities {
1033 intern_operand(&mut vars, operand);
1034 }
1035 }
1036 PlanOp::LookupProps { var, props } | PlanOp::JoinBound { var, props, .. } => {
1037 vars.intern(var);
1038 for (_, operand) in props {
1039 intern_operand(&mut vars, operand);
1040 }
1041 }
1042 PlanOp::Expand {
1043 from,
1044 rel_var,
1045 to,
1046 to_props,
1047 ..
1048 } => {
1049 vars.intern(from);
1050 vars.intern(to);
1051 if let Some(r) = rel_var {
1052 vars.intern(r);
1053 }
1054 for (_, operand) in to_props {
1055 intern_operand(&mut vars, operand);
1056 }
1057 }
1058 PlanOp::VarExpand {
1059 from, rel_var, to, ..
1060 } => {
1061 vars.intern(from);
1062 vars.intern(to);
1063 if let Some(r) = rel_var {
1064 vars.intern(r);
1065 }
1066 }
1067 PlanOp::ShortestPath {
1068 from, rel_var, to, ..
1069 } => {
1070 vars.intern(from);
1071 vars.intern(to);
1072 if let Some(r) = rel_var {
1073 vars.intern(r);
1074 }
1075 }
1076 PlanOp::Filter { expr } => intern_expr(&mut vars, expr),
1077 PlanOp::Project { items } => {
1078 for item in items {
1079 match &item.value {
1080 RetVal::Var(name) | RetVal::Prop { var: name, .. } => {
1081 vars.intern(name);
1082 }
1083 RetVal::Agg { .. } => {} RetVal::FuncCall { args, .. } => {
1085 for arg in args {
1086 intern_operand(&mut vars, arg);
1087 }
1088 }
1089 RetVal::ScalarExpr(op) => {
1090 intern_operand(&mut vars, op);
1091 }
1092 }
1093 }
1094 }
1095 PlanOp::Aggregate { arg, .. } => intern_agg_arg(&mut vars, arg),
1096 PlanOp::GroupAggregate { keys, aggs } => {
1097 for (col, item) in keys {
1099 match &item.value {
1100 RetVal::Var(name) | RetVal::Prop { var: name, .. } => {
1101 vars.intern(name);
1102 }
1103 RetVal::Agg { .. } => {}
1104 RetVal::FuncCall { args, .. } => {
1105 for arg in args {
1106 intern_operand(&mut vars, arg);
1107 }
1108 }
1109 RetVal::ScalarExpr(op) => {
1110 intern_operand(&mut vars, op);
1111 }
1112 }
1113 vars.intern(col);
1115 }
1116 for (_, arg, col) in aggs {
1117 intern_agg_arg(&mut vars, arg);
1118 vars.intern(col);
1120 }
1121 }
1122 PlanOp::With {
1123 items,
1124 where_expr,
1125 order_by,
1126 ..
1127 } => {
1128 for item in items {
1129 match &item.value {
1130 RetVal::Var(name) | RetVal::Prop { var: name, .. } => {
1131 vars.intern(name);
1132 }
1133 RetVal::Agg { .. } => {}
1134 RetVal::FuncCall { args, .. } => {
1135 for arg in args {
1136 intern_operand(&mut vars, arg);
1137 }
1138 }
1139 RetVal::ScalarExpr(op) => {
1140 intern_operand(&mut vars, op);
1141 }
1142 }
1143 if let Some(alias) = &item.alias {
1148 vars.intern(alias);
1149 } else if let Some(col) = ret_val_label(&item.value) {
1150 vars.intern(&col);
1151 }
1152 }
1153 if let Some(expr) = where_expr {
1154 intern_expr(&mut vars, expr);
1155 }
1156 for oi in order_by {
1157 match &oi.target {
1158 OrderTarget::Alias(name) | OrderTarget::Var(name) => {
1159 vars.intern(name);
1160 }
1161 OrderTarget::Prop { var, .. } => {
1162 vars.intern(var);
1163 }
1164 }
1165 }
1166 }
1167 PlanOp::Unwind { expr, alias } => {
1168 vars.intern(alias);
1169 match expr {
1170 UnwindExpr::Prop { var, .. } => {
1171 vars.intern(var);
1172 }
1173 UnwindExpr::Var(name) => {
1174 vars.intern(name);
1175 }
1176 UnwindExpr::Lit(_) => {}
1177 }
1178 }
1179 PlanOp::LeftOuterApply {
1180 inner,
1181 optional_vars,
1182 } => {
1183 for op in inner {
1185 match op {
1189 PlanOp::ScanLabel { var, .. } => {
1190 vars.intern(var);
1191 }
1192 PlanOp::ScanKey { var, key, .. } => {
1193 vars.intern(var);
1194 intern_operand(&mut vars, key);
1195 }
1196 PlanOp::IndexScan { var, value, .. } => {
1197 vars.intern(var);
1198 intern_operand(&mut vars, value);
1199 }
1200 PlanOp::IndexIntersect {
1201 var, equalities, ..
1202 } => {
1203 vars.intern(var);
1204 for (_, operand) in equalities {
1205 intern_operand(&mut vars, operand);
1206 }
1207 }
1208 PlanOp::Expand {
1209 from, rel_var, to, ..
1210 } => {
1211 vars.intern(from);
1212 vars.intern(to);
1213 if let Some(r) = rel_var {
1214 vars.intern(r);
1215 }
1216 }
1217 PlanOp::JoinBound { var, .. } | PlanOp::LookupProps { var, .. } => {
1218 vars.intern(var);
1219 }
1220 PlanOp::Filter { expr } => intern_expr(&mut vars, expr),
1221 _ => {}
1222 }
1223 }
1224 for v in optional_vars {
1225 vars.intern(v);
1226 }
1227 }
1228 _ => {}
1229 }
1230 }
1231 vars
1232}
1233
1234fn intern_operand(vars: &mut VarTable, operand: &Operand) {
1235 match operand {
1236 Operand::Prop { var, .. } | Operand::Var(var) => {
1237 vars.intern(var);
1238 }
1239 Operand::Lit(_) | Operand::Param(_) => {}
1240 Operand::BinArith { left, right, .. } => {
1241 intern_operand(vars, left);
1242 intern_operand(vars, right);
1243 }
1244 Operand::FuncCall { args, .. } => {
1245 for arg in args {
1246 intern_operand(vars, arg);
1247 }
1248 }
1249 Operand::Case { branches, default } => {
1250 for (cond, value) in branches {
1251 intern_expr(vars, cond);
1252 intern_operand(vars, value);
1253 }
1254 if let Some(d) = default {
1255 intern_operand(vars, d);
1256 }
1257 }
1258 Operand::Index { base, index } => {
1259 intern_operand(vars, base);
1260 intern_operand(vars, index);
1261 }
1262 }
1263}
1264
1265fn intern_agg_arg(vars: &mut VarTable, arg: &AggArg) {
1267 match arg {
1268 AggArg::Star => {}
1269 AggArg::Var(v) => {
1270 vars.intern(v);
1271 }
1272 AggArg::Prop { var, .. } => {
1273 vars.intern(var);
1274 }
1275 AggArg::Distinct(inner) => intern_agg_arg(vars, inner),
1276 }
1277}
1278
1279fn agg_arg_label(arg: &AggArg) -> String {
1281 match arg {
1282 AggArg::Star => "*".to_string(),
1283 AggArg::Var(v) => v.clone(),
1284 AggArg::Prop { var, field } => format!("{var}.{field}"),
1285 AggArg::Distinct(inner) => format!("DISTINCT {}", agg_arg_label(inner)),
1286 }
1287}
1288
1289fn intern_expr(vars: &mut VarTable, expr: &Expr) {
1290 match expr {
1291 Expr::And(lhs, rhs) | Expr::Or(lhs, rhs) => {
1292 intern_expr(vars, lhs);
1293 intern_expr(vars, rhs);
1294 }
1295 Expr::Not(inner) => intern_expr(vars, inner),
1296 Expr::Cmp { lhs, rhs, .. } => {
1297 intern_operand(vars, lhs);
1298 intern_operand(vars, rhs);
1299 }
1300 Expr::Truthy(op) => intern_operand(vars, op),
1301 Expr::IsNull(op) | Expr::IsNotNull(op) => intern_operand(vars, op),
1302 Expr::In { expr, list } => {
1303 intern_operand(vars, expr);
1304 for item in list {
1305 intern_operand(vars, item);
1306 }
1307 }
1308 }
1309}
1310
1311fn scan_ids(view: &GraphView, label: Option<&str>) -> Vec<u32> {
1312 let ids = match label {
1313 Some(label) => view.nodes_with_label(label),
1314 None => (0..view.ids.len() as u32)
1316 .filter(|&id| view.label_of(id).is_some())
1317 .collect(),
1318 };
1319 if view.mask.is_some() {
1320 ids.into_iter().filter(|&id| view.visible(id)).collect()
1321 } else {
1322 ids
1323 }
1324}
1325
1326fn resolve_scan_key_id(
1329 view: &GraphView,
1330 vars: &VarTable,
1331 row: &Row,
1332 key: &Operand,
1333 label: Option<&str>,
1334 params: &Params,
1335) -> Result<Option<u32>, String> {
1336 #[cfg(test)]
1337 SCAN_KEY_FIRES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1338
1339 let Some(val) = resolve_operand(view, vars, row, key, params)? else {
1340 return Ok(None);
1341 };
1342 let Value::Str(s) = val else {
1343 return Ok(None);
1344 };
1345 let Some(id) = view.node_id(&s) else {
1346 return Ok(None);
1347 };
1348 if !view.visible(id) {
1349 return Ok(None);
1350 }
1351 if let Some(want) = label {
1352 match view.label_of(id) {
1353 Some(got) if got == want => {}
1354 _ => return Ok(None),
1355 }
1356 }
1357 Ok(Some(id))
1358}
1359
1360fn scan_key(
1361 view: &GraphView,
1362 vars: &VarTable,
1363 rows: &[Row],
1364 var: &str,
1365 key: &Operand,
1366 label: Option<&str>,
1367 params: &Params,
1368) -> Result<Vec<Row>, String> {
1369 let slot = vars
1370 .slot(var)
1371 .ok_or_else(|| format!("unbound variable `{var}`"))?;
1372 let cap = max_intermediate_rows();
1373 let mut out = Vec::new();
1374 for row in rows {
1375 let Some(id) = resolve_scan_key_id(view, vars, row, key, label, params)? else {
1376 continue;
1377 };
1378 if out.len() >= cap {
1379 return Err(row_cap_err(cap));
1380 }
1381 let mut next = row.clone();
1382 next[slot] = Some(Cell::Node(id));
1383 out.push(next);
1384 }
1385 Ok(out)
1386}
1387
1388fn scan_label(
1389 view: &GraphView,
1390 vars: &VarTable,
1391 rows: &[Row],
1392 var: &str,
1393 label: Option<&str>,
1394) -> Result<Vec<Row>, String> {
1395 let ids = scan_ids(view, label);
1396 let slot = vars
1397 .slot(var)
1398 .ok_or_else(|| format!("unbound variable `{var}`"))?;
1399 let cap = max_intermediate_rows();
1400 let mut out = Vec::with_capacity(rows.len().saturating_mul(ids.len()).min(cap));
1401 for row in rows {
1402 for &id in &ids {
1403 if out.len() >= cap {
1404 return Err(row_cap_err(cap));
1405 }
1406 let mut next = row.clone();
1407 next[slot] = Some(Cell::Node(id));
1408 out.push(next);
1409 }
1410 }
1411 Ok(out)
1412}
1413
1414#[allow(clippy::too_many_arguments)]
1422fn index_scan_ids(
1423 view: &GraphView,
1424 vars: &VarTable,
1425 row: &Row,
1426 label: Option<&str>,
1427 field: &str,
1428 value: &Operand,
1429 params: &Params,
1430) -> Result<Vec<u32>, String> {
1431 if is_identity_eq_field(field) {
1432 return identity_eq_ids(view, vars, row, label, field, value, params);
1433 }
1434 let resolved = resolve_operand(view, vars, row, value, params)?;
1435 if let (Some(label_str), Some(val)) = (label, resolved.as_ref()) {
1436 if let Some(ids) = view.nodes_with_prop(label_str, field, val) {
1437 #[cfg(test)]
1438 INDEX_SCAN_FIRES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1439 return Ok(ids);
1440 }
1441 }
1442 let props = [(field.to_string(), value.clone())];
1444 let mut out = Vec::new();
1445 for id in scan_ids(view, label) {
1446 if node_matches(view, vars, row, id, None, &props, params)? {
1447 out.push(id);
1448 }
1449 }
1450 Ok(out)
1451}
1452
1453#[allow(clippy::too_many_arguments)]
1459fn identity_eq_ids(
1460 view: &GraphView,
1461 vars: &VarTable,
1462 row: &Row,
1463 label: Option<&str>,
1464 field: &str,
1465 value: &Operand,
1466 params: &Params,
1467) -> Result<Vec<u32>, String> {
1468 let resolved = resolve_operand(view, vars, row, value, params)?;
1469 let mut out = Vec::new();
1470 let mut seen = HashSet::new();
1471 if let Some(id) = resolve_scan_key_id(view, vars, row, value, label, params)? {
1472 let props = [(field.to_string(), value.clone())];
1473 if node_matches(view, vars, row, id, None, &props, params)? {
1474 seen.insert(id);
1475 out.push(id);
1476 }
1477 }
1478 if let Some(val) = resolved.as_ref() {
1479 for id in stored_identity_hits(view, label, field, val) {
1480 if seen.insert(id) {
1481 out.push(id);
1482 }
1483 }
1484 }
1485 Ok(out)
1486}
1487
1488fn stored_identity_hits(
1490 view: &GraphView,
1491 label: Option<&str>,
1492 field: &str,
1493 val: &Value,
1494) -> Vec<u32> {
1495 if let Some(label_str) = label {
1496 if let Some(ids) = view.nodes_with_prop(label_str, field, val) {
1497 return ids;
1498 }
1499 }
1500 let mut out = Vec::new();
1501 for id in scan_ids(view, label) {
1502 if let Some(got) = view.prop(id, field).map(|vr| vr.into_value()) {
1503 if values_equal(&got, val) {
1504 out.push(id);
1505 }
1506 }
1507 }
1508 out
1509}
1510
1511#[allow(clippy::too_many_arguments)]
1514fn scan_index(
1515 view: &GraphView,
1516 vars: &VarTable,
1517 rows: &[Row],
1518 var: &str,
1519 label: Option<&str>,
1520 field: &str,
1521 value: &Operand,
1522 params: &Params,
1523) -> Result<Vec<Row>, String> {
1524 let Some(first) = rows.first() else {
1525 return Ok(Vec::new());
1526 };
1527 let ids = index_scan_ids(view, vars, first, label, field, value, params)?;
1528 let slot = vars
1529 .slot(var)
1530 .ok_or_else(|| format!("unbound variable `{var}`"))?;
1531 let cap = max_intermediate_rows();
1532 let mut out = Vec::new();
1533 for row in rows {
1534 for &id in &ids {
1535 if out.len() >= cap {
1536 return Err(row_cap_err(cap));
1537 }
1538 let mut next = row.clone();
1539 next[slot] = Some(Cell::Node(id));
1540 out.push(next);
1541 }
1542 }
1543 Ok(out)
1544}
1545
1546#[allow(clippy::too_many_arguments)]
1555fn index_intersect_ids(
1556 view: &GraphView,
1557 vars: &VarTable,
1558 row: &Row,
1559 label: Option<&str>,
1560 equalities: &[(String, Operand)],
1561 params: &Params,
1562) -> Result<Vec<u32>, String> {
1563 let mut resolved: Vec<(String, Option<Value>)> = Vec::with_capacity(equalities.len());
1565 for (field, operand) in equalities {
1566 let val = resolve_operand(view, vars, row, operand, params)?;
1567 resolved.push((field.clone(), val));
1568 }
1569
1570 let mut indexed_lists: Vec<Vec<u32>> = Vec::new();
1572 let mut unindexed_props: Vec<(String, Operand)> = Vec::new();
1573
1574 for ((field, val_opt), (_, operand)) in resolved.iter().zip(equalities.iter()) {
1575 if let (Some(label_str), Some(val)) = (label, val_opt.as_ref()) {
1576 if let Some(ids) = view.nodes_with_prop(label_str, field, val) {
1577 indexed_lists.push(ids);
1578 continue;
1579 }
1580 }
1581 unindexed_props.push((field.clone(), operand.clone()));
1582 }
1583
1584 if indexed_lists.is_empty() {
1585 let mut out = Vec::new();
1587 for id in scan_ids(view, label) {
1588 if node_matches(view, vars, row, id, None, equalities, params)? {
1589 out.push(id);
1590 }
1591 }
1592 return Ok(out);
1593 }
1594
1595 #[cfg(test)]
1596 INDEX_INTERSECT_FIRES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1597
1598 indexed_lists.sort_unstable_by_key(|v| v.len());
1600 let mut result = indexed_lists.remove(0);
1601 for other in indexed_lists {
1602 let mut merged = Vec::new();
1603 let (mut i, mut j) = (0, 0);
1604 while i < result.len() && j < other.len() {
1605 match result[i].cmp(&other[j]) {
1606 std::cmp::Ordering::Equal => {
1607 merged.push(result[i]);
1608 i += 1;
1609 j += 1;
1610 }
1611 std::cmp::Ordering::Less => i += 1,
1612 std::cmp::Ordering::Greater => j += 1,
1613 }
1614 }
1615 result = merged;
1616 }
1617
1618 if !unindexed_props.is_empty() {
1622 let mut filtered = Vec::with_capacity(result.len());
1623 for id in result {
1624 if node_matches(view, vars, row, id, None, &unindexed_props, params)? {
1625 filtered.push(id);
1626 }
1627 }
1628 result = filtered;
1629 }
1630
1631 Ok(result)
1632}
1633
1634#[allow(clippy::too_many_arguments)]
1637fn scan_intersect(
1638 view: &GraphView,
1639 vars: &VarTable,
1640 rows: &[Row],
1641 var: &str,
1642 label: Option<&str>,
1643 equalities: &[(String, Operand)],
1644 params: &Params,
1645) -> Result<Vec<Row>, String> {
1646 let Some(first) = rows.first() else {
1647 return Ok(Vec::new());
1648 };
1649 let ids = index_intersect_ids(view, vars, first, label, equalities, params)?;
1650 let slot = vars
1651 .slot(var)
1652 .ok_or_else(|| format!("unbound variable `{var}`"))?;
1653 let cap = max_intermediate_rows();
1654 let mut out = Vec::new();
1655 for row in rows {
1656 for &id in &ids {
1657 if out.len() >= cap {
1658 return Err(row_cap_err(cap));
1659 }
1660 let mut next = row.clone();
1661 next[slot] = Some(Cell::Node(id));
1662 out.push(next);
1663 }
1664 }
1665 Ok(out)
1666}
1667
1668fn require_cell<'a>(row: &'a Row, vars: &VarTable, var: &str) -> Result<&'a Cell, String> {
1669 let slot = vars
1670 .slot(var)
1671 .ok_or_else(|| format!("unbound variable `{var}`"))?;
1672 row.get(slot)
1673 .and_then(|c| c.as_ref())
1674 .ok_or_else(|| format!("unbound variable `{var}`"))
1675}
1676
1677fn require_node(row: &Row, vars: &VarTable, var: &str) -> Result<u32, String> {
1678 match require_cell(row, vars, var)? {
1679 Cell::Node(id) => Ok(*id),
1680 Cell::Rel(_) => Err(format!("variable `{var}` is not a node")),
1681 Cell::Path(_) => Err(format!("variable `{var}` is a path, not a node")),
1682 Cell::Scalar(_) => Err(format!("variable `{var}` is a scalar value, not a node")),
1683 }
1684}
1685
1686const SCALAR_FUNCS: &[&str] = &[
1688 "toLower",
1689 "toUpper",
1690 "size",
1691 "coalesce",
1692 "type",
1693 "abs",
1694 "round",
1695 "textMatches",
1696 "contains",
1697 "startsWith",
1698 "endsWith",
1699 "toInteger",
1700 "toFloat",
1701 "toString",
1702 "decay",
1703 "key",
1704 "id",
1705 "labels",
1706];
1707
1708fn eval_string_predicate(
1711 name: &str,
1712 args: &[Operand],
1713 view: &GraphView,
1714 vars: &VarTable,
1715 row: &Row,
1716 params: &Params,
1717 f: impl Fn(&str, &str) -> bool,
1718) -> Result<Option<Value>, String> {
1719 if args.len() != 2 {
1720 return Err(format!(
1721 "{name}() requires exactly 2 arguments, got {}",
1722 args.len()
1723 ));
1724 }
1725 let a = resolve_operand(view, vars, row, &args[0], params)?;
1726 let b = resolve_operand(view, vars, row, &args[1], params)?;
1727 match (a, b) {
1728 (Some(Value::Str(s)), Some(Value::Str(sub))) => Ok(Some(Value::Bool(f(&s, &sub)))),
1729 (None, _) | (_, None) => Ok(None),
1730 _ => Ok(None),
1731 }
1732}
1733
1734fn eval_func(
1738 name: &str,
1739 args: &[Operand],
1740 view: &GraphView,
1741 vars: &VarTable,
1742 row: &Row,
1743 params: &Params,
1744) -> Result<Option<Value>, String> {
1745 let norm = name.to_ascii_lowercase();
1746 match norm.as_str() {
1747 "tolower" => {
1748 if args.len() != 1 {
1749 return Err(format!(
1750 "toLower() requires exactly 1 argument, got {}",
1751 args.len()
1752 ));
1753 }
1754 let v = resolve_operand(view, vars, row, &args[0], params)?;
1755 Ok(v.map(|val| match val {
1756 Value::Str(s) => Value::Str(s.to_ascii_lowercase()),
1757 other => other, }))
1759 }
1760 "toupper" => {
1761 if args.len() != 1 {
1762 return Err(format!(
1763 "toUpper() requires exactly 1 argument, got {}",
1764 args.len()
1765 ));
1766 }
1767 let v = resolve_operand(view, vars, row, &args[0], params)?;
1768 Ok(v.map(|val| match val {
1769 Value::Str(s) => Value::Str(s.to_ascii_uppercase()),
1770 other => other,
1771 }))
1772 }
1773 "size" => {
1774 if args.len() != 1 {
1775 return Err(format!(
1776 "size() requires exactly 1 argument, got {}",
1777 args.len()
1778 ));
1779 }
1780 let v = resolve_operand(view, vars, row, &args[0], params)?;
1781 match v {
1782 None => Ok(None), Some(Value::Str(s)) => Ok(Some(Value::Int(s.len() as i64))),
1784 Some(Value::List(items)) => Ok(Some(Value::Int(items.len() as i64))),
1785 Some(_) => Ok(None), }
1787 }
1788 "coalesce" => {
1789 for arg in args {
1791 if let Some(v) = resolve_operand(view, vars, row, arg, params)? {
1792 return Ok(Some(v));
1793 }
1794 }
1795 Ok(None)
1796 }
1797 "type" => {
1798 if args.len() != 1 {
1799 return Err(format!(
1800 "type() requires exactly 1 argument, got {}",
1801 args.len()
1802 ));
1803 }
1804 let Operand::Var(var_name) = &args[0] else {
1806 return Err(
1807 "type() argument must be a relationship variable (e.g. type(r))".to_string(),
1808 );
1809 };
1810 let slot = vars
1811 .slot(var_name)
1812 .ok_or_else(|| format!("unbound variable `{var_name}` in type()"))?;
1813 match row.get(slot).and_then(|c| c.as_ref()) {
1814 Some(Cell::Rel(e)) => {
1815 let etype = view.syms.resolve(e.etype).unwrap_or("").to_owned();
1817 Ok(Some(Value::Str(etype)))
1818 }
1819 Some(Cell::Node(_)) => Err(format!(
1820 "type() argument `{var_name}` is a node, not a relationship"
1821 )),
1822 Some(Cell::Scalar(_) | Cell::Path(_)) => Err(format!(
1823 "type() argument `{var_name}` is not a relationship"
1824 )),
1825 None => Ok(None), }
1827 }
1828 "key" | "id" => {
1829 let fname = if norm == "id" { "id" } else { "key" };
1830 if args.len() != 1 {
1831 return Err(format!(
1832 "{fname}() requires exactly 1 argument, got {}",
1833 args.len()
1834 ));
1835 }
1836 let Operand::Var(var_name) = &args[0] else {
1837 return Err(format!(
1838 "{fname}() argument must be a node variable (e.g. {fname}(n))"
1839 ));
1840 };
1841 let slot = vars
1842 .slot(var_name)
1843 .ok_or_else(|| format!("unbound variable `{var_name}` in {fname}()"))?;
1844 match row.get(slot).and_then(|c| c.as_ref()) {
1845 Some(Cell::Node(id)) => Ok(Some(Value::Str(view.key_of(*id).to_owned()))),
1846 Some(Cell::Rel(_)) => Err(format!(
1847 "{fname}() argument `{var_name}` is a relationship, not a node"
1848 )),
1849 Some(Cell::Scalar(_) | Cell::Path(_)) => {
1850 Err(format!("{fname}() argument `{var_name}` is not a node"))
1851 }
1852 None => Ok(None), }
1854 }
1855 "labels" => {
1856 if args.len() != 1 {
1857 return Err(format!(
1858 "labels() requires exactly 1 argument, got {}",
1859 args.len()
1860 ));
1861 }
1862 let Operand::Var(var_name) = &args[0] else {
1866 return Err(
1867 "labels() argument must be a node variable (e.g. labels(n))".to_string()
1868 );
1869 };
1870 let slot = vars
1871 .slot(var_name)
1872 .ok_or_else(|| format!("unbound variable `{var_name}` in labels()"))?;
1873 match row.get(slot).and_then(|c| c.as_ref()) {
1874 Some(Cell::Node(id)) => Ok(Some(Value::List(
1875 view.label_of(*id)
1876 .map(|l| vec![Value::Str(l.to_owned())])
1877 .unwrap_or_default(),
1878 ))),
1879 Some(Cell::Rel(_)) => Err(format!(
1880 "labels() argument `{var_name}` is a relationship, not a node"
1881 )),
1882 Some(Cell::Scalar(_) | Cell::Path(_)) => {
1883 Err(format!("labels() argument `{var_name}` is not a node"))
1884 }
1885 None => Ok(None), }
1887 }
1888 "abs" => {
1889 if args.len() != 1 {
1890 return Err(format!(
1891 "abs() requires exactly 1 argument, got {}",
1892 args.len()
1893 ));
1894 }
1895 let v = resolve_operand(view, vars, row, &args[0], params)?;
1896 match v {
1897 None => Ok(None),
1898 Some(Value::Int(n)) => Ok(Some(Value::Int(n.abs()))),
1899 Some(Value::Float(f)) => Ok(Some(Value::Float(f.abs()))),
1900 Some(_) => Ok(None), }
1902 }
1903 "round" => {
1904 if args.len() != 1 {
1905 return Err(format!(
1906 "round() requires exactly 1 argument, got {}",
1907 args.len()
1908 ));
1909 }
1910 let v = resolve_operand(view, vars, row, &args[0], params)?;
1911 match v {
1912 None => Ok(None),
1913 Some(Value::Int(n)) => Ok(Some(Value::Int(n))), Some(Value::Float(f)) => Ok(Some(Value::Float(f.round()))),
1915 Some(_) => Ok(None), }
1917 }
1918 "textmatches" => {
1919 if args.len() != 2 {
1931 return Err(format!(
1932 "textMatches() requires exactly 2 arguments (field_value, query), got {}",
1933 args.len()
1934 ));
1935 }
1936 let field_val = resolve_operand(view, vars, row, &args[0], params)?;
1937 let query_val = resolve_operand(view, vars, row, &args[1], params)?;
1938 match (field_val, query_val) {
1939 (None, _) | (_, None) => Ok(None),
1940 (Some(Value::Str(s)), Some(Value::Str(q))) => {
1941 Ok(Some(Value::Bool(core_storage::fulltext::eval_query_str(
1943 &s, &q,
1944 ))))
1945 }
1946 (Some(Value::List(items)), Some(Value::Str(q))) => Ok(Some(Value::Bool(
1947 core_storage::fulltext::eval_query_str_list(&items, &q),
1948 ))),
1949 _ => Ok(Some(Value::Bool(false))), }
1951 }
1952 "contains" => eval_string_predicate("contains", args, view, vars, row, params, |s, sub| {
1953 s.contains(sub)
1954 }),
1955 "startswith" => {
1956 eval_string_predicate("startsWith", args, view, vars, row, params, |s, p| {
1957 s.starts_with(p)
1958 })
1959 }
1960 "endswith" => eval_string_predicate("endsWith", args, view, vars, row, params, |s, p| {
1961 s.ends_with(p)
1962 }),
1963 "tointeger" => {
1964 if args.len() != 1 {
1965 return Err(format!(
1966 "toInteger() requires exactly 1 argument, got {}",
1967 args.len()
1968 ));
1969 }
1970 let v = resolve_operand(view, vars, row, &args[0], params)?;
1971 Ok(match v {
1972 None => None,
1973 Some(Value::Int(n)) => Some(Value::Int(n)),
1974 Some(Value::Float(f)) => Some(Value::Int(f.trunc() as i64)),
1975 Some(Value::Str(s)) => s
1978 .trim()
1979 .parse::<i64>()
1980 .ok()
1981 .or_else(|| s.trim().parse::<f64>().ok().map(|f| f.trunc() as i64))
1982 .map(Value::Int),
1983 Some(_) => None,
1984 })
1985 }
1986 "tofloat" => {
1987 if args.len() != 1 {
1988 return Err(format!(
1989 "toFloat() requires exactly 1 argument, got {}",
1990 args.len()
1991 ));
1992 }
1993 let v = resolve_operand(view, vars, row, &args[0], params)?;
1994 Ok(match v {
1995 None => None,
1996 Some(Value::Float(f)) => Some(Value::Float(f)),
1997 Some(Value::Int(n)) => Some(Value::Float(n as f64)),
1998 Some(Value::Str(s)) => s.trim().parse::<f64>().ok().map(Value::Float),
1999 Some(_) => None,
2000 })
2001 }
2002 "tostring" => {
2003 if args.len() != 1 {
2004 return Err(format!(
2005 "toString() requires exactly 1 argument, got {}",
2006 args.len()
2007 ));
2008 }
2009 let v = resolve_operand(view, vars, row, &args[0], params)?;
2010 Ok(match v {
2011 None => None,
2012 Some(Value::Str(s)) => Some(Value::Str(s)),
2013 Some(Value::Int(n)) => Some(Value::Str(n.to_string())),
2014 Some(Value::Float(f)) => Some(Value::Str(f.to_string())),
2015 Some(Value::Bool(b)) => Some(Value::Str(b.to_string())),
2016 Some(_) => None, })
2018 }
2019 "decay" => {
2020 if args.len() != 3 {
2025 return Err(format!(
2026 "decay() requires exactly 3 arguments, got {}",
2027 args.len()
2028 ));
2029 }
2030 let base = resolve_operand(view, vars, row, &args[0], params)?;
2031 let age = resolve_operand(view, vars, row, &args[1], params)?;
2032 let halflife = resolve_operand(view, vars, row, &args[2], params)?;
2033 match (base, age, halflife) {
2034 (None, _, _) | (_, None, _) | (_, _, None) => Ok(None),
2035 (Some(b), Some(a), Some(h)) => {
2036 let b = numeric_val(&b)
2037 .ok_or_else(|| "decay() requires numeric arguments".to_string())?;
2038 let a = numeric_val(&a)
2039 .ok_or_else(|| "decay() requires numeric arguments".to_string())?;
2040 let h = numeric_val(&h)
2041 .ok_or_else(|| "decay() requires numeric arguments".to_string())?;
2042 if h <= 0.0 {
2043 return Err("decay() requires halflife > 0".to_string());
2044 }
2045 Ok(Some(Value::Float(b * 0.5f64.powf(a / h))))
2046 }
2047 }
2048 }
2049 _ => Err(format!(
2050 "unknown function `{name}`; supported: {}",
2051 SCALAR_FUNCS.join(", ")
2052 )),
2053 }
2054}
2055
2056fn resolve_operand(
2057 view: &GraphView,
2058 vars: &VarTable,
2059 row: &Row,
2060 operand: &Operand,
2061 params: &Params,
2062) -> Result<Option<Value>, String> {
2063 match operand {
2064 Operand::Lit(v) => Ok(Some(v.clone())),
2065 Operand::Param(name) => match params.0.get(name) {
2066 Some(v) => Ok(Some(v.clone())),
2067 None => Err(format!("missing parameter `{name}`")),
2068 },
2069 Operand::Prop { var, field } => resolve_prop(view, vars, row, var, field),
2070 Operand::Var(name) => {
2071 match vars
2073 .slot(name)
2074 .and_then(|s| row.get(s))
2075 .and_then(|c| c.as_ref())
2076 {
2077 Some(Cell::Scalar(v)) => Ok(Some(v.clone())),
2078 Some(Cell::Node(id)) => match view.ids.key_of(*id) {
2079 Some(key) => Ok(Some(Value::Str(key.to_owned()))),
2080 None => Ok(None),
2081 },
2082 Some(Cell::Path(hops)) => Ok(Some(Value::Int(*hops as i64))),
2083 Some(Cell::Rel(_)) => Err(format!("variable `{name}` is a relationship")),
2084 None => Ok(None),
2085 }
2086 }
2087 Operand::FuncCall { name, args } => eval_func(name, args, view, vars, row, params),
2088 Operand::Index { base, index } => {
2089 let base_val = resolve_operand(view, vars, row, base, params)?;
2090 let idx_val = resolve_operand(view, vars, row, index, params)?;
2091 Ok(crate::value_ops::index_list(base_val, idx_val))
2092 }
2093 Operand::Case { branches, default } => {
2094 for (cond, value) in branches {
2095 if eval_expr(view, vars, row, cond, params, 0)? {
2096 return resolve_operand(view, vars, row, value, params);
2097 }
2098 }
2099 match default {
2100 Some(d) => resolve_operand(view, vars, row, d, params),
2101 None => Ok(None),
2102 }
2103 }
2104 Operand::BinArith { op, left, right } => {
2105 use super::ast::ArithOp;
2106 let lv = resolve_operand(view, vars, row, left, params)?;
2107 let rv = resolve_operand(view, vars, row, right, params)?;
2108 match (lv, rv) {
2109 (None, _) | (_, None) => Ok(None), (Some(Value::Int(a)), Some(Value::Int(b))) => {
2111 let result = match op {
2112 ArithOp::Sub => a.saturating_sub(b),
2113 ArithOp::Mul => a.saturating_mul(b),
2114 ArithOp::Add => a.saturating_add(b),
2115 ArithOp::Div => {
2116 if b == 0 {
2117 return Err("division by zero".into());
2118 }
2119 a.checked_div(b).unwrap_or(i64::MAX)
2120 }
2121 };
2122 Ok(Some(Value::Int(result)))
2123 }
2124 (Some(lv), Some(rv)) => {
2125 let a = match &lv {
2126 Value::Float(f) => *f,
2127 Value::Int(i) => *i as f64,
2128 _ => return Err(format!("arithmetic operand must be numeric, got {lv:?}")),
2129 };
2130 let b = match &rv {
2131 Value::Float(f) => *f,
2132 Value::Int(i) => *i as f64,
2133 _ => return Err(format!("arithmetic operand must be numeric, got {rv:?}")),
2134 };
2135 let result = match op {
2136 ArithOp::Sub => a - b,
2137 ArithOp::Mul => a * b,
2138 ArithOp::Add => a + b,
2139 ArithOp::Div => {
2140 if b == 0.0 {
2141 return Err("division by zero".into());
2142 }
2143 a / b
2144 }
2145 };
2146 Ok(Some(Value::Float(result)))
2147 }
2148 }
2149 }
2150 }
2151}
2152
2153fn is_identity_field(field: &str) -> bool {
2156 field == "key" || field == "id" || field == "label"
2157}
2158
2159fn is_identity_eq_field(field: &str) -> bool {
2162 field == "key" || field == "id"
2163}
2164
2165fn node_identity_prop(view: &GraphView, id: u32, field: &str) -> Option<Value> {
2172 match field {
2173 "key" | "id" => view.ids.key_of(id).map(|k| Value::Str(k.to_owned())),
2174 "label" => view.label_of(id).map(|l| Value::Str(l.to_owned())),
2175 _ => None,
2176 }
2177}
2178
2179fn resolve_prop(
2180 view: &GraphView,
2181 vars: &VarTable,
2182 row: &Row,
2183 var: &str,
2184 field: &str,
2185) -> Result<Option<Value>, String> {
2186 let slot = vars
2189 .slot(var)
2190 .ok_or_else(|| format!("unbound variable `{var}`"))?;
2191 let cell = match row.get(slot).and_then(|c| c.as_ref()) {
2192 Some(c) => c,
2193 None => return Ok(None),
2195 };
2196 match cell {
2197 Cell::Node(id) => Ok(view
2198 .prop(*id, field)
2199 .map(|vr| vr.into_value())
2200 .or_else(|| node_identity_prop(view, *id, field))),
2201 Cell::Rel(e) => Ok(view.edge_props.get(e.etype, e.src, e.dst, field)),
2202 Cell::Path(hops) => {
2204 if field == "length" {
2205 Ok(Some(Value::Int(*hops as i64)))
2206 } else {
2207 Ok(None)
2208 }
2209 }
2210 Cell::Scalar(_) => Ok(None),
2212 }
2213}
2214
2215fn node_matches(
2216 view: &GraphView,
2217 vars: &VarTable,
2218 row: &Row,
2219 id: u32,
2220 label: Option<&str>,
2221 props: &[(String, Operand)],
2222 params: &Params,
2223) -> Result<bool, String> {
2224 if let Some(want) = label {
2225 match view.label_of(id) {
2226 Some(got) if got == want => {}
2227 _ => return Ok(false),
2228 }
2229 }
2230 for (field, operand) in props {
2231 let Some(expected) = resolve_operand(view, vars, row, operand, params)? else {
2232 return Ok(false);
2233 };
2234 let got = view
2235 .prop(id, field)
2236 .map(|vr| vr.into_value())
2237 .or_else(|| node_identity_prop(view, id, field));
2238 match got {
2239 Some(got) if values_equal(&got, &expected) => {}
2240 _ => return Ok(false),
2241 }
2242 }
2243 Ok(true)
2244}
2245
2246fn retain_node(
2247 view: &GraphView,
2248 vars: &VarTable,
2249 rows: &[Row],
2250 var: &str,
2251 label: Option<&str>,
2252 props: &[(String, Operand)],
2253 params: &Params,
2254) -> Result<Vec<Row>, String> {
2255 let mut out = Vec::with_capacity(rows.len());
2256 for row in rows {
2257 let id = require_node(row, vars, var)?;
2258 if node_matches(view, vars, row, id, label, props, params)? {
2259 out.push(row.clone());
2260 }
2261 }
2262 Ok(out)
2263}
2264
2265fn map_dir(dir: RelDir) -> Dir {
2266 match dir {
2267 RelDir::Right => Dir::Out,
2268 RelDir::Left => Dir::In,
2269 RelDir::Undirected => Dir::Both,
2270 }
2271}
2272
2273fn neighbor(from: u32, e: &EdgeRef, dir: RelDir) -> u32 {
2274 match dir {
2275 RelDir::Right => e.dst,
2276 RelDir::Left => e.src,
2277 RelDir::Undirected => {
2278 if e.src == from {
2279 e.dst
2280 } else {
2281 e.src
2282 }
2283 }
2284 }
2285}
2286
2287fn row_has_edge(row: &Row, e: &EdgeRef) -> bool {
2288 row.iter()
2289 .any(|c| matches!(c, Some(Cell::Rel(existing)) if existing == e))
2290}
2291
2292fn resolve_etypes(view: &GraphView, etypes: &[String]) -> Option<Vec<u32>> {
2293 if etypes.is_empty() {
2294 None } else {
2296 Some(etypes.iter().filter_map(|n| view.syms.get(n)).collect())
2299 }
2300}
2301
2302fn exec_expand(
2303 view: &GraphView,
2304 vars: &VarTable,
2305 rows: &[Row],
2306 op: &PlanOp,
2307 params: &Params,
2308) -> Result<Vec<Row>, String> {
2309 let PlanOp::Expand {
2310 from,
2311 rel_var,
2312 etypes,
2313 dir,
2314 to,
2315 to_label,
2316 to_props,
2317 } = op
2318 else {
2319 return Err("internal: expected Expand".into());
2320 };
2321 let etypes = resolve_etypes(view, etypes);
2322 let exp_dir = map_dir(*dir);
2323 let to_slot = vars
2324 .slot(to)
2325 .ok_or_else(|| format!("unbound variable `{to}`"))?;
2326 let rel_slot = rel_var.as_ref().and_then(|rv| vars.slot(rv));
2327 let cap = max_intermediate_rows();
2328 let mut out = Vec::with_capacity(rows.len().saturating_mul(2).min(cap));
2329 for row in rows {
2330 let from_id = require_node(row, vars, from)?;
2331 let bound_to = match row.get(to_slot).and_then(|c| c.as_ref()) {
2332 Some(Cell::Node(id)) => Some(*id),
2333 Some(Cell::Rel(_) | Cell::Path(_) | Cell::Scalar(_)) => {
2334 return Err(format!("variable `{to}` is not a node"))
2335 }
2336 None => None,
2337 };
2338 for e in expand(view, from_id, etypes.as_deref(), exp_dir) {
2339 if row_has_edge(row, &e) {
2340 continue;
2341 }
2342 let nbr = neighbor(from_id, &e, *dir);
2343 if !view.visible(nbr) {
2344 continue;
2345 }
2346 if let Some(want) = bound_to {
2347 if nbr != want {
2348 continue;
2349 }
2350 }
2351 if !node_matches(view, vars, row, nbr, to_label.as_deref(), to_props, params)? {
2352 continue;
2353 }
2354 if out.len() >= cap {
2355 return Err(row_cap_err(cap));
2356 }
2357 let mut next = row.clone();
2358 if let Some(slot) = rel_slot {
2359 next[slot] = Some(Cell::Rel(e));
2360 }
2361 if bound_to.is_none() {
2362 next[to_slot] = Some(Cell::Node(nbr));
2363 }
2364 out.push(next);
2365 #[cfg(test)]
2366 record_expand_row();
2367 }
2368 }
2369 Ok(out)
2370}
2371
2372#[allow(clippy::too_many_arguments)]
2379fn exec_var_expand(
2380 view: &GraphView,
2381 vars: &VarTable,
2382 rows: &[Row],
2383 from: &str,
2384 rel_var: &Option<String>,
2385 etypes: &[String],
2386 dir: RelDir,
2387 to: &str,
2388 min: u8,
2389 max: u8,
2390) -> Result<Vec<Row>, String> {
2391 let etypes = resolve_etypes(view, etypes);
2392 let exp_dir = map_dir(dir);
2393 let to_slot = vars
2394 .slot(to)
2395 .ok_or_else(|| format!("unbound variable `{to}`"))?;
2396 let rel_slot = rel_var.as_ref().and_then(|rv| vars.slot(rv));
2397 let cap = max_intermediate_rows();
2398 let mut out: Vec<Row> = Vec::new();
2399
2400 for row in rows {
2401 let from_id = require_node(row, vars, from)?;
2402 let bound_to = match row.get(to_slot).and_then(|c| c.as_ref()) {
2403 Some(Cell::Node(id)) => Some(*id),
2404 Some(_) => return Err(format!("variable `{to}` is not a node")),
2405 None => None,
2406 };
2407
2408 struct PathState {
2411 node: u32,
2412 edges: Vec<EdgeRef>,
2413 }
2414
2415 let mut frontier: Vec<PathState> = vec![PathState {
2416 node: from_id,
2417 edges: Vec::new(),
2418 }];
2419
2420 let mut frontier_count: usize = 0;
2425
2426 for depth in 1u8..=max {
2427 let mut next_frontier: Vec<PathState> = Vec::new();
2428 for state in &frontier {
2429 for e in expand(view, state.node, etypes.as_deref(), exp_dir) {
2430 if state.edges.contains(&e) {
2432 continue;
2433 }
2434 let nbr = neighbor(state.node, &e, dir);
2435 if !view.visible(nbr) {
2438 continue;
2439 }
2440
2441 if depth >= min {
2444 let dest_matches = match bound_to {
2445 Some(want) => nbr == want,
2446 None => true,
2447 };
2448 if dest_matches {
2449 if out.len() >= cap {
2450 return Err(row_cap_err(cap));
2451 }
2452 let mut next = row.clone();
2453 if let Some(slot) = rel_slot {
2454 next[slot] = Some(Cell::Path(depth));
2455 }
2456 next[to_slot] = Some(Cell::Node(nbr));
2457 out.push(next);
2458 }
2459 }
2460
2461 if depth < max {
2465 frontier_count += 1;
2466 if frontier_count >= cap {
2467 return Err(row_cap_err(cap));
2468 }
2469 let mut new_edges = state.edges.clone();
2470 new_edges.push(e);
2471 next_frontier.push(PathState {
2472 node: nbr,
2473 edges: new_edges,
2474 });
2475 }
2476 }
2477 }
2478 frontier = next_frontier;
2479 if frontier.is_empty() {
2480 break;
2481 }
2482 }
2483 }
2484 Ok(out)
2485}
2486
2487#[allow(clippy::too_many_arguments)]
2493fn exec_shortest_path(
2494 view: &GraphView,
2495 vars: &VarTable,
2496 rows: &[Row],
2497 from: &str,
2498 rel_var: &Option<String>,
2499 etypes: &[String],
2500 dir: RelDir,
2501 to: &str,
2502 max_hops: u8,
2503) -> Result<Vec<Row>, String> {
2504 let etypes = resolve_etypes(view, etypes);
2505 let exp_dir = map_dir(dir);
2506 let rel_slot = rel_var.as_ref().and_then(|rv| vars.slot(rv));
2507 let mut out: Vec<Row> = Vec::new();
2508
2509 for row in rows {
2510 let from_id = require_node(row, vars, from)?;
2511 let to_id = require_node(row, vars, to)?;
2512
2513 let mut visited = std::collections::BTreeSet::new();
2515 visited.insert(from_id);
2516 let mut frontier: Vec<u32> = vec![from_id];
2517
2518 'bfs: for depth in 1u8..=max_hops {
2519 let mut next_frontier: Vec<u32> = Vec::new();
2520 for &node in &frontier {
2521 for e in expand(view, node, etypes.as_deref(), exp_dir) {
2522 let nbr = neighbor(node, &e, dir);
2523 if !view.visible(nbr) {
2527 continue;
2528 }
2529 if nbr == to_id {
2530 let mut next = row.clone();
2532 if let Some(slot) = rel_slot {
2533 next[slot] = Some(Cell::Path(depth));
2534 }
2535 out.push(next);
2536 break 'bfs;
2537 }
2538 if !visited.contains(&nbr) {
2539 visited.insert(nbr);
2540 next_frontier.push(nbr);
2541 }
2542 }
2543 }
2544 frontier = next_frontier;
2545 if frontier.is_empty() {
2546 break;
2547 }
2548 }
2549 }
2550 Ok(out)
2551}
2552
2553fn exec_filter(
2554 view: &GraphView,
2555 vars: &VarTable,
2556 rows: &[Row],
2557 expr: &Expr,
2558 params: &Params,
2559) -> Result<Vec<Row>, String> {
2560 let mut out = Vec::with_capacity(rows.len());
2561 for row in rows {
2562 if eval_expr(view, vars, row, expr, params, 0)? {
2563 out.push(row.clone());
2564 }
2565 }
2566 Ok(out)
2567}
2568
2569struct PullCtx<'a> {
2585 view: &'a GraphView<'a>,
2586 vars: &'a VarTable,
2587 project_items: &'a [RetItem],
2588 params: &'a Params<'a>,
2589 bound: usize,
2590}
2591
2592fn execute_pull(
2593 view: &GraphView,
2594 plan: &[PlanOp],
2595 params: &Params,
2596 bound: usize,
2597) -> Result<ResultSet, String> {
2598 let proj_pos = match plan
2599 .iter()
2600 .position(|op| matches!(op, PlanOp::Project { .. }))
2601 {
2602 Some(p) => p,
2603 None => return Ok(ResultSet::new(vec![])),
2604 };
2605 let producers = &plan[..proj_pos];
2606 let project_items = match &plan[proj_pos] {
2607 PlanOp::Project { items } => items,
2608 _ => unreachable!(),
2609 };
2610 let columns: Vec<String> = project_items.iter().map(column_name).collect();
2611 let vars = collect_vars(plan);
2612 let ctx = PullCtx {
2613 view,
2614 vars: &vars,
2615 project_items,
2616 params,
2617 bound,
2618 };
2619 let mut initial_row: Row = vec![None; vars.names.len()];
2620 let mut result_rows: Vec<Vec<Option<Value>>> = Vec::with_capacity(bound);
2621 pull_rows(&ctx, producers, &mut initial_row, &mut result_rows)?;
2622 let skip_n = plan[proj_pos + 1..]
2625 .iter()
2626 .find_map(|op| match op {
2627 PlanOp::Skip(ls) => Some(resolve_ls(ls, params)),
2628 _ => None,
2629 })
2630 .transpose()?
2631 .unwrap_or(0);
2632 let skip_n = usize::try_from(skip_n).unwrap_or(usize::MAX);
2633 let mut rs = ResultSet::new(columns);
2634 for row in result_rows.into_iter().skip(skip_n) {
2635 rs.push_row(row);
2636 }
2637 Ok(rs)
2638}
2639
2640enum AggAcc {
2654 Count(u64),
2655 Sum {
2656 val: f64,
2657 has_value: bool,
2658 },
2659 Avg {
2660 sum: f64,
2661 n: u64,
2662 },
2663 Min(Option<Value>),
2664 Max(Option<Value>),
2665 Collect(Vec<Value>),
2666 Distinct {
2669 seen: HashSet<Option<ValueKey>>,
2670 inner: Box<AggAcc>,
2671 },
2672}
2673
2674impl AggAcc {
2675 fn for_arg(func: &AggFunc, arg: &AggArg) -> Self {
2678 match arg {
2679 AggArg::Distinct(_) => AggAcc::Distinct {
2680 seen: HashSet::new(),
2681 inner: Box::new(AggAcc::new(func)),
2682 },
2683 _ => AggAcc::new(func),
2684 }
2685 }
2686
2687 fn new(func: &AggFunc) -> Self {
2688 match func {
2689 AggFunc::Count => AggAcc::Count(0),
2690 AggFunc::Sum => AggAcc::Sum {
2691 val: 0.0,
2692 has_value: false,
2693 },
2694 AggFunc::Avg => AggAcc::Avg { sum: 0.0, n: 0 },
2695 AggFunc::Min => AggAcc::Min(None),
2696 AggFunc::Max => AggAcc::Max(None),
2697 AggFunc::Collect => AggAcc::Collect(Vec::new()),
2698 }
2699 }
2700
2701 fn finish(self) -> Option<Value> {
2702 match self {
2703 AggAcc::Count(n) => Some(Value::Int(i64::try_from(n).unwrap_or(i64::MAX))),
2708 AggAcc::Sum { val, has_value } => {
2709 if has_value {
2710 Some(Value::Float(val))
2711 } else {
2712 None
2713 }
2714 }
2715 AggAcc::Avg { sum, n } => {
2716 if n > 0 {
2717 Some(Value::Float(sum / n as f64))
2718 } else {
2719 None
2720 }
2721 }
2722 AggAcc::Min(v) => v,
2723 AggAcc::Max(v) => v,
2724 AggAcc::Collect(items) => Some(Value::List(items)),
2726 AggAcc::Distinct { inner, .. } => inner.finish(),
2727 }
2728 }
2729}
2730
2731fn numeric_val(v: &Value) -> Option<f64> {
2734 match v {
2735 Value::Int(n) => Some(*n as f64),
2736 Value::Float(f) => Some(*f),
2737 _ => None,
2738 }
2739}
2740
2741struct AggStreamCtx<'a> {
2743 view: &'a GraphView<'a>,
2744 vars: &'a VarTable,
2745 params: &'a Params<'a>,
2746 func: &'a AggFunc,
2747 arg: &'a AggArg,
2748}
2749
2750fn execute_aggregate(
2755 view: &GraphView,
2756 plan: &[PlanOp],
2757 params: &Params,
2758) -> Result<ResultSet, String> {
2759 let agg_pos = match plan
2760 .iter()
2761 .position(|op| matches!(op, PlanOp::Aggregate { .. }))
2762 {
2763 Some(p) => p,
2764 None => return Ok(ResultSet::new(vec![])),
2765 };
2766 let producers = &plan[..agg_pos];
2767 let (func, arg, column) = match &plan[agg_pos] {
2768 PlanOp::Aggregate { func, arg, column } => (func, arg, column),
2769 _ => unreachable!(),
2770 };
2771 let vars = collect_vars(plan);
2772 let ctx = AggStreamCtx {
2773 view,
2774 vars: &vars,
2775 params,
2776 func,
2777 arg,
2778 };
2779 let initial_row: Row = vec![None; vars.names.len()];
2780 let mut acc = AggAcc::for_arg(func, arg);
2781 agg_stream(&ctx, producers, &initial_row, &mut acc)?;
2782
2783 let value = acc.finish();
2784 let mut rs = ResultSet::new(vec![column.clone()]);
2785 rs.push_row(vec![value]);
2786 Ok(rs)
2787}
2788
2789fn distinct_value(
2813 view: &GraphView,
2814 vars: &VarTable,
2815 row: &Row,
2816 arg: &AggArg,
2817) -> Result<Option<Value>, String> {
2818 match arg {
2819 AggArg::Star => Ok(None),
2820 AggArg::Var(v) => {
2821 let Some(slot) = vars.slot(v) else {
2822 return Ok(None);
2823 };
2824 Ok(match row.get(slot).and_then(|c| c.as_ref()) {
2825 Some(Cell::Node(id)) => view.ids.key_of(*id).map(|k| Value::Str(k.to_owned())),
2826 Some(Cell::Scalar(val)) => Some(val.clone()),
2827 Some(Cell::Path(hops)) => Some(Value::Int(*hops as i64)),
2828 Some(Cell::Rel(e)) => Some(Value::Str(format!(
2829 "{}\u{1}{}\u{1}{}",
2830 e.etype, e.src, e.dst
2831 ))),
2832 None => None,
2833 })
2834 }
2835 AggArg::Prop { var, field } => resolve_prop(view, vars, row, var, field),
2836 AggArg::Distinct(inner) => distinct_value(view, vars, row, inner),
2838 }
2839}
2840
2841fn update_acc(
2847 view: &GraphView,
2848 vars: &VarTable,
2849 row: &Row,
2850 func: &AggFunc,
2851 arg: &AggArg,
2852 acc: &mut AggAcc,
2853) -> Result<(), String> {
2854 if let AggArg::Distinct(inner_arg) = arg {
2858 let AggAcc::Distinct { seen, inner } = acc else {
2859 return Ok(());
2860 };
2861 let val = distinct_value(view, vars, row, inner_arg)?;
2862 let Some(val) = val else {
2865 return Ok(());
2866 };
2867 if !seen.insert(group_key_normalize(&val)) {
2868 return Ok(());
2869 }
2870 return update_acc(view, vars, row, func, inner_arg, inner);
2871 }
2872 match (func, arg) {
2873 (AggFunc::Count, AggArg::Star) => {
2874 if let AggAcc::Count(n) = acc {
2875 *n += 1;
2876 }
2877 }
2878 (AggFunc::Count, AggArg::Var(v)) => {
2879 let slot = vars.slot(v);
2880 let is_bound = slot
2881 .and_then(|s| row.get(s))
2882 .and_then(|c| c.as_ref())
2883 .is_some();
2884 if is_bound {
2885 if let AggAcc::Count(n) = acc {
2886 *n += 1;
2887 }
2888 }
2889 }
2890 (AggFunc::Count, AggArg::Prop { var, field }) => {
2891 let val = resolve_prop(view, vars, row, var, field)?;
2892 if val.is_some() {
2893 if let AggAcc::Count(n) = acc {
2894 *n += 1;
2895 }
2896 }
2897 }
2898 (AggFunc::Sum, AggArg::Prop { var, field }) => {
2899 if let Some(v) = resolve_prop(view, vars, row, var, field)? {
2900 if let Some(num) = numeric_val(&v) {
2901 if let AggAcc::Sum { val, has_value } = acc {
2902 *val += num;
2903 *has_value = true;
2904 }
2905 }
2906 }
2907 }
2908 (AggFunc::Avg, AggArg::Prop { var, field }) => {
2909 if let Some(v) = resolve_prop(view, vars, row, var, field)? {
2910 if let Some(num) = numeric_val(&v) {
2911 if let AggAcc::Avg { sum, n } = acc {
2912 *sum += num;
2913 *n += 1;
2914 }
2915 }
2916 }
2917 }
2918 (AggFunc::Min, AggArg::Prop { var, field }) => {
2919 if let Some(v) = resolve_prop(view, vars, row, var, field)? {
2920 if numeric_val(&v).is_some() {
2921 if let AggAcc::Min(current) = acc {
2922 *current = Some(match current.take() {
2923 None => v,
2924 Some(prev) => {
2925 if cmp_optional(Some(&prev), Some(&v), false)
2926 == std::cmp::Ordering::Greater
2927 {
2928 v
2929 } else {
2930 prev
2931 }
2932 }
2933 });
2934 }
2935 }
2936 }
2937 }
2938 (AggFunc::Max, AggArg::Prop { var, field }) => {
2939 if let Some(v) = resolve_prop(view, vars, row, var, field)? {
2940 if numeric_val(&v).is_some() {
2941 if let AggAcc::Max(current) = acc {
2942 *current = Some(match current.take() {
2943 None => v,
2944 Some(prev) => {
2945 if cmp_optional(Some(&prev), Some(&v), true)
2946 == std::cmp::Ordering::Greater
2947 {
2948 v
2949 } else {
2950 prev
2951 }
2952 }
2953 });
2954 }
2955 }
2956 }
2957 }
2958 (AggFunc::Collect, AggArg::Var(v)) => {
2959 let val = vars
2962 .slot(v)
2963 .and_then(|s| row.get(s))
2964 .and_then(|c| c.as_ref())
2965 .and_then(|cell| match cell {
2966 Cell::Scalar(x) => Some(x.clone()),
2967 Cell::Node(id) => view.ids.key_of(*id).map(|k| Value::Str(k.to_owned())),
2968 Cell::Path(h) => Some(Value::Int(*h as i64)),
2969 Cell::Rel(_) => None,
2970 });
2971 if let Some(val) = val {
2972 if let AggAcc::Collect(items) = acc {
2973 items.push(val);
2974 }
2975 }
2976 }
2977 (AggFunc::Collect, AggArg::Prop { var, field }) => {
2978 if let Some(val) = resolve_prop(view, vars, row, var, field)? {
2979 if let AggAcc::Collect(items) = acc {
2980 items.push(val);
2981 }
2982 }
2983 }
2984 _ => {}
2987 }
2988 Ok(())
2989}
2990
2991fn agg_stream(
2994 ctx: &AggStreamCtx<'_>,
2995 ops: &[PlanOp],
2996 row: &Row,
2997 acc: &mut AggAcc,
2998) -> Result<(), String> {
2999 let (op, rest) = match ops.split_first() {
3000 Some(pair) => pair,
3001 None => {
3002 update_acc(ctx.view, ctx.vars, row, ctx.func, ctx.arg, acc)?;
3004 return Ok(());
3005 }
3006 };
3007
3008 match op {
3009 PlanOp::ScanLabel { var, label } => {
3010 let ids = scan_ids(ctx.view, label.as_deref());
3011 let slot = ctx
3012 .vars
3013 .slot(var)
3014 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3015 for &id in &ids {
3016 let mut next = row.clone();
3017 next[slot] = Some(Cell::Node(id));
3018 agg_stream(ctx, rest, &next, acc)?;
3019 }
3020 }
3021 PlanOp::ScanKey { var, key, label } => {
3022 let slot = ctx
3023 .vars
3024 .slot(var)
3025 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3026 if let Some(id) =
3027 resolve_scan_key_id(ctx.view, ctx.vars, row, key, label.as_deref(), ctx.params)?
3028 {
3029 let mut next = row.clone();
3030 next[slot] = Some(Cell::Node(id));
3031 agg_stream(ctx, rest, &next, acc)?;
3032 }
3033 }
3034 PlanOp::IndexScan {
3035 var,
3036 label,
3037 field,
3038 value,
3039 } => {
3040 let ids = index_scan_ids(
3041 ctx.view,
3042 ctx.vars,
3043 row,
3044 label.as_deref(),
3045 field,
3046 value,
3047 ctx.params,
3048 )?;
3049 let slot = ctx
3050 .vars
3051 .slot(var)
3052 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3053 for &id in &ids {
3054 let mut next = row.clone();
3055 next[slot] = Some(Cell::Node(id));
3056 agg_stream(ctx, rest, &next, acc)?;
3057 }
3058 }
3059 PlanOp::IndexIntersect {
3060 var,
3061 label,
3062 equalities,
3063 } => {
3064 let ids = index_intersect_ids(
3065 ctx.view,
3066 ctx.vars,
3067 row,
3068 label.as_deref(),
3069 equalities,
3070 ctx.params,
3071 )?;
3072 let slot = ctx
3073 .vars
3074 .slot(var)
3075 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3076 for id in ids {
3077 let mut next = row.clone();
3078 next[slot] = Some(Cell::Node(id));
3079 agg_stream(ctx, rest, &next, acc)?;
3080 }
3081 }
3082 PlanOp::Expand {
3083 from,
3084 rel_var,
3085 etypes,
3086 dir,
3087 to,
3088 to_label,
3089 to_props,
3090 } => {
3091 let etypes = resolve_etypes(ctx.view, etypes);
3092 let exp_dir = map_dir(*dir);
3093 let to_slot = ctx
3094 .vars
3095 .slot(to)
3096 .ok_or_else(|| format!("unbound variable `{to}`"))?;
3097 let rel_slot = rel_var.as_ref().and_then(|rv| ctx.vars.slot(rv));
3098 let from_id = require_node(row, ctx.vars, from)?;
3099 let bound_to = match row.get(to_slot).and_then(|c| c.as_ref()) {
3100 Some(Cell::Node(id)) => Some(*id),
3101 Some(Cell::Rel(_) | Cell::Path(_) | Cell::Scalar(_)) => {
3102 return Err(format!("variable `{to}` is not a node"))
3103 }
3104 None => None,
3105 };
3106 for e in expand(ctx.view, from_id, etypes.as_deref(), exp_dir) {
3107 if row_has_edge(row, &e) {
3108 continue;
3109 }
3110 let nbr = neighbor(from_id, &e, *dir);
3111 if !ctx.view.visible(nbr) {
3112 continue;
3113 }
3114 if let Some(want) = bound_to {
3115 if nbr != want {
3116 continue;
3117 }
3118 }
3119 if !node_matches(
3120 ctx.view,
3121 ctx.vars,
3122 row,
3123 nbr,
3124 to_label.as_deref(),
3125 to_props,
3126 ctx.params,
3127 )? {
3128 continue;
3129 }
3130 let mut next = row.clone();
3131 if let Some(slot) = rel_slot {
3132 next[slot] = Some(Cell::Rel(e));
3133 }
3134 if bound_to.is_none() {
3135 next[to_slot] = Some(Cell::Node(nbr));
3136 }
3137 agg_stream(ctx, rest, &next, acc)?;
3138 }
3139 }
3140 PlanOp::Filter { expr } => {
3141 if eval_expr(ctx.view, ctx.vars, row, expr, ctx.params, 0)? {
3142 agg_stream(ctx, rest, row, acc)?;
3143 }
3144 }
3145 PlanOp::LookupProps { var, props } => {
3146 let id = require_node(row, ctx.vars, var)?;
3147 if node_matches(ctx.view, ctx.vars, row, id, None, props, ctx.params)? {
3148 agg_stream(ctx, rest, row, acc)?;
3149 }
3150 }
3151 PlanOp::JoinBound { var, label, props } => {
3152 let id = require_node(row, ctx.vars, var)?;
3153 if node_matches(
3154 ctx.view,
3155 ctx.vars,
3156 row,
3157 id,
3158 label.as_deref(),
3159 props,
3160 ctx.params,
3161 )? {
3162 agg_stream(ctx, rest, row, acc)?;
3163 }
3164 }
3165 PlanOp::VarExpand {
3166 from,
3167 rel_var,
3168 etypes,
3169 dir,
3170 to,
3171 min,
3172 max,
3173 } => {
3174 let new_rows = exec_var_expand(
3175 ctx.view,
3176 ctx.vars,
3177 std::slice::from_ref(row),
3178 from,
3179 rel_var,
3180 etypes,
3181 *dir,
3182 to,
3183 *min,
3184 *max,
3185 )?;
3186 for nr in &new_rows {
3187 agg_stream(ctx, rest, nr, acc)?;
3188 }
3189 }
3190 PlanOp::ShortestPath {
3191 from,
3192 rel_var,
3193 etypes,
3194 dir,
3195 to,
3196 max_hops,
3197 } => {
3198 let new_rows = exec_shortest_path(
3199 ctx.view,
3200 ctx.vars,
3201 std::slice::from_ref(row),
3202 from,
3203 rel_var,
3204 etypes,
3205 *dir,
3206 to,
3207 *max_hops,
3208 )?;
3209 for nr in &new_rows {
3210 agg_stream(ctx, rest, nr, acc)?;
3211 }
3212 }
3213 PlanOp::Project { .. } => {
3215 return Err(
3216 "agg executor: Project in producer slice — plan is structurally malformed"
3217 .to_string(),
3218 );
3219 }
3220 PlanOp::Distinct => {
3221 return Err(
3222 "agg executor: Distinct in producer slice — plan is structurally malformed"
3223 .to_string(),
3224 );
3225 }
3226 PlanOp::OrderBy { .. } => {
3227 return Err(
3228 "agg executor: OrderBy in producer slice — structurally malformed".to_string(),
3229 );
3230 }
3231 PlanOp::Skip(_) => {
3232 return Err(
3233 "agg executor: Skip in producer slice — structurally malformed".to_string(),
3234 );
3235 }
3236 PlanOp::Limit(_) => {
3237 return Err(
3238 "agg executor: Limit in producer slice — structurally malformed".to_string(),
3239 );
3240 }
3241 PlanOp::Aggregate { .. } => {
3242 return Err(
3243 "agg executor: nested Aggregate in producer slice — structurally malformed"
3244 .to_string(),
3245 );
3246 }
3247 PlanOp::GroupAggregate { .. } => {
3248 return Err(
3249 "agg executor: GroupAggregate in producer slice — structurally malformed"
3250 .to_string(),
3251 );
3252 }
3253 PlanOp::With { .. } => {
3254 return Err(
3255 "agg executor: With in producer slice — structurally malformed".to_string(),
3256 );
3257 }
3258 PlanOp::Unwind { .. } => {
3259 return Err(
3260 "agg executor: Unwind in producer slice — structurally malformed".to_string(),
3261 );
3262 }
3263 PlanOp::LeftOuterApply { .. } => {
3264 return Err(
3265 "agg executor: LeftOuterApply in producer slice — structurally malformed"
3266 .to_string(),
3267 );
3268 }
3269 }
3270 Ok(())
3271}
3272
3273struct GroupStreamCtx<'a> {
3281 view: &'a GraphView<'a>,
3282 vars: &'a VarTable,
3283 params: &'a Params<'a>,
3284 keys: &'a [(String, RetItem)],
3285 aggs: &'a [(AggFunc, AggArg, String)],
3286}
3287
3288fn build_group_projected(
3290 keys: &[(String, RetItem)],
3291 aggs: &[(AggFunc, AggArg, String)],
3292 key_order: Vec<GroupKey>,
3293 groups: &mut HashMap<GroupKey, GroupEntry>,
3294) -> Projected {
3295 let columns: Vec<String> = keys
3296 .iter()
3297 .map(|(col, _)| col.clone())
3298 .chain(aggs.iter().map(|(_, _, col)| col.clone()))
3299 .collect();
3300 let mut rows: Vec<Vec<Option<Value>>> = Vec::with_capacity(key_order.len());
3301 for gk in key_order {
3302 let (display_keys, accs) = groups.remove(&gk).unwrap_or_default();
3303 let mut row: Vec<Option<Value>> = Vec::with_capacity(columns.len());
3304 for display_val in display_keys {
3307 row.push(display_val);
3308 }
3309 for acc in accs {
3310 row.push(acc.finish());
3311 }
3312 rows.push(row);
3313 }
3314 Projected { columns, rows }
3315}
3316
3317fn key_source_cells(vars: &VarTable, row: &Row, keys: &[(String, RetItem)]) -> Vec<Option<Cell>> {
3328 keys.iter()
3329 .map(|(_, item)| match &item.value {
3330 RetVal::Var(v) => vars
3331 .slot(v)
3332 .and_then(|s| row.get(s))
3333 .and_then(|c| c.clone()),
3334 _ => None,
3335 })
3336 .collect()
3337}
3338
3339fn group_result_to_rows(
3340 keys: &[(String, RetItem)],
3341 aggs: &[(AggFunc, AggArg, String)],
3342 key_order: Vec<GroupKey>,
3343 groups: &mut HashMap<GroupKey, GroupEntry>,
3344 key_cells: &mut HashMap<GroupKey, Vec<Option<Cell>>>,
3345 vars: &VarTable,
3346) -> Vec<Row> {
3347 let row_len = vars.names.len();
3348 let mut out: Vec<Row> = Vec::with_capacity(key_order.len());
3349 for gk in key_order {
3350 let (display_keys, accs) = groups.remove(&gk).unwrap_or_default();
3351 let mut cells = key_cells.remove(&gk).unwrap_or_default();
3352 cells.resize(keys.len(), None);
3353 let mut row: Row = vec![None; row_len];
3354 for (((col, _), val), cell) in keys.iter().zip(display_keys).zip(cells) {
3355 if let Some(slot) = vars.slot(col) {
3356 row[slot] = cell.or_else(|| val.map(Cell::Scalar));
3357 }
3358 }
3359 for ((_, _, col), acc) in aggs.iter().zip(accs) {
3360 if let Some(slot) = vars.slot(col) {
3361 row[slot] = acc.finish().map(Cell::Scalar);
3362 }
3363 }
3364 out.push(row);
3365 }
3366 out
3367}
3368
3369fn exec_order_by_rows(vars: &VarTable, rows: &mut Vec<Row>, items: &[OrderItem], view: &GraphView) {
3376 let mut key_table: Vec<Vec<Option<Value>>> = Vec::with_capacity(rows.len());
3379 for row in rows.iter() {
3380 let mut row_key: Vec<Option<Value>> = Vec::with_capacity(items.len());
3381 for item in items {
3382 let val = match &item.target {
3383 OrderTarget::Alias(name) | OrderTarget::Var(name) => vars
3384 .slot(name)
3385 .and_then(|s| row.get(s))
3386 .and_then(|c| c.as_ref())
3387 .and_then(|c| match c {
3388 Cell::Scalar(v) => Some(v.clone()),
3389 Cell::Node(id) => view.ids.key_of(*id).map(|k| Value::Str(k.to_owned())),
3390 Cell::Path(hops) => Some(Value::Int(*hops as i64)),
3391 Cell::Rel(_) => None,
3392 }),
3393 OrderTarget::Prop { var, field } => vars
3394 .slot(var)
3395 .and_then(|s| row.get(s))
3396 .and_then(|c| c.as_ref())
3397 .and_then(|c| match c {
3398 Cell::Node(id) => view
3399 .prop(*id, field)
3400 .map(|vr| vr.into_value())
3401 .or_else(|| node_identity_prop(view, *id, field)),
3402 Cell::Rel(e) => view.edge_props.get(e.etype, e.src, e.dst, field),
3403 _ => None,
3404 }),
3405 };
3406 row_key.push(val);
3407 }
3408 key_table.push(row_key);
3409 }
3410 let mut indices: Vec<usize> = (0..rows.len()).collect();
3412 indices.sort_by(|&a, &b| {
3413 for (ki, item) in items.iter().enumerate() {
3414 let c = cmp_optional(
3415 key_table[a].get(ki).and_then(|x| x.as_ref()),
3416 key_table[b].get(ki).and_then(|x| x.as_ref()),
3417 item.descending,
3418 );
3419 if c != std::cmp::Ordering::Equal {
3420 return c;
3421 }
3422 }
3423 std::cmp::Ordering::Equal
3424 });
3425 let sorted: Vec<Row> = indices.into_iter().map(|i| rows[i].clone()).collect();
3426 *rows = sorted;
3427}
3428
3429fn execute_group_aggregate(
3435 view: &GraphView,
3436 plan: &[PlanOp],
3437 params: &Params,
3438) -> Result<ResultSet, String> {
3439 let gagg_pos = plan
3440 .iter()
3441 .position(|op| matches!(op, PlanOp::GroupAggregate { .. }))
3442 .ok_or_else(|| "internal: GroupAggregate op not found in plan".to_string())?;
3443 let producers = &plan[..gagg_pos];
3444 let (keys, aggs) = match &plan[gagg_pos] {
3445 PlanOp::GroupAggregate { keys, aggs } => (keys, aggs),
3446 _ => unreachable!(),
3447 };
3448 let tail = &plan[gagg_pos + 1..];
3449 let vars = collect_vars(plan);
3450 let initial_row: Row = vec![None; vars.names.len()];
3451 let mut groups: HashMap<GroupKey, GroupEntry> = HashMap::new();
3452 let mut key_order: Vec<GroupKey> = Vec::new();
3453 let ctx = GroupStreamCtx {
3454 view,
3455 vars: &vars,
3456 params,
3457 keys,
3458 aggs,
3459 };
3460 group_stream(&ctx, producers, &initial_row, &mut groups, &mut key_order)?;
3461 if keys.is_empty() && key_order.is_empty() {
3465 let empty_key: GroupKey = vec![];
3466 key_order.push(empty_key.clone());
3467 groups.insert(
3468 empty_key,
3469 (
3470 vec![],
3471 aggs.iter().map(|(f, a, _)| AggAcc::for_arg(f, a)).collect(),
3472 ),
3473 );
3474 }
3475 let mut projected = build_group_projected(keys, aggs, key_order, &mut groups);
3476 for op in tail {
3478 match op {
3479 PlanOp::OrderBy { items } => exec_order_by(&mut projected, items)?,
3480 PlanOp::Skip(ls) => {
3481 let n = resolve_ls(ls, params)?;
3482 apply_skip(&mut projected.rows, n);
3483 }
3484 PlanOp::Limit(ls) => {
3485 let n = resolve_ls(ls, params)?;
3486 apply_limit(&mut projected.rows, n);
3487 }
3488 _ => {} }
3490 }
3491 Ok(finish(projected))
3492}
3493
3494fn group_stream(
3501 ctx: &GroupStreamCtx<'_>,
3502 ops: &[PlanOp],
3503 row: &Row,
3504 groups: &mut HashMap<GroupKey, GroupEntry>,
3505 key_order: &mut Vec<GroupKey>,
3506) -> Result<(), String> {
3507 let (op, rest) = match ops.split_first() {
3508 Some(pair) => pair,
3509 None => {
3510 let mut gk: GroupKey = Vec::with_capacity(ctx.keys.len());
3513 let mut display_vals: Vec<Option<Value>> = Vec::with_capacity(ctx.keys.len());
3514 for (_, item) in ctx.keys {
3515 let val = project_item(ctx.view, ctx.vars, row, item, ctx.params)?;
3516 gk.push(val.as_ref().and_then(group_key_normalize));
3517 display_vals.push(val);
3518 }
3519 if !groups.contains_key(&gk) {
3520 if groups.len() >= max_groups() {
3521 return Err(group_cap_err());
3522 }
3523 key_order.push(gk.clone());
3524 let init: Vec<AggAcc> = ctx
3525 .aggs
3526 .iter()
3527 .map(|(f, a, _)| AggAcc::for_arg(f, a))
3528 .collect();
3529 groups.insert(gk.clone(), (display_vals, init));
3530 }
3531 let (_, accs) = groups.get_mut(&gk).unwrap();
3532 for (acc, (func, arg, _)) in accs.iter_mut().zip(ctx.aggs.iter()) {
3533 update_acc(ctx.view, ctx.vars, row, func, arg, acc)?;
3534 }
3535 return Ok(());
3536 }
3537 };
3538
3539 match op {
3540 PlanOp::ScanLabel { var, label } => {
3541 let ids = scan_ids(ctx.view, label.as_deref());
3542 let slot = ctx
3543 .vars
3544 .slot(var)
3545 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3546 for &id in &ids {
3547 let mut next = row.clone();
3548 next[slot] = Some(Cell::Node(id));
3549 group_stream(ctx, rest, &next, groups, key_order)?;
3550 }
3551 }
3552 PlanOp::ScanKey { var, key, label } => {
3553 let slot = ctx
3554 .vars
3555 .slot(var)
3556 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3557 if let Some(id) =
3558 resolve_scan_key_id(ctx.view, ctx.vars, row, key, label.as_deref(), ctx.params)?
3559 {
3560 let mut next = row.clone();
3561 next[slot] = Some(Cell::Node(id));
3562 group_stream(ctx, rest, &next, groups, key_order)?;
3563 }
3564 }
3565 PlanOp::IndexScan {
3566 var,
3567 label,
3568 field,
3569 value,
3570 } => {
3571 let ids = index_scan_ids(
3572 ctx.view,
3573 ctx.vars,
3574 row,
3575 label.as_deref(),
3576 field,
3577 value,
3578 ctx.params,
3579 )?;
3580 let slot = ctx
3581 .vars
3582 .slot(var)
3583 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3584 for &id in &ids {
3585 let mut next = row.clone();
3586 next[slot] = Some(Cell::Node(id));
3587 group_stream(ctx, rest, &next, groups, key_order)?;
3588 }
3589 }
3590 PlanOp::IndexIntersect {
3591 var,
3592 label,
3593 equalities,
3594 } => {
3595 let ids = index_intersect_ids(
3596 ctx.view,
3597 ctx.vars,
3598 row,
3599 label.as_deref(),
3600 equalities,
3601 ctx.params,
3602 )?;
3603 let slot = ctx
3604 .vars
3605 .slot(var)
3606 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3607 for id in ids {
3608 let mut next = row.clone();
3609 next[slot] = Some(Cell::Node(id));
3610 group_stream(ctx, rest, &next, groups, key_order)?;
3611 }
3612 }
3613 PlanOp::Expand {
3614 from,
3615 rel_var,
3616 etypes,
3617 dir,
3618 to,
3619 to_label,
3620 to_props,
3621 } => {
3622 let etypes = resolve_etypes(ctx.view, etypes);
3623 let exp_dir = map_dir(*dir);
3624 let to_slot = ctx
3625 .vars
3626 .slot(to)
3627 .ok_or_else(|| format!("unbound variable `{to}`"))?;
3628 let rel_slot = rel_var.as_ref().and_then(|rv| ctx.vars.slot(rv));
3629 let from_id = require_node(row, ctx.vars, from)?;
3630 let bound_to = match row.get(to_slot).and_then(|c| c.as_ref()) {
3631 Some(Cell::Node(id)) => Some(*id),
3632 Some(Cell::Rel(_) | Cell::Path(_) | Cell::Scalar(_)) => {
3633 return Err(format!("variable `{to}` is not a node"))
3634 }
3635 None => None,
3636 };
3637 for e in expand(ctx.view, from_id, etypes.as_deref(), exp_dir) {
3638 if row_has_edge(row, &e) {
3639 continue;
3640 }
3641 let nbr = neighbor(from_id, &e, *dir);
3642 if !ctx.view.visible(nbr) {
3643 continue;
3644 }
3645 if let Some(want) = bound_to {
3646 if nbr != want {
3647 continue;
3648 }
3649 }
3650 if !node_matches(
3651 ctx.view,
3652 ctx.vars,
3653 row,
3654 nbr,
3655 to_label.as_deref(),
3656 to_props,
3657 ctx.params,
3658 )? {
3659 continue;
3660 }
3661 let mut next = row.clone();
3662 if let Some(slot) = rel_slot {
3663 next[slot] = Some(Cell::Rel(e));
3664 }
3665 if bound_to.is_none() {
3666 next[to_slot] = Some(Cell::Node(nbr));
3667 }
3668 group_stream(ctx, rest, &next, groups, key_order)?;
3669 }
3670 }
3671 PlanOp::Filter { expr } => {
3672 if eval_expr(ctx.view, ctx.vars, row, expr, ctx.params, 0)? {
3673 group_stream(ctx, rest, row, groups, key_order)?;
3674 }
3675 }
3676 PlanOp::LookupProps { var, props } => {
3677 let id = require_node(row, ctx.vars, var)?;
3678 if node_matches(ctx.view, ctx.vars, row, id, None, props, ctx.params)? {
3679 group_stream(ctx, rest, row, groups, key_order)?;
3680 }
3681 }
3682 PlanOp::JoinBound { var, label, props } => {
3683 let id = require_node(row, ctx.vars, var)?;
3684 if node_matches(
3685 ctx.view,
3686 ctx.vars,
3687 row,
3688 id,
3689 label.as_deref(),
3690 props,
3691 ctx.params,
3692 )? {
3693 group_stream(ctx, rest, row, groups, key_order)?;
3694 }
3695 }
3696 PlanOp::VarExpand {
3697 from,
3698 rel_var,
3699 etypes,
3700 dir,
3701 to,
3702 min,
3703 max,
3704 } => {
3705 let new_rows = exec_var_expand(
3706 ctx.view,
3707 ctx.vars,
3708 std::slice::from_ref(row),
3709 from,
3710 rel_var,
3711 etypes,
3712 *dir,
3713 to,
3714 *min,
3715 *max,
3716 )?;
3717 for nr in &new_rows {
3718 group_stream(ctx, rest, nr, groups, key_order)?;
3719 }
3720 }
3721 PlanOp::ShortestPath {
3722 from,
3723 rel_var,
3724 etypes,
3725 dir,
3726 to,
3727 max_hops,
3728 } => {
3729 let new_rows = exec_shortest_path(
3730 ctx.view,
3731 ctx.vars,
3732 std::slice::from_ref(row),
3733 from,
3734 rel_var,
3735 etypes,
3736 *dir,
3737 to,
3738 *max_hops,
3739 )?;
3740 for nr in &new_rows {
3741 group_stream(ctx, rest, nr, groups, key_order)?;
3742 }
3743 }
3744 PlanOp::Project { .. } => {
3746 return Err(
3747 "group executor: Project in producer slice — structurally malformed".to_string(),
3748 );
3749 }
3750 PlanOp::Distinct => {
3751 return Err(
3752 "group executor: Distinct in producer slice — structurally malformed".to_string(),
3753 );
3754 }
3755 PlanOp::OrderBy { .. } => {
3756 return Err(
3757 "group executor: OrderBy in producer slice — structurally malformed".to_string(),
3758 );
3759 }
3760 PlanOp::Skip(_) => {
3761 return Err(
3762 "group executor: Skip in producer slice — structurally malformed".to_string(),
3763 );
3764 }
3765 PlanOp::Limit(_) => {
3766 return Err(
3767 "group executor: Limit in producer slice — structurally malformed".to_string(),
3768 );
3769 }
3770 PlanOp::Aggregate { .. } => {
3771 return Err(
3772 "group executor: Aggregate in producer slice — structurally malformed".to_string(),
3773 );
3774 }
3775 PlanOp::GroupAggregate { .. } => {
3776 return Err(
3777 "group executor: nested GroupAggregate in producer slice — structurally malformed"
3778 .to_string(),
3779 );
3780 }
3781 PlanOp::With { .. } => {
3782 return Err(
3783 "group executor: With in producer slice — structurally malformed".to_string(),
3784 );
3785 }
3786 PlanOp::Unwind { .. } => {
3787 return Err(
3788 "group executor: Unwind in producer slice — structurally malformed".to_string(),
3789 );
3790 }
3791 PlanOp::LeftOuterApply { .. } => {
3792 return Err(
3793 "group executor: LeftOuterApply in producer slice — structurally malformed"
3794 .to_string(),
3795 );
3796 }
3797 }
3798 Ok(())
3799}
3800
3801fn pull_rows(
3815 ctx: &PullCtx<'_>,
3816 ops: &[PlanOp],
3817 row: &mut Row,
3818 result: &mut Vec<Vec<Option<Value>>>,
3819) -> Result<(), String> {
3820 if result.len() >= ctx.bound {
3821 return Ok(());
3822 }
3823 let (op, rest) = match ops.split_first() {
3824 Some(pair) => pair,
3825 None => {
3826 let mut cells = Vec::with_capacity(ctx.project_items.len());
3828 for item in ctx.project_items {
3829 cells.push(project_item(ctx.view, ctx.vars, row, item, ctx.params)?);
3830 }
3831 result.push(cells);
3832 return Ok(());
3833 }
3834 };
3835 match op {
3836 PlanOp::ScanLabel { var, label } => {
3837 let slot = ctx
3838 .vars
3839 .slot(var)
3840 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3841 let want_sym = label.as_deref().and_then(|l| ctx.view.syms.get(l));
3844 if label.is_some() && want_sym.is_none() {
3845 return Ok(());
3846 }
3847 let prev = row[slot].clone();
3849
3850 let fused_filter = rest.first().and_then(|next_op| {
3856 if let PlanOp::Filter {
3857 expr:
3858 Expr::Cmp {
3859 lhs:
3860 Operand::Prop {
3861 var: ref fv,
3862 field: ref f,
3863 },
3864 op: ref cmp_op_ref,
3865 rhs: Operand::Lit(ref lit),
3866 },
3867 } = *next_op
3868 {
3869 if fv == var {
3870 return Some((f.as_str(), cmp_op_ref, lit));
3871 }
3872 }
3873 None
3874 });
3875
3876 let fused_filter = fused_filter.filter(|(f, _, _)| !is_identity_field(f));
3880 if let Some((field, cmp_op_ref, lit)) = fused_filter {
3881 #[cfg(test)]
3884 FUSED_SCAN_FIRES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3885 let col = ctx.view.props.column(field);
3886 let rest_after_filter = &rest[1..];
3887 for (i, &sym) in ctx.view.labels.iter().enumerate() {
3888 if result.len() >= ctx.bound {
3889 break;
3890 }
3891 if sym == u32::MAX {
3892 continue;
3893 }
3894 if let Some(ws) = want_sym {
3895 if sym != ws {
3896 continue;
3897 }
3898 }
3899 let id = i as u32;
3900 if !ctx.view.visible(id) {
3901 continue;
3902 }
3903 if let Some(v) = col.get(id) {
3904 if eval_cmp(cmp_op_ref, v, lit) {
3905 row[slot] = Some(Cell::Node(id));
3906 pull_rows(ctx, rest_after_filter, row, result)?;
3907 }
3908 }
3909 }
3910 } else {
3911 for (i, &sym) in ctx.view.labels.iter().enumerate() {
3914 if result.len() >= ctx.bound {
3915 break;
3916 }
3917 if sym == u32::MAX {
3919 continue;
3920 }
3921 if let Some(ws) = want_sym {
3923 if sym != ws {
3924 continue;
3925 }
3926 }
3927 let id = i as u32;
3928 if !ctx.view.visible(id) {
3929 continue;
3930 }
3931 row[slot] = Some(Cell::Node(id));
3932 pull_rows(ctx, rest, row, result)?;
3933 }
3934 }
3935 row[slot] = prev;
3941 }
3942 PlanOp::ScanKey { var, key, label } => {
3943 let slot = ctx
3944 .vars
3945 .slot(var)
3946 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3947 if let Some(id) =
3948 resolve_scan_key_id(ctx.view, ctx.vars, row, key, label.as_deref(), ctx.params)?
3949 {
3950 let prev = row[slot].clone();
3951 row[slot] = Some(Cell::Node(id));
3952 pull_rows(ctx, rest, row, result)?;
3953 row[slot] = prev;
3954 }
3955 }
3956 PlanOp::IndexScan {
3957 var,
3958 label,
3959 field,
3960 value,
3961 } => {
3962 let slot = ctx
3963 .vars
3964 .slot(var)
3965 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3966 let ids = index_scan_ids(
3967 ctx.view,
3968 ctx.vars,
3969 row,
3970 label.as_deref(),
3971 field,
3972 value,
3973 ctx.params,
3974 )?;
3975 let prev = row[slot].clone();
3976 for id in ids {
3977 if result.len() >= ctx.bound {
3978 break;
3979 }
3980 row[slot] = Some(Cell::Node(id));
3981 pull_rows(ctx, rest, row, result)?;
3982 }
3983 row[slot] = prev;
3984 }
3985 PlanOp::IndexIntersect {
3986 var,
3987 label,
3988 equalities,
3989 } => {
3990 let slot = ctx
3991 .vars
3992 .slot(var)
3993 .ok_or_else(|| format!("unbound variable `{var}`"))?;
3994 let ids = index_intersect_ids(
3995 ctx.view,
3996 ctx.vars,
3997 row,
3998 label.as_deref(),
3999 equalities,
4000 ctx.params,
4001 )?;
4002 let prev = row[slot].clone();
4003 for id in ids {
4004 if result.len() >= ctx.bound {
4005 break;
4006 }
4007 row[slot] = Some(Cell::Node(id));
4008 pull_rows(ctx, rest, row, result)?;
4009 }
4010 row[slot] = prev;
4011 }
4012 PlanOp::Expand {
4013 from,
4014 rel_var,
4015 etypes,
4016 dir,
4017 to,
4018 to_label,
4019 to_props,
4020 } => {
4021 let etypes = resolve_etypes(ctx.view, etypes);
4022 let exp_dir = map_dir(*dir);
4023 let to_slot = ctx
4024 .vars
4025 .slot(to)
4026 .ok_or_else(|| format!("unbound variable `{to}`"))?;
4027 let rel_slot = rel_var.as_ref().and_then(|rv| ctx.vars.slot(rv));
4028 let from_id = require_node(row, ctx.vars, from)?;
4029 let bound_to = match row.get(to_slot).and_then(|c| c.as_ref()) {
4030 Some(Cell::Node(id)) => Some(*id),
4031 Some(Cell::Rel(_) | Cell::Path(_) | Cell::Scalar(_)) => {
4032 return Err(format!("variable `{to}` is not a node"))
4033 }
4034 None => None,
4035 };
4036 for e in expand(ctx.view, from_id, etypes.as_deref(), exp_dir) {
4037 if result.len() >= ctx.bound {
4038 break;
4039 }
4040 if row_has_edge(row, &e) {
4041 continue;
4042 }
4043 let nbr = neighbor(from_id, &e, *dir);
4044 if !ctx.view.visible(nbr) {
4045 continue;
4046 }
4047 if let Some(want) = bound_to {
4048 if nbr != want {
4049 continue;
4050 }
4051 }
4052 if !node_matches(
4053 ctx.view,
4054 ctx.vars,
4055 row,
4056 nbr,
4057 to_label.as_deref(),
4058 to_props,
4059 ctx.params,
4060 )? {
4061 continue;
4062 }
4063 let mut next = row.clone();
4064 if let Some(slot) = rel_slot {
4065 next[slot] = Some(Cell::Rel(e));
4066 }
4067 if bound_to.is_none() {
4068 next[to_slot] = Some(Cell::Node(nbr));
4069 }
4070 #[cfg(test)]
4071 record_expand_row();
4072 pull_rows(ctx, rest, &mut next, result)?;
4073 }
4074 }
4075 PlanOp::Filter { expr } => {
4076 if eval_expr(ctx.view, ctx.vars, row, expr, ctx.params, 0)? {
4077 pull_rows(ctx, rest, row, result)?;
4078 }
4079 }
4080 PlanOp::LookupProps { var, props } => {
4081 let id = require_node(row, ctx.vars, var)?;
4082 if node_matches(ctx.view, ctx.vars, row, id, None, props, ctx.params)? {
4083 pull_rows(ctx, rest, row, result)?;
4084 }
4085 }
4086 PlanOp::JoinBound { var, label, props } => {
4087 let id = require_node(row, ctx.vars, var)?;
4088 if node_matches(
4089 ctx.view,
4090 ctx.vars,
4091 row,
4092 id,
4093 label.as_deref(),
4094 props,
4095 ctx.params,
4096 )? {
4097 pull_rows(ctx, rest, row, result)?;
4098 }
4099 }
4100 PlanOp::Project { .. } => {
4105 return Err(
4106 "pull executor: Project reached pull_rows — plan is structurally malformed"
4107 .to_string(),
4108 );
4109 }
4110 PlanOp::Distinct => {
4111 return Err(
4112 "pull executor: Distinct reached pull_rows — DISTINCT queries must use \
4113 the staged path (row_bound returns None)"
4114 .to_string(),
4115 );
4116 }
4117 PlanOp::OrderBy { .. } => {
4118 return Err(
4119 "pull executor: OrderBy reached pull_rows — queries with ORDER BY \
4120 must use the staged path (row_bound returns None)"
4121 .to_string(),
4122 );
4123 }
4124 PlanOp::Skip(_) => {
4125 return Err(
4126 "pull executor: Skip reached pull_rows — Skip must appear after Project"
4127 .to_string(),
4128 );
4129 }
4130 PlanOp::Limit(_) => {
4131 return Err(
4132 "pull executor: Limit reached pull_rows — Limit must appear after Project"
4133 .to_string(),
4134 );
4135 }
4136 PlanOp::Aggregate { .. } => {
4137 return Err(
4138 "pull executor: Aggregate reached pull_rows — aggregate plans must use \
4139 the execute_aggregate path (routed before pull in execute_inner)"
4140 .to_string(),
4141 );
4142 }
4143 PlanOp::VarExpand { .. } => {
4148 return Err(
4149 "pull executor: VarExpand reached pull_rows — variable-length path \
4150 plans must use the staged path (row_bound returns None)"
4151 .to_string(),
4152 );
4153 }
4154 PlanOp::ShortestPath { .. } => {
4155 return Err(
4156 "pull executor: ShortestPath reached pull_rows — shortestPath plans \
4157 must use the staged path (row_bound returns None)"
4158 .to_string(),
4159 );
4160 }
4161 PlanOp::GroupAggregate { .. } => {
4162 return Err(
4163 "pull executor: GroupAggregate reached pull_rows — grouped aggregate plans \
4164 must use the execute_group_aggregate path (routed before pull in execute_inner)"
4165 .to_string(),
4166 );
4167 }
4168 PlanOp::With { .. } => {
4169 return Err(
4170 "pull executor: With reached pull_rows — pipeline plans must use the staged path \
4171 (row_bound returns None for plans containing With)"
4172 .to_string(),
4173 );
4174 }
4175 PlanOp::Unwind { .. } => {
4176 return Err(
4177 "pull executor: Unwind reached pull_rows — pipeline plans must use the staged path \
4178 (row_bound returns None for plans containing Unwind)"
4179 .to_string(),
4180 );
4181 }
4182 PlanOp::LeftOuterApply { .. } => {
4183 return Err(
4184 "pull executor: LeftOuterApply reached pull_rows — OPTIONAL MATCH plans must use \
4185 the staged path (row_bound returns None for plans containing LeftOuterApply)"
4186 .to_string(),
4187 );
4188 }
4189 }
4190 Ok(())
4191}
4192
4193fn eval_expr(
4194 view: &GraphView,
4195 vars: &VarTable,
4196 row: &Row,
4197 expr: &Expr,
4198 params: &Params,
4199 depth: u32,
4200) -> Result<bool, String> {
4201 if depth > 256 {
4202 return Err("expression nesting too deep".into());
4203 }
4204 match expr {
4205 Expr::And(lhs, rhs) => {
4206 let l = eval_expr(view, vars, row, lhs, params, depth + 1)?;
4207 let r = eval_expr(view, vars, row, rhs, params, depth + 1)?;
4208 Ok(l && r)
4209 }
4210 Expr::Or(lhs, rhs) => {
4211 let l = eval_expr(view, vars, row, lhs, params, depth + 1)?;
4212 let r = eval_expr(view, vars, row, rhs, params, depth + 1)?;
4213 Ok(l || r)
4214 }
4215 Expr::Not(inner) => Ok(!eval_expr(view, vars, row, inner, params, depth + 1)?),
4216 Expr::Cmp { lhs, op, rhs } => {
4217 let l = resolve_operand(view, vars, row, lhs, params)?;
4218 let r = resolve_operand(view, vars, row, rhs, params)?;
4219 match (l, r) {
4220 (Some(a), Some(b)) => Ok(eval_cmp(op, &a, &b)),
4221 _ => Ok(false),
4222 }
4223 }
4224 Expr::Truthy(op) => {
4225 let val = resolve_operand(view, vars, row, op, params)?;
4226 Ok(match val {
4227 None => false,
4228 Some(Value::Bool(b)) => b,
4229 Some(Value::Int(n)) => n != 0,
4230 Some(Value::Float(f)) => f != 0.0,
4231 Some(Value::Str(s)) => !s.is_empty(),
4232 Some(Value::List(v)) => !v.is_empty(),
4233 Some(Value::Map(m)) => !m.is_empty(),
4234 })
4235 }
4236 Expr::IsNull(op) => {
4237 let val = resolve_operand(view, vars, row, op, params)?;
4238 Ok(val.is_none())
4239 }
4240 Expr::IsNotNull(op) => {
4241 let val = resolve_operand(view, vars, row, op, params)?;
4242 Ok(val.is_some())
4243 }
4244 Expr::In { expr, list } => eval_in(view, vars, row, expr, list, params),
4245 }
4246}
4247
4248fn eval_in(
4249 view: &GraphView,
4250 vars: &VarTable,
4251 row: &Row,
4252 expr: &Operand,
4253 list: &[Operand],
4254 params: &Params,
4255) -> Result<bool, String> {
4256 let Some(needle) = resolve_operand(view, vars, row, expr, params)? else {
4257 return Ok(false);
4258 };
4259 for item_op in list {
4260 match resolve_operand(view, vars, row, item_op, params)? {
4261 None => {}
4262 Some(Value::List(items)) => {
4263 for item in items {
4264 if crate::filter::eval_cmp(&crate::filter::CmpOp::Eq, &needle, &item) {
4265 return Ok(true);
4266 }
4267 }
4268 }
4269 Some(item) if crate::filter::eval_cmp(&crate::filter::CmpOp::Eq, &needle, &item) => {
4270 return Ok(true);
4271 }
4272 Some(_) => {}
4273 }
4274 }
4275 Ok(false)
4276}
4277
4278fn exec_distinct(table: &mut Projected) -> Result<(), String> {
4280 let cap = max_intermediate_rows();
4281 let mut seen: BTreeSet<Vec<Option<ValueKey>>> = BTreeSet::new();
4282 let mut out = Vec::with_capacity(table.rows.len().min(cap));
4283 for row in table.rows.drain(..) {
4284 let key: Vec<Option<ValueKey>> = row
4285 .iter()
4286 .map(|cell| cell.as_ref().and_then(group_key_normalize))
4287 .collect();
4288 if seen.insert(key) {
4289 if out.len() >= cap {
4290 return Err(row_cap_err(cap));
4291 }
4292 out.push(row);
4293 }
4294 }
4295 table.rows = out;
4296 Ok(())
4297}
4298
4299fn column_name(item: &RetItem) -> String {
4300 if let Some(alias) = &item.alias {
4301 return alias.clone();
4302 }
4303 ret_val_label(&item.value).unwrap_or_else(|| match &item.value {
4307 RetVal::Agg { func, arg } => {
4308 let f = match func {
4309 AggFunc::Count => "COUNT",
4310 AggFunc::Sum => "SUM",
4311 AggFunc::Avg => "AVG",
4312 AggFunc::Min => "MIN",
4313 AggFunc::Max => "MAX",
4314 AggFunc::Collect => "COLLECT",
4315 };
4316 format!("{f}({})", agg_arg_label(arg))
4317 }
4318 _ => unreachable!("ret_val_label names every non-aggregate item"),
4319 })
4320}
4321
4322fn exec_project(
4323 view: &GraphView,
4324 vars: &VarTable,
4325 rows: &[Row],
4326 items: &[RetItem],
4327 params: &Params,
4328) -> Result<Projected, String> {
4329 let columns: Vec<String> = items.iter().map(column_name).collect();
4330 let mut out_rows = Vec::with_capacity(rows.len());
4331 for row in rows {
4332 let mut cells = Vec::with_capacity(items.len());
4333 for item in items {
4334 cells.push(project_item(view, vars, row, item, params)?);
4335 }
4336 out_rows.push(cells);
4337 }
4338 Ok(Projected {
4339 columns,
4340 rows: out_rows,
4341 })
4342}
4343
4344fn project_item(
4345 view: &GraphView,
4346 vars: &VarTable,
4347 row: &Row,
4348 item: &RetItem,
4349 params: &Params,
4350) -> Result<Option<Value>, String> {
4351 match &item.value {
4352 RetVal::Var(v) => {
4353 let slot = vars.slot(v).ok_or_else(|| format!("unbound variable `{v}`"))?;
4355 match row.get(slot).and_then(|c| c.as_ref()) {
4356 None => Ok(None),
4358 Some(Cell::Node(id)) => match view.ids.key_of(*id) {
4359 Some(key) => Ok(Some(Value::Str(key.to_owned()))),
4360 None => Err(format!("unknown node id {id}")),
4361 },
4362 Some(Cell::Scalar(val)) => Ok(Some(val.clone())),
4364 Some(Cell::Rel(_)) => Err(format!(
4365 "variable `{v}` is a relationship; return its properties ({v}.field) instead"
4366 )),
4367 Some(Cell::Path(hops)) => Ok(Some(Value::Int(*hops as i64))),
4368 }
4369 }
4370 RetVal::Prop { var, field } => resolve_prop(view, vars, row, var, field),
4371 RetVal::Agg { .. } => Err(
4374 "project_item: Agg variant reached exec_project — aggregate plans must not contain Project"
4375 .to_string(),
4376 ),
4377 RetVal::FuncCall { name, args } => eval_func(name, args, view, vars, row, params),
4378 RetVal::ScalarExpr(op) => resolve_operand(view, vars, row, op, params),
4379 }
4380}
4381
4382fn order_column(item: &OrderItem) -> String {
4383 match &item.target {
4384 OrderTarget::Alias(name) | OrderTarget::Var(name) => name.clone(),
4385 OrderTarget::Prop { var, field } => format!("{var}.{field}"),
4386 }
4387}
4388
4389fn exec_order_by(table: &mut Projected, items: &[OrderItem]) -> Result<(), String> {
4390 let mut keys = Vec::with_capacity(items.len());
4391 for item in items {
4392 let name = order_column(item);
4393 let idx = table
4394 .columns
4395 .iter()
4396 .position(|c| c == &name)
4397 .ok_or_else(|| format!("ORDER BY target `{name}` is not a projected column"))?;
4398 keys.push((idx, item.descending));
4399 }
4400 table.rows.sort_by(|a, b| {
4401 for &(idx, desc) in &keys {
4402 let c = cmp_optional(
4403 a.get(idx).and_then(|x| x.as_ref()),
4404 b.get(idx).and_then(|x| x.as_ref()),
4405 desc,
4406 );
4407 if c != std::cmp::Ordering::Equal {
4408 return c;
4409 }
4410 }
4411 std::cmp::Ordering::Equal
4412 });
4413 Ok(())
4414}
4415
4416fn resolve_ls(ls: &LimitSkip, params: &Params) -> Result<u64, String> {
4421 match ls {
4422 LimitSkip::Exact(n) => Ok(*n),
4423 LimitSkip::Param(name) => {
4424 let val = params
4425 .0
4426 .get(name)
4427 .ok_or_else(|| format!("missing parameter `{name}` (used in LIMIT/SKIP)"))?;
4428 match val {
4429 Value::Int(i) if *i >= 0 => Ok(*i as u64),
4430 Value::Int(i) => Err(format!(
4431 "LIMIT/SKIP parameter `{name}` must be a non-negative integer, got {i}"
4432 )),
4433 other => Err(format!(
4434 "LIMIT/SKIP parameter `{name}` must be an integer, got {other:?}"
4435 )),
4436 }
4437 }
4438 }
4439}
4440
4441fn apply_skip<T>(rows: &mut Vec<T>, n: u64) {
4442 let n = usize::try_from(n).unwrap_or(usize::MAX);
4443 if n >= rows.len() {
4444 rows.clear();
4445 } else {
4446 rows.drain(0..n);
4447 }
4448}
4449
4450fn apply_limit<T>(rows: &mut Vec<T>, n: u64) {
4451 let n = usize::try_from(n).unwrap_or(usize::MAX);
4452 rows.truncate(n);
4453}
4454
4455#[cfg(test)]
4456mod tests {
4457 use super::{execute, resolve_operand, Params, Row, VarTable};
4458 use crate::cypher::ast::{
4459 ArithOp, LimitSkip, Operand, OrderItem, OrderTarget, RetItem, RetVal,
4460 };
4461 use crate::cypher::plan::{plan, PlanOp};
4462 use crate::cypher::{lex, parse, RelDir};
4463 use crate::result::ResultSet;
4464 use crate::view::GraphView;
4465 use core_storage::v8::seam::{ColumnsView, EdgePropsView, TopologyView};
4466 use core_storage::{ColumnStore, EdgeProps, IdMap, Interner, Topology, Value};
4467 use proptest::prelude::*;
4468 use std::collections::BTreeMap;
4469
4470 struct Fx {
4471 ids: IdMap,
4472 syms: Interner,
4473 labels: Vec<u32>,
4474 props: ColumnStore,
4475 topo: Topology,
4476 eprops: EdgeProps,
4477 }
4478
4479 impl Fx {
4480 fn new() -> Self {
4481 Fx {
4482 ids: IdMap::new(),
4483 syms: Interner::new(),
4484 labels: vec![],
4485 props: ColumnStore::new(),
4486 topo: Topology::new(),
4487 eprops: EdgeProps::new(),
4488 }
4489 }
4490
4491 fn add(&mut self, label: &str, key: &str, props: Vec<(&str, Value)>) -> u32 {
4492 let id = self.ids.get_or_insert(key);
4493 let sym = self.syms.intern(label);
4494 self.labels.resize(id as usize + 1, u32::MAX);
4495 self.labels[id as usize] = sym;
4496 for (f, v) in props {
4497 self.props.set(id, f, v);
4498 }
4499 id
4500 }
4501
4502 fn edge(&mut self, etype: &str, src: u32, dst: u32, props: Vec<(&str, Value)>) {
4503 let et = self.syms.intern(etype);
4504 self.topo.add_edge(et, src, dst);
4505 for (f, v) in props {
4506 self.eprops.set(et, src, dst, f, v);
4507 }
4508 }
4509
4510 fn view(&self) -> GraphView<'_> {
4511 GraphView {
4512 ids: &self.ids,
4513 syms: &self.syms,
4514 labels: &self.labels,
4515 props: ColumnsView::owned(&self.props),
4516 topo: TopologyView::owned(&self.topo),
4517 edge_props: EdgePropsView::owned(&self.eprops),
4518 mask: None,
4519 prop_index: None,
4520 }
4521 }
4522
4523 fn view_indexed<'a>(
4524 &'a self,
4525 index: &'a core_storage::property_index::PropertyIndex,
4526 ) -> GraphView<'a> {
4527 GraphView {
4528 prop_index: Some(index),
4529 ..self.view()
4530 }
4531 }
4532 }
4533
4534 fn compile(src: &str) -> Vec<PlanOp> {
4535 plan(&parse(&lex(src).expect("lex")).expect("parse")).expect("plan")
4536 }
4537
4538 fn run(
4539 view: &GraphView,
4540 src: &str,
4541 params: &BTreeMap<String, Value>,
4542 ) -> Result<ResultSet, String> {
4543 execute(view, &compile(src), &Params(params))
4544 }
4545
4546 fn s(v: &str) -> Value {
4547 Value::Str(v.into())
4548 }
4549
4550 fn f(v: f64) -> Value {
4551 Value::Float(v)
4552 }
4553
4554 fn i(v: i64) -> Value {
4555 Value::Int(v)
4556 }
4557
4558 fn rows_of(rs: &ResultSet) -> Vec<Vec<Option<Value>>> {
4559 (0..rs.len()).map(|i| rs.row(i).to_vec()).collect()
4560 }
4561
4562 fn col(rs: &ResultSet, name: &str) -> Vec<Option<Value>> {
4563 (0..rs.len()).map(|i| rs.get(i, name).cloned()).collect()
4564 }
4565
4566 fn hop_graph() -> Fx {
4567 let mut fx = Fx::new();
4568 let ada = fx.add("Person", "ada", vec![]);
4569 let bob = fx.add("Person", "bob", vec![]);
4570 let cam = fx.add("Person", "cam", vec![]);
4571 let acme = fx.add("Company", "acme", vec![]);
4572 fx.edge("KNOWS", ada, bob, vec![]);
4573 fx.edge("KNOWS", ada, cam, vec![]);
4574 fx.edge("KNOWS", bob, cam, vec![]);
4575 fx.edge("LIKES", ada, acme, vec![]);
4576 fx
4577 }
4578
4579 fn undirected_graph() -> Fx {
4580 let mut fx = Fx::new();
4581 let a = fx.add("N", "a", vec![]);
4582 let b = fx.add("N", "b", vec![]);
4583 fx.edge("T", a, b, vec![("w", i(42))]);
4584 fx
4585 }
4586
4587 fn triangle() -> Fx {
4588 let mut fx = Fx::new();
4589 let a = fx.add("N", "a", vec![]);
4590 let b = fx.add("N", "b", vec![]);
4591 let c = fx.add("N", "c", vec![]);
4592 fx.edge("T", a, b, vec![("eid", i(1))]);
4593 fx.edge("T", b, c, vec![("eid", i(2))]);
4594 fx.edge("T", c, a, vec![("eid", i(3))]);
4595 fx
4596 }
4597
4598 fn single_edge() -> (Fx, u32, u32) {
4599 let mut fx = Fx::new();
4600 let a = fx.add("N", "a", vec![]);
4601 let b = fx.add("N", "b", vec![]);
4602 fx.edge("T", a, b, vec![]);
4603 (fx, a, b)
4604 }
4605
4606 fn dogfood_graph() -> Fx {
4608 let mut fx = Fx::new();
4609 let t1 = fx.add("Talent", "t1", vec![("id", s("t1"))]);
4610 let acme = fx.add("Company", "acme", vec![]);
4611 let beta = fx.add("Company", "beta", vec![]);
4612 let gamma = fx.add("Company", "gamma", vec![]);
4613 let delta = fx.add("Company", "delta", vec![]);
4614 let echo = fx.add("Company", "echo", vec![]);
4615 let foxtrot = fx.add("Company", "foxtrot", vec![]);
4616 let zeta = fx.add("Company", "zeta", vec![]);
4617 fx.edge("INDUSTRY_ALIGNMENT", acme, t1, vec![("score", f(0.9))]);
4618 fx.edge("SPECIALTY_MATCH", acme, t1, vec![("score", f(0.8))]);
4619 fx.edge("INDUSTRY_ALIGNMENT", beta, t1, vec![("score", f(0.6))]);
4620 fx.edge("SPECIALTY_MATCH", beta, t1, vec![("score", f(0.7))]);
4621 fx.edge("INDUSTRY_ALIGNMENT", gamma, t1, vec![("score", f(0.4))]);
4622 fx.edge("SPECIALTY_MATCH", gamma, t1, vec![("score", f(0.9))]);
4623 fx.edge("INDUSTRY_ALIGNMENT", delta, t1, vec![("score", f(0.8))]);
4624 fx.edge("SPECIALTY_MATCH", delta, t1, vec![("score", f(0.3))]);
4625 fx.edge("INDUSTRY_ALIGNMENT", echo, t1, vec![("score", f(0.5))]);
4626 fx.edge("SPECIALTY_MATCH", echo, t1, vec![("score", f(0.5))]);
4627 fx.edge("INDUSTRY_ALIGNMENT", foxtrot, t1, vec![("score", f(0.95))]);
4628 fx.edge("INDUSTRY_ALIGNMENT", zeta, t1, vec![("score", f(0.9))]);
4629 fx.edge("SPECIALTY_MATCH", zeta, t1, vec![("score", f(0.6))]);
4630 fx
4631 }
4632
4633 const DOGFOOD: &str = "\
4634MATCH (t:Talent {id: $tid}) \
4635MATCH (c:Company)-[i:INDUSTRY_ALIGNMENT]->(t) \
4636MATCH (c)-[s:SPECIALTY_MATCH]->(t) \
4637WHERE i.score >= 0.5 AND s.score >= 0.5 \
4638RETURN c, i.score AS industry, s.score AS specialty \
4639ORDER BY industry DESC, specialty DESC \
4640LIMIT 10";
4641
4642 fn tid_params() -> BTreeMap<String, Value> {
4643 let mut p = BTreeMap::new();
4644 p.insert("tid".into(), s("t1"));
4645 p
4646 }
4647
4648 #[test]
4649 fn single_hop_match_label_and_etype_filters() {
4650 let fx = hop_graph();
4651 let v = fx.view();
4652 let rs = run(
4653 &v,
4654 "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b",
4655 &BTreeMap::new(),
4656 )
4657 .expect("single-hop");
4658 assert_eq!(rs.columns(), &["a".to_string(), "b".to_string()]);
4659 assert_eq!(
4660 rows_of(&rs),
4661 vec![
4662 vec![Some(s("ada")), Some(s("bob"))],
4663 vec![Some(s("ada")), Some(s("cam"))],
4664 vec![Some(s("bob")), Some(s("cam"))],
4665 ]
4666 );
4667 let likes = run(
4669 &v,
4670 "MATCH (a:Person)-[:LIKES]->(b:Company) RETURN a, b",
4671 &BTreeMap::new(),
4672 )
4673 .unwrap();
4674 assert_eq!(rows_of(&likes), vec![vec![Some(s("ada")), Some(s("acme"))]]);
4675 let no_combo = run(
4676 &v,
4677 "MATCH (a:Person)-[:KNOWS]->(b:Company) RETURN a, b",
4678 &BTreeMap::new(),
4679 )
4680 .unwrap();
4681 assert!(no_combo.is_empty());
4682 }
4683
4684 #[test]
4685 fn undirected_match_finds_both_orientations_and_binds_true_triple() {
4686 let fx = undirected_graph();
4687 let v = fx.view();
4688 let rs =
4691 run(&v, "MATCH (x)-[r:T]-(y) RETURN x, y, r.w", &BTreeMap::new()).expect("undirected");
4692 assert_eq!(
4693 rows_of(&rs),
4694 vec![
4695 vec![Some(s("a")), Some(s("b")), Some(i(42))],
4696 vec![Some(s("b")), Some(s("a")), Some(i(42))],
4697 ]
4698 );
4699
4700 let left = run(
4701 &v,
4702 "MATCH (y)<-[r:T]-(x) RETURN x, y, r.w",
4703 &BTreeMap::new(),
4704 )
4705 .unwrap();
4706 assert_eq!(
4707 rows_of(&left),
4708 vec![vec![Some(s("a")), Some(s("b")), Some(i(42))]]
4709 );
4710 }
4711
4712 #[test]
4713 fn relationship_uniqueness_triangle_and_two_hop_cycle() {
4714 let tri = triangle();
4715 let v = tri.view();
4716 let rs = run(
4717 &v,
4718 "MATCH (x)-[r1:T]->(y)-[r2:T]->(z) RETURN x, y, z, r1.eid, r2.eid",
4719 &BTreeMap::new(),
4720 )
4721 .expect("triangle 2-hop");
4722 assert_eq!(
4723 rows_of(&rs),
4724 vec![
4725 vec![
4726 Some(s("a")),
4727 Some(s("b")),
4728 Some(s("c")),
4729 Some(i(1)),
4730 Some(i(2))
4731 ],
4732 vec![
4733 Some(s("b")),
4734 Some(s("c")),
4735 Some(s("a")),
4736 Some(i(2)),
4737 Some(i(3))
4738 ],
4739 vec![
4740 Some(s("c")),
4741 Some(s("a")),
4742 Some(s("b")),
4743 Some(i(3)),
4744 Some(i(1))
4745 ],
4746 ]
4747 );
4748 for row in rows_of(&rs) {
4749 assert_ne!(row[3], row[4], "r1 must never bind the same edge as r2");
4750 }
4751
4752 let (mut one, a, b) = single_edge();
4753 let v = one.view();
4754 let cycle = run(
4755 &v,
4756 "MATCH (x)-[:T]->(y)-[:T]->(x) RETURN x",
4757 &BTreeMap::new(),
4758 )
4759 .expect("2-hop cycle");
4760 assert!(
4761 cycle.is_empty(),
4762 "single directed edge cannot close a 2-hop cycle"
4763 );
4764
4765 let undirected_cycle = run(&v, "MATCH (x)-[:T]-(y)-[:T]-(x) RETURN x", &BTreeMap::new())
4767 .expect("undirected uniqueness");
4768 assert!(
4769 undirected_cycle.is_empty(),
4770 "relationship uniqueness must reject walking the same triple back"
4771 );
4772
4773 one.edge("T", b, a, vec![]);
4774 let v = one.view();
4775 let with_recip = run(
4776 &v,
4777 "MATCH (x)-[:T]->(y)-[:T]->(x) RETURN x",
4778 &BTreeMap::new(),
4779 )
4780 .unwrap();
4781 assert_eq!(col(&with_recip, "x"), vec![Some(s("a")), Some(s("b"))]);
4782 }
4783
4784 #[test]
4785 fn multi_match_join_bound_and_bound_destination_expand() {
4786 let fx = dogfood_graph();
4787 let v = fx.view();
4788 let rs = run(
4791 &v,
4792 "MATCH (t:Talent {id: $tid}) \
4793 MATCH (c:Company)-[i:INDUSTRY_ALIGNMENT]->(t) \
4794 MATCH (c)-[s:SPECIALTY_MATCH]->(t) \
4795 RETURN c",
4796 &tid_params(),
4797 )
4798 .expect("join + bound dest");
4799 assert_eq!(
4800 col(&rs, "c"),
4801 vec![
4802 Some(s("acme")),
4803 Some(s("beta")),
4804 Some(s("gamma")),
4805 Some(s("delta")),
4806 Some(s("echo")),
4807 Some(s("zeta")),
4808 ]
4809 );
4810
4811 let keep = run(&v, "MATCH (c:Company) MATCH (c) RETURN c", &BTreeMap::new()).unwrap();
4813 assert_eq!(keep.len(), 7);
4814 let drop = run(
4816 &v,
4817 "MATCH (c:Company) MATCH (c:Talent) RETURN c",
4818 &BTreeMap::new(),
4819 )
4820 .unwrap();
4821 assert!(drop.is_empty());
4822 }
4823
4824 #[test]
4825 fn scan_key_exec_does_not_use_label_scan() {
4826 let mut fx = Fx::new();
4827 fx.add("Person", "p1", vec![]);
4828 fx.add("Person", "p2", vec![]);
4829 fx.add("Person", "p3", vec![]);
4830 fx.add("Company", "c1", vec![]);
4831 let v = fx.view();
4832 let params = BTreeMap::new();
4833
4834 let fires_before = super::SCAN_KEY_FIRES.load(std::sync::atomic::Ordering::Relaxed);
4835 let rs = run(&v, "MATCH (n:Person {id: 'p2'}) RETURN n", ¶ms).expect("scan key");
4836 let fires_after = super::SCAN_KEY_FIRES.load(std::sync::atomic::Ordering::Relaxed);
4837 assert!(
4838 fires_after > fires_before,
4839 "SCAN_KEY_FIRES must increment; before={fires_before} after={fires_after}"
4840 );
4841 assert_eq!(rows_of(&rs), vec![vec![Some(s("p2"))]]);
4842
4843 let miss = run(&v, "MATCH (n:Person {id: 'nope'}) RETURN n", ¶ms).unwrap();
4844 assert!(
4845 miss.is_empty(),
4846 "missing key must be zero rows, not an error"
4847 );
4848
4849 let wrong = run(&v, "MATCH (n:Person {id: 'c1'}) RETURN n", ¶ms).unwrap();
4850 assert!(
4851 wrong.is_empty(),
4852 "wrong label must be zero rows, not an error"
4853 );
4854 }
4855
4856 #[test]
4857 fn rel_var_edge_prop_filter() {
4858 let mut fx = Fx::new();
4859 let a = fx.add("N", "a", vec![]);
4860 let b = fx.add("N", "b", vec![]);
4861 let c = fx.add("N", "c", vec![]);
4862 fx.edge("T", a, b, vec![("w", f(0.7))]);
4863 fx.edge("T", a, c, vec![("w", f(0.3))]);
4864 let v = fx.view();
4865 let rs = run(
4866 &v,
4867 "MATCH (x)-[r:T]->(y) WHERE r.w >= 0.5 RETURN y, r.w",
4868 &BTreeMap::new(),
4869 )
4870 .expect("edge-prop filter");
4871 assert_eq!(rows_of(&rs), vec![vec![Some(s("b")), Some(f(0.7))]]);
4872 let fail = run(
4873 &v,
4874 "MATCH (x)-[r:T]->(y) WHERE r.w >= 0.8 RETURN y",
4875 &BTreeMap::new(),
4876 )
4877 .unwrap();
4878 assert!(fail.is_empty());
4879 }
4880
4881 #[test]
4882 fn params_present_resolve_missing_is_err_before_rows() {
4883 let fx = dogfood_graph();
4884 let v = fx.view();
4885 let hit =
4886 run(&v, "MATCH (t:Talent {id: $tid}) RETURN t", &tid_params()).expect("present param");
4887 assert_eq!(col(&hit, "t"), vec![Some(s("t1"))]);
4888
4889 let err = run(
4891 &v,
4892 "MATCH (t:NoSuchLabel {id: $tid}) RETURN t",
4893 &BTreeMap::new(),
4894 )
4895 .expect_err("missing param must be Err, not Ok(empty)");
4896 assert!(
4897 err.contains("tid")
4898 && (err.contains("param") || err.contains("Param") || err.contains("missing")),
4899 "missing-param error must name the parameter, got: {err}"
4900 );
4901
4902 let err = run(
4903 &v,
4904 "MATCH (t:Talent) WHERE t.id = $tid RETURN t",
4905 &BTreeMap::new(),
4906 )
4907 .expect_err("missing WHERE param");
4908 assert!(err.contains("tid"), "got: {err}");
4909 }
4910
4911 #[test]
4912 fn order_by_none_last_then_skip_limit() {
4913 let mut fx = Fx::new();
4914 fx.add("Person", "ada", vec![("age", i(30))]);
4915 fx.add("Person", "bob", vec![]); fx.add("Person", "cam", vec![("age", i(10))]);
4917 fx.add("Person", "dan", vec![("age", i(20))]);
4918 let v = fx.view();
4919
4920 let asc = run(
4921 &v,
4922 "MATCH (p:Person) RETURN p, p.age AS age ORDER BY age",
4923 &BTreeMap::new(),
4924 )
4925 .unwrap();
4926 assert_eq!(
4927 rows_of(&asc),
4928 vec![
4929 vec![Some(s("cam")), Some(i(10))],
4930 vec![Some(s("dan")), Some(i(20))],
4931 vec![Some(s("ada")), Some(i(30))],
4932 vec![Some(s("bob")), None],
4933 ]
4934 );
4935
4936 let desc = run(
4937 &v,
4938 "MATCH (p:Person) RETURN p, p.age AS age ORDER BY age DESC",
4939 &BTreeMap::new(),
4940 )
4941 .unwrap();
4942 assert_eq!(
4943 rows_of(&desc),
4944 vec![
4945 vec![Some(s("ada")), Some(i(30))],
4946 vec![Some(s("dan")), Some(i(20))],
4947 vec![Some(s("cam")), Some(i(10))],
4948 vec![Some(s("bob")), None],
4949 ]
4950 );
4951
4952 let skip_lim = run(
4953 &v,
4954 "MATCH (p:Person) RETURN p, p.age AS age ORDER BY age SKIP 1 LIMIT 2",
4955 &BTreeMap::new(),
4956 )
4957 .unwrap();
4958 assert_eq!(
4959 rows_of(&skip_lim),
4960 vec![
4961 vec![Some(s("dan")), Some(i(20))],
4962 vec![Some(s("ada")), Some(i(30))],
4963 ]
4964 );
4965
4966 let desc_sl = run(
4967 &v,
4968 "MATCH (p:Person) RETURN p, p.age AS age ORDER BY age DESC SKIP 1 LIMIT 2",
4969 &BTreeMap::new(),
4970 )
4971 .unwrap();
4972 assert_eq!(
4973 rows_of(&desc_sl),
4974 vec![
4975 vec![Some(s("dan")), Some(i(20))],
4976 vec![Some(s("cam")), Some(i(10))],
4977 ]
4978 );
4979 }
4980
4981 #[test]
4982 fn unknown_label_and_etype_are_ok_empty() {
4983 let fx = hop_graph();
4984 let v = fx.view();
4985 let lab = run(&v, "MATCH (x:Nope) RETURN x", &BTreeMap::new()).expect("unknown label");
4986 assert!(lab.is_empty());
4987 let et = run(
4988 &v,
4989 "MATCH (a)-[:NO_SUCH_ETYPE]->(b) RETURN a",
4990 &BTreeMap::new(),
4991 )
4992 .expect("unknown etype");
4993 assert!(et.is_empty());
4994 }
4995
4996 #[test]
4997 fn execute_is_deterministic() {
4998 let fx = dogfood_graph();
4999 let v = fx.view();
5000 let p = tid_params();
5001 let a = run(&v, DOGFOOD, &p).expect("first");
5002 let b = run(&v, DOGFOOD, &p).expect("second");
5003 assert_eq!(a, b);
5004 let hop = hop_graph();
5005 let hv = hop.view();
5006 let q = "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b";
5007 assert_eq!(run(&hv, q, &BTreeMap::new()), run(&hv, q, &BTreeMap::new()));
5008 }
5009
5010 #[test]
5011 fn dogfood_pipeline_exact_rows() {
5012 let fx = dogfood_graph();
5013 let v = fx.view();
5014 let rs = run(&v, DOGFOOD, &tid_params()).expect("dogfood");
5015 assert_eq!(
5016 rs.columns(),
5017 &[
5018 "c".to_string(),
5019 "industry".to_string(),
5020 "specialty".to_string()
5021 ]
5022 );
5023 assert_eq!(
5025 rows_of(&rs),
5026 vec![
5027 vec![Some(s("acme")), Some(f(0.9)), Some(f(0.8))],
5028 vec![Some(s("zeta")), Some(f(0.9)), Some(f(0.6))],
5029 vec![Some(s("beta")), Some(f(0.6)), Some(f(0.7))],
5030 vec![Some(s("echo")), Some(f(0.5)), Some(f(0.5))],
5031 ]
5032 );
5033 }
5034
5035 #[test]
5036 fn unknown_var_in_op_is_err_not_panic() {
5037 let fx = hop_graph();
5038 let v = fx.view();
5039 let plan = vec![
5040 PlanOp::ScanLabel {
5041 var: "a".into(),
5042 label: None,
5043 },
5044 PlanOp::Expand {
5045 from: "zzz".into(),
5046 rel_var: Some("r".into()),
5047 etypes: vec![],
5048 dir: RelDir::Right,
5049 to: "b".into(),
5050 to_label: None,
5051 to_props: vec![],
5052 },
5053 PlanOp::Project {
5054 items: vec![RetItem {
5055 value: RetVal::Var("a".into()),
5056 alias: None,
5057 }],
5058 },
5059 ];
5060 let params = BTreeMap::new();
5061 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5062 execute(&v, &plan, &Params(¶ms))
5063 }));
5064 assert!(caught.is_ok(), "execute panicked on unknown var");
5065 let err = caught.unwrap().expect_err("unknown var must be Err");
5066 assert!(
5067 err.contains("zzz") && err.to_ascii_lowercase().contains("unbound"),
5068 "got: {err}"
5069 );
5070
5071 let join = vec![
5072 PlanOp::JoinBound {
5073 var: "ghost".into(),
5074 label: None,
5075 props: vec![],
5076 },
5077 PlanOp::Project {
5078 items: vec![RetItem {
5079 value: RetVal::Var("ghost".into()),
5080 alias: None,
5081 }],
5082 },
5083 ];
5084 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5085 execute(&v, &join, &Params(¶ms))
5086 }));
5087 assert!(caught.is_ok(), "execute panicked on JoinBound unknown var");
5088 assert!(caught.unwrap().is_err());
5089 }
5090
5091 #[test]
5092 fn dest_props_on_bound_expand_are_applied() {
5093 let fx = dogfood_graph();
5094 let v = fx.view();
5095 let rs = run(
5096 &v,
5097 "MATCH (t:Talent {id: $tid}) \
5098 MATCH (c:Company)-[r:INDUSTRY_ALIGNMENT]->(t:Talent {id: $tid}) \
5099 RETURN c",
5100 &tid_params(),
5101 )
5102 .unwrap();
5103 assert_eq!(
5105 col(&rs, "c"),
5106 vec![
5107 Some(s("acme")),
5108 Some(s("beta")),
5109 Some(s("gamma")),
5110 Some(s("delta")),
5111 Some(s("echo")),
5112 Some(s("foxtrot")),
5113 Some(s("zeta")),
5114 ]
5115 );
5116 let miss = run(
5117 &v,
5118 "MATCH (t:Talent {id: $tid}) \
5119 MATCH (c:Company)-[r:INDUSTRY_ALIGNMENT]->(t {id: 'nope'}) \
5120 RETURN c",
5121 &tid_params(),
5122 )
5123 .unwrap();
5124 assert!(miss.is_empty());
5125 }
5126
5127 #[test]
5128 fn missing_node_prop_projects_none_and_pattern_misses() {
5129 let mut fx = Fx::new();
5130 fx.add("Person", "ada", vec![("age", i(30))]);
5131 fx.add("Person", "bob", vec![]);
5132 let v = fx.view();
5133 let rs = run(&v, "MATCH (p:Person) RETURN p.age", &BTreeMap::new()).unwrap();
5134 assert_eq!(col(&rs, "p.age"), vec![Some(i(30)), None]);
5135 let pat = run(&v, "MATCH (p:Person {age: 30}) RETURN p", &BTreeMap::new()).unwrap();
5136 assert_eq!(col(&pat, "p"), vec![Some(s("ada"))]);
5137 }
5138
5139 #[test]
5140 fn unlabeled_match_does_not_project_sentinel_ghost_key() {
5141 let mut fx = Fx::new();
5142 fx.add("Person", "ada", vec![]);
5143 fx.ids.get_or_insert("ghost");
5146 fx.labels.resize(fx.ids.len(), u32::MAX);
5147 fx.add("Person", "bob", vec![]);
5148 let v = fx.view();
5149 let rs = run(&v, "MATCH (n) RETURN n", &BTreeMap::new()).expect("unlabeled scan");
5150 assert_eq!(col(&rs, "n"), vec![Some(s("ada")), Some(s("bob"))]);
5151 assert!(
5152 !rows_of(&rs)
5153 .iter()
5154 .any(|row| row.iter().any(|c| *c == Some(s("ghost")))),
5155 "sentinel slot must not project a ghost key"
5156 );
5157 }
5158
5159 #[test]
5160 fn execute_never_panics_on_hostile_plans() {
5161 let fx = hop_graph();
5162 let v = fx.view();
5163 let params = BTreeMap::new();
5164 let hostile = vec![
5165 vec![],
5166 vec![PlanOp::Project { items: vec![] }],
5167 vec![PlanOp::OrderBy {
5168 items: vec![OrderItem {
5169 target: OrderTarget::Alias("nope".into()),
5170 descending: false,
5171 }],
5172 }],
5173 vec![PlanOp::Filter {
5174 expr: crate::cypher::ast::Expr::Cmp {
5175 lhs: Operand::Prop {
5176 var: "missing".into(),
5177 field: "x".into(),
5178 },
5179 op: crate::filter::CmpOp::Eq,
5180 rhs: Operand::Lit(i(1)),
5181 },
5182 }],
5183 vec![
5184 PlanOp::ScanLabel {
5185 var: "a".into(),
5186 label: None,
5187 },
5188 PlanOp::LookupProps {
5189 var: "zzz".into(),
5190 props: vec![("k".into(), Operand::Lit(i(1)))],
5191 },
5192 ],
5193 vec![
5194 PlanOp::Skip(LimitSkip::Exact(99)),
5195 PlanOp::Limit(LimitSkip::Exact(0)),
5196 ],
5197 ];
5198 for plan in hostile {
5199 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5200 execute(&v, &plan, &Params(¶ms))
5201 }));
5202 assert!(caught.is_ok(), "execute panicked on hostile plan: {plan:?}");
5203 }
5204 }
5205
5206 #[test]
5207 fn unlabeled_and_labeled_scans_skip_tombstoned_ids() {
5208 let mut fx = Fx::new();
5209 let ada = fx.add("Person", "ada", vec![]);
5210 let bob = fx.add("Person", "bob", vec![]);
5211 fx.edge("KNOWS", ada, bob, vec![]);
5212 fx.ids.delete("ada");
5214 fx.labels[ada as usize] = u32::MAX;
5215 let knows = fx.syms.get("KNOWS").unwrap();
5216 fx.topo.remove_edge(knows, ada, bob);
5217 let v = fx.view();
5218
5219 let labeled = run(&v, "MATCH (p:Person) RETURN p", &BTreeMap::new()).unwrap();
5220 assert_eq!(col(&labeled, "p"), vec![Some(s("bob"))]);
5221 let unlabeled = run(&v, "MATCH (n) RETURN n", &BTreeMap::new()).unwrap();
5222 assert_eq!(col(&unlabeled, "n"), vec![Some(s("bob"))]);
5223 let hop = run(&v, "MATCH (x)-[:KNOWS]->(y) RETURN x, y", &BTreeMap::new()).unwrap();
5224 assert!(
5225 hop.is_empty(),
5226 "expand cannot yield edges to a deleted node once topology is swept"
5227 );
5228 }
5229
5230 proptest! {
5235 #[test]
5248 fn prop_bounded_equals_unbounded_slice(
5249 n_nodes in 2u32..10u32,
5250 edge_pairs in proptest::collection::vec(
5251 (any::<u32>(), any::<u32>()), 0..20usize
5252 ),
5253 recip_pairs in proptest::collection::vec(
5255 (any::<u32>(), any::<u32>()), 0..8usize
5256 ),
5257 n_hops in 1u32..4u32, use_filter in any::<bool>(),
5259 threshold in 0i64..8i64, limit in 1u64..10u64,
5261 skip in 0u64..4u64,
5262 ) {
5263 let mut fx = Fx::new();
5264 let mut node_ids = Vec::new();
5265 for idx in 0..n_nodes {
5266 let id = fx.add("N", &format!("n{idx}"), vec![("v", i(idx as i64 % 8))]);
5268 node_ids.push(id);
5269 }
5270 let n = node_ids.len();
5271
5272 for (si, di) in &edge_pairs {
5274 let si = (*si as usize) % n;
5275 let di = (*di as usize) % n;
5276 if si != di {
5277 fx.edge("T", node_ids[si], node_ids[di], vec![]);
5278 }
5279 }
5280 for (si, di) in &recip_pairs {
5283 let si = (*si as usize) % n;
5284 let di = (*di as usize) % n;
5285 if si != di {
5286 fx.edge("T", node_ids[si], node_ids[di], vec![]);
5287 fx.edge("T", node_ids[di], node_ids[si], vec![]);
5288 }
5289 }
5290
5291 let v = fx.view();
5292 let params = BTreeMap::new();
5293
5294 let var_names = ["a", "b", "c", "d"];
5297 let hop_count = n_hops as usize;
5298 let mut pattern = format!("({}:N)", var_names[0]);
5299 for h in 0..hop_count {
5300 pattern.push_str(&format!("-[:T]->({}", var_names[h + 1]));
5301 if h + 1 == hop_count {
5303 pattern.push_str(":N)");
5304 } else {
5305 pattern.push(')');
5306 }
5307 }
5308 let last_var = var_names[hop_count];
5309 let where_clause = if use_filter {
5310 format!(" WHERE {last_var}.v > {threshold}")
5311 } else {
5312 String::new()
5313 };
5314 let ret_vars: Vec<&str> = var_names[..=hop_count].to_vec();
5315 let ret_clause = ret_vars.join(", ");
5316
5317 let full_q = format!("MATCH {pattern}{where_clause} RETURN {ret_clause}");
5318 let bounded_q = format!(
5319 "MATCH {pattern}{where_clause} RETURN {ret_clause} SKIP {skip} LIMIT {limit}"
5320 );
5321
5322 let full_plan = compile(&full_q);
5323 let unbounded = super::with_max_intermediate_rows(100_000, || {
5326 super::execute_unbounded(&v, &full_plan, &Params(¶ms))
5327 });
5328 let unbounded = match unbounded {
5331 Ok(rs) => rs,
5332 Err(_) => return Ok(()),
5333 };
5334 let total = unbounded.len();
5335 let full_rows = rows_of(&unbounded);
5336
5337 let bounded = super::with_max_intermediate_rows(100_000, || {
5338 run(&v, &bounded_q, ¶ms)
5339 }).expect("bounded must not error");
5340
5341 let s = (skip as usize).min(total);
5342 let e = (skip as usize + limit as usize).min(total);
5343 prop_assert_eq!(
5344 rows_of(&bounded),
5345 full_rows[s..e].to_vec(),
5346 "hops={} filter={} threshold={} SKIP {} LIMIT {}: \
5347 bounded != unbounded[{}..{}]",
5348 hop_count, use_filter, threshold, skip, limit, s, e
5349 );
5350 }
5351 }
5352
5353 proptest! {
5354 #[test]
5372 fn prop_scan_filter_fused_equals_unbounded_slice(
5373 n_nodes in 0u32..20u32,
5374 prop_mask in any::<u32>(),
5376 float_mask in any::<u32>(),
5378 threshold in -1i64..8i64,
5381 op_idx in 0u32..6u32,
5383 skip in 0u64..5u64,
5384 limit in 1u64..8u64,
5385 ) {
5386 let op_str = match op_idx {
5387 0 => "=",
5388 1 => "<>",
5389 2 => "<",
5390 3 => "<=",
5391 4 => ">",
5392 _ => ">=",
5393 };
5394
5395 let mut fx = Fx::new();
5396 for idx in 0..n_nodes {
5397 let has_prop = (prop_mask >> (idx % 32)) & 1 == 1;
5398 let use_float = (float_mask >> (idx % 32)) & 1 == 1;
5399 let props: Vec<(&str, Value)> = if has_prop {
5401 if use_float {
5402 vec![("v", f(idx as f64 % 7.0))]
5403 } else {
5404 vec![("v", i(idx as i64 % 7))]
5405 }
5406 } else {
5407 vec![]
5408 };
5409 fx.add("N", &format!("n{idx}"), props);
5410 }
5411
5412 let v = fx.view();
5413 let params = BTreeMap::new();
5414
5415 let full_q = format!("MATCH (n:N) WHERE n.v {op_str} {threshold} RETURN n");
5417 let bounded_q = format!(
5418 "MATCH (n:N) WHERE n.v {op_str} {threshold} RETURN n SKIP {skip} LIMIT {limit}"
5419 );
5420
5421 let full_plan = compile(&full_q);
5422 let unbounded = super::execute_unbounded(&v, &full_plan, &Params(¶ms));
5423 let unbounded = match unbounded {
5424 Ok(rs) => rs,
5425 Err(_) => return Ok(()),
5426 };
5427 let total = unbounded.len();
5428 let full_rows = rows_of(&unbounded);
5429
5430 let fires_before =
5432 super::FUSED_SCAN_FIRES.load(std::sync::atomic::Ordering::Relaxed);
5433 let bounded = run(&v, &bounded_q, ¶ms).expect("fused bounded must not error");
5434 let fires_after =
5435 super::FUSED_SCAN_FIRES.load(std::sync::atomic::Ordering::Relaxed);
5436
5437 if n_nodes > 0 && op_idx != 0 {
5445 prop_assert!(
5446 fires_after > fires_before,
5447 "fused arm did NOT fire for op={} threshold={} n_nodes={}: \
5448 counter before={} after={}",
5449 op_str,
5450 threshold,
5451 n_nodes,
5452 fires_before,
5453 fires_after
5454 );
5455 }
5456
5457 let s = (skip as usize).min(total);
5458 let e = (skip as usize + limit as usize).min(total);
5459 prop_assert_eq!(
5460 rows_of(&bounded),
5461 full_rows[s..e].to_vec(),
5462 "fused path: op={} threshold={} n_nodes={} SKIP {} LIMIT {}: \
5463 bounded != unbounded[{}..{}]",
5464 op_str, threshold, n_nodes, skip, limit, s, e
5465 );
5466
5467 let compound_q = format!(
5471 "MATCH (n:N) WHERE n.v {} {} AND n.v >= -999 RETURN n SKIP {} LIMIT {}",
5472 op_str, threshold, skip, limit
5473 );
5474 let fires_before_compound =
5475 super::FUSED_SCAN_FIRES.load(std::sync::atomic::Ordering::Relaxed);
5476 let compound_bounded =
5477 run(&v, &compound_q, ¶ms).expect("compound-AND bounded must not error");
5478 let fires_after_compound =
5479 super::FUSED_SCAN_FIRES.load(std::sync::atomic::Ordering::Relaxed);
5480
5481 prop_assert_eq!(
5483 fires_after_compound,
5484 fires_before_compound,
5485 "fused arm fired for compound-AND shape (should use generic path): \
5486 op={} threshold={} n_nodes={}",
5487 op_str,
5488 threshold,
5489 n_nodes
5490 );
5491 prop_assert_eq!(
5492 rows_of(&compound_bounded),
5493 full_rows[s..e].to_vec(),
5494 "compound-AND fallback: op={} threshold={} n_nodes={} SKIP {} LIMIT {}: \
5495 result differs from fused",
5496 op_str, threshold, n_nodes, skip, limit
5497 );
5498 }
5499 }
5500
5501 #[test]
5512 fn dense_hop1_with_filter_survives_pull() {
5513 const LEAVES: usize = 120; const CAP: usize = 100;
5515
5516 let mut fx = Fx::new();
5517 let src = fx.add("Src", "src", vec![]);
5518 for idx in 0..LEAVES {
5519 let leaf = fx.add("Leaf", &format!("l{idx}"), vec![("v", i(idx as i64))]);
5520 fx.edge("T", src, leaf, vec![]);
5521 }
5522 let v = fx.view();
5523 let params = BTreeMap::new();
5524
5525 let staged_err = super::with_max_intermediate_rows(CAP, || {
5527 super::execute_unbounded(
5528 &v,
5529 &compile("MATCH (s:Src)-[:T]->(l:Leaf) WHERE l.v >= 110 RETURN l, l.v"),
5530 &Params(¶ms),
5531 )
5532 });
5533 assert!(
5534 staged_err.is_err(),
5535 "staged path must error on 120 leaves with cap={CAP}"
5536 );
5537 assert!(
5538 staged_err
5539 .unwrap_err()
5540 .contains("intermediate result exceeds"),
5541 "wrong error message"
5542 );
5543
5544 let ok = super::with_max_intermediate_rows(CAP, || {
5548 run(
5549 &v,
5550 "MATCH (s:Src)-[:T]->(l:Leaf) WHERE l.v >= 110 RETURN l, l.v LIMIT 5",
5551 ¶ms,
5552 )
5553 });
5554 let rs = ok.expect("pull-based must survive despite dense hop-1 exceeding cap");
5555 assert_eq!(rs.len(), 5, "LIMIT 5 must return exactly 5 rows");
5556
5557 let vs: Vec<i64> = (0..rs.len())
5559 .filter_map(|i| match rs.get(i, "l.v") {
5560 Some(Value::Int(n)) => Some(*n),
5561 _ => None,
5562 })
5563 .collect();
5564 assert_eq!(vs.len(), 5, "all projected rows must have v");
5565 for v_val in &vs {
5566 assert!(*v_val >= 110, "filter must hold: v={v_val} is not >= 110");
5567 }
5568
5569 let (pull_result, pull_produced) = super::with_expand_counter(|| {
5573 super::with_max_intermediate_rows(1_000_000, || {
5574 run(
5575 &v,
5576 "MATCH (s:Src)-[:T]->(l:Leaf) WHERE l.v < 10 RETURN l LIMIT 5",
5577 ¶ms,
5578 )
5579 })
5580 });
5581 pull_result.expect("pull must succeed without cap");
5582 assert!(
5584 pull_produced <= 5,
5585 "pull expand count {pull_produced} should be ≤ 5 (stops after 5 passing leaves)"
5586 );
5587
5588 let (staged_result, staged_produced) = super::with_expand_counter(|| {
5589 super::with_max_intermediate_rows(1_000_000, || {
5590 super::execute_unbounded(
5591 &v,
5592 &compile("MATCH (s:Src)-[:T]->(l:Leaf) WHERE l.v < 10 RETURN l"),
5593 &Params(¶ms),
5594 )
5595 })
5596 });
5597 staged_result.expect("staged must succeed with 1M cap");
5598 assert_eq!(
5599 staged_produced, LEAVES,
5600 "staged must expand all {LEAVES} leaves"
5601 );
5602
5603 assert!(
5604 staged_produced >= pull_produced * 10,
5605 "staged ({staged_produced}) must be ≥ 10× pull ({pull_produced})"
5606 );
5607 }
5608
5609 #[test]
5627 fn harness_shape_two_hop_dense_survives_pull() {
5628 const N_TALENT: usize = 70;
5629 const N_COMPANY: usize = 20;
5630 const N_INDUSTRY: usize = 3;
5631 const CAP: usize = 100;
5632 const LIMIT: usize = 10;
5633
5634 let mut fx = Fx::new();
5635
5636 let mut talent_ids: Vec<u32> = Vec::new();
5638 let mut talent_industry: Vec<usize> = Vec::new();
5639 for i in 0..N_TALENT {
5640 let ind = i % N_INDUSTRY;
5641 let id = fx.add(
5642 "Talent",
5643 &format!("t{i}"),
5644 vec![("industry", s(&ind.to_string()))],
5645 );
5646 talent_ids.push(id);
5647 talent_industry.push(ind);
5648 }
5649
5650 let mut company_ids: Vec<u32> = Vec::new();
5652 let mut company_industry: Vec<usize> = Vec::new();
5653 for i in 0..N_COMPANY {
5654 let ind = i % N_INDUSTRY;
5655 let id = fx.add(
5656 "Company",
5657 &format!("c{i}"),
5658 vec![("industry", s(&ind.to_string()))],
5659 );
5660 company_ids.push(id);
5661 company_industry.push(ind);
5662 }
5663
5664 for (ti, &tid) in talent_ids.iter().enumerate() {
5666 for (ci, &cid) in company_ids.iter().enumerate() {
5667 if talent_industry[ti] == company_industry[ci] {
5668 fx.edge("INDUSTRY_ALIGNMENT", tid, cid, vec![]);
5669 }
5670 }
5671 }
5672
5673 let v = fx.view();
5674 let params = BTreeMap::new();
5675 let query = format!(
5676 "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company)\
5677 <-[:INDUSTRY_ALIGNMENT]-(t2:Talent) RETURN t, c, t2 LIMIT {LIMIT}"
5678 );
5679
5680 const UNBOUNDED_Q: &str = "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company)\
5682 <-[:INDUSTRY_ALIGNMENT]-(t2:Talent) RETURN t, c, t2";
5683 let staged_err = super::with_max_intermediate_rows(CAP, || {
5684 super::execute_unbounded(&v, &compile(UNBOUNDED_Q), &Params(¶ms))
5685 });
5686 assert!(
5687 staged_err.is_err(),
5688 "staged must error with cap={CAP} on harness-shape graph"
5689 );
5690 assert!(
5691 staged_err
5692 .unwrap_err()
5693 .contains("intermediate result exceeds"),
5694 "wrong error"
5695 );
5696
5697 let ok = super::with_max_intermediate_rows(CAP, || run(&v, &query, ¶ms));
5699 let rs = ok.expect("pull-based must complete on harness-shape with LIMIT 10");
5700 assert_eq!(rs.len(), LIMIT, "must return exactly {LIMIT} rows");
5701
5702 for i in 0..rs.len() {
5707 let row = rs.row(i);
5708 assert_eq!(row.len(), 3, "each row must have 3 columns (t, c, t2)");
5709 assert!(row.iter().all(|c| c.is_some()), "all cells must be Some");
5710 }
5711 }
5712
5713 #[test]
5714 fn intermediate_row_cap_errors_on_scan_and_expand() {
5715 let cap_msg = |n: usize| {
5716 format!(
5717 "intermediate result exceeds {n} rows; add a LIMIT or constrain patterns with shared variables"
5718 )
5719 };
5720
5721 let mut scan_fx = Fx::new();
5722 scan_fx.add("N", "a", vec![]);
5723 scan_fx.add("N", "b", vec![]);
5724 scan_fx.add("N", "c", vec![]);
5725 let sv = scan_fx.view();
5726 let scan_err = super::with_max_intermediate_rows(2, || {
5727 run(&sv, "MATCH (n:N) RETURN n", &BTreeMap::new())
5728 })
5729 .expect_err("3-row scan must exceed cap 2");
5730 assert_eq!(scan_err, cap_msg(2));
5731
5732 let mut exp_fx = Fx::new();
5733 let src = exp_fx.add("Src", "s", vec![]);
5734 let d1 = exp_fx.add("Dst", "d1", vec![]);
5735 let d2 = exp_fx.add("Dst", "d2", vec![]);
5736 exp_fx.edge("T", src, d1, vec![]);
5737 exp_fx.edge("T", src, d2, vec![]);
5738 let ev = exp_fx.view();
5739 let exp_err = super::with_max_intermediate_rows(1, || {
5741 run(&ev, "MATCH (x:Src)-[:T]->(y) RETURN x, y", &BTreeMap::new())
5742 })
5743 .expect_err("2-row expand must exceed cap 1");
5744 assert_eq!(exp_err, cap_msg(1));
5745 }
5746
5747 #[test]
5757 fn bounded_matches_unbounded_slice_various_limits() {
5758 let fx = hop_graph();
5760 let v = fx.view();
5761 let params = BTreeMap::new();
5762
5763 let full_plan = compile("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b");
5765 let full_rs = super::execute_unbounded(&v, &full_plan, &Params(¶ms)).unwrap();
5766 let full_rows = rows_of(&full_rs);
5767 assert_eq!(full_rows.len(), 3, "hop_graph has exactly 3 KNOWS paths");
5768
5769 for limit in [1u64, 2, 3, 10] {
5770 let q = format!("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b LIMIT {limit}");
5771 let rs = run(&v, &q, ¶ms).unwrap();
5772 let expected_len = (limit as usize).min(full_rows.len());
5773 assert_eq!(
5774 rs.len(),
5775 expected_len,
5776 "LIMIT {limit}: expected {expected_len} rows, got {}",
5777 rs.len()
5778 );
5779 assert_eq!(
5780 rows_of(&rs),
5781 full_rows[..expected_len],
5782 "LIMIT {limit}: rows differ from reference slice"
5783 );
5784 }
5785
5786 let skip_rs = run(
5788 &v,
5789 "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b SKIP 1 LIMIT 2",
5790 ¶ms,
5791 )
5792 .unwrap();
5793 assert_eq!(rows_of(&skip_rs), full_rows[1..3]);
5794
5795 let mut fx2 = Fx::new();
5797 fx2.add("N", "a", vec![("v", i(1))]);
5798 fx2.add("N", "b", vec![("v", i(2))]);
5799 fx2.add("N", "c", vec![("v", i(3))]);
5800 let v2 = fx2.view();
5801 let filter_full = super::execute_unbounded(
5803 &v2,
5804 &compile("MATCH (n:N) WHERE n.v > 1 RETURN n"),
5805 &Params(¶ms),
5806 )
5807 .unwrap();
5808 assert_eq!(filter_full.len(), 2);
5809
5810 let filter_lim = run(&v2, "MATCH (n:N) WHERE n.v > 1 RETURN n LIMIT 1", ¶ms).unwrap();
5811 assert_eq!(filter_lim.len(), 1, "LIMIT 1 on filter query");
5812 assert_eq!(rows_of(&filter_lim), rows_of(&filter_full)[..1]);
5813
5814 let tri = triangle();
5816 let tv = tri.view();
5817 let tri_full = super::execute_unbounded(
5819 &tv,
5820 &compile("MATCH (x)-[r1:T]->(y)-[r2:T]->(z) RETURN x, y, z"),
5821 &Params(¶ms),
5822 )
5823 .unwrap();
5824 assert_eq!(tri_full.len(), 3, "triangle has 3 unique two-hop paths");
5825 for limit in [1u64, 2, 3, 5] {
5826 let q = format!("MATCH (x)-[r1:T]->(y)-[r2:T]->(z) RETURN x, y, z LIMIT {limit}");
5827 let rs = run(&tv, &q, ¶ms).unwrap();
5828 let expected_len = (limit as usize).min(3);
5829 assert_eq!(
5830 rs.len(),
5831 expected_len,
5832 "triangle LIMIT {limit}: got {} rows",
5833 rs.len()
5834 );
5835 assert_eq!(
5836 rows_of(&rs),
5837 rows_of(&tri_full)[..expected_len],
5838 "triangle LIMIT {limit}: rows differ"
5839 );
5840 }
5841 }
5842
5843 #[test]
5847 fn expand_terminates_early_with_row_bound() {
5848 const LEAVES: usize = 500;
5849 let mut fx = Fx::new();
5850 let hub = fx.add("Hub", "hub", vec![]);
5851 for i in 0..LEAVES {
5852 let leaf = fx.add("Leaf", &format!("leaf-{i}"), vec![]);
5853 fx.edge("T", hub, leaf, vec![]);
5854 }
5855 let v = fx.view();
5856 let params = BTreeMap::new();
5857 let full_plan = compile("MATCH (h:Hub)-[:T]->(x:Leaf) RETURN x");
5858
5859 let (bounded_result, bounded_produced) = super::with_expand_counter(|| {
5861 run(&v, "MATCH (h:Hub)-[:T]->(x:Leaf) RETURN x LIMIT 5", ¶ms)
5862 });
5863 let bounded_rs = bounded_result.unwrap();
5864 assert_eq!(bounded_rs.len(), 5, "LIMIT 5 must return exactly 5 rows");
5865 assert!(
5866 bounded_produced <= 5,
5867 "bounded: exec_expand emitted {bounded_produced} rows, expected ≤ 5"
5868 );
5869
5870 let (unbounded_result, unbounded_produced) = super::with_expand_counter(|| {
5872 super::execute_unbounded(&v, &full_plan, &Params(¶ms))
5873 });
5874 let unbounded_rs = unbounded_result.unwrap();
5875 assert_eq!(
5876 unbounded_rs.len(),
5877 LEAVES,
5878 "unbounded must return all {LEAVES} rows"
5879 );
5880 assert_eq!(
5881 unbounded_produced, LEAVES,
5882 "unbounded: exec_expand must emit all {LEAVES} rows"
5883 );
5884
5885 assert!(
5887 unbounded_produced >= bounded_produced * 100,
5888 "unbounded ({unbounded_produced}) must be ≥ 100× bounded ({bounded_produced})"
5889 );
5890 }
5891
5892 #[test]
5898 fn bounded_query_survives_low_intermediate_row_cap() {
5899 let mut fx = Fx::new();
5901 let src = fx.add("Src", "s", vec![]);
5902 for i in 0..20usize {
5903 let dst = fx.add("Dst", &format!("d{i}"), vec![]);
5904 fx.edge("T", src, dst, vec![]);
5905 }
5906 let v = fx.view();
5907 let params = BTreeMap::new();
5908 let full_plan = compile("MATCH (s:Src)-[:T]->(d:Dst) RETURN d");
5909
5910 let cap_err = super::with_max_intermediate_rows(10, || {
5912 super::execute_unbounded(&v, &full_plan, &Params(¶ms))
5913 });
5914 assert!(
5915 cap_err.is_err(),
5916 "unbounded must hit the intermediate-row cap"
5917 );
5918 assert!(
5919 cap_err.unwrap_err().contains("intermediate result exceeds"),
5920 "cap error message must be the budget message"
5921 );
5922
5923 let ok = super::with_max_intermediate_rows(10, || {
5925 run(&v, "MATCH (s:Src)-[:T]->(d:Dst) RETURN d LIMIT 5", ¶ms)
5926 });
5927 assert_eq!(
5928 ok.unwrap().len(),
5929 5,
5930 "bounded (LIMIT 5) must complete with 5 rows, not a cap error"
5931 );
5932
5933 let at_cap = super::with_max_intermediate_rows(10, || {
5935 run(&v, "MATCH (s:Src)-[:T]->(d:Dst) RETURN d LIMIT 10", ¶ms)
5936 });
5937 assert_eq!(
5938 at_cap.unwrap().len(),
5939 10,
5940 "bounded at LIMIT==cap must complete with 10 rows"
5941 );
5942 }
5943
5944 #[test]
5949 fn count_star_returns_total_node_count() {
5950 let mut fx = Fx::new();
5951 fx.add("Person", "ada", vec![]);
5952 fx.add("Person", "bob", vec![]);
5953 fx.add("Person", "cam", vec![]);
5954 let v = fx.view();
5955 let params = BTreeMap::new();
5956
5957 let rs = run(&v, "MATCH (n:Person) RETURN COUNT(*)", ¶ms).expect("COUNT(*)");
5958 assert_eq!(rs.columns(), &["COUNT(*)".to_string()]);
5959 assert_eq!(rs.len(), 1);
5960 assert_eq!(rs.row(0), &[Some(i(3))]);
5961
5962 let rs_empty = run(&v, "MATCH (n:Ghost) RETURN COUNT(*)", ¶ms).expect("COUNT(*) empty");
5964 assert_eq!(rs_empty.row(0), &[Some(i(0))]);
5965 }
5966
5967 #[test]
5968 fn count_star_alias_sets_column_name() {
5969 let mut fx = Fx::new();
5970 fx.add("N", "a", vec![]);
5971 let v = fx.view();
5972 let params = BTreeMap::new();
5973 let rs = run(&v, "MATCH (n:N) RETURN COUNT(*) AS total", ¶ms).expect("COUNT AS");
5974 assert_eq!(rs.columns(), &["total".to_string()]);
5975 assert_eq!(rs.row(0), &[Some(i(1))]);
5976 }
5977
5978 #[test]
5979 fn count_var_skips_null_node_bindings() {
5980 let mut fx = Fx::new();
5983 fx.add("N", "a", vec![]);
5984 fx.add("N", "b", vec![]);
5985 let v = fx.view();
5986 let params = BTreeMap::new();
5987 let rs = run(&v, "MATCH (n:N) RETURN COUNT(n)", ¶ms).expect("COUNT(n)");
5988 assert_eq!(rs.columns(), &["COUNT(n)".to_string()]);
5989 assert_eq!(rs.row(0), &[Some(i(2))]);
5990 }
5991
5992 #[test]
5993 fn sum_numeric_prop_ignores_null_and_non_numeric() {
5994 let mut fx = Fx::new();
5995 fx.add("N", "a", vec![("v", i(10))]);
5996 fx.add("N", "b", vec![("v", i(20))]);
5997 fx.add("N", "c", vec![]); let v = fx.view();
5999 let params = BTreeMap::new();
6000 let rs = run(&v, "MATCH (n:N) RETURN SUM(n.v)", ¶ms).expect("SUM");
6001 assert_eq!(rs.columns(), &["SUM(n.v)".to_string()]);
6002 assert_eq!(rs.row(0), &[Some(f(30.0))]);
6004
6005 let rs_null = run(&v, "MATCH (n:N) RETURN SUM(n.missing)", ¶ms).expect("SUM null");
6007 assert_eq!(rs_null.row(0), &[None]);
6008 }
6009
6010 #[test]
6011 fn avg_numeric_prop() {
6012 let mut fx = Fx::new();
6013 fx.add("N", "a", vec![("v", i(10))]);
6014 fx.add("N", "b", vec![("v", i(30))]);
6015 let v = fx.view();
6016 let params = BTreeMap::new();
6017 let rs = run(&v, "MATCH (n:N) RETURN AVG(n.v) AS avg_v", ¶ms).expect("AVG");
6018 assert_eq!(rs.columns(), &["avg_v".to_string()]);
6019 assert_eq!(rs.row(0), &[Some(f(20.0))]);
6021
6022 let rs_empty = run(&v, "MATCH (n:Ghost) RETURN AVG(n.v)", ¶ms).expect("AVG empty");
6024 assert_eq!(rs_empty.row(0), &[None]);
6025 }
6026
6027 #[test]
6028 fn min_max_numeric_prop() {
6029 let mut fx = Fx::new();
6030 fx.add("N", "a", vec![("v", i(5))]);
6031 fx.add("N", "b", vec![("v", i(1))]);
6032 fx.add("N", "c", vec![("v", i(9))]);
6033 fx.add("N", "d", vec![]); let v = fx.view();
6035 let params = BTreeMap::new();
6036
6037 let min_rs = run(&v, "MATCH (n:N) RETURN MIN(n.v)", ¶ms).expect("MIN");
6038 assert_eq!(min_rs.row(0), &[Some(i(1))]);
6039
6040 let max_rs = run(&v, "MATCH (n:N) RETURN MAX(n.v)", ¶ms).expect("MAX");
6041 assert_eq!(max_rs.row(0), &[Some(i(9))]);
6042 }
6043
6044 #[test]
6047 fn min_max_mixed_int_float_props() {
6048 let mut fx = Fx::new();
6049 fx.add("N", "a", vec![("v", i(3))]);
6051 fx.add("N", "b", vec![("v", f(1.5))]);
6052 fx.add("N", "c", vec![("v", i(7))]);
6053 fx.add("N", "d", vec![("v", f(2.0))]);
6054 let v = fx.view();
6055 let params = BTreeMap::new();
6056
6057 let min_rs = run(&v, "MATCH (n:N) RETURN MIN(n.v)", ¶ms).expect("MIN mixed");
6058 assert_eq!(min_rs.row(0), &[Some(f(1.5))]);
6060
6061 let max_rs = run(&v, "MATCH (n:N) RETURN MAX(n.v)", ¶ms).expect("MAX mixed");
6062 assert_eq!(max_rs.row(0), &[Some(i(7))]);
6064 }
6065
6066 #[test]
6069 fn aggregate_limit_skip_order_by_are_no_ops() {
6070 let mut fx = Fx::new();
6071 fx.add("N", "a", vec![]);
6072 fx.add("N", "b", vec![]);
6073 fx.add("N", "c", vec![]);
6074 let v = fx.view();
6075 let params = BTreeMap::new();
6076
6077 let rs_lim5 =
6079 run(&v, "MATCH (n:N) RETURN COUNT(*) LIMIT 5", ¶ms).expect("COUNT(*) LIMIT 5");
6080 assert_eq!(
6081 rs_lim5.len(),
6082 1,
6083 "aggregate with LIMIT 5 must still return 1 row"
6084 );
6085 assert_eq!(rs_lim5.row(0), &[Some(i(3))]);
6086
6087 let rs_lim0 =
6089 run(&v, "MATCH (n:N) RETURN COUNT(*) LIMIT 0", ¶ms).expect("COUNT(*) LIMIT 0");
6090 assert_eq!(
6091 rs_lim0.len(),
6092 1,
6093 "aggregate with LIMIT 0 must still return 1 row"
6094 );
6095 assert_eq!(rs_lim0.row(0), &[Some(i(3))]);
6096
6097 let rs_skip =
6099 run(&v, "MATCH (n:N) RETURN COUNT(*) SKIP 100", ¶ms).expect("COUNT(*) SKIP 100");
6100 assert_eq!(
6101 rs_skip.len(),
6102 1,
6103 "aggregate with large SKIP must still return 1 row"
6104 );
6105
6106 let rs_ord = plan_src("MATCH (n:N) RETURN COUNT(*) ORDER BY n");
6110 assert!(
6113 rs_ord.is_ok(),
6114 "COUNT(*) ORDER BY should plan without error (ORDER BY dropped)"
6115 );
6116 let plan_ops = rs_ord.unwrap();
6117 assert!(
6119 !plan_ops
6120 .iter()
6121 .any(|op| matches!(op, crate::cypher::plan::PlanOp::OrderBy { .. })),
6122 "aggregate plan must not contain OrderBy"
6123 );
6124 }
6125
6126 #[test]
6127 fn count_star_no_budget_cap_applies() {
6128 let mut fx = Fx::new();
6131 let src = fx.add("Src", "s", vec![]);
6132 for i in 0..30usize {
6133 let dst = fx.add("Dst", &format!("d{i}"), vec![]);
6134 fx.edge("T", src, dst, vec![]);
6135 }
6136 let v = fx.view();
6137 let params = BTreeMap::new();
6138
6139 let cap_err =
6141 super::with_max_intermediate_rows(10, || run(&v, "MATCH (n:Dst) RETURN n", ¶ms));
6142 assert!(
6143 cap_err.is_err(),
6144 "staged path must error on 30 nodes with cap=10"
6145 );
6146
6147 let agg_ok = super::with_max_intermediate_rows(10, || {
6149 run(&v, "MATCH (n:Dst) RETURN COUNT(*)", ¶ms)
6150 })
6151 .expect("aggregate must not hit the intermediate-row cap");
6152 assert_eq!(
6153 agg_ok.row(0),
6154 &[Some(i(30))],
6155 "COUNT(*) must count all 30 nodes regardless of cap"
6156 );
6157 }
6158
6159 fn plan_src(src: &str) -> Result<Vec<crate::cypher::plan::PlanOp>, String> {
6160 use crate::cypher::{lex, parse, plan};
6161 let toks = lex(src).map_err(|e| format!("lex: {e}"))?;
6162 let ast = parse(&toks).map_err(|e| format!("parse: {e}"))?;
6163 plan(&ast).map_err(|e| format!("plan: {e}"))
6164 }
6165
6166 #[test]
6169 fn grouped_aggregation_plan_routing() {
6170 use crate::cypher::plan::PlanOp;
6171
6172 let ops = plan_src("MATCH (a:N) RETURN a, COUNT(*)")
6174 .expect("grouped aggregation must now succeed");
6175 assert!(
6176 ops.iter()
6177 .any(|op| matches!(op, PlanOp::GroupAggregate { .. })),
6178 "grouped aggregation plan must contain GroupAggregate op, got: {ops:?}"
6179 );
6180
6181 let ops2 = plan_src("MATCH (a:N) RETURN COUNT(*), COUNT(a)")
6183 .expect("multi-aggregate must now succeed");
6184 assert!(
6185 ops2.iter()
6186 .any(|op| matches!(op, PlanOp::GroupAggregate { .. })),
6187 "multi-aggregate plan must contain GroupAggregate op, got: {ops2:?}"
6188 );
6189
6190 let err3 = plan_src("MATCH (a:N) RETURN SUM(*)").expect_err("SUM(*) must be plan error");
6192 assert!(
6193 err3.to_ascii_lowercase().contains("sum") || err3.to_ascii_lowercase().contains("*"),
6194 "error must mention SUM or *, got: {err3}"
6195 );
6196 }
6197
6198 #[test]
6201 fn grouped_single_key_count() {
6202 let mut fx = Fx::new();
6204 fx.add("N", "a", vec![("t", s("X"))]);
6205 fx.add("N", "b", vec![("t", s("X"))]);
6206 fx.add("N", "c", vec![("t", s("Y"))]);
6207 let v = fx.view();
6208 let params = BTreeMap::new();
6209
6210 let rs = run(&v, "MATCH (n:N) RETURN n.t, COUNT(*) AS cnt", ¶ms)
6211 .expect("single-key grouped COUNT must succeed");
6212 assert_eq!(
6213 rs.columns(),
6214 &["n.t".to_string(), "cnt".to_string()],
6215 "columns must match RETURN clause"
6216 );
6217 assert_eq!(rs.len(), 2, "must produce exactly 2 groups (X and Y)");
6218
6219 let find = |label: &Value| (0..rs.len()).find(|&i| rs.row(i)[0].as_ref() == Some(label));
6221 let xi = find(&s("X")).expect("group X must exist");
6222 let yi = find(&s("Y")).expect("group Y must exist");
6223 assert_eq!(rs.row(xi)[1], Some(i(2)), "X group count must be 2");
6224 assert_eq!(rs.row(yi)[1], Some(i(1)), "Y group count must be 1");
6225 }
6226
6227 #[test]
6228 fn grouped_two_keys_sum_avg() {
6229 let mut fx = Fx::new();
6231 fx.add(
6232 "N",
6233 "a",
6234 vec![("cat", s("A")), ("sub", s("1")), ("v", i(10))],
6235 );
6236 fx.add(
6237 "N",
6238 "b",
6239 vec![("cat", s("A")), ("sub", s("1")), ("v", i(20))],
6240 );
6241 fx.add(
6242 "N",
6243 "c",
6244 vec![("cat", s("A")), ("sub", s("2")), ("v", i(5))],
6245 );
6246 fx.add(
6247 "N",
6248 "d",
6249 vec![("cat", s("B")), ("sub", s("1")), ("v", i(100))],
6250 );
6251 let v = fx.view();
6252 let params = BTreeMap::new();
6253
6254 let rs = run(
6255 &v,
6256 "MATCH (n:N) RETURN n.cat, n.sub, SUM(n.v) AS total, AVG(n.v) AS avg_v",
6257 ¶ms,
6258 )
6259 .expect("two-key SUM + AVG must succeed");
6260 assert_eq!(
6261 rs.columns(),
6262 &[
6263 "n.cat".to_string(),
6264 "n.sub".to_string(),
6265 "total".to_string(),
6266 "avg_v".to_string()
6267 ]
6268 );
6269 assert_eq!(rs.len(), 3, "must produce 3 groups: (A,1), (A,2), (B,1)");
6270
6271 let find = |cat: &Value, sub: &Value| {
6272 (0..rs.len())
6273 .find(|&i| rs.row(i)[0].as_ref() == Some(cat) && rs.row(i)[1].as_ref() == Some(sub))
6274 };
6275 let a1 = find(&s("A"), &s("1")).expect("group (A,1) must exist");
6276 assert_eq!(rs.row(a1)[2], Some(f(30.0)), "(A,1) SUM must be 30.0");
6277 assert_eq!(rs.row(a1)[3], Some(f(15.0)), "(A,1) AVG must be 15.0");
6278
6279 let a2 = find(&s("A"), &s("2")).expect("group (A,2) must exist");
6280 assert_eq!(rs.row(a2)[2], Some(f(5.0)), "(A,2) SUM must be 5.0");
6281
6282 let b1 = find(&s("B"), &s("1")).expect("group (B,1) must exist");
6283 assert_eq!(rs.row(b1)[2], Some(f(100.0)), "(B,1) SUM must be 100.0");
6284 assert_eq!(rs.row(b1)[3], Some(f(100.0)), "(B,1) AVG must be 100.0");
6285 }
6286
6287 #[test]
6288 fn grouped_order_by_count_desc_limit() {
6289 let mut fx = Fx::new();
6291 fx.add("N", "a1", vec![("cat", s("A"))]);
6292 fx.add("N", "a2", vec![("cat", s("A"))]);
6293 fx.add("N", "a3", vec![("cat", s("A"))]);
6294 fx.add("N", "b1", vec![("cat", s("B"))]);
6295 fx.add("N", "b2", vec![("cat", s("B"))]);
6296 fx.add("N", "c1", vec![("cat", s("C"))]);
6297 fx.add("N", "d1", vec![("cat", s("D"))]);
6298 fx.add("N", "d2", vec![("cat", s("D"))]);
6299 fx.add("N", "d3", vec![("cat", s("D"))]);
6300 fx.add("N", "d4", vec![("cat", s("D"))]);
6301 fx.add("N", "e1", vec![("cat", s("E"))]);
6302 let v = fx.view();
6303 let params = BTreeMap::new();
6304
6305 let rs = run(
6306 &v,
6307 "MATCH (n:N) RETURN n.cat, COUNT(*) AS cnt ORDER BY cnt DESC LIMIT 3",
6308 ¶ms,
6309 )
6310 .expect("ORDER BY count DESC LIMIT 3 must succeed");
6311 assert_eq!(rs.len(), 3, "LIMIT 3 must return exactly 3 groups");
6312 assert_eq!(rs.row(0)[1], Some(i(4)), "row 0 must be count 4");
6314 assert_eq!(rs.row(0)[0], Some(s("D")), "row 0 must be category D");
6315 assert_eq!(rs.row(1)[1], Some(i(3)), "row 1 must be count 3");
6316 assert_eq!(rs.row(1)[0], Some(s("A")), "row 1 must be category A");
6317 assert_eq!(rs.row(2)[1], Some(i(2)), "row 2 must be count 2");
6318 assert_eq!(rs.row(2)[0], Some(s("B")), "row 2 must be category B");
6319
6320 let plan_ops =
6322 plan_src("MATCH (n:N) RETURN n.cat, COUNT(*) AS cnt ORDER BY cnt DESC LIMIT 3")
6323 .expect("plan must succeed");
6324 assert_eq!(
6325 crate::cypher::plan::row_bound(&plan_ops),
6326 None,
6327 "GroupAggregate plan with LIMIT must have row_bound = None"
6328 );
6329 }
6330
6331 #[test]
6332 fn grouped_empty_input_yields_zero_groups() {
6333 let fx = Fx::new(); let v = fx.view();
6335 let params = BTreeMap::new();
6336
6337 let rs = run(&v, "MATCH (n:N) RETURN n.t, COUNT(*) AS cnt", ¶ms)
6338 .expect("grouped aggregate on empty graph must succeed");
6339 assert_eq!(rs.len(), 0, "empty input must yield zero groups");
6340 assert_eq!(
6341 rs.columns(),
6342 &["n.t".to_string(), "cnt".to_string()],
6343 "columns must still be present even with zero rows"
6344 );
6345 }
6346
6347 #[test]
6348 fn grouped_null_key_groups_together() {
6349 let mut fx = Fx::new();
6351 fx.add("N", "a", vec![("t", s("X"))]);
6352 fx.add("N", "b", vec![]); fx.add("N", "c", vec![]); fx.add("N", "d", vec![("t", s("Y"))]);
6355 let v = fx.view();
6356 let params = BTreeMap::new();
6357
6358 let rs = run(&v, "MATCH (n:N) RETURN n.t, COUNT(*) AS cnt", ¶ms)
6359 .expect("null-key grouped aggregate must succeed");
6360 assert_eq!(rs.len(), 3, "must produce 3 groups: X, null, Y");
6361
6362 let null_row = (0..rs.len())
6364 .find(|&i| rs.row(i)[0].is_none())
6365 .expect("null group must be present");
6366 assert_eq!(
6367 rs.row(null_row)[1],
6368 Some(i(2)),
6369 "null group must count 2 rows (b and c)"
6370 );
6371
6372 let x_row = (0..rs.len())
6373 .find(|&i| rs.row(i)[0] == Some(s("X")))
6374 .expect("X group must exist");
6375 assert_eq!(rs.row(x_row)[1], Some(i(1)));
6376
6377 let y_row = (0..rs.len())
6378 .find(|&i| rs.row(i)[0] == Some(s("Y")))
6379 .expect("Y group must exist");
6380 assert_eq!(rs.row(y_row)[1], Some(i(1)));
6381 }
6382
6383 #[test]
6384 fn grouped_cap_error_on_high_cardinality() {
6385 let mut fx = Fx::new();
6388 fx.add("N", "a", vec![("t", s("A"))]);
6389 fx.add("N", "b", vec![("t", s("B"))]);
6390 fx.add("N", "c", vec![("t", s("C"))]);
6391 let v = fx.view();
6392 let params = BTreeMap::new();
6393
6394 let err = super::with_max_groups(2, || {
6395 run(&v, "MATCH (n:N) RETURN n.t, COUNT(*) AS cnt", ¶ms)
6396 })
6397 .expect_err("must error when group count exceeds cap");
6398 assert!(
6399 err.to_ascii_lowercase().contains("group count"),
6400 "error must mention group count, got: {err}"
6401 );
6402 }
6403
6404 #[test]
6405 fn multi_aggregate_no_keys() {
6406 let mut fx = Fx::new();
6409 fx.add("N", "a", vec![]);
6410 fx.add("N", "b", vec![]);
6411 let v = fx.view();
6412 let params = BTreeMap::new();
6413
6414 let rs = run(&v, "MATCH (n:N) RETURN COUNT(*), COUNT(n)", ¶ms)
6415 .expect("multi-aggregate no keys must succeed");
6416 assert_eq!(rs.len(), 1, "must produce exactly one result row");
6417 assert_eq!(rs.row(0)[0], Some(i(2)), "COUNT(*) must be 2");
6418 assert_eq!(rs.row(0)[1], Some(i(2)), "COUNT(n) must be 2");
6419 }
6420
6421 #[test]
6422 fn aggregate_over_hop_counts_edges() {
6423 let fx = hop_graph();
6424 let v = fx.view();
6425 let params = BTreeMap::new();
6426 let rs = run(
6428 &v,
6429 "MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN COUNT(*)",
6430 ¶ms,
6431 )
6432 .expect("COUNT(*) on hop graph");
6433 assert_eq!(rs.row(0), &[Some(i(3))]);
6434 }
6435
6436 #[test]
6441 fn multi_aggregate_no_keys_empty_graph() {
6442 let fx = Fx::new(); let v = fx.view();
6448 let params = BTreeMap::new();
6449
6450 let rs = run(&v, "MATCH (n:N) RETURN COUNT(*), COUNT(n)", ¶ms)
6451 .expect("empty-graph multi-agg must succeed");
6452 assert_eq!(rs.len(), 1, "must produce exactly 1 row on empty input");
6453 assert_eq!(
6454 rs.row(0)[0],
6455 Some(i(0)),
6456 "COUNT(*) on empty graph must be 0"
6457 );
6458 assert_eq!(
6459 rs.row(0)[1],
6460 Some(i(0)),
6461 "COUNT(n) on empty graph must be 0"
6462 );
6463 }
6464
6465 #[test]
6466 fn fast_path_and_group_path_agree_on_empty_input() {
6467 let fx = Fx::new();
6473 let v = fx.view();
6474 let params = BTreeMap::new();
6475
6476 let fast = run(&v, "MATCH (n:N) RETURN COUNT(*)", ¶ms)
6477 .expect("fast-path COUNT on empty graph");
6478 assert_eq!(fast.len(), 1, "fast path: 1 row on empty input");
6479 let fast_count = fast.row(0)[0].clone();
6480
6481 let grouped = run(&v, "MATCH (n:N) RETURN COUNT(*), COUNT(n)", ¶ms)
6482 .expect("group-path COUNT on empty graph");
6483 assert_eq!(grouped.len(), 1, "group path: 1 row on empty input");
6484 let group_count = grouped.row(0)[0].clone();
6485
6486 assert_eq!(
6487 fast_count, group_count,
6488 "fast path and group path must agree on COUNT(*) for empty input"
6489 );
6490
6491 let mut fx2 = Fx::new();
6493 fx2.add("N", "x", vec![]);
6494 fx2.add("N", "y", vec![]);
6495 let v2 = fx2.view();
6496
6497 let fast2 = run(&v2, "MATCH (n:N) RETURN COUNT(*)", ¶ms)
6498 .expect("fast-path COUNT on 2-node graph");
6499 assert_eq!(fast2.row(0)[0], Some(i(2)), "fast path: COUNT(*) = 2");
6500
6501 let grouped2 = run(&v2, "MATCH (n:N) RETURN COUNT(*), COUNT(n)", ¶ms)
6502 .expect("group-path COUNT on 2-node graph");
6503 assert_eq!(grouped2.row(0)[0], Some(i(2)), "group path: COUNT(*) = 2");
6504 assert_eq!(grouped2.row(0)[1], Some(i(2)), "group path: COUNT(n) = 2");
6505 }
6506
6507 #[test]
6512 fn grouped_int_float_key_unification() {
6513 let mut fx = Fx::new();
6520 fx.add("N", "a", vec![("score", i(1))]); fx.add("N", "b", vec![("score", f(1.0))]);
6522 let v = fx.view();
6523 let params = BTreeMap::new();
6524
6525 let rs = run(&v, "MATCH (n:N) RETURN n.score, COUNT(*) AS cnt", ¶ms)
6526 .expect("int/float unification must succeed");
6527 assert_eq!(
6528 rs.len(),
6529 1,
6530 "Int(1) and Float(1.0) must group together into 1 group"
6531 );
6532 assert_eq!(
6534 rs.row(0)[0],
6535 Some(i(1)),
6536 "key column must display Int(1) (first-seen)"
6537 );
6538 assert_eq!(rs.row(0)[1], Some(i(2)), "unified group must have count=2");
6539 }
6540
6541 #[test]
6542 fn distinct_collapses_duplicate_projected_values() {
6543 let mut fx = Fx::new();
6544 fx.add("N", "a1", vec![("city", s("Austin"))]);
6545 fx.add("N", "a2", vec![("city", s("Austin"))]);
6546 let v = fx.view();
6547 let params = BTreeMap::new();
6548 let rs = run(&v, "MATCH (n:N) RETURN DISTINCT n.city", ¶ms).unwrap();
6549 assert_eq!(rs.len(), 1);
6550 assert_eq!(rs.row(0)[0], Some(s("Austin")));
6551 }
6552
6553 #[test]
6554 fn distinct_int_float_unify() {
6555 let mut fx = Fx::new();
6556 fx.add("N", "a", vec![("score", i(1))]);
6557 fx.add("N", "b", vec![("score", f(1.0))]);
6558 let v = fx.view();
6559 let params = BTreeMap::new();
6560 let rs = run(&v, "MATCH (n:N) RETURN DISTINCT n.score", ¶ms).unwrap();
6561 assert_eq!(
6562 rs.len(),
6563 1,
6564 "Int(1) and Float(1.0) must DISTINCT as one row"
6565 );
6566 assert_eq!(rs.row(0)[0], Some(i(1)), "first-seen Int(1) wins display");
6567 }
6568
6569 #[test]
6570 fn distinct_caps_at_intermediate_row_budget() {
6571 let mut fx = Fx::new();
6572 fx.add("N", "a", vec![("city", s("Austin"))]);
6573 fx.add("N", "b", vec![("city", s("Paris"))]);
6574 let v = fx.view();
6575 let params = BTreeMap::new();
6576 let err = super::with_max_intermediate_rows(1, || {
6577 run(&v, "MATCH (n:N) RETURN DISTINCT n.city", ¶ms)
6578 })
6579 .expect_err("two distinct cities must exceed cap=1");
6580 assert!(
6581 err.contains("1") || err.contains("row"),
6582 "cap error must mention the budget, got: {err}"
6583 );
6584 }
6585
6586 #[test]
6587 fn where_in_list_filters_rows() {
6588 let mut fx = Fx::new();
6589 fx.add("N", "a", vec![("city", s("Austin"))]);
6590 fx.add("N", "p", vec![("city", s("Paris"))]);
6591 fx.add("N", "l", vec![("city", s("London"))]);
6592 let v = fx.view();
6593 let mut params = BTreeMap::new();
6594 params.insert("c".into(), s("Paris"));
6595 let rs = run(
6596 &v,
6597 "MATCH (n:N) WHERE n.city IN ['Austin', $c] RETURN n.city",
6598 ¶ms,
6599 )
6600 .unwrap();
6601 assert_eq!(rs.len(), 2);
6602 }
6603
6604 #[test]
6605 fn pure_int_keys_display_as_int() {
6606 let mut fx = Fx::new();
6611 fx.add("N", "a", vec![("age", i(10))]);
6612 fx.add("N", "b", vec![("age", i(20))]);
6613 fx.add("N", "c", vec![("age", i(10))]);
6614 let v = fx.view();
6615 let params = BTreeMap::new();
6616
6617 let rs = run(
6618 &v,
6619 "MATCH (n:N) RETURN n.age, COUNT(*) AS cnt ORDER BY n.age",
6620 ¶ms,
6621 )
6622 .expect("pure-Int group keys must succeed");
6623 assert_eq!(rs.len(), 2, "must have 2 groups: age=10 and age=20");
6624 assert_eq!(
6626 rs.row(0)[0],
6627 Some(i(10)),
6628 "age=10 key column must display as Int(10)"
6629 );
6630 assert_eq!(rs.row(0)[1], Some(i(2)), "age=10 group has 2 nodes");
6631 assert_eq!(
6632 rs.row(1)[0],
6633 Some(i(20)),
6634 "age=20 key column must display as Int(20)"
6635 );
6636 assert_eq!(rs.row(1)[1], Some(i(1)), "age=20 group has 1 node");
6637 }
6638
6639 #[test]
6644 fn var_expand_group_aggregate_staged_path() {
6645 let mut fx = Fx::new();
6655 let a = fx.add("N", "a", vec![("key", s("a"))]);
6656 let b = fx.add("N", "b", vec![("key", s("b"))]);
6657 let c = fx.add("N", "c", vec![("key", s("c"))]);
6658 fx.edge("T", a, b, vec![]);
6659 fx.edge("T", b, c, vec![]);
6660 let v = fx.view();
6661 let params = BTreeMap::new();
6662
6663 let rs = run(
6664 &v,
6665 "MATCH (x:N)-[*1..2]->(y:N) RETURN y.key, COUNT(*) AS cnt ORDER BY y.key",
6666 ¶ms,
6667 )
6668 .expect("VarExpand + GroupAggregate must succeed");
6669
6670 assert_eq!(rs.len(), 2, "must have 2 destination groups: b and c");
6671 assert_eq!(rs.row(0)[0], Some(s("b")), "first group key must be 'b'");
6672 assert_eq!(rs.row(0)[1], Some(i(1)), "b is reached via 1 path");
6673 assert_eq!(rs.row(1)[0], Some(s("c")), "second group key must be 'c'");
6674 assert_eq!(
6675 rs.row(1)[1],
6676 Some(i(2)),
6677 "c is reached via 2 paths (1-hop and 2-hop)"
6678 );
6679 }
6680
6681 #[test]
6689 fn pull_rows_var_expand_arm_returns_named_err() {
6690 let fx = Fx::new();
6691 let view = fx.view();
6692 let vars = super::VarTable {
6693 names: vec!["a".into(), "b".into()],
6694 };
6695 let project_items: Vec<crate::cypher::ast::RetItem> = vec![];
6696 let empty_params = BTreeMap::new();
6697 let params = super::Params(&empty_params);
6698 let ctx = super::PullCtx {
6699 view: &view,
6700 vars: &vars,
6701 project_items: &project_items,
6702 params: ¶ms,
6703 bound: 100,
6704 };
6705 let ops = vec![PlanOp::VarExpand {
6706 from: "a".into(),
6707 rel_var: None,
6708 etypes: vec![],
6709 dir: crate::cypher::RelDir::Right,
6710 to: "b".into(),
6711 min: 1,
6712 max: 3,
6713 }];
6714 let mut row = vec![None; vars.names.len()];
6715 let mut result = Vec::new();
6716 let err = super::pull_rows(&ctx, &ops, &mut row, &mut result)
6717 .expect_err("VarExpand must Err in pull_rows");
6718 assert!(
6719 err.contains("VarExpand") && err.contains("pull executor"),
6720 "error must name VarExpand and pull executor, got: {err}"
6721 );
6722 }
6723
6724 #[test]
6725 fn pull_rows_shortest_path_arm_returns_named_err() {
6726 let fx = Fx::new();
6727 let view = fx.view();
6728 let vars = super::VarTable {
6729 names: vec!["a".into(), "b".into()],
6730 };
6731 let project_items: Vec<crate::cypher::ast::RetItem> = vec![];
6732 let empty_params = BTreeMap::new();
6733 let params = super::Params(&empty_params);
6734 let ctx = super::PullCtx {
6735 view: &view,
6736 vars: &vars,
6737 project_items: &project_items,
6738 params: ¶ms,
6739 bound: 100,
6740 };
6741 let ops = vec![PlanOp::ShortestPath {
6742 from: "a".into(),
6743 rel_var: None,
6744 etypes: vec![],
6745 dir: crate::cypher::RelDir::Right,
6746 to: "b".into(),
6747 max_hops: 5,
6748 }];
6749 let mut row = vec![None; vars.names.len()];
6750 let mut result = Vec::new();
6751 let err = super::pull_rows(&ctx, &ops, &mut row, &mut result)
6752 .expect_err("ShortestPath must Err in pull_rows");
6753 assert!(
6754 err.contains("ShortestPath") && err.contains("pull executor"),
6755 "error must name ShortestPath and pull executor, got: {err}"
6756 );
6757 }
6758
6759 fn city_graph() -> Fx {
6763 let mut fx = Fx::new();
6764 fx.add(
6765 "Person",
6766 "alice",
6767 vec![("city", s("Boston")), ("age", i(30))],
6768 );
6769 fx.add("Person", "bob", vec![("city", s("Boston")), ("age", i(25))]);
6770 fx.add(
6771 "Person",
6772 "carol",
6773 vec![("city", s("Austin")), ("age", i(35))],
6774 );
6775 fx.add(
6776 "Person",
6777 "dave",
6778 vec![("city", s("Austin")), ("age", i(28))],
6779 );
6780 fx.add("Person", "eve", vec![("city", s("Boston")), ("age", i(22))]);
6781 fx
6782 }
6783
6784 #[test]
6787 fn with_aggregate_having_filters_groups() {
6788 let fx = city_graph();
6789 let view = fx.view();
6790 let params = BTreeMap::new();
6791 let rs = run(
6792 &view,
6793 "MATCH (p:Person) WITH p.city AS city, COUNT(*) AS cnt WHERE cnt > 2 RETURN city, cnt",
6794 ¶ms,
6795 )
6796 .expect("WITH HAVING query must succeed");
6797 assert_eq!(rs.len(), 1, "only Boston group has cnt > 2");
6798 assert_eq!(rs.get(0, "city"), Some(&s("Boston")));
6799 assert_eq!(rs.get(0, "cnt"), Some(&i(3)));
6800 }
6801
6802 #[test]
6804 fn with_order_limit_then_return() {
6805 let fx = city_graph();
6806 let view = fx.view();
6807 let params = BTreeMap::new();
6808 let rs = run(&view, "MATCH (p:Person) WITH p, p.age AS age ORDER BY age DESC LIMIT 2 RETURN p.city AS city, age", ¶ms)
6810 .expect("WITH ORDER LIMIT must succeed");
6811 assert_eq!(rs.len(), 2, "LIMIT 2");
6812 let ages: Vec<Option<Value>> = (0..rs.len()).map(|i| rs.get(i, "age").cloned()).collect();
6814 assert_eq!(ages[0], Some(i(35)), "oldest person first");
6815 assert_eq!(ages[1], Some(i(30)), "second oldest");
6816 }
6817
6818 #[test]
6820 fn chained_with_stages() {
6821 let fx = city_graph();
6822 let view = fx.view();
6823 let params = BTreeMap::new();
6824 let rs = run(&view,
6826 "MATCH (p:Person) WITH p.city AS city, p.age AS age WITH city, age WHERE age > 25 RETURN city, age",
6827 ¶ms).expect("chained WITH must succeed");
6828 assert_eq!(rs.len(), 3, "3 people with age > 25");
6830 }
6831
6832 #[test]
6834 fn with_then_match_reentry() {
6835 let mut fx = Fx::new();
6836 let alice = fx.add("Person", "alice", vec![]);
6837 let bob = fx.add("Person", "bob", vec![]);
6838 let corp = fx.add("Company", "acme", vec![]);
6839 fx.edge("WORKS_AT", alice, corp, vec![("years", i(5))]);
6840 fx.edge("WORKS_AT", bob, corp, vec![("years", i(3))]);
6841 let view = fx.view();
6842 let params = BTreeMap::new();
6843 let rs = run(&view,
6845 "MATCH (p:Person)-[r:WORKS_AT]->(c:Company) WITH p, c MATCH (c)-[r2:WORKS_AT]-(colleague:Person) RETURN p, colleague",
6846 ¶ms).expect("WITH MATCH re-entry must succeed");
6847 assert!(rs.len() >= 2, "cross join via company: got {}", rs.len());
6849 }
6850
6851 #[test]
6853 fn unwind_literal_list_produces_rows() {
6854 let fx = city_graph();
6855 let view = fx.view();
6856 let params = BTreeMap::new();
6857 let rs = run(
6858 &view,
6859 "MATCH (p:Person) WHERE p.city = 'Boston' UNWIND [1, 2, 3] AS x RETURN p, x",
6860 ¶ms,
6861 )
6862 .expect("UNWIND literal must succeed");
6863 assert_eq!(rs.len(), 9, "UNWIND [1,2,3] × 3 Boston people = 9 rows");
6865 let xs: Vec<Option<Value>> = (0..rs.len())
6867 .map(|row_i| rs.get(row_i, "x").cloned())
6868 .collect();
6869 assert!(xs.contains(&Some(i(1))));
6870 assert!(xs.contains(&Some(i(2))));
6871 assert!(xs.contains(&Some(i(3))));
6872 }
6873
6874 #[test]
6876 fn unwind_list_property() {
6877 let mut fx = Fx::new();
6878 fx.add(
6879 "Tag",
6880 "post1",
6881 vec![("tags", Value::List(vec![s("rust"), s("graph")]))],
6882 );
6883 fx.add("Tag", "post2", vec![("tags", Value::List(vec![s("db")]))]);
6884 let view = fx.view();
6885 let params = BTreeMap::new();
6886 let rs = run(
6887 &view,
6888 "MATCH (p:Tag) UNWIND p.tags AS tag RETURN p, tag",
6889 ¶ms,
6890 )
6891 .expect("UNWIND property must succeed");
6892 assert_eq!(rs.len(), 3, "2+1 tag elements");
6893 }
6894
6895 #[test]
6897 fn unwind_empty_list_yields_zero_rows() {
6898 let fx = city_graph();
6899 let view = fx.view();
6900 let params = BTreeMap::new();
6901 let rs = run(
6902 &view,
6903 "MATCH (p:Person) WHERE p.city = 'Boston' UNWIND [] AS x RETURN x",
6904 ¶ms,
6905 )
6906 .expect("UNWIND [] must succeed with 0 rows");
6907 assert_eq!(rs.len(), 0, "UNWIND [] should produce 0 rows");
6908 }
6909
6910 #[test]
6912 fn unwind_non_list_is_named_error() {
6913 let fx = city_graph();
6914 let view = fx.view();
6915 let params = BTreeMap::new();
6916 let err = run(
6918 &view,
6919 "MATCH (p:Person) WHERE p.city = 'Boston' UNWIND p.city AS x RETURN x",
6920 ¶ms,
6921 )
6922 .expect_err("UNWIND non-list must error");
6923 assert!(
6924 err.contains("UNWIND") && err.contains("list"),
6925 "error must mention UNWIND and list: {err}"
6926 );
6927 }
6928
6929 #[test]
6931 fn unwind_cross_product_trips_budget() {
6932 let mut fx = Fx::new();
6933 for i in 0..10 {
6935 fx.add("N", &format!("n{i}"), vec![]);
6936 }
6937 let view = fx.view();
6938 let params = BTreeMap::new();
6939 let err = super::with_max_intermediate_rows(5, || {
6940 run(
6941 &view,
6942 "MATCH (n:N) UNWIND [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] AS x RETURN n, x",
6943 ¶ms,
6944 )
6945 })
6946 .expect_err("must hit budget");
6947 assert!(
6948 err.contains("intermediate result exceeds"),
6949 "error must mention intermediate result limit: {err}"
6950 );
6951 }
6952
6953 #[test]
6956 fn routing_with_plan_uses_staged_path() {
6957 use crate::cypher::plan::row_bound;
6958 let ops_with = compile("MATCH (a) WITH a RETURN a");
6959 assert_eq!(
6960 row_bound(&ops_with),
6961 None,
6962 "WITH plan must have row_bound=None"
6963 );
6964
6965 let ops_unwind = compile("MATCH (a) UNWIND [1, 2] AS x RETURN a");
6966 assert_eq!(
6967 row_bound(&ops_unwind),
6968 None,
6969 "UNWIND plan must have row_bound=None"
6970 );
6971
6972 let ops_plain = compile("MATCH (a) RETURN a LIMIT 5");
6974 assert!(
6975 row_bound(&ops_plain).is_some(),
6976 "plain LIMIT plan must have a row bound"
6977 );
6978 }
6979
6980 #[test]
6983 fn unwind_null_property_yields_zero_rows_for_that_node() {
6984 let mut fx = Fx::new();
6985 fx.add(
6987 "Post",
6988 "post1",
6989 vec![("tags", Value::List(vec![s("rust"), s("graph")]))],
6990 );
6991 fx.add("Post", "post2", vec![("other", Value::Str("hello".into()))]);
6992 let view = fx.view();
6993 let params = BTreeMap::new();
6994 let rs = run(
6995 &view,
6996 "MATCH (p:Post) UNWIND p.tags AS tag RETURN tag",
6997 ¶ms,
6998 )
6999 .expect("UNWIND null property must succeed (not error)");
7000 assert_eq!(
7002 rs.len(),
7003 2,
7004 "null property must yield 0 rows; list property yields N rows"
7005 );
7006 let tag0 = rs.get(0, "tag");
7007 let tag1 = rs.get(1, "tag");
7008 let tags = [tag0, tag1];
7009 assert!(tags.contains(&Some(&s("rust"))), "rust tag present");
7010 assert!(tags.contains(&Some(&s("graph"))), "graph tag present");
7011 }
7012
7013 #[test]
7015 fn where_before_unwind_filters_nodes() {
7016 let mut fx = Fx::new();
7017 fx.add(
7018 "P",
7019 "a",
7020 vec![
7021 ("group", Value::Str("keep".into())),
7022 ("items", Value::List(vec![Value::Int(1), Value::Int(2)])),
7023 ],
7024 );
7025 fx.add(
7026 "P",
7027 "b",
7028 vec![
7029 ("group", Value::Str("drop".into())),
7030 ("items", Value::List(vec![Value::Int(3), Value::Int(4)])),
7031 ],
7032 );
7033 let view = fx.view();
7034 let params = BTreeMap::new();
7035 let rs = run(
7037 &view,
7038 "MATCH (n:P) WHERE n.group = 'keep' UNWIND n.items AS x RETURN x",
7039 ¶ms,
7040 )
7041 .expect("WHERE before UNWIND must succeed");
7042 assert_eq!(rs.len(), 2, "only matching node expands; 2 items");
7043 }
7044
7045 #[test]
7047 fn where_after_unwind_filters_expanded_rows() {
7048 let mut fx = Fx::new();
7049 fx.add(
7050 "N",
7051 "n1",
7052 vec![(
7053 "vals",
7054 Value::List(vec![Value::Int(1), Value::Int(3), Value::Int(5)]),
7055 )],
7056 );
7057 let view = fx.view();
7058 let params = BTreeMap::new();
7059 let rs = run(
7061 &view,
7062 "MATCH (n:N) UNWIND n.vals AS x WHERE x > 2 RETURN x",
7063 ¶ms,
7064 )
7065 .expect("WHERE after UNWIND must succeed");
7066 assert_eq!(rs.len(), 2, "x=3 and x=5 pass; x=1 filtered out");
7067 let v0 = rs.get(0, "x").cloned();
7068 let v1 = rs.get(1, "x").cloned();
7069 let vals = [v0, v1];
7070 assert!(vals.contains(&Some(Value::Int(3))), "x=3 present");
7071 assert!(vals.contains(&Some(Value::Int(5))), "x=5 present");
7072 }
7073
7074 #[test]
7076 fn aggregate_with_order_by_unknown_alias_is_named_error() {
7077 use crate::cypher::{lex, parser::parse, plan::plan};
7078 let src = "MATCH (p:Person) WITH p.city AS city, COUNT(*) AS cnt ORDER BY cntt DESC RETURN city, cnt";
7080 let plan_result = plan(&parse(&lex(src).expect("lex")).expect("parse"));
7081 let err = plan_result
7082 .expect_err("typo'd ORDER BY alias in aggregate WITH must be a named plan error");
7083 assert!(
7084 err.contains("cntt") || err.contains("unbound"),
7085 "error must mention the unknown alias: {err}"
7086 );
7087 }
7088
7089 #[test]
7091 fn non_aggregate_with_order_by_unknown_alias_is_named_error() {
7092 use crate::cypher::{lex, parser::parse, plan::plan};
7093 let src = "MATCH (p:Person) WITH p, p.age AS age ORDER BY nope DESC RETURN p.city AS city";
7094 let plan_result = plan(&parse(&lex(src).expect("lex")).expect("parse"));
7095 let err = plan_result
7096 .expect_err("typo'd ORDER BY alias in non-aggregate WITH must be a named plan error");
7097 assert!(
7098 err.contains("nope") || err.contains("unbound"),
7099 "error must mention the unknown alias: {err}"
7100 );
7101 }
7102
7103 #[test]
7107 fn post_unwind_where_references_pre_match_variable() {
7108 let mut fx = Fx::new();
7109 fx.add(
7111 "N",
7112 "n1",
7113 vec![
7114 ("threshold", Value::Int(3)),
7115 (
7116 "xs",
7117 Value::List(vec![
7118 Value::Int(1),
7119 Value::Int(2),
7120 Value::Int(3),
7121 Value::Int(4),
7122 Value::Int(5),
7123 ]),
7124 ),
7125 ],
7126 );
7127 fx.add(
7129 "N",
7130 "n2",
7131 vec![
7132 ("threshold", Value::Int(10)),
7133 ("xs", Value::List(vec![Value::Int(1), Value::Int(2)])),
7134 ],
7135 );
7136 let view = fx.view();
7137 let params = BTreeMap::new();
7138 let rs = run(
7139 &view,
7140 "MATCH (n:N) UNWIND n.xs AS x WHERE x > n.threshold RETURN x",
7141 ¶ms,
7142 )
7143 .expect("post-UNWIND WHERE referencing MATCH variable must succeed");
7144 assert_eq!(rs.len(), 2, "exactly 2 rows: x=4 and x=5 from n1");
7146 let v0 = rs.get(0, "x").cloned();
7147 let v1 = rs.get(1, "x").cloned();
7148 let got = [v0, v1];
7149 assert!(got.contains(&Some(Value::Int(4))), "x=4 present");
7150 assert!(got.contains(&Some(Value::Int(5))), "x=5 present");
7151 }
7152
7153 #[test]
7157 fn with_stage_aggregate_order_by_descending() {
7158 let fx = city_graph();
7161 let view = fx.view();
7162 let params = BTreeMap::new();
7163 let rs = run(
7164 &view,
7165 "MATCH (p:Person) WITH p.city AS city, COUNT(*) AS cnt ORDER BY cnt DESC RETURN city, cnt",
7166 ¶ms,
7167 )
7168 .expect("aggregate WITH ORDER BY must succeed");
7169 assert_eq!(rs.len(), 2, "two city groups");
7170 assert_eq!(
7172 rs.get(0, "cnt"),
7173 Some(&Value::Int(3)),
7174 "first row must be the group with cnt=3 (Boston)"
7175 );
7176 assert_eq!(
7177 rs.get(1, "cnt"),
7178 Some(&Value::Int(2)),
7179 "second row must be the group with cnt=2 (Austin)"
7180 );
7181 }
7182
7183 #[test]
7187 fn unwind_then_with_composition() {
7188 let mut fx = Fx::new();
7189 fx.add(
7190 "Doc",
7191 "d1",
7192 vec![(
7193 "scores",
7194 Value::List(vec![Value::Int(10), Value::Int(20), Value::Int(30)]),
7195 )],
7196 );
7197 fx.add(
7198 "Doc",
7199 "d2",
7200 vec![("scores", Value::List(vec![Value::Int(5), Value::Int(15)]))],
7201 );
7202 let view = fx.view();
7203 let params = BTreeMap::new();
7204
7205 let rs = run(
7207 &view,
7208 "MATCH (n:Doc) UNWIND n.scores AS x WITH x RETURN x",
7209 ¶ms,
7210 )
7211 .expect("UNWIND then WITH pass-through must succeed");
7212 assert_eq!(rs.len(), 5, "3 + 2 = 5 expanded rows carried through WITH");
7213
7214 let rs2 = run(
7216 &view,
7217 "MATCH (n:Doc) UNWIND n.scores AS x WITH COUNT(*) AS total RETURN total",
7218 ¶ms,
7219 )
7220 .expect("UNWIND then aggregate WITH must succeed");
7221 assert_eq!(rs2.len(), 1, "single aggregate row");
7222 assert_eq!(
7223 rs2.get(0, "total"),
7224 Some(&Value::Int(5)),
7225 "total must be 5 (3+2 expanded rows)"
7226 );
7227 }
7228
7229 #[test]
7235 fn binarith_div_min_over_neg1_does_not_panic() {
7236 let fx = Fx::new();
7237 let view = fx.view();
7238 let vars = VarTable { names: vec![] };
7239 let row: Row = vec![];
7240 let params = BTreeMap::new();
7241
7242 let operand = Operand::BinArith {
7244 op: ArithOp::Div,
7245 left: Box::new(Operand::Lit(Value::Int(i64::MIN))),
7246 right: Box::new(Operand::Lit(Value::Int(-1))),
7247 };
7248
7249 let result = resolve_operand(&view, &vars, &row, &operand, &Params(¶ms));
7250 assert!(
7251 result.is_ok(),
7252 "overflow division must not return Err: {result:?}"
7253 );
7254 assert_eq!(
7255 result.unwrap(),
7256 Some(Value::Int(i64::MAX)),
7257 "i64::MIN / -1 must saturate to i64::MAX, not panic"
7258 );
7259 }
7260
7261 #[test]
7266 fn binarith_missing_param_caught_at_preflight() {
7267 let fx = Fx::new();
7268 let view = fx.view();
7269 let err = run(
7274 &view,
7275 "MATCH (n:X) RETURN abs($missing - 1)",
7276 &BTreeMap::new(),
7277 )
7278 .expect_err("missing param inside BinArith must be caught at preflight");
7279 assert!(
7280 err.contains("missing"),
7281 "error must name the param 'missing', got: {err}"
7282 );
7283 }
7284
7285 #[test]
7289 fn is_null_filters_absent_prop() {
7290 let mut fx = Fx::new();
7291 fx.add("Person", "alice", vec![("age", Value::Int(30))]);
7292 fx.add("Person", "bob", vec![]); let view = fx.view();
7294 let rs = run(
7295 &view,
7296 "MATCH (n:Person) WHERE n.age IS NULL RETURN n",
7297 &BTreeMap::new(),
7298 )
7299 .unwrap();
7300 assert_eq!(rs.len(), 1, "only bob lacks age");
7301 assert_eq!(rs.get(0, "n"), Some(&s("bob")));
7302 }
7303
7304 #[test]
7306 fn is_not_null_filters_present_prop() {
7307 let mut fx = Fx::new();
7308 fx.add("Person", "alice", vec![("age", Value::Int(30))]);
7309 fx.add("Person", "bob", vec![]); let view = fx.view();
7311 let rs = run(
7312 &view,
7313 "MATCH (n:Person) WHERE n.age IS NOT NULL RETURN n",
7314 &BTreeMap::new(),
7315 )
7316 .unwrap();
7317 assert_eq!(rs.len(), 1, "only alice has age");
7318 assert_eq!(rs.get(0, "n"), Some(&s("alice")));
7319 }
7320
7321 #[test]
7324 fn optional_match_is_null_anti_join() {
7325 let mut fx = Fx::new();
7326 let alice = fx.add("Person", "alice", vec![]);
7327 let bob = fx.add("Person", "bob", vec![]);
7328 let carol = fx.add("Person", "carol", vec![]);
7329 fx.edge("KNOWS", alice, carol, vec![]); fx.edge("KNOWS", carol, bob, vec![]); let view = fx.view();
7333 let rs = run(
7334 &view,
7335 "MATCH (a:Person) OPTIONAL MATCH (a)-[:KNOWS]->(b) WITH a, b WHERE b IS NULL RETURN a",
7336 &BTreeMap::new(),
7337 )
7338 .unwrap();
7339 assert_eq!(rs.len(), 1, "only bob has no outgoing KNOWS edge");
7340 assert_eq!(rs.get(0, "a"), Some(&s("bob")));
7341 }
7342
7343 #[test]
7345 fn is_null_combined_with_and_exec() {
7346 let mut fx = Fx::new();
7347 fx.add("N", "a", vec![("x", Value::Int(1))]);
7348 fx.add("N", "b", vec![("x", Value::Int(2)), ("y", Value::Int(9))]);
7349 fx.add("N", "c", vec![]); let view = fx.view();
7351 let rs = run(
7353 &view,
7354 "MATCH (n:N) WHERE n.y IS NULL AND n.x IS NOT NULL RETURN n",
7355 &BTreeMap::new(),
7356 )
7357 .unwrap();
7358 assert_eq!(rs.len(), 1);
7359 assert_eq!(rs.get(0, "n"), Some(&s("a")));
7360 }
7361
7362 #[test]
7366 fn arithmetic_add_in_return() {
7367 let mut fx = Fx::new();
7368 fx.add("Person", "alice", vec![("age", Value::Int(29))]);
7369 let view = fx.view();
7370 let rs = run(
7371 &view,
7372 "MATCH (n:Person) RETURN n.age + 1 AS adjusted",
7373 &BTreeMap::new(),
7374 )
7375 .unwrap();
7376 assert_eq!(rs.len(), 1);
7377 assert_eq!(rs.get(0, "adjusted"), Some(&Value::Int(30)));
7378 }
7379
7380 #[test]
7382 fn arithmetic_precedence_parens_pin() {
7383 let mut fx = Fx::new();
7384 fx.add("N", "x", vec![]);
7385 let view = fx.view();
7386 let rs = run(
7387 &view,
7388 "MATCH (n:N) RETURN (1 + 2) * 3 AS r",
7389 &BTreeMap::new(),
7390 )
7391 .unwrap();
7392 assert_eq!(rs.len(), 1);
7393 assert_eq!(rs.get(0, "r"), Some(&Value::Int(9)));
7394 }
7395
7396 #[test]
7398 fn arithmetic_precedence_mul_over_add_pin() {
7399 let mut fx = Fx::new();
7400 fx.add("N", "x", vec![]);
7401 let view = fx.view();
7402 let rs = run(&view, "MATCH (n:N) RETURN 1 + 2 * 3 AS r", &BTreeMap::new()).unwrap();
7403 assert_eq!(rs.len(), 1);
7404 assert_eq!(rs.get(0, "r"), Some(&Value::Int(7)));
7405 }
7406
7407 #[test]
7409 fn arithmetic_div_by_zero_returns_error() {
7410 let mut fx = Fx::new();
7411 fx.add("N", "x", vec![]);
7412 let view = fx.view();
7413 let err = run(
7414 &view,
7415 "MATCH (n:N) WHERE 1 / 0 > 0 RETURN n",
7416 &BTreeMap::new(),
7417 )
7418 .expect_err("division by zero must error");
7419 assert!(
7420 err.contains("division by zero"),
7421 "error must mention division by zero, got: {err}"
7422 );
7423 }
7424
7425 #[test]
7427 fn arithmetic_null_propagates() {
7428 let mut fx = Fx::new();
7429 fx.add("N", "x", vec![]); let view = fx.view();
7431 let rs = run(
7433 &view,
7434 "MATCH (n:N) WHERE n.val + 1 > 0 RETURN n",
7435 &BTreeMap::new(),
7436 )
7437 .unwrap();
7438 assert_eq!(rs.len(), 0, "null arithmetic must not match");
7439 }
7440
7441 #[test]
7443 fn case_when_expression_in_return() {
7444 let mut fx = Fx::new();
7445 fx.add("N", "a", vec![("id", s("a")), ("age", i(20))]);
7446 fx.add("N", "b", vec![("id", s("b")), ("age", i(65))]);
7447 let v = fx.view();
7448 let rs = run(
7449 &v,
7450 "MATCH (n:N) RETURN n.id AS id, \
7451 CASE WHEN n.age >= 65 THEN 'senior' ELSE 'other' END AS band",
7452 &BTreeMap::new(),
7453 )
7454 .unwrap();
7455 let band_of = |who: &str| {
7456 (0..rs.len())
7457 .find(|&i| rs.get(i, "id") == Some(&s(who)))
7458 .and_then(|i| rs.get(i, "band").cloned())
7459 };
7460 assert_eq!(band_of("a"), Some(s("other")));
7461 assert_eq!(band_of("b"), Some(s("senior")));
7462 }
7463
7464 #[test]
7465 fn case_when_no_else_yields_null() {
7466 let mut fx = Fx::new();
7467 fx.add("N", "a", vec![("age", i(20))]);
7468 let v = fx.view();
7469 let rs = run(
7470 &v,
7471 "MATCH (n:N) RETURN CASE WHEN n.age >= 65 THEN 'senior' END AS band",
7472 &BTreeMap::new(),
7473 )
7474 .unwrap();
7475 assert_eq!(rs.get(0, "band"), None);
7476 }
7477
7478 #[test]
7479 fn multi_relationship_type_pattern_matches_either() {
7480 let mut fx = Fx::new();
7481 let a = fx.add("N", "a", vec![("id", s("a"))]);
7482 let b = fx.add("N", "b", vec![("id", s("b"))]);
7483 let c = fx.add("N", "c", vec![("id", s("c"))]);
7484 let d = fx.add("N", "d", vec![("id", s("d"))]);
7485 fx.edge("KNOWS", a, b, vec![]);
7486 fx.edge("LIKES", a, c, vec![]);
7487 fx.edge("HATES", a, d, vec![]);
7488 let v = fx.view();
7489 let rs = run(
7491 &v,
7492 "MATCH (a:N {id: 'a'})-[r:KNOWS|:LIKES]->(x) RETURN x",
7493 &BTreeMap::new(),
7494 )
7495 .unwrap();
7496 assert_eq!(rs.len(), 2, "KNOWS|LIKES reaches exactly b and c");
7497 }
7498
7499 #[test]
7500 fn collect_grouped_gathers_values_per_group() {
7501 let mut fx = Fx::new();
7502 fx.add("P", "a", vec![("city", s("austin")), ("name", s("Ann"))]);
7503 fx.add("P", "b", vec![("city", s("austin")), ("name", s("Bob"))]);
7504 fx.add("P", "c", vec![("city", s("boston")), ("name", s("Cy"))]);
7505 let v = fx.view();
7506 let rs = run(
7507 &v,
7508 "MATCH (n:P) RETURN n.city AS city, collect(n.name) AS names",
7509 &BTreeMap::new(),
7510 )
7511 .unwrap();
7512 assert_eq!(rs.len(), 2, "two city groups");
7513 let austin = (0..rs.len())
7515 .find(|&i| rs.get(i, "city") == Some(&s("austin")))
7516 .expect("austin group present");
7517 assert_eq!(
7518 rs.get(austin, "names"),
7519 Some(&Value::List(vec![s("Ann"), s("Bob")]))
7520 );
7521 }
7522
7523 #[test]
7524 fn collect_ungrouped_gathers_all_into_one_list() {
7525 let mut fx = Fx::new();
7526 fx.add("P", "a", vec![("name", s("Ann"))]);
7527 fx.add("P", "b", vec![("name", s("Bob"))]);
7528 let v = fx.view();
7529 let rs = run(
7530 &v,
7531 "MATCH (n:P) RETURN collect(n.name) AS names",
7532 &BTreeMap::new(),
7533 )
7534 .unwrap();
7535 assert_eq!(rs.len(), 1);
7536 assert_eq!(
7537 rs.get(0, "names"),
7538 Some(&Value::List(vec![s("Ann"), s("Bob")]))
7539 );
7540 }
7541
7542 #[test]
7543 fn string_predicate_functions_in_where() {
7544 let mut fx = Fx::new();
7545 fx.add("N", "a", vec![("email", s("alice@acme.com"))]);
7546 fx.add("N", "b", vec![("email", s("bob@other.org"))]);
7547 let v = fx.view();
7548 let p = BTreeMap::new();
7549 assert_eq!(
7550 run(
7551 &v,
7552 "MATCH (n:N) WHERE endsWith(n.email, '.com') RETURN n",
7553 &p
7554 )
7555 .unwrap()
7556 .len(),
7557 1
7558 );
7559 assert_eq!(
7560 run(
7561 &v,
7562 "MATCH (n:N) WHERE startsWith(n.email, 'bob') RETURN n",
7563 &p
7564 )
7565 .unwrap()
7566 .len(),
7567 1
7568 );
7569 assert_eq!(
7570 run(
7571 &v,
7572 "MATCH (n:N) WHERE contains(n.email, 'acme') RETURN n",
7573 &p
7574 )
7575 .unwrap()
7576 .len(),
7577 1
7578 );
7579 }
7580
7581 #[test]
7582 fn coercion_functions_in_return() {
7583 let mut fx = Fx::new();
7584 fx.add("N", "a", vec![("s", s("42")), ("n", i(7)), ("g", f(3.9))]);
7585 let v = fx.view();
7586 let rs = run(
7587 &v,
7588 "MATCH (n:N) RETURN toInteger(n.s) AS ti, toFloat(n.n) AS tf, toString(n.g) AS ts",
7589 &BTreeMap::new(),
7590 )
7591 .unwrap();
7592 assert_eq!(rs.get(0, "ti"), Some(&Value::Int(42)));
7593 assert_eq!(rs.get(0, "tf"), Some(&Value::Float(7.0)));
7594 assert_eq!(rs.get(0, "ts"), Some(&Value::Str("3.9".into())));
7595 }
7596
7597 #[test]
7598 fn to_integer_unparseable_string_is_null() {
7599 let mut fx = Fx::new();
7600 fx.add("N", "a", vec![("s", s("not-a-number"))]);
7601 let v = fx.view();
7602 let rs = run(
7603 &v,
7604 "MATCH (n:N) RETURN toInteger(n.s) AS ti",
7605 &BTreeMap::new(),
7606 )
7607 .unwrap();
7608 assert_eq!(rs.get(0, "ti"), None);
7609 }
7610
7611 #[test]
7612 fn index_scan_uses_index_and_matches_fallback() {
7613 use core_storage::property_index::PropertyIndex;
7614 use std::sync::atomic::Ordering;
7615
7616 let mut fx = Fx::new();
7617 let a = fx.add("Person", "a", vec![("city", s("austin"))]);
7618 let _b = fx.add("Person", "b", vec![("city", s("boston"))]);
7619 let c = fx.add("Person", "c", vec![("city", s("austin"))]);
7620
7621 let mut pi = PropertyIndex::new();
7622 pi.enable("Person", "city");
7623 pi.set("Person", "city", a, &s("austin"));
7624 pi.set("Person", "city", 1, &s("boston"));
7625 pi.set("Person", "city", c, &s("austin"));
7626
7627 let q = "MATCH (n:Person {city: 'austin'}) RETURN n";
7628
7629 let before = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7631 let indexed = run(&fx.view_indexed(&pi), q, &BTreeMap::new()).unwrap();
7632 let after = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7633 assert!(after > before, "IndexScan must take the indexed path");
7634 assert_eq!(indexed.len(), 2);
7635
7636 let before2 = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7639 let fallback = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7640 let after2 = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7641 assert_eq!(after2, before2, "fallback must not touch the index counter");
7642 assert_eq!(
7643 fallback.len(),
7644 indexed.len(),
7645 "fallback matches indexed result"
7646 );
7647 }
7648
7649 #[test]
7650 fn arithmetic_in_where_comparison() {
7651 let mut fx = Fx::new();
7652 fx.add("Person", "alice", vec![("age", Value::Int(5))]); fx.add("Person", "bob", vec![("age", Value::Int(4))]); let view = fx.view();
7655 let rs = run(
7656 &view,
7657 "MATCH (n:Person) WHERE n.age + 1 > 5 RETURN n",
7658 &BTreeMap::new(),
7659 )
7660 .unwrap();
7661 assert_eq!(rs.len(), 1);
7662 assert_eq!(rs.get(0, "n"), Some(&s("alice")));
7663 }
7664
7665 #[test]
7669 fn where_fold_indexed_matches_fallback() {
7670 use core_storage::property_index::PropertyIndex;
7671 use std::sync::atomic::Ordering;
7672
7673 let mut fx = Fx::new();
7674 let a = fx.add("Person", "alice", vec![("city", s("austin"))]);
7675 let b = fx.add("Person", "bob", vec![("city", s("boston"))]);
7676 let c = fx.add("Person", "carol", vec![("city", s("austin"))]);
7677
7678 let mut pi = PropertyIndex::new();
7679 pi.enable("Person", "city");
7680 pi.set("Person", "city", a, &s("austin"));
7681 pi.set("Person", "city", b, &s("boston"));
7682 pi.set("Person", "city", c, &s("austin"));
7683
7684 let q = "MATCH (n:Person) WHERE n.city = 'austin' RETURN n";
7685
7686 let before = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7688 let indexed = run(&fx.view_indexed(&pi), q, &BTreeMap::new()).unwrap();
7689 let after = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7690 assert!(
7691 after > before,
7692 "WHERE equality fold must take the indexed path"
7693 );
7694 assert_eq!(indexed.len(), 2);
7695
7696 let fallback = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7698 assert_eq!(
7699 rows_of(&fallback),
7700 rows_of(&indexed),
7701 "fallback must return identical rows, not just the same count"
7702 );
7703 }
7704
7705 #[test]
7707 fn where_fold_miss_returns_empty() {
7708 let mut fx = Fx::new();
7709 fx.add("Person", "alice", vec![("city", s("austin"))]);
7710 let q = "MATCH (n:Person) WHERE n.city = 'berlin' RETURN n";
7711 let rs = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7712 assert_eq!(rs.len(), 0);
7713 }
7714
7715 #[test]
7717 fn where_fold_param_uses_index() {
7718 use core_storage::property_index::PropertyIndex;
7719 use std::sync::atomic::Ordering;
7720
7721 let mut fx = Fx::new();
7722 let a = fx.add("Person", "alice", vec![("city", s("austin"))]);
7723 let b = fx.add("Person", "bob", vec![("city", s("boston"))]);
7724
7725 let mut pi = PropertyIndex::new();
7726 pi.enable("Person", "city");
7727 pi.set("Person", "city", a, &s("austin"));
7728 pi.set("Person", "city", b, &s("boston"));
7729
7730 let q = "MATCH (n:Person) WHERE n.city = $c RETURN n";
7731 let mut params = BTreeMap::new();
7732 params.insert("c".to_string(), s("austin"));
7733
7734 let before = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7735 let rs = run(&fx.view_indexed(&pi), q, ¶ms).unwrap();
7736 let after = super::INDEX_SCAN_FIRES.load(Ordering::Relaxed);
7737 assert!(after > before, "$param WHERE equality must use index");
7738 assert_eq!(rs.len(), 1);
7739 }
7740
7741 #[test]
7743 fn where_fold_residual_filter_applied() {
7744 let mut fx = Fx::new();
7745 fx.add(
7746 "Person",
7747 "young-austin",
7748 vec![("city", s("austin")), ("age", Value::Int(20))],
7749 );
7750 fx.add(
7751 "Person",
7752 "old-austin",
7753 vec![("city", s("austin")), ("age", Value::Int(40))],
7754 );
7755 fx.add(
7756 "Person",
7757 "boston",
7758 vec![("city", s("boston")), ("age", Value::Int(20))],
7759 );
7760
7761 let q = "MATCH (n:Person) WHERE n.city = 'austin' AND n.age > 30 RETURN n";
7762 let rs = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7763 assert_eq!(rs.len(), 1, "only old-austin should match city+age filter");
7764 assert_eq!(rs.get(0, "n"), Some(&s("old-austin")));
7765 }
7766
7767 #[test]
7773 fn where_fold_unindexed_field_fallback() {
7774 let mut fx = Fx::new();
7775 fx.add("Person", "a", vec![("notindexed", s("x"))]);
7776 fx.add("Person", "b", vec![("notindexed", s("y"))]);
7777 let rs = run(
7778 &fx.view(),
7779 "MATCH (n:Person) WHERE n.notindexed = 'x' RETURN n",
7780 &BTreeMap::new(),
7781 )
7782 .unwrap();
7783 assert_eq!(rs.len(), 1);
7784 assert_eq!(rs.get(0, "n"), Some(&s("a")));
7785 }
7786
7787 #[test]
7791 fn index_intersect_both_indexed_fires() {
7792 use core_storage::property_index::PropertyIndex;
7793 use std::sync::atomic::Ordering;
7794
7795 let mut fx = Fx::new();
7796 let a = fx.add(
7798 "Person",
7799 "alice",
7800 vec![("city", s("austin")), ("age", Value::Int(30))],
7801 );
7802 let b = fx.add(
7804 "Person",
7805 "bob",
7806 vec![("city", s("austin")), ("age", Value::Int(25))],
7807 );
7808 let c = fx.add(
7810 "Person",
7811 "carol",
7812 vec![("city", s("boston")), ("age", Value::Int(30))],
7813 );
7814
7815 let mut pi = PropertyIndex::new();
7816 pi.enable("Person", "city");
7817 pi.enable("Person", "age");
7818 pi.set("Person", "city", a, &s("austin"));
7819 pi.set("Person", "city", b, &s("austin"));
7820 pi.set("Person", "city", c, &s("boston"));
7821 pi.set("Person", "age", a, &Value::Int(30));
7822 pi.set("Person", "age", b, &Value::Int(25));
7823 pi.set("Person", "age", c, &Value::Int(30));
7824
7825 let q = "MATCH (n:Person) WHERE n.city = 'austin' AND n.age = 30 RETURN n";
7826
7827 let before = super::INDEX_INTERSECT_FIRES.load(Ordering::Relaxed);
7828 let indexed = run(&fx.view_indexed(&pi), q, &BTreeMap::new()).unwrap();
7829 let after = super::INDEX_INTERSECT_FIRES.load(Ordering::Relaxed);
7830 assert!(
7831 after > before,
7832 "IndexIntersect must advance counter on indexed path"
7833 );
7834 assert_eq!(indexed.len(), 1);
7835 assert_eq!(indexed.get(0, "n"), Some(&s("alice")));
7836
7837 let fallback = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7839 assert_eq!(
7840 rows_of(&fallback),
7841 rows_of(&indexed),
7842 "fallback must return identical rows"
7843 );
7844 }
7845
7846 #[test]
7848 fn index_intersect_one_indexed_one_not() {
7849 use core_storage::property_index::PropertyIndex;
7850 use std::sync::atomic::Ordering;
7851
7852 let mut fx = Fx::new();
7853 let a = fx.add(
7854 "Person",
7855 "alice",
7856 vec![("city", s("austin")), ("role", s("eng"))],
7857 );
7858 let b = fx.add(
7859 "Person",
7860 "bob",
7861 vec![("city", s("austin")), ("role", s("mgr"))],
7862 );
7863 let _c = fx.add(
7864 "Person",
7865 "carol",
7866 vec![("city", s("boston")), ("role", s("eng"))],
7867 );
7868
7869 let mut pi = PropertyIndex::new();
7871 pi.enable("Person", "city");
7872 pi.set("Person", "city", a, &s("austin"));
7873 pi.set("Person", "city", b, &s("austin"));
7874
7875 let q = "MATCH (n:Person) WHERE n.city = 'austin' AND n.role = 'eng' RETURN n";
7876
7877 let before = super::INDEX_INTERSECT_FIRES.load(Ordering::Relaxed);
7878 let indexed = run(&fx.view_indexed(&pi), q, &BTreeMap::new()).unwrap();
7879 let after = super::INDEX_INTERSECT_FIRES.load(Ordering::Relaxed);
7880 assert!(
7881 after > before,
7882 "IndexIntersect must fire when at least one field is indexed"
7883 );
7884 assert_eq!(indexed.len(), 1);
7885 assert_eq!(indexed.get(0, "n"), Some(&s("alice")));
7886
7887 let fallback = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7888 assert_eq!(
7889 rows_of(&fallback),
7890 rows_of(&indexed),
7891 "fallback must return identical rows"
7892 );
7893 }
7894
7895 #[test]
7900 fn index_intersect_no_indexed_fallback() {
7901 let mut fx = Fx::new();
7902 fx.add("Person", "alice", vec![("x", s("1")), ("y", s("a"))]);
7903 fx.add("Person", "bob", vec![("x", s("1")), ("y", s("b"))]);
7904 fx.add("Person", "carol", vec![("x", s("2")), ("y", s("a"))]);
7905
7906 let q = "MATCH (n:Person) WHERE n.x = '1' AND n.y = 'a' RETURN n";
7907 let rs = run(&fx.view(), q, &BTreeMap::new()).unwrap();
7908 assert_eq!(rs.len(), 1);
7909 assert_eq!(rs.get(0, "n"), Some(&s("alice")));
7910 }
7911
7912 #[test]
7914 fn index_intersect_empty_intersection() {
7915 use core_storage::property_index::PropertyIndex;
7916
7917 let mut fx = Fx::new();
7918 let a = fx.add(
7919 "Person",
7920 "alice",
7921 vec![("city", s("austin")), ("age", Value::Int(30))],
7922 );
7923 let b = fx.add(
7924 "Person",
7925 "bob",
7926 vec![("city", s("boston")), ("age", Value::Int(25))],
7927 );
7928
7929 let mut pi = PropertyIndex::new();
7930 pi.enable("Person", "city");
7931 pi.enable("Person", "age");
7932 pi.set("Person", "city", a, &s("austin"));
7933 pi.set("Person", "city", b, &s("boston"));
7934 pi.set("Person", "age", a, &Value::Int(30));
7935 pi.set("Person", "age", b, &Value::Int(25));
7936
7937 let q = "MATCH (n:Person) WHERE n.city = 'boston' AND n.age = 30 RETURN n";
7939 let rs = run(&fx.view_indexed(&pi), q, &BTreeMap::new()).unwrap();
7940 assert_eq!(rs.len(), 0);
7941 }
7942
7943 #[test]
7945 fn index_intersect_with_params() {
7946 use core_storage::property_index::PropertyIndex;
7947 use std::sync::atomic::Ordering;
7948
7949 let mut fx = Fx::new();
7950 let a = fx.add(
7951 "Person",
7952 "alice",
7953 vec![("city", s("austin")), ("age", Value::Int(30))],
7954 );
7955 let b = fx.add(
7956 "Person",
7957 "bob",
7958 vec![("city", s("boston")), ("age", Value::Int(30))],
7959 );
7960
7961 let mut pi = PropertyIndex::new();
7962 pi.enable("Person", "city");
7963 pi.enable("Person", "age");
7964 pi.set("Person", "city", a, &s("austin"));
7965 pi.set("Person", "city", b, &s("boston"));
7966 pi.set("Person", "age", a, &Value::Int(30));
7967 pi.set("Person", "age", b, &Value::Int(30));
7968
7969 let q = "MATCH (n:Person) WHERE n.city = $c AND n.age = $a RETURN n";
7970 let mut params = BTreeMap::new();
7971 params.insert("c".to_string(), s("austin"));
7972 params.insert("a".to_string(), Value::Int(30));
7973
7974 let before = super::INDEX_INTERSECT_FIRES.load(Ordering::Relaxed);
7975 let indexed = run(&fx.view_indexed(&pi), q, ¶ms).unwrap();
7976 let after = super::INDEX_INTERSECT_FIRES.load(Ordering::Relaxed);
7977 assert!(after > before, "$param intersect must fire indexed path");
7978 assert_eq!(indexed.len(), 1);
7979 assert_eq!(indexed.get(0, "n"), Some(&s("alice")));
7980
7981 let fallback = run(&fx.view(), q, ¶ms).unwrap();
7982 assert_eq!(
7983 rows_of(&fallback),
7984 rows_of(&indexed),
7985 "fallback must return identical rows"
7986 );
7987 }
7988
7989 #[test]
7991 fn index_intersect_three_fields() {
7992 use core_storage::property_index::PropertyIndex;
7993
7994 let mut fx = Fx::new();
7995 let a = fx.add(
7997 "Person",
7998 "alice",
7999 vec![
8000 ("city", s("austin")),
8001 ("age", Value::Int(30)),
8002 ("role", s("eng")),
8003 ],
8004 );
8005 let b = fx.add(
8007 "Person",
8008 "bob",
8009 vec![
8010 ("city", s("austin")),
8011 ("age", Value::Int(30)),
8012 ("role", s("mgr")),
8013 ],
8014 );
8015
8016 let mut pi = PropertyIndex::new();
8017 pi.enable("Person", "city");
8018 pi.enable("Person", "age");
8019 pi.set("Person", "city", a, &s("austin"));
8021 pi.set("Person", "city", b, &s("austin"));
8022 pi.set("Person", "age", a, &Value::Int(30));
8023 pi.set("Person", "age", b, &Value::Int(30));
8024
8025 let q =
8026 "MATCH (n:Person) WHERE n.city = 'austin' AND n.age = 30 AND n.role = 'eng' RETURN n";
8027 let indexed = run(&fx.view_indexed(&pi), q, &BTreeMap::new()).unwrap();
8028 assert_eq!(indexed.len(), 1);
8029 assert_eq!(indexed.get(0, "n"), Some(&s("alice")));
8030
8031 let fallback = run(&fx.view(), q, &BTreeMap::new()).unwrap();
8032 assert_eq!(
8033 rows_of(&fallback),
8034 rows_of(&indexed),
8035 "fallback must return identical rows"
8036 );
8037 }
8038 fn assoc_graph() -> Fx {
8051 let mut fx = Fx::new();
8052 let t1 = fx.add(
8053 "Talent",
8054 "t1",
8055 vec![
8056 ("status", s("published")),
8057 ("years_of_experience", i(12)),
8058 (
8059 "specialties",
8060 Value::List(vec![s("hospitality"), s("retail")]),
8061 ),
8062 ("location", Value::List(vec![f(40.71), f(-74.01)])),
8063 ],
8064 );
8065 let t2 = fx.add(
8066 "Talent",
8067 "t2",
8068 vec![
8069 ("status", s("published")),
8070 ("years_of_experience", i(11)),
8071 ("location", Value::List(vec![f(41.88), f(-87.63)])),
8072 ],
8073 );
8074 let t3 = fx.add(
8075 "Talent",
8076 "t3",
8077 vec![("status", s("published")), ("years_of_experience", i(3))],
8078 );
8079 let c1 = fx.add("Company", "c1", vec![("name", s("Acme Design Works"))]);
8080 let c2 = fx.add("Company", "c2", vec![("name", s("Beta Studio"))]);
8081 let c3 = fx.add("Company", "c3", vec![("name", s("Gamma Works"))]);
8082 for (t, c) in [(t1, c1), (t2, c1), (t3, c3)] {
8083 fx.edge("INDUSTRY_ALIGNMENT", t, c, vec![]);
8084 fx.edge("SPECIALTY_MATCH", t, c, vec![]);
8085 fx.edge("LOCATION_FIT", t, c, vec![]);
8086 }
8087 fx.edge("INDUSTRY_ALIGNMENT", t1, c2, vec![]);
8089 fx.edge("SPECIALTY_MATCH", t1, c2, vec![]);
8090 fx
8091 }
8092
8093 #[test]
8096 fn node_key_reads_as_a_property() {
8097 let fx = assoc_graph();
8098 let rs = run(
8099 &fx.view(),
8100 "MATCH (n:Company) RETURN n.key",
8101 &BTreeMap::new(),
8102 )
8103 .unwrap();
8104 assert_eq!(
8105 rows_of(&rs),
8106 vec![
8107 vec![Some(s("c1"))],
8108 vec![Some(s("c2"))],
8109 vec![Some(s("c3"))]
8110 ]
8111 );
8112 }
8113
8114 #[test]
8118 fn node_key_survives_a_with_aggregation() {
8119 let fx = assoc_graph();
8120 let rs = run(
8121 &fx.view(),
8122 "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company) \
8123 WITH c, count(*) AS n WHERE n >= 1 RETURN c.key, key(c), n",
8124 &BTreeMap::new(),
8125 )
8126 .unwrap();
8127 assert_eq!(
8128 rows_of(&rs),
8129 vec![
8130 vec![Some(s("c1")), Some(s("c1")), Some(i(2))],
8131 vec![Some(s("c2")), Some(s("c2")), Some(i(1))],
8132 vec![Some(s("c3")), Some(s("c3")), Some(i(1))],
8133 ]
8134 );
8135 }
8136
8137 #[test]
8139 fn stored_key_property_wins_over_node_key() {
8140 let mut fx = Fx::new();
8141 fx.add("N", "a", vec![("key", s("stored"))]);
8142 let rs = run(&fx.view(), "MATCH (n:N) RETURN n.key", &BTreeMap::new()).unwrap();
8143 assert_eq!(rows_of(&rs), vec![vec![Some(s("stored"))]]);
8144 }
8145
8146 #[test]
8148 fn n_id_falls_back_to_key_when_unstored() {
8149 let mut fx = Fx::new();
8150 fx.add("Person", "alice", vec![]);
8151 let rs = run(
8152 &fx.view(),
8153 "MATCH (n:Person) WHERE n.id = 'alice' RETURN n.id, id(n), n.key",
8154 &BTreeMap::new(),
8155 )
8156 .unwrap();
8157 assert_eq!(
8158 rows_of(&rs),
8159 vec![vec![Some(s("alice")), Some(s("alice")), Some(s("alice"))]]
8160 );
8161 }
8162
8163 #[test]
8171 fn stored_id_property_wins_over_key() {
8172 let mut fx = Fx::new();
8173 fx.add("N", "k", vec![("id", s("other"))]);
8174 fx.add("N", "fallback", vec![]);
8175 let v = fx.view();
8176 let by_key = run(
8177 &v,
8178 "MATCH (n:N) WHERE n.id = 'fallback' RETURN key(n) AS k",
8179 &BTreeMap::new(),
8180 )
8181 .unwrap();
8182 assert_eq!(
8183 col(&by_key, "k"),
8184 vec![Some(s("fallback"))],
8185 "a node with no stored id is found by its key, and the stored-id \
8186 node is not swept in with it"
8187 );
8188 let miss = run(
8189 &v,
8190 "MATCH (n:N) WHERE n.id = 'k' RETURN n.id",
8191 &BTreeMap::new(),
8192 )
8193 .unwrap();
8194 assert!(
8195 miss.is_empty(),
8196 "stored id must win: WHERE n.id = key is empty"
8197 );
8198 let hit = run(
8199 &v,
8200 "MATCH (n:N) WHERE n.id = 'other' RETURN n.id",
8201 &BTreeMap::new(),
8202 )
8203 .unwrap();
8204 assert_eq!(rows_of(&hit), vec![vec![Some(s("other"))]]);
8205 let projected = run(&v, "MATCH (n:N) RETURN n.id AS i", &BTreeMap::new()).unwrap();
8206 let mut got: Vec<String> = col(&projected, "i")
8207 .into_iter()
8208 .map(|v| match v {
8209 Some(Value::Str(s)) => s,
8210 other => panic!("expected a string, got {other:?}"),
8211 })
8212 .collect();
8213 got.sort();
8214 assert_eq!(
8215 got,
8216 vec!["fallback".to_string(), "other".to_string()],
8217 "projection is stored-wins per node: the stored id for one, the \
8218 key fallback for the other"
8219 );
8220 }
8221
8222 #[test]
8224 fn id_function_rejects_non_node() {
8225 let (fx, _, _) = single_edge();
8226 let v = fx.view();
8227 let key_err = run(&v, "MATCH (a)-[r:T]->(b) RETURN key(r)", &BTreeMap::new())
8228 .expect_err("key() on a relationship must error");
8229 let id_err = run(&v, "MATCH (a)-[r:T]->(b) RETURN id(r)", &BTreeMap::new())
8230 .expect_err("id() on a relationship must error");
8231 assert!(
8232 key_err.contains("not a node"),
8233 "key() error class: {key_err}"
8234 );
8235 assert!(id_err.contains("not a node"), "id() error class: {id_err}");
8236 assert!(
8237 id_err.contains("id()"),
8238 "id() error must name itself: {id_err}"
8239 );
8240 }
8241
8242 #[test]
8244 fn where_n_id_eq_literal_uses_scan_key() {
8245 let mut fx = Fx::new();
8246 fx.add("Person", "alice", vec![]);
8247 let fires_before = super::SCAN_KEY_FIRES.load(std::sync::atomic::Ordering::Relaxed);
8248 let rs = run(
8249 &fx.view(),
8250 "MATCH (n:Person) WHERE n.id = 'alice' RETURN n",
8251 &BTreeMap::new(),
8252 )
8253 .unwrap();
8254 let fires_after = super::SCAN_KEY_FIRES.load(std::sync::atomic::Ordering::Relaxed);
8255 assert!(
8256 fires_after > fires_before,
8257 "SCAN_KEY_FIRES must increment; before={fires_before} after={fires_after}"
8258 );
8259 assert_eq!(rows_of(&rs), vec![vec![Some(s("alice"))]]);
8260 }
8261
8262 #[test]
8264 fn where_n_key_eq_param_uses_scan_key() {
8265 let mut fx = Fx::new();
8266 fx.add("Person", "alice", vec![]);
8267 let mut params = BTreeMap::new();
8268 params.insert("k".to_string(), s("alice"));
8269 let fires_before = super::SCAN_KEY_FIRES.load(std::sync::atomic::Ordering::Relaxed);
8270 let rs = run(
8271 &fx.view(),
8272 "MATCH (n:Person) WHERE n.key = $k RETURN n",
8273 ¶ms,
8274 )
8275 .unwrap();
8276 let fires_after = super::SCAN_KEY_FIRES.load(std::sync::atomic::Ordering::Relaxed);
8277 assert!(
8278 fires_after > fires_before,
8279 "SCAN_KEY_FIRES must increment; before={fires_before} after={fires_after}"
8280 );
8281 assert_eq!(rows_of(&rs), vec![vec![Some(s("alice"))]]);
8282 }
8283
8284 #[test]
8289 fn where_key_func_ignores_a_stored_key_property() {
8290 let mut fx = Fx::new();
8291 fx.add("N", "a", vec![("key", s("K"))]);
8292 fx.add("N", "K", vec![]);
8293 let rs = run(
8294 &fx.view(),
8295 "MATCH (n:N) WHERE key(n) = 'K' RETURN key(n) AS k",
8296 &BTreeMap::new(),
8297 )
8298 .unwrap();
8299 let got: Vec<Option<Value>> = col(&rs, "k");
8300 assert_eq!(
8301 got,
8302 vec![Some(s("K"))],
8303 "key(n) is the id-map key: only the node keyed K matches, not the \
8304 node whose stored `key` property is K"
8305 );
8306 }
8307
8308 #[test]
8311 fn where_id_func_ignores_a_stored_id_property() {
8312 let mut fx = Fx::new();
8313 fx.add("N", "k", vec![("id", s("other"))]);
8314 let rs = run(
8315 &fx.view(),
8316 "MATCH (n:N) WHERE id(n) = 'k' RETURN id(n) AS k",
8317 &BTreeMap::new(),
8318 )
8319 .unwrap();
8320 assert_eq!(
8321 col(&rs, "k"),
8322 vec![Some(s("k"))],
8323 "id(n) is the id-map key: a stored `id` property must not suppress \
8324 the match"
8325 );
8326 }
8327
8328 #[test]
8332 fn where_n_id_respects_stored_wins() {
8333 let mut fx = Fx::new();
8334 fx.add("N", "k", vec![("id", s("other"))]);
8335 fx.add("N", "other", vec![]);
8336 let rs = run(
8337 &fx.view(),
8338 "MATCH (n:N) WHERE n.id = 'other' RETURN key(n) AS k",
8339 &BTreeMap::new(),
8340 )
8341 .unwrap();
8342 let mut got: Vec<String> = col(&rs, "k")
8343 .into_iter()
8344 .map(|v| match v {
8345 Some(Value::Str(s)) => s,
8346 other => panic!("expected a string key, got {other:?}"),
8347 })
8348 .collect();
8349 got.sort();
8350 assert_eq!(
8351 got,
8352 vec!["k".to_string(), "other".to_string()],
8353 "stored-wins union: the stored-id node and the key-fallback node"
8354 );
8355 }
8356
8357 #[test]
8360 fn labels_and_label_read_the_node_label() {
8361 let fx = assoc_graph();
8362 let rs = run(
8363 &fx.view(),
8364 "MATCH (n:Company) RETURN labels(n), n.label LIMIT 1",
8365 &BTreeMap::new(),
8366 )
8367 .unwrap();
8368 assert_eq!(
8369 rows_of(&rs),
8370 vec![vec![
8371 Some(Value::List(vec![s("Company")])),
8372 Some(s("Company"))
8373 ]]
8374 );
8375 }
8376
8377 #[test]
8379 fn unknown_function_is_a_named_error() {
8380 let fx = assoc_graph();
8381 let err = run(
8382 &fx.view(),
8383 "MATCH (n:Company) RETURN nodes(n)",
8384 &BTreeMap::new(),
8385 )
8386 .expect_err("unknown function must error");
8387 assert!(err.contains("unknown function `nodes`"), "{err}");
8388 assert!(
8389 err.contains("labels"),
8390 "error must list what is supported: {err}"
8391 );
8392 }
8393
8394 #[test]
8397 fn infix_string_predicates_filter() {
8398 let fx = assoc_graph();
8399 let p = BTreeMap::new();
8400 for (q, want) in [
8401 (
8402 "MATCH (c:Company) WHERE c.name STARTS WITH 'Acme' RETURN c.key",
8403 vec!["c1"],
8404 ),
8405 (
8406 "MATCH (c:Company) WHERE c.name ENDS WITH 'Works' RETURN c.key",
8407 vec!["c1", "c3"],
8408 ),
8409 (
8410 "MATCH (c:Company) WHERE c.name CONTAINS 'Studio' RETURN c.key",
8411 vec!["c2"],
8412 ),
8413 (
8414 "MATCH (c:Company) WHERE NOT c.name CONTAINS 'Works' RETURN c.key",
8415 vec!["c2"],
8416 ),
8417 ] {
8418 let rs = run(&fx.view(), q, &p).unwrap_or_else(|e| panic!("{q}: {e}"));
8419 let got: Vec<String> = (0..rs.len())
8420 .map(|r| match rs.row(r)[0].clone() {
8421 Some(Value::Str(k)) => k,
8422 other => panic!("{q}: {other:?}"),
8423 })
8424 .collect();
8425 assert_eq!(got, want, "{q}");
8426 }
8427 }
8428
8429 #[test]
8432 fn infix_string_predicate_on_missing_property_is_false() {
8433 let fx = assoc_graph();
8434 let rs = run(
8435 &fx.view(),
8436 "MATCH (t:Talent) WHERE t.name STARTS WITH 'x' RETURN t.key",
8437 &BTreeMap::new(),
8438 )
8439 .unwrap();
8440 assert_eq!(rows_of(&rs), Vec::<Vec<Option<Value>>>::new());
8441 }
8442
8443 #[test]
8446 fn starts_without_with_is_a_named_error() {
8447 let err = parse(&lex("MATCH (c:Company) WHERE c.name STARTS 'Acme' RETURN c").unwrap())
8448 .expect_err("must not parse");
8449 assert!(err.contains("expected WITH after STARTS"), "{err}");
8450 }
8451
8452 #[test]
8455 fn list_subscript_reads_one_element() {
8456 let fx = assoc_graph();
8457 let rs = run(
8458 &fx.view(),
8459 "MATCH (t:Talent) WHERE t.key = 't1' \
8460 RETURN t.location[0] AS lat, t.location[1] AS lon, \
8461 t.location[-1] AS last, t.location[7] AS oob, t.status[0] AS notalist",
8462 &BTreeMap::new(),
8463 )
8464 .unwrap();
8465 assert_eq!(
8466 rows_of(&rs),
8467 vec![vec![
8468 Some(f(40.71)),
8469 Some(f(-74.01)),
8470 Some(f(-74.01)),
8471 None,
8472 None
8473 ]]
8474 );
8475 }
8476
8477 #[test]
8481 fn node_key_works_in_every_position() {
8482 let fx = assoc_graph();
8483 let p = BTreeMap::new();
8484 for (q, want) in [
8485 (
8486 "MATCH (c:Company) WHERE c.key = 'c2' RETURN c.key",
8487 vec!["c2"],
8488 ),
8489 (
8490 "MATCH (c:Company) WHERE c.key <> 'c1' RETURN c.key LIMIT 1",
8491 vec!["c2"],
8492 ),
8493 (
8494 "MATCH (c:Company) WITH c ORDER BY c.key DESC RETURN c.key LIMIT 1",
8495 vec!["c3"],
8496 ),
8497 ("MATCH (c:Company {key: 'c3'}) RETURN c.key", vec!["c3"]),
8498 (
8499 "MATCH (c:Company) WHERE c.label = 'Company' RETURN c.key LIMIT 1",
8500 vec!["c1"],
8501 ),
8502 ] {
8503 let rs = run(&fx.view(), q, &p).unwrap_or_else(|e| panic!("{q}: {e}"));
8504 let got: Vec<String> = (0..rs.len())
8505 .map(|r| match rs.row(r)[0].clone() {
8506 Some(Value::Str(k)) => k,
8507 other => panic!("{q}: {other:?}"),
8508 })
8509 .collect();
8510 assert_eq!(got, want, "{q}");
8511 }
8512 }
8513
8514 #[test]
8516 fn list_subscript_filters() {
8517 let fx = assoc_graph();
8518 let rs = run(
8519 &fx.view(),
8520 "MATCH (t:Talent) WHERE t.location[0] > 41.0 RETURN t.key",
8521 &BTreeMap::new(),
8522 )
8523 .unwrap();
8524 assert_eq!(rows_of(&rs), vec![vec![Some(s("t2"))]]);
8525 }
8526
8527 #[test]
8531 fn count_distinct_counts_each_binding_once() {
8532 let fx = assoc_graph();
8533 let q = "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT|:SPECIALTY_MATCH]->(c:Company) \
8534 WITH c, count(t) AS raw, count(DISTINCT t) AS uniq WHERE raw >= 1 \
8535 RETURN c.key, raw, uniq";
8536 let rs = run(&fx.view(), q, &BTreeMap::new()).unwrap();
8537 assert_eq!(
8538 rows_of(&rs),
8539 vec![
8540 vec![Some(s("c1")), Some(i(4)), Some(i(2))],
8541 vec![Some(s("c2")), Some(i(2)), Some(i(1))],
8542 vec![Some(s("c3")), Some(i(2)), Some(i(1))],
8543 ]
8544 );
8545 }
8546
8547 #[test]
8557 fn count_distinct_on_a_relationship_counts_edges() {
8558 let fx = assoc_graph();
8559 let rs = run(
8560 &fx.view(),
8561 "MATCH (a)-[r]->(b) RETURN count(r) AS raw, count(DISTINCT r) AS uniq",
8562 &BTreeMap::new(),
8563 )
8564 .unwrap();
8565 let row = &rows_of(&rs)[0];
8566 assert_eq!(
8567 row[0], row[1],
8568 "no pair is joined twice by one type: {row:?}"
8569 );
8570 assert_ne!(row[1], Some(i(0)), "a graph full of edges counts them");
8571 }
8572
8573 #[test]
8577 fn count_distinct_on_a_relationship_survives_an_alternation() {
8578 let fx = assoc_graph();
8579 let rs = run(
8580 &fx.view(),
8581 "MATCH (t:Talent)-[r:INDUSTRY_ALIGNMENT|:SPECIALTY_MATCH]->(c:Company) \
8582 RETURN count(r) AS raw, count(DISTINCT r) AS uniq",
8583 &BTreeMap::new(),
8584 )
8585 .unwrap();
8586 let row = &rows_of(&rs)[0];
8587 assert_eq!(row[0], row[1], "every row bound a different edge: {row:?}");
8588 }
8589
8590 #[test]
8592 fn collect_distinct_dedupes() {
8593 let fx = assoc_graph();
8594 let rs = run(
8595 &fx.view(),
8596 "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT|:SPECIALTY_MATCH]->(c:Company) \
8597 WHERE c.key = 'c1' WITH collect(DISTINCT t.status) AS st RETURN st",
8598 &BTreeMap::new(),
8599 )
8600 .unwrap();
8601 assert_eq!(
8602 rows_of(&rs),
8603 vec![vec![Some(Value::List(vec![s("published")]))]]
8604 );
8605 }
8606
8607 #[test]
8609 fn count_distinct_star_is_rejected() {
8610 let err =
8611 parse(&lex("MATCH (n) RETURN count(DISTINCT *)").unwrap()).expect_err("must not parse");
8612 assert!(
8613 err.contains("DISTINCT * is not a valid aggregate argument"),
8614 "{err}"
8615 );
8616 }
8617
8618 #[test]
8620 fn distinct_is_still_usable_as_a_variable_name() {
8621 let q = parse(&lex("MATCH (distinct) RETURN count(distinct)").unwrap()).unwrap();
8622 assert_eq!(
8623 q.returns[0].value,
8624 RetVal::Agg {
8625 func: crate::cypher::ast::AggFunc::Count,
8626 arg: crate::cypher::ast::AggArg::Var("distinct".into()),
8627 }
8628 );
8629 }
8630
8631 #[test]
8635 fn comma_patterns_intersect_on_shared_variables() {
8636 let fx = assoc_graph();
8637 let q = "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company), \
8638 (t)-[:SPECIALTY_MATCH]->(c), \
8639 (t)-[:LOCATION_FIT]->(c) \
8640 WHERE t.status = 'published' AND t.years_of_experience >= 10 \
8641 WITH c, count(DISTINCT t) AS n WHERE n >= 2 \
8642 RETURN c.key, n ORDER BY n DESC";
8643 let rs = run(&fx.view(), q, &BTreeMap::new()).unwrap();
8644 assert_eq!(rows_of(&rs), vec![vec![Some(s("c1")), Some(i(2))]]);
8645 }
8646
8647 #[test]
8651 fn comma_patterns_exclude_partial_matches() {
8652 let fx = assoc_graph();
8653 let q = "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company), \
8654 (t)-[:SPECIALTY_MATCH]->(c), \
8655 (t)-[:LOCATION_FIT]->(c) \
8656 WHERE t.years_of_experience >= 10 \
8657 WITH c, count(DISTINCT t) AS n RETURN c.key, n";
8658 let rs = run(&fx.view(), q, &BTreeMap::new()).unwrap();
8659 assert_eq!(rows_of(&rs), vec![vec![Some(s("c1")), Some(i(2))]]);
8660 }
8661
8662 #[test]
8665 fn comma_patterns_equal_separate_match_clauses() {
8666 let commas =
8667 parse(&lex("MATCH (a:A)-[:X]->(b:B), (a)-[:Y]->(b) RETURN a.key").unwrap()).unwrap();
8668 let clauses =
8669 parse(&lex("MATCH (a:A)-[:X]->(b:B) MATCH (a)-[:Y]->(b) RETURN a.key").unwrap())
8670 .unwrap();
8671 assert_eq!(commas.matches, clauses.matches);
8672 }
8673
8674 #[test]
8686 fn aggregate_with_projects_without_a_having_clause() {
8687 let fx = assoc_graph();
8688 let rs = run(
8689 &fx.view(),
8690 "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company) \
8691 WITH c, count(t) AS n RETURN c.name, n * 2 AS dbl",
8692 &BTreeMap::new(),
8693 )
8694 .unwrap();
8695 assert_eq!(rs.columns(), ["c.name", "dbl"]);
8696 assert_eq!(
8697 rows_of(&rs),
8698 vec![
8699 vec![Some(s("Acme Design Works")), Some(i(4))],
8700 vec![Some(s("Beta Studio")), Some(i(2))],
8701 vec![Some(s("Gamma Works")), Some(i(2))],
8702 ]
8703 );
8704 }
8705
8706 #[test]
8709 fn aggregate_with_names_its_projected_columns() {
8710 let fx = assoc_graph();
8711 let rs = run(
8712 &fx.view(),
8713 "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company) \
8714 WITH c, count(t) AS n RETURN key(c), n",
8715 &BTreeMap::new(),
8716 )
8717 .unwrap();
8718 assert_eq!(rs.columns(), ["key(c)", "n"]);
8719 assert_eq!(
8720 rows_of(&rs),
8721 vec![
8722 vec![Some(s("c1")), Some(i(2))],
8723 vec![Some(s("c2")), Some(i(1))],
8724 vec![Some(s("c3")), Some(i(1))],
8725 ]
8726 );
8727 }
8728
8729 #[test]
8732 fn aggregate_with_returning_the_bare_node_keeps_the_key() {
8733 let fx = assoc_graph();
8734 let rs = run(
8735 &fx.view(),
8736 "MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company) \
8737 WITH c, count(t) AS n RETURN c",
8738 &BTreeMap::new(),
8739 )
8740 .unwrap();
8741 assert_eq!(rs.columns(), ["c"]);
8742 assert_eq!(
8743 rows_of(&rs),
8744 vec![
8745 vec![Some(s("c1"))],
8746 vec![Some(s("c2"))],
8747 vec![Some(s("c3"))]
8748 ]
8749 );
8750 }
8751
8752 #[test]
8755 fn a_plain_aggregate_is_unchanged() {
8756 let fx = assoc_graph();
8757 let rs = run(
8758 &fx.view(),
8759 "MATCH (c:Company) RETURN c.name, count(*)",
8760 &BTreeMap::new(),
8761 )
8762 .unwrap();
8763 assert_eq!(rs.columns(), ["c.name", "COUNT(*)"]);
8764 assert_eq!(rs.len(), 3);
8765 }
8766
8767 #[test]
8771 fn two_subscripts_of_one_list_are_two_named_columns() {
8772 let fx = assoc_graph();
8773 let rs = run(
8774 &fx.view(),
8775 "MATCH (t:Talent) RETURN t.location[0], t.location[1]",
8776 &BTreeMap::new(),
8777 )
8778 .unwrap();
8779 assert_eq!(rs.columns(), ["t.location[0]", "t.location[1]"]);
8780 assert_eq!(
8781 rows_of(&rs),
8782 vec![
8783 vec![Some(f(40.71)), Some(f(-74.01))],
8784 vec![Some(f(41.88)), Some(f(-87.63))],
8785 vec![None, None],
8787 ]
8788 );
8789 }
8790
8791 #[test]
8795 fn a_subscript_carries_through_a_with_stage() {
8796 let fx = assoc_graph();
8797 let rs = run(
8798 &fx.view(),
8799 "MATCH (t:Talent) WHERE t.location[0] > 41.0 \
8800 WITH t, t.location[0] AS lat, t.location[1] \
8801 RETURN key(t), lat, t.location[1]",
8802 &BTreeMap::new(),
8803 )
8804 .unwrap();
8805 assert_eq!(rs.columns(), ["key(t)", "lat", "t.location[1]"]);
8806 assert_eq!(
8807 rows_of(&rs),
8808 vec![vec![Some(s("t2")), Some(f(41.88)), Some(f(-87.63))]]
8809 );
8810 }
8811
8812 #[test]
8815 fn subscript_column_names_cover_nested_and_computed_forms() {
8816 use crate::cypher::ast::operand_label;
8817 let prop = || Operand::Prop {
8818 var: "t".into(),
8819 field: "location".into(),
8820 };
8821 let at = |idx: Operand| Operand::Index {
8822 base: Box::new(prop()),
8823 index: Box::new(idx),
8824 };
8825 assert_eq!(operand_label(&at(Operand::Lit(i(0)))), "t.location[0]");
8826 assert_eq!(operand_label(&at(Operand::Lit(i(-1)))), "t.location[-1]");
8827 assert_eq!(
8828 operand_label(&at(Operand::Param("k".into()))),
8829 "t.location[$k]"
8830 );
8831 assert_eq!(
8832 operand_label(&at(Operand::Var("j".into()))),
8833 "t.location[j]"
8834 );
8835 assert_eq!(
8836 operand_label(&Operand::Index {
8837 base: Box::new(at(Operand::Lit(i(0)))),
8838 index: Box::new(Operand::Lit(i(1))),
8839 }),
8840 "t.location[0][1]"
8841 );
8842 }
8843
8844 #[test]
8853 fn a_with_alias_is_visible_to_its_where() {
8854 let fx = assoc_graph();
8855 let rs = run(
8856 &fx.view(),
8857 "MATCH (t:Talent) WITH t, t.years_of_experience AS x WHERE x > 11 \
8858 RETURN key(t), x",
8859 &BTreeMap::new(),
8860 )
8861 .unwrap();
8862 assert_eq!(rs.columns(), ["key(t)", "x"]);
8863 assert_eq!(rows_of(&rs), vec![vec![Some(s("t1")), Some(i(12))]]);
8864 }
8865
8866 #[test]
8868 fn a_with_alias_is_visible_to_where_and_order_by_together() {
8869 let fx = assoc_graph();
8870 let rs = run(
8871 &fx.view(),
8872 "MATCH (t:Talent) WITH t, t.years_of_experience AS x WHERE x > 3 \
8873 ORDER BY x DESC RETURN key(t), x",
8874 &BTreeMap::new(),
8875 )
8876 .unwrap();
8877 assert_eq!(rs.columns(), ["key(t)", "x"]);
8878 assert_eq!(
8879 rows_of(&rs),
8880 vec![
8881 vec![Some(s("t1")), Some(i(12))],
8882 vec![Some(s("t2")), Some(i(11))],
8883 ]
8884 );
8885 }
8886
8887 #[test]
8891 fn a_with_carrying_only_a_variable_still_filters_on_it() {
8892 let fx = assoc_graph();
8893 let rs = run(
8894 &fx.view(),
8895 "MATCH (t:Talent) WHERE t.status = 'published' \
8896 WITH t WHERE t.years_of_experience > 11 RETURN key(t)",
8897 &BTreeMap::new(),
8898 )
8899 .unwrap();
8900 assert_eq!(rs.columns(), ["key(t)"]);
8901 assert_eq!(rows_of(&rs), vec![vec![Some(s("t1"))]]);
8902 }
8903
8904 #[test]
8907 fn a_with_alias_projects_under_its_new_name() {
8908 let fx = assoc_graph();
8909 let rs = run(
8910 &fx.view(),
8911 "MATCH (c:Company) WITH c, c.name AS nm RETURN nm",
8912 &BTreeMap::new(),
8913 )
8914 .unwrap();
8915 assert_eq!(rs.columns(), ["nm"]);
8916 assert_eq!(
8917 rows_of(&rs),
8918 vec![
8919 vec![Some(s("Acme Design Works"))],
8920 vec![Some(s("Beta Studio"))],
8921 vec![Some(s("Gamma Works"))],
8922 ]
8923 );
8924 }
8925
8926 #[test]
8930 fn every_with_alias_shape_is_in_scope_for_the_where() {
8931 let fx = assoc_graph();
8932 let p = BTreeMap::new();
8933 for (q, want) in [
8934 (
8935 "MATCH (t:Talent) WITH t, t.years_of_experience + 1 AS x WHERE x > 12 \
8936 RETURN key(t)",
8937 vec!["t1"],
8938 ),
8939 (
8940 "MATCH (t:Talent) WITH t, t.location[0] AS lat WHERE lat > 41.0 RETURN key(t)",
8941 vec!["t2"],
8942 ),
8943 (
8944 "MATCH (t:Talent) WITH t AS u WHERE u.years_of_experience > 11 RETURN key(u)",
8945 vec!["t1"],
8946 ),
8947 (
8948 "MATCH (t:Talent) WITH t.key AS k WHERE k = 't2' RETURN k",
8949 vec!["t2"],
8950 ),
8951 ] {
8952 let rs = run(&fx.view(), q, &p).unwrap_or_else(|e| panic!("{q}: {e}"));
8953 let got: Vec<String> = (0..rs.len())
8954 .map(|r| match rs.row(r)[0].clone() {
8955 Some(Value::Str(k)) => k,
8956 other => panic!("{q}: {other:?}"),
8957 })
8958 .collect();
8959 assert_eq!(got, want, "{q}");
8960 }
8961 }
8962
8963 #[test]
8966 fn an_unknown_name_in_a_with_where_is_still_an_error() {
8967 let err = plan(
8968 &parse(
8969 &lex(
8970 "MATCH (t:Talent) WITH t, t.years_of_experience AS x WHERE nope > 1 \
8971 RETURN key(t)",
8972 )
8973 .unwrap(),
8974 )
8975 .unwrap(),
8976 )
8977 .expect_err("must not plan");
8978 assert_eq!(err, "unbound variable `nope` in WHERE");
8979 }
8980
8981 #[test]
8984 fn comma_patterns_without_shared_vars_are_a_product() {
8985 let mut fx = Fx::new();
8986 fx.add("A", "a1", vec![]);
8987 fx.add("A", "a2", vec![]);
8988 fx.add("B", "b1", vec![]);
8989 let rs = run(
8990 &fx.view(),
8991 "MATCH (a:A), (b:B) RETURN a.key, b.key",
8992 &BTreeMap::new(),
8993 )
8994 .unwrap();
8995 assert_eq!(
8996 rows_of(&rs),
8997 vec![
8998 vec![Some(s("a1")), Some(s("b1"))],
8999 vec![Some(s("a2")), Some(s("b1"))],
9000 ]
9001 );
9002 }
9003}