1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeMap, HashMap, HashSet};
3use std::sync::{
4 atomic::{AtomicBool, Ordering as AtomicOrdering},
5 Arc,
6};
7use std::time::{Duration, Instant};
8
9use marsdb_graph::{
10 AdjEntry, Direction, Edge, EdgeId, GraphStore, NodeId, PropertyValue, Txn, TzId as GraphTzId,
11 WriteTransaction,
12};
13
14use crate::aggregate::{property_value_hash_key, value_hash_key, AggAcc, HashKey};
15use crate::ast::{
16 is_aggregate_name, is_percentile_name, ArithOp, CallClause, CallYield, CompareOp, Expr,
17 Literal, MergeClause, NodePattern, Pattern, PropAccess, QuantifierKind, QueryClause, QueryPart,
18 RelDirection, RemoveItem, ReturnExpr, ReturnItem, ReturnTail, SetItem, SortDir, Statement,
19 Tail, UnwindClause, WithClause, WithExpr,
20};
21use crate::error::QueryError;
22use crate::ir::{ExpandDirection, LogicalPlan};
23use crate::parse_helpers::validate_named_path_pattern;
24use crate::planner::{apply_index_seeks, build_match_plan, pattern_all_vars, pattern_new_vars};
25use crate::procedure::{ProcedureProvider, ProcedureSignature};
26use crate::result::QueryResult;
27use crate::temporal;
28use crate::value::{PathElem, Value};
29
30const OPTIONAL_SEED_IDX_KEY: &str = "__seed_idx";
34
35const MERGE_CREATED_KEY: &str = "__merge_created";
39
40#[derive(Debug, Clone, Default)]
43pub struct CancellationToken(Arc<AtomicBool>);
44
45impl CancellationToken {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn cancel(&self) {
51 self.0.store(true, AtomicOrdering::Release);
52 }
53
54 pub fn is_cancelled(&self) -> bool {
55 self.0.load(AtomicOrdering::Acquire)
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ExecutionOutcome {
64 Success,
65 SyntaxError,
67 SemanticError,
70 TypeError,
74 GraphError,
75 UnboundVariable,
76 MissingParameter,
77 Cancelled,
78 Timeout,
79 ResourceLimit,
80}
81
82impl ExecutionOutcome {
83 pub fn from_error(error: &QueryError) -> Self {
84 match error {
85 QueryError::Syntax(_) => Self::SyntaxError,
86 QueryError::Semantic(_) => Self::SemanticError,
87 QueryError::Type(_) => Self::TypeError,
88 QueryError::Graph(_) => Self::GraphError,
89 QueryError::UnboundVariable(_) => Self::UnboundVariable,
90 QueryError::MissingParam(_) => Self::MissingParameter,
91 QueryError::Cancelled => Self::Cancelled,
92 QueryError::Timeout => Self::Timeout,
93 QueryError::ResourceLimit(_) => Self::ResourceLimit,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
99pub struct ExecutionEvent {
100 pub elapsed: Duration,
101 pub statement_read_only: Option<bool>,
103 pub result_rows: Option<usize>,
104 pub relationship_expansions: u64,
105 pub outcome: ExecutionOutcome,
106}
107
108#[derive(Clone)]
111pub struct ExecutionObserver(Arc<dyn Fn(&ExecutionEvent) + Send + Sync>);
112
113impl ExecutionObserver {
114 pub fn new(callback: impl Fn(&ExecutionEvent) + Send + Sync + 'static) -> Self {
115 Self(Arc::new(callback))
116 }
117
118 pub fn observe(&self, event: &ExecutionEvent) {
119 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.0)(event)));
123 }
124}
125
126impl std::fmt::Debug for ExecutionObserver {
127 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 formatter.write_str("ExecutionObserver(..)")
129 }
130}
131
132#[derive(Debug, Clone, Default)]
135pub struct ExecutionOptions {
136 pub max_intermediate_rows: Option<usize>,
137 pub max_result_rows: Option<usize>,
138 pub max_relationship_expansions: Option<u64>,
139 pub timeout: Option<Duration>,
140 pub cancellation_token: Option<CancellationToken>,
141 pub observer: Option<ExecutionObserver>,
142 pub procedures: Option<crate::procedure::Procedures>,
146 pub params: HashMap<String, PropertyValue>,
158}
159
160struct ExecutionGuard<'a> {
161 options: &'a ExecutionOptions,
162 deadline: Option<Instant>,
163 relationship_expansions: Cell<u64>,
164 deleted_edge_types: RefCell<HashMap<EdgeId, String>>,
177}
178
179impl<'a> ExecutionGuard<'a> {
180 fn new(options: &'a ExecutionOptions) -> Self {
181 Self {
182 options,
183 deadline: options
184 .timeout
185 .and_then(|timeout| Instant::now().checked_add(timeout)),
186 relationship_expansions: Cell::new(0),
187 deleted_edge_types: RefCell::new(HashMap::new()),
188 }
189 }
190
191 fn record_deleted_edge_type(&self, id: EdgeId, label: String) {
192 self.deleted_edge_types.borrow_mut().insert(id, label);
193 }
194
195 fn deleted_edge_type(&self, id: EdgeId) -> Option<String> {
196 self.deleted_edge_types.borrow().get(&id).cloned()
197 }
198
199 fn procedure_provider(&self) -> Option<&dyn ProcedureProvider> {
200 self.options.procedures.as_ref().map(|p| p.0.as_ref())
201 }
202
203 fn checkpoint(&self) -> Result<(), QueryError> {
204 if self
205 .options
206 .cancellation_token
207 .as_ref()
208 .is_some_and(CancellationToken::is_cancelled)
209 {
210 return Err(QueryError::Cancelled);
211 }
212 if self
213 .deadline
214 .is_some_and(|deadline| Instant::now() >= deadline)
215 {
216 return Err(QueryError::Timeout);
217 }
218 Ok(())
219 }
220
221 fn check_intermediate_rows(&self, rows: usize) -> Result<(), QueryError> {
222 self.checkpoint()?;
223 if self
224 .options
225 .max_intermediate_rows
226 .is_some_and(|limit| rows > limit)
227 {
228 return Err(QueryError::ResourceLimit(format!(
229 "intermediate row count {rows} exceeds configured maximum {}",
230 self.options.max_intermediate_rows.unwrap()
231 )));
232 }
233 Ok(())
234 }
235
236 fn check_result_rows(&self, rows: usize) -> Result<(), QueryError> {
237 self.checkpoint()?;
238 if self
239 .options
240 .max_result_rows
241 .is_some_and(|limit| rows > limit)
242 {
243 return Err(QueryError::ResourceLimit(format!(
244 "result row count {rows} exceeds configured maximum {}",
245 self.options.max_result_rows.unwrap()
246 )));
247 }
248 Ok(())
249 }
250
251 fn relationship_expansion(&self) -> Result<(), QueryError> {
252 self.checkpoint()?;
253 let count = self
254 .relationship_expansions
255 .get()
256 .checked_add(1)
257 .ok_or_else(|| {
258 QueryError::ResourceLimit("relationship expansion counter overflow".into())
259 })?;
260 self.relationship_expansions.set(count);
261 if self
262 .options
263 .max_relationship_expansions
264 .is_some_and(|limit| count > limit)
265 {
266 return Err(QueryError::ResourceLimit(format!(
267 "relationship expansion count {count} exceeds configured maximum {}",
268 self.options.max_relationship_expansions.unwrap()
269 )));
270 }
271 Ok(())
272 }
273}
274
275#[derive(Debug, Clone)]
276enum Binding {
277 Node(NodeId),
278 Edge(EdgeId),
279 Value(PropertyValue),
283 List(Vec<Value>),
292 Map(BTreeMap<String, Value>),
297 Path(Vec<PathBinding>),
304}
305
306#[derive(Debug, Clone)]
313enum PathBinding {
314 Node(NodeId),
315 Edge(EdgeId),
316}
317
318struct ShortestPathSpec<'a> {
319 direction: ExpandDirection,
320 rel_labels: &'a [String],
321 min_hops: u32,
322 max_hops: Option<u32>,
323}
324
325struct VarExpandSpec<'a> {
326 from_var: &'a str,
327 to_var: &'a str,
328 rel_labels: &'a [String],
329 direction: ExpandDirection,
330 min_hops: u32,
331 max_hops: Option<u32>,
332 exclude_edge_vars: &'a [String],
335 exclude_edge_sets: &'a [String],
337 exclude_edge_var: &'a str,
339 path_segment_var: Option<&'a str>,
341 rel_list_var: Option<&'a str>,
343 rel_props: &'a [(String, ReturnExpr)],
345}
346
347struct MatchRelListSpec<'a> {
348 from_var: &'a str,
349 to_var: &'a str,
350 rel_list_var: &'a str,
351 rel_labels: &'a [String],
352 direction: ExpandDirection,
353 min_hops: u32,
354 max_hops: Option<u32>,
355}
356
357struct PatternComprehensionSpec<'a> {
358 path_var: &'a Option<String>,
359 pattern: &'a Pattern,
360 where_clause: &'a Option<Box<Expr>>,
361 projection: &'a ReturnExpr,
362}
363
364struct IndexSeekSpec<'a> {
365 var: &'a str,
366 label: &'a str,
367 prop: &'a str,
368 value: &'a PropertyValue,
369}
370
371struct GroupFinishCtx<'a> {
374 items: &'a [ReturnItem],
375 key_bindings: &'a [Option<Binding>],
376}
377
378struct ResultModifiers<'a> {
384 order_by: &'a Option<Vec<(ReturnExpr, SortDir)>>,
385 skip: Option<i64>,
386 limit: Option<i64>,
387}
388
389type BindingRow = HashMap<String, Binding>;
390type RowStream<'a> = Box<dyn Iterator<Item = Result<BindingRow, QueryError>> + 'a>;
391
392const VAR_EXPAND_DEPTH_CAP: u32 = 30;
398
399pub struct Executor<'a> {
400 store: &'a GraphStore,
401 now: Cell<Option<temporal::NowSnapshot>>,
408}
409
410impl<'a> Executor<'a> {
411 pub fn new(store: &'a GraphStore) -> Self {
412 Self {
413 store,
414 now: Cell::new(None),
415 }
416 }
417
418 fn now_snapshot(&self) -> temporal::NowSnapshot {
419 if let Some(n) = self.now.get() {
420 return n;
421 }
422 let n = temporal::capture_now();
423 self.now.set(Some(n));
424 n
425 }
426
427 pub fn execute(&self, stmt: &Statement) -> Result<QueryResult, QueryError> {
439 self.execute_with_options(stmt, &ExecutionOptions::default())
440 }
441
442 pub fn execute_with_options(
443 &self,
444 stmt: &Statement,
445 options: &ExecutionOptions,
446 ) -> Result<QueryResult, QueryError> {
447 let started = Instant::now();
448 let guard = ExecutionGuard::new(options);
449 let result = self.execute_with_guard(stmt, &guard);
450 Self::notify_observer(options, stmt, started, &guard, &result);
451 result
452 }
453
454 fn execute_with_guard(
455 &self,
456 stmt: &Statement,
457 guard: &ExecutionGuard<'_>,
458 ) -> Result<QueryResult, QueryError> {
459 crate::semantic::validate_statement(stmt)?;
460 guard.checkpoint()?;
461 if let Statement::Explain(inner) = stmt {
462 return self.execute_explain(inner);
466 }
467 if is_read_only(stmt) {
468 let read_txn = self.store.begin_read()?;
469 return match stmt {
472 Statement::Union { parts, all } => {
473 self.materialize_union(Txn::Read(&read_txn), parts, *all, guard)
474 }
475 Statement::Match {
476 clauses,
477 tail,
478 order_by,
479 skip,
480 limit,
481 } => {
482 let skip = self.resolve_skip_limit(
483 Txn::Read(&read_txn),
484 skip.as_deref(),
485 "SKIP",
486 guard,
487 )?;
488 let limit = self.resolve_skip_limit(
489 Txn::Read(&read_txn),
490 limit.as_deref(),
491 "LIMIT",
492 guard,
493 )?;
494 self.execute_match(
495 Txn::Read(&read_txn),
496 clauses,
497 tail,
498 ResultModifiers {
499 order_by,
500 skip,
501 limit,
502 },
503 guard,
504 )
505 }
506 _ => unreachable!("is_read_only only returns true for Statement::Match/Union"),
507 };
508 }
509 let write_txn = self.store.begin_write()?;
510 let outcome = self.execute_in_write_transaction_validated(stmt, &write_txn, guard);
511 match outcome {
512 Ok(result) => {
513 GraphStore::commit(write_txn)?;
514 Ok(result)
515 }
516 Err(e) => {
517 let _ = GraphStore::abort(write_txn);
519 Err(e)
520 }
521 }
522 }
523
524 pub fn execute_in_write_transaction(
529 &self,
530 stmt: &Statement,
531 write_txn: &WriteTransaction,
532 ) -> Result<QueryResult, QueryError> {
533 self.execute_in_write_transaction_with_options(
534 stmt,
535 write_txn,
536 &ExecutionOptions::default(),
537 )
538 }
539
540 pub fn execute_in_write_transaction_with_options(
541 &self,
542 stmt: &Statement,
543 write_txn: &WriteTransaction,
544 options: &ExecutionOptions,
545 ) -> Result<QueryResult, QueryError> {
546 let started = Instant::now();
547 let guard = ExecutionGuard::new(options);
548 let result = self.execute_in_write_transaction_with_guard(stmt, write_txn, &guard);
549 Self::notify_observer(options, stmt, started, &guard, &result);
550 result
551 }
552
553 fn execute_in_write_transaction_with_guard(
554 &self,
555 stmt: &Statement,
556 write_txn: &WriteTransaction,
557 guard: &ExecutionGuard<'_>,
558 ) -> Result<QueryResult, QueryError> {
559 crate::semantic::validate_statement(stmt)?;
560 guard.checkpoint()?;
561 if let Statement::Explain(inner) = stmt {
562 return self.execute_explain(inner);
567 }
568 self.execute_in_write_transaction_validated(stmt, write_txn, guard)
569 }
570
571 fn execute_explain(&self, inner: &Statement) -> Result<QueryResult, QueryError> {
576 let read_txn = self.store.begin_read()?;
577 let lines = crate::explain::explain_statement(inner, Txn::Read(&read_txn))?;
578 Ok(QueryResult {
579 columns: vec!["plan".to_string()],
580 rows: lines
581 .into_iter()
582 .map(|line| vec![Value::Literal(Literal::String(line))])
583 .collect(),
584 })
585 }
586
587 fn notify_observer(
588 options: &ExecutionOptions,
589 stmt: &Statement,
590 started: Instant,
591 guard: &ExecutionGuard<'_>,
592 result: &Result<QueryResult, QueryError>,
593 ) {
594 let Some(observer) = &options.observer else {
595 return;
596 };
597 let (result_rows, outcome) = match result {
598 Ok(result) => (Some(result.rows.len()), ExecutionOutcome::Success),
599 Err(error) => (None, ExecutionOutcome::from_error(error)),
600 };
601 observer.observe(&ExecutionEvent {
602 elapsed: started.elapsed(),
603 statement_read_only: Some(is_read_only(stmt)),
604 result_rows,
605 relationship_expansions: guard.relationship_expansions.get(),
606 outcome,
607 });
608 }
609
610 fn execute_in_write_transaction_validated(
611 &self,
612 stmt: &Statement,
613 write_txn: &WriteTransaction,
614 guard: &ExecutionGuard<'_>,
615 ) -> Result<QueryResult, QueryError> {
616 match stmt {
617 Statement::Create(patterns) => {
618 guard.checkpoint()?;
619 self.execute_create(write_txn, patterns, guard)
620 }
621 Statement::CreateIndex {
622 label,
623 prop,
624 unique,
625 } => {
626 guard.checkpoint()?;
627 GraphStore::create_index_in_txn(write_txn, label, prop, *unique)?;
628 Ok(QueryResult {
629 columns: vec![],
630 rows: vec![],
631 })
632 }
633 Statement::Match {
634 clauses,
635 tail,
636 order_by,
637 skip,
638 limit,
639 } => {
640 let skip =
641 self.resolve_skip_limit(Txn::Write(write_txn), skip.as_deref(), "SKIP", guard)?;
642 let limit = self.resolve_skip_limit(
643 Txn::Write(write_txn),
644 limit.as_deref(),
645 "LIMIT",
646 guard,
647 )?;
648 self.execute_match(
649 Txn::Write(write_txn),
650 clauses,
651 tail,
652 ResultModifiers {
653 order_by,
654 skip,
655 limit,
656 },
657 guard,
658 )
659 }
660 Statement::Explain(inner) => {
661 self.execute_explain(inner)
668 }
669 Statement::Union { parts, all } => {
670 self.materialize_union(Txn::Write(write_txn), parts, *all, guard)
671 }
672 Statement::StandaloneCall(call) => {
673 self.eval_standalone_call(Txn::Write(write_txn), call, guard)
674 }
675 }
676 }
677
678 fn eval_call_clause(
696 &self,
697 txn: Txn,
698 call: &CallClause,
699 current_rows: &[BindingRow],
700 guard: &ExecutionGuard<'_>,
701 ) -> Result<Vec<BindingRow>, QueryError> {
702 let mut out = Vec::new();
703 for row in current_rows {
704 guard.checkpoint()?;
705 let (sig, proc_rows) = self.call_procedure(txn, call, row, guard)?;
706 let Some(yield_items) = &call.yield_items else {
707 out.push(row.clone());
708 continue;
709 };
710 let names: Vec<String> = match yield_items {
711 CallYield::Star => sig.outputs.clone(),
712 CallYield::Items(items, _) => items
713 .iter()
714 .map(|(name, alias)| alias.clone().unwrap_or_else(|| name.clone()))
715 .collect(),
716 };
717 for proc_row in &proc_rows {
718 let projected = project_call_row(&sig, proc_row, yield_items)?;
719 let mut new_row = row.clone();
720 for (name, value) in names.iter().zip(&projected) {
721 new_row.insert(name.clone(), value_to_binding_restore(value));
722 }
723 if let CallYield::Items(_, Some(where_expr)) = yield_items {
724 if self.eval_expr(txn, where_expr, &new_row, guard)? != Some(true) {
725 continue;
726 }
727 }
728 out.push(new_row);
729 guard.check_intermediate_rows(out.len())?;
730 }
731 }
732 Ok(out)
733 }
734
735 fn eval_standalone_call(
736 &self,
737 txn: Txn,
738 call: &CallClause,
739 guard: &ExecutionGuard<'_>,
740 ) -> Result<QueryResult, QueryError> {
741 let empty_row = BindingRow::new();
742 let (sig, proc_rows) = self.call_procedure(txn, call, &empty_row, guard)?;
743 let yield_items = call.yield_items.clone().unwrap_or(CallYield::Star);
744 let columns: Vec<String> = match &yield_items {
745 CallYield::Star => sig.outputs.clone(),
746 CallYield::Items(items, _) => items
747 .iter()
748 .map(|(name, alias)| alias.clone().unwrap_or_else(|| name.clone()))
749 .collect(),
750 };
751 let mut rows = Vec::with_capacity(proc_rows.len());
752 for proc_row in &proc_rows {
753 rows.push(project_call_row(&sig, proc_row, &yield_items)?);
754 }
755 if let CallYield::Items(_, Some(where_expr)) = &yield_items {
756 let mut filtered = Vec::with_capacity(rows.len());
757 for row_values in &rows {
758 let mut binding_row = BindingRow::new();
759 for (col, v) in columns.iter().zip(row_values) {
760 binding_row.insert(col.clone(), value_to_binding_restore(v));
761 }
762 if self.eval_expr(txn, where_expr, &binding_row, guard)? == Some(true) {
763 filtered.push(row_values.clone());
764 }
765 }
766 rows = filtered;
767 }
768 Ok(QueryResult { columns, rows })
769 }
770
771 fn call_procedure(
780 &self,
781 txn: Txn,
782 call: &CallClause,
783 row: &BindingRow,
784 guard: &ExecutionGuard<'_>,
785 ) -> Result<(ProcedureSignature, Vec<Vec<Value>>), QueryError> {
786 let provider = guard.procedure_provider().ok_or_else(|| {
787 QueryError::Semantic(format!(
788 "procedure '{}' not found -- no procedure provider is configured",
789 call.name
790 ))
791 })?;
792 let sig = provider
793 .signature(&call.name)
794 .ok_or_else(|| QueryError::Semantic(format!("procedure '{}' not found", call.name)))?;
795 let args = self.eval_call_args(txn, call, &sig, row, guard)?;
796 let rows = provider.call(&call.name, &args)?;
797 Ok((sig, rows))
798 }
799
800 fn eval_call_args(
801 &self,
802 txn: Txn,
803 call: &CallClause,
804 sig: &ProcedureSignature,
805 row: &BindingRow,
806 guard: &ExecutionGuard<'_>,
807 ) -> Result<Vec<Value>, QueryError> {
808 let values: Vec<Value> = match &call.args {
809 Some(args) => {
810 if args.len() != sig.inputs.len() {
811 return Err(QueryError::Semantic(format!(
812 "'{}' expects {} argument(s), got {}",
813 call.name,
814 sig.inputs.len(),
815 args.len()
816 )));
817 }
818 args.iter()
819 .map(|a| self.eval_return_expr(txn, a, row, guard))
820 .collect::<Result<_, _>>()?
821 }
822 None => sig
828 .inputs
829 .iter()
830 .map(|input_name| {
831 guard
832 .options
833 .params
834 .get(input_name)
835 .cloned()
836 .map(property_value_to_value)
837 .ok_or_else(|| QueryError::MissingParam(input_name.clone()))
838 })
839 .collect::<Result<_, _>>()?,
840 };
841 for (value, (input_name, declared_type)) in
842 values.iter().zip(sig.inputs.iter().zip(&sig.input_types))
843 {
844 if !value_matches_declared_type(value, declared_type) {
845 return Err(QueryError::Type(format!(
846 "'{}' argument '{input_name}' expects {declared_type}, got {value:?}",
847 call.name
848 )));
849 }
850 }
851 Ok(values)
852 }
853
854 fn execute_create(
855 &self,
856 write_txn: &WriteTransaction,
857 patterns: &[Pattern],
858 guard: &ExecutionGuard<'_>,
859 ) -> Result<QueryResult, QueryError> {
860 self.materialize_create(write_txn, patterns, &[BindingRow::new()], guard)?;
868 Ok(QueryResult {
869 columns: vec![],
870 rows: vec![],
871 })
872 }
873
874 fn materialize_create(
889 &self,
890 write_txn: &WriteTransaction,
891 patterns: &[Pattern],
892 rows: &[BindingRow],
893 guard: &ExecutionGuard<'_>,
894 ) -> Result<Vec<BindingRow>, QueryError> {
895 let mut out = Vec::with_capacity(rows.len());
896 for row in rows {
897 let mut row = row.clone();
903 for pattern in patterns {
904 let mut prev_id =
905 self.resolve_or_create_node(write_txn, &pattern.start, &row, guard)?;
906 if let Some(var) = &pattern.start.var {
907 row.insert(var.clone(), Binding::Node(prev_id));
908 }
909 for (rel, node) in &pattern.hops {
910 if rel.hop_range.is_some() {
911 return Err(QueryError::Semantic(
912 "CREATE doesn't support variable-length relationship patterns (e.g. [:TYPE*1..3])".into(),
913 ));
914 }
915 let node_id = self.resolve_or_create_node(write_txn, node, &row, guard)?;
916 if let Some(var) = &node.var {
917 row.insert(var.clone(), Binding::Node(node_id));
918 }
919
920 let rel_label = rel.rel_types.first().cloned().expect(
921 "CREATE relationship has exactly one type -- checked by \
922 semantic::bind_create_pattern",
923 );
924 let rel_props =
925 self.eval_props_to_values(Txn::Write(write_txn), &rel.props, &row, guard)?;
926 let (src, dst) = match rel.direction {
927 RelDirection::Right => (prev_id, node_id),
928 RelDirection::Left => (node_id, prev_id),
929 RelDirection::Either => {
930 return Err(QueryError::Semantic(
931 "CREATE requires a directed relationship (-> or <-), not an undirected pattern".into(),
932 ))
933 }
934 };
935 let edge_id =
936 GraphStore::create_edge_in_txn(write_txn, &rel_label, src, dst, rel_props)?;
937 if let Some(var) = &rel.var {
938 row.insert(var.clone(), Binding::Edge(edge_id));
939 }
940 prev_id = node_id;
941 }
942 }
943 out.push(row);
944 }
945 Ok(out)
946 }
947
948 fn resolve_or_create_node(
957 &self,
958 write_txn: &WriteTransaction,
959 node: &NodePattern,
960 row: &BindingRow,
961 guard: &ExecutionGuard<'_>,
962 ) -> Result<NodeId, QueryError> {
963 if let Some(var) = &node.var {
964 if let Some(binding) = row.get(var) {
965 let Binding::Node(id) = binding else {
966 return Err(QueryError::Type(format!(
967 "'{var}' is not a node — can't use it as a CREATE pattern endpoint"
968 )));
969 };
970 return Ok(*id);
974 }
975 }
976 let labels: Vec<&str> = node.labels.iter().map(String::as_str).collect();
977 let props = self.eval_props_to_values(Txn::Write(write_txn), &node.props, row, guard)?;
978 Ok(GraphStore::create_node_in_txn(write_txn, &labels, props)?)
979 }
980
981 fn eval_props_to_values(
990 &self,
991 txn: Txn,
992 props: &[(String, ReturnExpr)],
993 row: &BindingRow,
994 guard: &ExecutionGuard<'_>,
995 ) -> Result<BTreeMap<String, PropertyValue>, QueryError> {
996 props
997 .iter()
998 .filter_map(|(k, expr)| {
999 let value = match self.eval_return_expr(txn, expr, row, guard) {
1000 Ok(v) => v,
1001 Err(e) => return Some(Err(e)),
1002 };
1003 if matches!(value, Value::Null) {
1013 return None;
1014 }
1015 let pv = match value_to_storable_property(&value).ok_or_else(|| {
1016 QueryError::Type(format!(
1017 "property '{k}' can't be stored -- MarsDB's node/edge properties are limited to null/\
1018 bool/int/float/string/date/duration; a list/map/node/edge/path value (got {value:?}) \
1019 isn't storable, matching PropertyValue's real, deliberately fixed set of variants (see \
1020 its doc comment)"
1021 ))
1022 }) {
1023 Ok(pv) => pv,
1024 Err(e) => return Some(Err(e)),
1025 };
1026 Some(Ok((k.clone(), pv)))
1027 })
1028 .collect()
1029 }
1030
1031 fn eval_merge(
1037 &self,
1038 write_txn: &WriteTransaction,
1039 clause: &MergeClause,
1040 rows: &[BindingRow],
1041 guard: &ExecutionGuard<'_>,
1042 ) -> Result<Vec<BindingRow>, QueryError> {
1043 let mut out = Vec::new();
1044 for row in rows {
1045 guard.checkpoint()?;
1046 out.extend(self.merge_one_row(write_txn, clause, row, guard)?);
1047 guard.check_intermediate_rows(out.len())?;
1048 }
1049 self.apply_merge_set(write_txn, clause, &mut out, guard)?;
1050 Ok(out)
1051 }
1052
1053 fn merge_pattern_has_null_property(
1058 &self,
1059 txn: Txn,
1060 clause: &MergeClause,
1061 row: &BindingRow,
1062 guard: &ExecutionGuard<'_>,
1063 ) -> Result<bool, QueryError> {
1064 let any_null = |props: &[(String, ReturnExpr)]| -> Result<bool, QueryError> {
1065 for (_, expr) in props {
1066 if matches!(self.eval_return_expr(txn, expr, row, guard)?, Value::Null) {
1067 return Ok(true);
1068 }
1069 }
1070 Ok(false)
1071 };
1072 if any_null(&clause.pattern.start.props)? {
1073 return Ok(true);
1074 }
1075 for (rel, node) in &clause.pattern.hops {
1076 if any_null(&rel.props)? || any_null(&node.props)? {
1077 return Ok(true);
1078 }
1079 }
1080 Ok(false)
1081 }
1082
1083 fn merge_one_row(
1084 &self,
1085 write_txn: &WriteTransaction,
1086 clause: &MergeClause,
1087 row: &BindingRow,
1088 guard: &ExecutionGuard<'_>,
1089 ) -> Result<Vec<BindingRow>, QueryError> {
1090 for (rel, _node) in &clause.pattern.hops {
1102 if rel.hop_range.is_some() {
1103 return Err(QueryError::Semantic(
1104 "MERGE doesn't support variable-length relationship patterns (e.g. [:TYPE*1..3])".into(),
1105 ));
1106 }
1107 }
1108 let (pattern, synthesized) = if clause.path_var.is_some() {
1119 name_pattern_for_path(&clause.pattern)
1120 } else {
1121 (clause.pattern.clone(), HashSet::new())
1122 };
1123 let pattern = &pattern;
1124 if self.merge_pattern_has_null_property(Txn::Write(write_txn), clause, row, guard)? {
1136 return Err(QueryError::Semantic(
1137 "MERGE pattern property is null — a MERGE's own {...} properties can never be \
1138 null (searching for null never matches anything, but storing null is the same \
1139 as not storing the property at all)"
1140 .into(),
1141 ));
1142 }
1143
1144 let carried_vars: HashSet<String> = row.keys().cloned().collect();
1154 let plan = apply_index_seeks(
1155 build_match_plan(pattern, &None, &carried_vars)?,
1156 Txn::Write(write_txn),
1157 )?;
1158 let found = self.eval_plan(
1159 Txn::Write(write_txn),
1160 &plan,
1161 std::slice::from_ref(row),
1162 guard,
1163 )?;
1164 if !found.is_empty() {
1165 return Ok(found
1166 .into_iter()
1167 .map(|mut r| {
1168 if let Some(path_var) = &clause.path_var {
1169 let path_binding = assemble_path(pattern, &r);
1170 for key in &synthesized {
1171 r.remove(key);
1172 }
1173 r.insert(path_var.clone(), path_binding);
1174 }
1175 tag_merge_created(r, false)
1176 })
1177 .collect());
1178 }
1179
1180 let mut new_row = row.clone();
1185 let start_id = self.resolve_or_create_node(write_txn, &pattern.start, &new_row, guard)?;
1186 if let Some(var) = &pattern.start.var {
1187 new_row.insert(var.clone(), Binding::Node(start_id));
1188 }
1189 if let Some((rel, node)) = pattern.hops.first() {
1193 let node_id = self.resolve_or_create_node(write_txn, node, &new_row, guard)?;
1194 if let Some(var) = &node.var {
1195 new_row.insert(var.clone(), Binding::Node(node_id));
1196 }
1197 let rel_label = rel.rel_types.first().cloned().expect(
1198 "MERGE relationship has exactly one type -- checked by semantic::bind_merge",
1199 );
1200 let rel_props =
1201 self.eval_props_to_values(Txn::Write(write_txn), &rel.props, &new_row, guard)?;
1202 let (src, dst) = match rel.direction {
1207 RelDirection::Right | RelDirection::Either => (start_id, node_id),
1208 RelDirection::Left => (node_id, start_id),
1209 };
1210 let edge_id =
1211 GraphStore::create_edge_in_txn(write_txn, &rel_label, src, dst, rel_props)?;
1212 if let Some(var) = &rel.var {
1213 new_row.insert(var.clone(), Binding::Edge(edge_id));
1214 }
1215 }
1216 if let Some(path_var) = &clause.path_var {
1217 let path_binding = assemble_path(pattern, &new_row);
1218 for key in &synthesized {
1219 new_row.remove(key);
1220 }
1221 new_row.insert(path_var.clone(), path_binding);
1222 }
1223 Ok(vec![tag_merge_created(new_row, true)])
1224 }
1225
1226 fn apply_merge_set(
1236 &self,
1237 write_txn: &WriteTransaction,
1238 clause: &MergeClause,
1239 rows: &mut [BindingRow],
1240 guard: &ExecutionGuard<'_>,
1241 ) -> Result<(), QueryError> {
1242 for row in rows.iter_mut() {
1243 let created = match row.remove(MERGE_CREATED_KEY) {
1244 Some(Binding::Value(PropertyValue::Bool(b))) => b,
1245 other => unreachable!(
1246 "{MERGE_CREATED_KEY} tagged internally as Binding::Value(Bool), got {other:?}"
1247 ),
1248 };
1249 let items = if created {
1250 &clause.on_create
1251 } else {
1252 &clause.on_match
1253 };
1254 for item in items {
1255 self.apply_set_item(Txn::Write(write_txn), write_txn, row, item, guard)?;
1256 }
1257 }
1258 Ok(())
1259 }
1260
1261 fn execute_match(
1262 &self,
1263 txn: Txn,
1264 clauses: &[QueryClause],
1265 tail: &Option<Tail>,
1266 modifiers: ResultModifiers<'_>,
1267 guard: &ExecutionGuard<'_>,
1268 ) -> Result<QueryResult, QueryError> {
1269 self.execute_match_seeded(txn, clauses, tail, modifiers, None, guard)
1270 }
1271
1272 fn execute_match_seeded(
1283 &self,
1284 txn: Txn,
1285 clauses: &[QueryClause],
1286 tail: &Option<Tail>,
1287 modifiers: ResultModifiers<'_>,
1288 seed: Option<&BindingRow>,
1289 guard: &ExecutionGuard<'_>,
1290 ) -> Result<QueryResult, QueryError> {
1291 let ResultModifiers {
1292 order_by,
1293 skip,
1294 limit,
1295 } = modifiers;
1296 let mut carried_vars: HashSet<String> = match seed {
1303 Some(row) => row.keys().cloned().collect(),
1304 None => HashSet::new(),
1305 };
1306 let mut current_rows: Vec<BindingRow> = vec![seed.cloned().unwrap_or_default()];
1307 let final_stream_limit = match (order_by, limit, tail) {
1315 (None, Some(limit), Some(Tail::Return(items, false))) if !has_aggregate(items) => {
1316 Some(skip.unwrap_or(0).max(0) as usize + limit.max(0) as usize)
1317 }
1318 _ => None,
1319 };
1320 for (clause_index, clause) in clauses.iter().enumerate() {
1321 let is_final_clause = clause_index + 1 == clauses.len();
1322 match clause {
1323 QueryClause::Match(part) => {
1324 let plan_limit = is_final_clause
1325 .then_some(final_stream_limit)
1326 .flatten()
1327 .filter(|_| !part.shortest_path && !part.optional && part.with.is_none());
1328 current_rows = if part.shortest_path {
1329 self.eval_shortest_path(txn, part, ¤t_rows, guard)?
1332 } else if let Some(path_var) = &part.path_var {
1333 let (named_pattern, synthesized) = name_pattern_for_path(&part.pattern);
1334 let defer_where = !part.optional && part.where_clause.is_some();
1349 let plan_where = if defer_where {
1350 &None
1351 } else {
1352 &part.where_clause
1353 };
1354 let plan = apply_index_seeks(
1355 build_match_plan(&named_pattern, plan_where, &carried_vars)?,
1356 txn,
1357 )?;
1358 let mut rows = if part.optional {
1359 let new_vars = pattern_new_vars(&named_pattern, &carried_vars);
1360 self.eval_optional_part(txn, &plan, ¤t_rows, &new_vars, guard)?
1361 } else {
1362 let limit = plan_limit.filter(|_| !defer_where);
1371 self.eval_plan_with_limit(txn, &plan, ¤t_rows, guard, limit)?
1372 };
1373 for row in &mut rows {
1374 let path_binding = assemble_path(&named_pattern, row);
1375 for key in &synthesized {
1376 row.remove(key);
1377 }
1378 row.insert(path_var.clone(), path_binding);
1379 }
1380 if defer_where {
1381 let where_clause = part
1382 .where_clause
1383 .as_ref()
1384 .expect("defer_where implies where_clause is Some");
1385 let mut filtered = Vec::with_capacity(rows.len());
1386 for row in rows {
1387 if self.eval_expr(txn, where_clause, &row, guard)? == Some(true) {
1388 filtered.push(row);
1389 }
1390 }
1391 rows = filtered;
1392 }
1393 rows
1394 } else {
1395 let plan = apply_index_seeks(
1396 build_match_plan(&part.pattern, &part.where_clause, &carried_vars)?,
1397 txn,
1398 )?;
1399 if part.optional {
1400 let new_vars = pattern_new_vars(&part.pattern, &carried_vars);
1401 self.eval_optional_part(txn, &plan, ¤t_rows, &new_vars, guard)?
1402 } else {
1403 self.eval_plan_with_limit(txn, &plan, ¤t_rows, guard, plan_limit)?
1404 }
1405 };
1406 let mut new_vars = pattern_all_vars(&part.pattern);
1407 if let Some(path_var) = &part.path_var {
1408 new_vars.insert(path_var.clone());
1409 }
1410 current_rows = self.apply_with_or_carry(
1411 txn,
1412 &part.with,
1413 current_rows,
1414 new_vars,
1415 &mut carried_vars,
1416 guard,
1417 )?;
1418 }
1419 QueryClause::Unwind(u) => {
1420 current_rows = self.eval_unwind(txn, u, ¤t_rows, guard)?;
1421 current_rows = self.apply_with_or_carry(
1422 txn,
1423 &u.with,
1424 current_rows,
1425 HashSet::from([u.var.clone()]),
1426 &mut carried_vars,
1427 guard,
1428 )?;
1429 }
1430 QueryClause::Call(call) => {
1431 current_rows = self.eval_call_clause(txn, call, ¤t_rows, guard)?;
1432 let new_vars: HashSet<String> = match &call.yield_items {
1433 Some(CallYield::Items(items, _)) => items
1434 .iter()
1435 .map(|(name, alias)| alias.clone().unwrap_or_else(|| name.clone()))
1436 .collect(),
1437 Some(CallYield::Star) | None => HashSet::new(),
1441 };
1442 current_rows = self.apply_with_or_carry(
1443 txn,
1444 &call.with,
1445 current_rows,
1446 new_vars,
1447 &mut carried_vars,
1448 guard,
1449 )?;
1450 }
1451 QueryClause::Merge(m) => {
1452 let write_txn = require_write_txn(txn);
1459 current_rows = self.eval_merge(write_txn, m, ¤t_rows, guard)?;
1460 let mut new_vars = pattern_all_vars(&m.pattern);
1461 if let Some(path_var) = &m.path_var {
1462 new_vars.insert(path_var.clone());
1463 }
1464 current_rows = self.apply_with_or_carry(
1465 txn,
1466 &m.with,
1467 current_rows,
1468 new_vars,
1469 &mut carried_vars,
1470 guard,
1471 )?;
1472 }
1473 QueryClause::With(with) => {
1480 current_rows = self.apply_with_or_carry(
1481 txn,
1482 &Some(with.clone()),
1483 current_rows,
1484 HashSet::new(),
1485 &mut carried_vars,
1486 guard,
1487 )?;
1488 }
1489 QueryClause::Set(items) => {
1499 let write_txn = require_write_txn(txn);
1500 for row in ¤t_rows {
1501 for item in items {
1502 self.apply_set_item(txn, write_txn, row, item, guard)?;
1503 }
1504 }
1505 }
1506 QueryClause::Delete { items, detach } => {
1512 let write_txn = require_write_txn(txn);
1513 self.delete_targets(txn, write_txn, items, ¤t_rows, *detach, guard)?;
1514 }
1515 QueryClause::Remove(items) => {
1519 let write_txn = require_write_txn(txn);
1520 for row in ¤t_rows {
1521 for item in items {
1522 apply_remove_item(write_txn, row, item)?;
1523 }
1524 }
1525 }
1526 QueryClause::Create(patterns) => {
1537 let write_txn = require_write_txn(txn);
1538 current_rows =
1539 self.materialize_create(write_txn, patterns, ¤t_rows, guard)?;
1540 carried_vars.extend(patterns.iter().flat_map(pattern_all_vars));
1541 }
1542 }
1543 guard.check_intermediate_rows(current_rows.len())?;
1544 }
1545 let distinct_return = tail_is_distinct_return(tail);
1556 if order_by.is_none() && !distinct_return {
1557 let skip_n = skip.unwrap_or(0).max(0) as usize;
1558 if skip_n > 0 {
1559 current_rows.drain(0..skip_n.min(current_rows.len()));
1560 }
1561 if let Some(count) = limit {
1562 current_rows.truncate(count.max(0) as usize);
1563 }
1564 }
1565 let mut order_by_pre_applied = false;
1584 let mut result = match tail {
1585 None => QueryResult {
1591 columns: vec![],
1592 rows: vec![],
1593 },
1594 Some(Tail::Return(items, distinct)) => {
1595 if let Some(ob) = order_by {
1596 if !has_aggregate(items) && !distinct {
1603 let projected =
1604 self.materialize_return(txn, items, ¤t_rows, *distinct, guard)?;
1605 order_by_pre_applied = true;
1606 self.apply_order_by_with_scope(
1607 txn,
1608 ¤t_rows,
1609 projected,
1610 ob,
1611 skip,
1612 limit,
1613 )?
1614 } else if !distinct {
1615 order_by_pre_applied = true;
1616 self.materialize_aggregating_return_with_order(
1617 txn,
1618 items,
1619 ¤t_rows,
1620 ob,
1621 (skip, limit),
1622 guard,
1623 )?
1624 } else {
1625 self.materialize_return(txn, items, ¤t_rows, *distinct, guard)?
1626 }
1627 } else {
1628 self.materialize_return(txn, items, ¤t_rows, *distinct, guard)?
1629 }
1630 }
1631 Some(Tail::ReturnStar(distinct)) => {
1632 let items = return_star_items(carried_vars.iter().cloned())?;
1633 let projected =
1634 self.materialize_return(txn, &items, ¤t_rows, *distinct, guard)?;
1635 if let Some(ob) = order_by {
1636 if !distinct {
1637 order_by_pre_applied = true;
1638 self.apply_order_by_with_scope(
1639 txn,
1640 ¤t_rows,
1641 projected,
1642 ob,
1643 skip,
1644 limit,
1645 )?
1646 } else {
1647 projected
1648 }
1649 } else {
1650 projected
1651 }
1652 }
1653 Some(Tail::Delete(vars, ret)) => {
1654 self.materialize_delete(txn, vars, ¤t_rows, false, ret, guard)?
1655 }
1656 Some(Tail::DetachDelete(vars, ret)) => {
1657 self.materialize_delete(txn, vars, ¤t_rows, true, ret, guard)?
1658 }
1659 Some(Tail::Set(items, ret)) => {
1660 self.materialize_set(txn, items, ¤t_rows, ret, guard)?
1661 }
1662 Some(Tail::Remove(items, ret)) => {
1663 self.materialize_remove(txn, items, ¤t_rows, ret, guard)?
1664 }
1665 Some(Tail::Create(patterns, ret)) => {
1666 let updated_rows = self.materialize_create(
1667 require_write_txn(txn),
1668 patterns,
1669 ¤t_rows,
1670 guard,
1671 )?;
1672 match ret {
1673 Some(rt) => {
1674 self.materialize_return(txn, &rt.items, &updated_rows, rt.distinct, guard)?
1675 }
1676 None => QueryResult {
1677 columns: vec![],
1678 rows: vec![],
1679 },
1680 }
1681 }
1682 };
1683 if let Some(order_by) = order_by {
1684 if !order_by_pre_applied {
1685 let tail_items: Option<&[ReturnItem]> = match tail {
1686 Some(Tail::Return(items, _)) => Some(items),
1687 _ => None,
1688 };
1689 result.rows = apply_order_by(
1690 result.rows,
1691 &result.columns,
1692 order_by,
1693 tail_items,
1694 skip,
1695 limit,
1696 )?;
1697 }
1698 } else if distinct_return {
1699 let skip_n = skip.unwrap_or(0).max(0) as usize;
1703 if skip_n > 0 {
1704 result.rows.drain(0..skip_n.min(result.rows.len()));
1705 }
1706 if let Some(count) = limit {
1707 result.rows.truncate(count.max(0) as usize);
1708 }
1709 }
1710 guard.check_result_rows(result.rows.len())?;
1711 Ok(result)
1712 }
1713
1714 fn apply_with_or_carry(
1721 &self,
1722 txn: Txn,
1723 with: &Option<WithClause>,
1724 rows: Vec<BindingRow>,
1725 new_vars: HashSet<String>,
1726 carried_vars: &mut HashSet<String>,
1727 guard: &ExecutionGuard<'_>,
1728 ) -> Result<Vec<BindingRow>, QueryError> {
1729 let Some(with) = with else {
1730 carried_vars.extend(new_vars);
1731 return Ok(rows);
1732 };
1733 let with_owned;
1743 let with: &WithClause = if with.star {
1744 let star_items = with_star_items(carried_vars.union(&new_vars).cloned());
1749 let mut owned = with.clone();
1750 let mut items = star_items;
1751 items.extend(owned.items);
1752 owned.items = items;
1753 with_owned = owned;
1754 &with_owned
1755 } else {
1756 with
1757 };
1758 let with_skip = self.resolve_skip_limit(txn, with.skip.as_ref(), "SKIP", guard)?;
1759 let with_limit = self.resolve_skip_limit(txn, with.limit.as_ref(), "LIMIT", guard)?;
1760 let rows = if let Some(with_order_by) = with
1761 .order_by
1762 .as_ref()
1763 .filter(|_| has_aggregate(&with.items))
1764 {
1765 self.materialize_aggregating_with_with_order(
1774 txn,
1775 &with.items,
1776 &rows,
1777 with_order_by,
1778 (with_skip, with_limit),
1779 guard,
1780 )?
1781 } else {
1782 let pre_with_rows = (with.order_by.is_some() && !with.distinct).then(|| rows.clone());
1786 let mut rows = self.materialize_with(txn, with, &rows, guard)?;
1787 if let Some(with_order_by) = &with.order_by {
1788 rows = self.apply_order_by_bindings(
1793 txn,
1794 rows,
1795 pre_with_rows.as_deref(),
1796 &with.items,
1797 with_order_by,
1798 (with_skip, with_limit),
1799 )?;
1800 } else {
1801 let skip_n = with_skip.unwrap_or(0).max(0) as usize;
1802 if skip_n > 0 {
1803 rows.drain(0..skip_n.min(rows.len()));
1804 }
1805 if let Some(with_limit) = with_limit {
1806 rows.truncate(with_limit.max(0) as usize);
1807 }
1808 }
1809 rows
1810 };
1811 *carried_vars = with
1812 .items
1813 .iter()
1814 .enumerate()
1815 .map(with_item_output_name)
1816 .collect();
1817 Ok(rows)
1818 }
1819
1820 fn eval_unwind(
1826 &self,
1827 txn: Txn,
1828 clause: &UnwindClause,
1829 rows: &[BindingRow],
1830 guard: &ExecutionGuard<'_>,
1831 ) -> Result<Vec<BindingRow>, QueryError> {
1832 let mut out = Vec::new();
1833 for row in rows {
1834 let source_value = self.eval_return_expr(txn, &clause.source.0, row, guard)?;
1835 let elements: Vec<Binding> = match source_value {
1836 Value::List(items) => items.iter().map(value_to_binding_restore).collect(),
1837 Value::Null => Vec::new(),
1840 other => {
1841 return Err(QueryError::Type(format!(
1842 "UNWIND needs a list, got {other:?}"
1843 )))
1844 }
1845 };
1846 for element in elements {
1847 let mut new_row = row.clone();
1848 new_row.insert(clause.var.clone(), element);
1849 out.push(new_row);
1850 }
1851 }
1852 if let Some(where_clause) = &clause.where_clause {
1853 let mut filtered = Vec::with_capacity(out.len());
1854 for row in out {
1855 if self.eval_with_expr(txn, where_clause, &row, guard)? == Some(true) {
1856 filtered.push(row);
1857 }
1858 }
1859 out = filtered;
1860 }
1861 Ok(out)
1862 }
1863
1864 fn eval_shortest_path(
1894 &self,
1895 txn: Txn,
1896 part: &QueryPart,
1897 rows: &[BindingRow],
1898 guard: &ExecutionGuard<'_>,
1899 ) -> Result<Vec<BindingRow>, QueryError> {
1900 let Some(path_var) = &part.path_var else {
1901 return Ok(rows.to_vec());
1904 };
1905 let start_var = part.pattern.start.var.as_deref().expect(
1906 "shortestPath()'s start node always has a var — validated at parse time by \
1907 validate_shortest_path_pattern",
1908 );
1909 let (rel, end_node) = &part.pattern.hops[0];
1910 let end_var = end_node.var.as_deref().expect(
1911 "shortestPath()'s end node always has a var — validated at parse time by \
1912 validate_shortest_path_pattern",
1913 );
1914 let (min_hops, max_hops) = rel.hop_range.expect(
1915 "shortestPath()'s relationship is always variable-length — validated at parse time by \
1916 validate_shortest_path_pattern",
1917 );
1918 let direction = match rel.direction {
1919 RelDirection::Right => ExpandDirection::Out,
1920 RelDirection::Left => ExpandDirection::In,
1921 RelDirection::Either => ExpandDirection::Either,
1922 };
1923 let rel_labels = &rel.rel_types;
1924
1925 let mut out = Vec::with_capacity(rows.len());
1926 for row in rows {
1927 let start_id = require_bound_node(row, start_var)?;
1928 let end_id = require_bound_node(row, end_var)?;
1929 let path = self.shortest_path_between(
1930 txn,
1931 start_id,
1932 end_id,
1933 ShortestPathSpec {
1934 direction,
1935 rel_labels,
1936 min_hops,
1937 max_hops,
1938 },
1939 )?;
1940 let mut new_row = row.clone();
1941 let binding = match path {
1942 Some(elems) => Binding::Path(elems),
1943 None => Binding::Value(PropertyValue::Null),
1944 };
1945 new_row.insert(path_var.clone(), binding);
1946 out.push(new_row);
1947 }
1948 if let Some(where_clause) = &part.where_clause {
1949 let mut filtered = Vec::with_capacity(out.len());
1950 for row in out {
1951 if self.eval_expr(txn, where_clause, &row, guard)? == Some(true) {
1952 filtered.push(row);
1953 }
1954 }
1955 out = filtered;
1956 }
1957 Ok(out)
1958 }
1959
1960 fn shortest_path_between(
1969 &self,
1970 txn: Txn,
1971 start: NodeId,
1972 end: NodeId,
1973 spec: ShortestPathSpec<'_>,
1974 ) -> Result<Option<Vec<PathBinding>>, QueryError> {
1975 if start == end && spec.min_hops == 0 {
1976 return Ok(Some(vec![PathBinding::Node(start)]));
1977 }
1978 let cap = spec.max_hops.unwrap_or(VAR_EXPAND_DEPTH_CAP);
1979 let mut parent: HashMap<NodeId, (NodeId, EdgeId)> = HashMap::new();
1980 let mut visited: HashSet<NodeId> = HashSet::new();
1981 visited.insert(start);
1982 let mut frontier = vec![start];
1983 let mut depth = 0u32;
1984 while depth < cap && !frontier.is_empty() {
1985 depth += 1;
1986 let mut next_frontier = Vec::new();
1987 for node in frontier {
1988 for entry in neighbors_for_direction(txn, node, spec.direction, spec.rel_labels)? {
1989 if entry.other == end {
1990 parent.insert(entry.other, (node, entry.edge_id));
1991 return Ok(Some(reconstruct_path(&parent, start, end)));
1992 }
1993 if visited.insert(entry.other) {
1994 parent.insert(entry.other, (node, entry.edge_id));
1995 next_frontier.push(entry.other);
1996 }
1997 }
1998 }
1999 frontier = next_frontier;
2000 }
2001 Ok(None)
2002 }
2003
2004 fn materialize_with(
2011 &self,
2012 txn: Txn,
2013 with: &WithClause,
2014 rows: &[BindingRow],
2015 guard: &ExecutionGuard<'_>,
2016 ) -> Result<Vec<BindingRow>, QueryError> {
2017 let is_aggregating = has_aggregate(&with.items);
2018 let mut out = if !is_aggregating {
2019 let mut out = Vec::with_capacity(rows.len());
2020 for row in rows {
2021 let mut new_row = BindingRow::new();
2022 for (i, item) in with.items.iter().enumerate() {
2023 let name = with_item_output_name((i, item));
2024 let binding = self.item_binding(txn, &item.expr, row, guard)?;
2025 new_row.insert(name, binding);
2026 }
2027 out.push(new_row);
2028 }
2029 out
2030 } else {
2031 validate_return_items(&with.items)?;
2032 let grouped = self.resolve_grouped_rows(txn, &with.items, rows, guard)?;
2033 grouped
2034 .into_iter()
2035 .map(|bindings| {
2036 with.items
2037 .iter()
2038 .enumerate()
2039 .zip(bindings)
2040 .map(|((i, item), b)| (with_item_output_name((i, item)), b))
2041 .collect()
2042 })
2043 .collect()
2044 };
2045 if let Some(where_clause) = &with.where_clause {
2046 let mut filtered = Vec::with_capacity(out.len());
2047 if is_aggregating {
2048 for row in out {
2053 if self.eval_with_expr(txn, where_clause, &row, guard)? == Some(true) {
2054 filtered.push(row);
2055 }
2056 }
2057 } else {
2058 for (row, new_row) in rows.iter().zip(out) {
2072 let mut merged = row.clone();
2073 merged.extend(new_row.iter().map(|(k, v)| (k.clone(), v.clone())));
2074 if self.eval_with_expr(txn, where_clause, &merged, guard)? == Some(true) {
2075 filtered.push(new_row);
2076 }
2077 }
2078 }
2079 out = filtered;
2080 }
2081 if with.distinct {
2082 out = dedup_binding_rows(&with.items, out)?;
2083 }
2084 Ok(out)
2085 }
2086
2087 fn materialize_aggregating_with_with_order(
2095 &self,
2096 txn: Txn,
2097 with_items: &[ReturnItem],
2098 rows: &[BindingRow],
2099 order_by: &[(ReturnExpr, SortDir)],
2100 skip_limit: (Option<i64>, Option<i64>),
2101 guard: &ExecutionGuard<'_>,
2102 ) -> Result<Vec<BindingRow>, QueryError> {
2103 let (skip, limit) = skip_limit;
2104 enum OrderKeySource {
2105 RealColumn(usize),
2106 Extra(usize),
2107 }
2108 let mut extra_exprs: Vec<ReturnExpr> = Vec::new();
2109 let order_by_source: Vec<OrderKeySource> = order_by
2110 .iter()
2111 .map(|(expr, _)| {
2112 match with_items
2113 .iter()
2114 .enumerate()
2115 .position(|(i, it)| item_matches_leaf(expr, i, it))
2116 {
2117 Some(i) => OrderKeySource::RealColumn(i),
2118 None => {
2119 let idx = extra_exprs.len();
2120 extra_exprs.push(expr.clone());
2121 OrderKeySource::Extra(idx)
2122 }
2123 }
2124 })
2125 .collect();
2126 let extended_items: Vec<ReturnItem> = with_items
2127 .iter()
2128 .cloned()
2129 .chain(
2130 extra_exprs
2131 .into_iter()
2132 .map(|expr| ReturnItem { expr, alias: None }),
2133 )
2134 .collect();
2135 validate_return_items(&extended_items)?;
2136 let grouped = self.resolve_grouped_rows(txn, &extended_items, rows, guard)?;
2137 let real_len = with_items.len();
2138 let mut keyed: Vec<(Vec<Value>, BindingRow)> = Vec::with_capacity(grouped.len());
2139 for bindings in grouped {
2140 let (real, extra) = bindings.split_at(real_len);
2141 let real_values: Vec<Value> = real
2142 .iter()
2143 .map(|b| self.binding_to_value(txn, b))
2144 .collect::<Result<Vec<_>, _>>()?;
2145 let extra_values: Vec<Value> = extra
2146 .iter()
2147 .map(|b| self.binding_to_value(txn, b))
2148 .collect::<Result<Vec<_>, _>>()?;
2149 let keys: Vec<Value> = order_by_source
2150 .iter()
2151 .map(|src| match src {
2152 OrderKeySource::RealColumn(i) => real_values[*i].clone(),
2153 OrderKeySource::Extra(k) => extra_values[*k].clone(),
2154 })
2155 .collect();
2156 let real_row: BindingRow = with_items
2157 .iter()
2158 .enumerate()
2159 .zip(real)
2160 .map(|((i, item), binding)| (with_item_output_name((i, item)), binding.clone()))
2161 .collect();
2162 keyed.push((keys, real_row));
2163 }
2164 Ok(top_k_by(keyed, order_by, skip, limit)
2165 .into_iter()
2166 .map(|(_, row)| row)
2167 .collect())
2168 }
2169
2170 fn item_binding(
2176 &self,
2177 txn: Txn,
2178 expr: &ReturnExpr,
2179 row: &BindingRow,
2180 guard: &ExecutionGuard<'_>,
2181 ) -> Result<Binding, QueryError> {
2182 match expr {
2183 ReturnExpr::Var(v) => row
2184 .get(v)
2185 .cloned()
2186 .ok_or_else(|| QueryError::UnboundVariable(v.clone())),
2187 other => {
2188 let value = self.eval_return_expr(txn, other, row, guard)?;
2189 Ok(match value {
2201 Value::Node(n) => Binding::Node(n.id),
2202 Value::Edge(e) => Binding::Edge(e.id),
2203 Value::List(items) => Binding::List(items),
2204 Value::Map(m) => Binding::Map(m),
2205 other => Binding::Value(value_to_property_value(&other)),
2206 })
2207 }
2208 }
2209 }
2210
2211 fn apply_order_by_bindings(
2216 &self,
2217 txn: Txn,
2218 rows: Vec<BindingRow>,
2219 pre_with_rows: Option<&[BindingRow]>,
2229 with_items: &[ReturnItem],
2230 order_by: &[(ReturnExpr, SortDir)],
2231 skip_limit: (Option<i64>, Option<i64>),
2232 ) -> Result<Vec<BindingRow>, QueryError> {
2233 let (skip, limit) = skip_limit;
2234 let order_by_output: Vec<Option<String>> = order_by
2245 .iter()
2246 .map(|(expr, _)| {
2247 with_items
2248 .iter()
2249 .enumerate()
2250 .find(|(_, item)| item.expr == *expr)
2251 .map(with_item_output_name)
2252 })
2253 .collect();
2254 let mut keyed: Vec<(Vec<Value>, BindingRow)> = Vec::with_capacity(rows.len());
2255 for (i, row) in rows.into_iter().enumerate() {
2256 let mut value_map = self.binding_row_to_value_map(txn, &row)?;
2257 if let Some(pre) = pre_with_rows {
2258 for (k, v) in self.binding_row_to_value_map(txn, &pre[i])? {
2263 value_map.entry(k).or_insert(v);
2264 }
2265 }
2266 let keys = order_by
2267 .iter()
2268 .zip(&order_by_output)
2269 .map(|((expr, _), output_name)| match output_name {
2270 Some(name) => Ok(value_map.get(name).cloned().unwrap_or(Value::Null)),
2271 None => eval_projected_expr(expr, &value_map),
2272 })
2273 .collect::<Result<Vec<_>, _>>()?;
2274 keyed.push((keys, row));
2275 }
2276 Ok(top_k_by(keyed, order_by, skip, limit)
2277 .into_iter()
2278 .map(|(_, row)| row)
2279 .collect())
2280 }
2281
2282 fn apply_order_by_with_scope(
2292 &self,
2293 txn: Txn,
2294 binding_rows: &[BindingRow],
2295 result: QueryResult,
2296 order_by: &[(ReturnExpr, SortDir)],
2297 skip: Option<i64>,
2298 limit: Option<i64>,
2299 ) -> Result<QueryResult, QueryError> {
2300 let QueryResult { columns, rows } = result;
2301 let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(rows.len());
2302 for (binding_row, row) in binding_rows.iter().zip(rows) {
2303 let mut value_map = self.binding_row_to_value_map(txn, binding_row)?;
2304 for (col, val) in columns.iter().zip(&row) {
2305 value_map.insert(col.clone(), val.clone());
2306 }
2307 let keys = order_by
2308 .iter()
2309 .map(|(expr, _)| eval_projected_expr(expr, &value_map))
2310 .collect::<Result<Vec<_>, _>>()?;
2311 keyed.push((keys, row));
2312 }
2313 let rows = top_k_by(keyed, order_by, skip, limit)
2314 .into_iter()
2315 .map(|(_, row)| row)
2316 .collect();
2317 Ok(QueryResult { columns, rows })
2318 }
2319
2320 fn binding_row_to_value_map(
2321 &self,
2322 txn: Txn,
2323 row: &BindingRow,
2324 ) -> Result<HashMap<String, Value>, QueryError> {
2325 let mut map = HashMap::with_capacity(row.len());
2326 for (k, binding) in row {
2327 map.insert(k.clone(), self.binding_to_value(txn, binding)?);
2328 }
2329 Ok(map)
2330 }
2331
2332 fn binding_to_value(&self, txn: Txn, b: &Binding) -> Result<Value, QueryError> {
2337 Ok(match b {
2338 Binding::Node(id) => Value::Node(deleted_entity_access(GraphStore::get_node_in_txn(
2339 txn, *id,
2340 )?)?),
2341 Binding::Edge(id) => Value::Edge(deleted_entity_access(GraphStore::get_edge_in_txn(
2342 txn, *id,
2343 )?)?),
2344 Binding::Value(PropertyValue::Null) => Value::Null,
2345 Binding::Value(pv) => property_value_to_value(pv.clone()),
2346 Binding::List(items) => Value::List(items.clone()),
2347 Binding::Map(m) => Value::Map(m.clone()),
2348 Binding::Path(elems) => Value::Path(self.resolve_path_elems(txn, elems)?),
2349 })
2350 }
2351
2352 fn start_or_end_node(
2360 &self,
2361 txn: Txn,
2362 which: &str,
2363 arg: Option<&Value>,
2364 ) -> Result<Value, QueryError> {
2365 match arg {
2366 None | Some(Value::Null) => Ok(Value::Null),
2367 Some(Value::Edge(e)) => {
2368 let id = if which == "startnode" { e.src } else { e.dst };
2369 let node = deleted_entity_access(GraphStore::get_node_in_txn(txn, id)?)?;
2370 Ok(Value::Node(node))
2371 }
2372 Some(other) => Err(QueryError::Type(format!(
2373 "{which}() expects a relationship, got {other:?}"
2374 ))),
2375 }
2376 }
2377
2378 fn eval_type_call(
2391 &self,
2392 txn: Txn,
2393 arg_expr: Option<&ReturnExpr>,
2394 row: &BindingRow,
2395 guard: &ExecutionGuard<'_>,
2396 ) -> Result<Value, QueryError> {
2397 let Some(arg_expr) = arg_expr else {
2398 return type_builtin(None);
2399 };
2400 match self.eval_return_expr(txn, arg_expr, row, guard) {
2401 Ok(v) => type_builtin(Some(&v)),
2402 Err(err) => {
2403 if let ReturnExpr::Var(v) = arg_expr {
2404 if let Some(Binding::Edge(id)) = row.get(v) {
2405 if let Some(label) = guard.deleted_edge_type(*id) {
2406 return Ok(Value::Property(PropertyValue::String(label)));
2407 }
2408 }
2409 }
2410 Err(err)
2411 }
2412 }
2413 }
2414
2415 fn resolve_path_elems(
2420 &self,
2421 txn: Txn,
2422 elems: &[PathBinding],
2423 ) -> Result<Vec<PathElem>, QueryError> {
2424 elems
2425 .iter()
2426 .map(|e| {
2427 Ok(match e {
2428 PathBinding::Node(id) => PathElem::Node(deleted_entity_access(
2429 GraphStore::get_node_in_txn(txn, *id)?,
2430 )?),
2431 PathBinding::Edge(id) => PathElem::Edge(deleted_entity_access(
2432 GraphStore::get_edge_in_txn(txn, *id)?,
2433 )?),
2434 })
2435 })
2436 .collect()
2437 }
2438
2439 fn resolve_grouped_rows(
2473 &self,
2474 txn: Txn,
2475 items: &[ReturnItem],
2476 rows: &[BindingRow],
2477 guard: &ExecutionGuard<'_>,
2478 ) -> Result<Vec<Vec<Binding>>, QueryError> {
2479 struct Group {
2480 key_bindings: Vec<Option<Binding>>,
2485 accs: Vec<Vec<AggAcc>>,
2486 row_count: i64,
2487 }
2488 fn fresh_accs(items: &[ReturnItem]) -> Vec<Vec<AggAcc>> {
2489 items
2490 .iter()
2491 .map(|item| {
2492 let mut nodes = Vec::new();
2493 collect_agg_nodes(&item.expr, &mut nodes);
2494 nodes
2495 .into_iter()
2496 .map(|node| match node {
2497 ReturnExpr::CountStar => AggAcc::identity("count", false),
2498 ReturnExpr::Call { name, distinct, .. } => {
2499 AggAcc::identity(name, *distinct)
2500 }
2501 _ => unreachable!(
2502 "collect_agg_nodes only ever collects CountStar/aggregate Call nodes"
2503 ),
2504 })
2505 .collect()
2506 })
2507 .collect()
2508 }
2509 let item_agg_nodes: Vec<Vec<&ReturnExpr>> = items
2512 .iter()
2513 .map(|item| {
2514 let mut nodes = Vec::new();
2515 collect_agg_nodes(&item.expr, &mut nodes);
2516 nodes
2517 })
2518 .collect();
2519
2520 let mut groups: Vec<Group> = Vec::new();
2528 let mut group_index: HashMap<Vec<Option<HashKey>>, usize> = HashMap::new();
2529 for row in rows {
2530 let mut key_bindings = Vec::with_capacity(items.len());
2531 for item in items {
2532 key_bindings.push(if contains_aggregate(&item.expr) {
2533 None
2534 } else {
2535 Some(self.item_binding(txn, &item.expr, row, guard)?)
2536 });
2537 }
2538 let hash_key: Vec<Option<HashKey>> = key_bindings
2539 .iter()
2540 .map(|b| b.as_ref().map(binding_hash_key).transpose())
2541 .collect::<Result<Vec<_>, _>>()?;
2542 let group_idx = *group_index.entry(hash_key).or_insert_with(|| {
2543 groups.push(Group {
2544 key_bindings: key_bindings.clone(),
2545 accs: fresh_accs(items),
2546 row_count: 0,
2547 });
2548 groups.len() - 1
2549 });
2550 let group = &mut groups[group_idx];
2551 group.row_count += 1;
2552 for (i, nodes) in item_agg_nodes.iter().enumerate() {
2553 for (k, node) in nodes.iter().enumerate() {
2554 match node {
2555 ReturnExpr::CountStar => {
2562 group.accs[i][k].fold(&Value::Literal(Literal::Bool(true)))?;
2563 }
2564 ReturnExpr::Call { name, args, .. } => {
2565 let value = self.eval_return_expr(txn, &args[0], row, guard)?;
2570 if is_percentile_name(name) {
2571 let percentile =
2579 self.eval_return_expr(txn, &args[1], row, guard)?;
2580 if !matches!(value, Value::Null) {
2581 group.accs[i][k].fold_percentile(&value, &percentile)?;
2582 }
2583 } else if !matches!(value, Value::Null) {
2584 group.accs[i][k].fold(&value)?;
2585 }
2586 }
2587 _ => unreachable!(
2588 "collect_agg_nodes only ever collects CountStar/aggregate Call nodes"
2589 ),
2590 }
2591 }
2592 }
2593 }
2594
2595 let no_key_items = items.iter().all(|item| contains_aggregate(&item.expr));
2602 if groups.is_empty() && no_key_items {
2603 groups.push(Group {
2604 key_bindings: vec![None; items.len()],
2605 accs: fresh_accs(items),
2606 row_count: 0,
2607 });
2608 }
2609
2610 let mut out = Vec::with_capacity(groups.len());
2611 for mut group in groups {
2612 let ctx = GroupFinishCtx {
2613 items,
2614 key_bindings: &group.key_bindings,
2615 };
2616 let mut row_out = Vec::with_capacity(items.len());
2617 for (i, item) in items.iter().enumerate() {
2618 let binding = match &group.key_bindings[i] {
2619 Some(b) => b.clone(),
2620 None => {
2621 let mut accs = std::mem::take(&mut group.accs[i]).into_iter();
2622 let mut subst = HashMap::new();
2623 let rewritten = self
2624 .rewrite_composed_item(txn, &item.expr, &ctx, &mut accs, &mut subst)?;
2625 value_to_binding(eval_projected_expr(&rewritten, &subst)?)
2626 }
2627 };
2628 row_out.push(binding);
2629 }
2630 out.push(row_out);
2631 }
2632 Ok(out)
2633 }
2634
2635 fn rewrite_composed_item(
2650 &self,
2651 txn: Txn,
2652 expr: &ReturnExpr,
2653 ctx: &GroupFinishCtx<'_>,
2654 accs: &mut std::vec::IntoIter<AggAcc>,
2655 subst: &mut HashMap<String, Value>,
2656 ) -> Result<ReturnExpr, QueryError> {
2657 if matches!(expr, ReturnExpr::CountStar)
2658 || matches!(expr, ReturnExpr::Call { name, .. } if is_aggregate_name(name))
2659 {
2660 let value = accs
2661 .next()
2662 .expect("accs is aligned with this same expr's collect_agg_nodes traversal order")
2663 .finish();
2664 let slot = format!("__slot{}", subst.len());
2665 subst.insert(slot.clone(), value);
2666 return Ok(ReturnExpr::Var(slot));
2667 }
2668 if matches!(expr, ReturnExpr::Var(_) | ReturnExpr::Prop(_)) {
2669 let j = ctx
2670 .items
2671 .iter()
2672 .enumerate()
2673 .position(|(i, it)| item_matches_leaf(expr, i, it) && !contains_aggregate(&it.expr))
2674 .expect(
2675 "validate_return_items already checked this leaf matches a grouping-key item",
2676 );
2677 let binding = ctx.key_bindings[j]
2678 .clone()
2679 .expect("a non-aggregating item always has a key binding");
2680 let value = self.binding_to_value(txn, &binding)?;
2681 let slot = format!("__slot{}", subst.len());
2682 subst.insert(slot.clone(), value);
2683 return Ok(ReturnExpr::Var(slot));
2684 }
2685 Ok(match expr {
2686 ReturnExpr::Lit(lit) => ReturnExpr::Lit(lit.clone()),
2687 ReturnExpr::Call {
2688 name,
2689 args,
2690 distinct,
2691 } => ReturnExpr::Call {
2692 name: name.clone(),
2693 distinct: *distinct,
2694 args: args
2695 .iter()
2696 .map(|a| self.rewrite_composed_item(txn, a, ctx, accs, subst))
2697 .collect::<Result<_, _>>()?,
2698 },
2699 ReturnExpr::Case { test, whens, else_ } => ReturnExpr::Case {
2700 test: test
2701 .as_deref()
2702 .map(|t| self.rewrite_composed_item(txn, t, ctx, accs, subst))
2703 .transpose()?
2704 .map(Box::new),
2705 whens: whens
2706 .iter()
2707 .map(|(w, t)| {
2708 Ok::<_, QueryError>((
2709 self.rewrite_composed_item(txn, w, ctx, accs, subst)?,
2710 self.rewrite_composed_item(txn, t, ctx, accs, subst)?,
2711 ))
2712 })
2713 .collect::<Result<_, _>>()?,
2714 else_: else_
2715 .as_deref()
2716 .map(|e| self.rewrite_composed_item(txn, e, ctx, accs, subst))
2717 .transpose()?
2718 .map(Box::new),
2719 },
2720 ReturnExpr::Arith(l, op, r) => ReturnExpr::Arith(
2721 Box::new(self.rewrite_composed_item(txn, l, ctx, accs, subst)?),
2722 *op,
2723 Box::new(self.rewrite_composed_item(txn, r, ctx, accs, subst)?),
2724 ),
2725 ReturnExpr::Neg(e) => ReturnExpr::Neg(Box::new(
2726 self.rewrite_composed_item(txn, e, ctx, accs, subst)?,
2727 )),
2728 ReturnExpr::ListLit(list_items) => ReturnExpr::ListLit(
2729 list_items
2730 .iter()
2731 .map(|i| self.rewrite_composed_item(txn, i, ctx, accs, subst))
2732 .collect::<Result<_, _>>()?,
2733 ),
2734 ReturnExpr::Index(base, index) => ReturnExpr::Index(
2735 Box::new(self.rewrite_composed_item(txn, base, ctx, accs, subst)?),
2736 Box::new(self.rewrite_composed_item(txn, index, ctx, accs, subst)?),
2737 ),
2738 ReturnExpr::PropOf(base, prop) => ReturnExpr::PropOf(
2739 Box::new(self.rewrite_composed_item(txn, base, ctx, accs, subst)?),
2740 prop.clone(),
2741 ),
2742 ReturnExpr::Slice(base, start, end) => ReturnExpr::Slice(
2743 Box::new(self.rewrite_composed_item(txn, base, ctx, accs, subst)?),
2744 start
2745 .as_deref()
2746 .map(|s| self.rewrite_composed_item(txn, s, ctx, accs, subst))
2747 .transpose()?
2748 .map(Box::new),
2749 end.as_deref()
2750 .map(|e| self.rewrite_composed_item(txn, e, ctx, accs, subst))
2751 .transpose()?
2752 .map(Box::new),
2753 ),
2754 ReturnExpr::ListComp {
2771 var,
2772 source,
2773 where_clause,
2774 project,
2775 } => ReturnExpr::ListComp {
2776 var: var.clone(),
2777 source: Box::new(self.rewrite_composed_item(txn, source, ctx, accs, subst)?),
2778 where_clause: where_clause.clone(),
2779 project: project.clone(),
2780 },
2781 ReturnExpr::Quantifier {
2782 kind,
2783 var,
2784 source,
2785 where_clause,
2786 } => ReturnExpr::Quantifier {
2787 kind: *kind,
2788 var: var.clone(),
2789 source: Box::new(self.rewrite_composed_item(txn, source, ctx, accs, subst)?),
2790 where_clause: where_clause.clone(),
2791 },
2792 ReturnExpr::MapLit(entries) => ReturnExpr::MapLit(
2793 entries
2794 .iter()
2795 .map(|(k, v)| {
2796 Ok::<_, QueryError>((
2797 k.clone(),
2798 self.rewrite_composed_item(txn, v, ctx, accs, subst)?,
2799 ))
2800 })
2801 .collect::<Result<_, _>>()?,
2802 ),
2803 ReturnExpr::And(l, r) => ReturnExpr::And(
2804 Box::new(self.rewrite_composed_item(txn, l, ctx, accs, subst)?),
2805 Box::new(self.rewrite_composed_item(txn, r, ctx, accs, subst)?),
2806 ),
2807 ReturnExpr::Or(l, r) => ReturnExpr::Or(
2808 Box::new(self.rewrite_composed_item(txn, l, ctx, accs, subst)?),
2809 Box::new(self.rewrite_composed_item(txn, r, ctx, accs, subst)?),
2810 ),
2811 ReturnExpr::Xor(l, r) => ReturnExpr::Xor(
2812 Box::new(self.rewrite_composed_item(txn, l, ctx, accs, subst)?),
2813 Box::new(self.rewrite_composed_item(txn, r, ctx, accs, subst)?),
2814 ),
2815 ReturnExpr::Not(e) => ReturnExpr::Not(Box::new(
2816 self.rewrite_composed_item(txn, e, ctx, accs, subst)?,
2817 )),
2818 ReturnExpr::Compare(l, op, r) => ReturnExpr::Compare(
2819 Box::new(self.rewrite_composed_item(txn, l, ctx, accs, subst)?),
2820 *op,
2821 Box::new(self.rewrite_composed_item(txn, r, ctx, accs, subst)?),
2822 ),
2823 ReturnExpr::IsNull(e) => ReturnExpr::IsNull(Box::new(
2824 self.rewrite_composed_item(txn, e, ctx, accs, subst)?,
2825 )),
2826 ReturnExpr::In(needle, haystack) => ReturnExpr::In(
2827 Box::new(self.rewrite_composed_item(txn, needle, ctx, accs, subst)?),
2828 Box::new(self.rewrite_composed_item(txn, haystack, ctx, accs, subst)?),
2829 ),
2830 ReturnExpr::HasLabel(v, l) => ReturnExpr::HasLabel(v.clone(), l.clone()),
2831 ReturnExpr::PatternPredicate(p) => ReturnExpr::PatternPredicate(p.clone()),
2832 ReturnExpr::PatternComprehension { .. } => expr.clone(),
2833 ReturnExpr::ExistsPattern { .. } => expr.clone(),
2834 ReturnExpr::ExistsSubquery(_) => expr.clone(),
2835 ReturnExpr::Var(_) | ReturnExpr::Prop(_) | ReturnExpr::CountStar => {
2836 unreachable!("handled above, before this match")
2837 }
2838 })
2839 }
2840
2841 fn eval_with_expr(
2852 &self,
2853 txn: Txn,
2854 expr: &WithExpr,
2855 row: &BindingRow,
2856 guard: &ExecutionGuard<'_>,
2857 ) -> Result<Option<bool>, QueryError> {
2858 Ok(match expr {
2859 WithExpr::And(l, r) => and3(
2860 self.eval_with_expr(txn, l, row, guard)?,
2861 self.eval_with_expr(txn, r, row, guard)?,
2862 ),
2863 WithExpr::Or(l, r) => or3(
2864 self.eval_with_expr(txn, l, row, guard)?,
2865 self.eval_with_expr(txn, r, row, guard)?,
2866 ),
2867 WithExpr::Not(e) => self.eval_with_expr(txn, e, row, guard)?.map(|b| !b),
2868 WithExpr::Compare(lhs, op, rhs) => {
2869 let lv = self.eval_return_expr(txn, lhs, row, guard)?;
2870 let rv = self.eval_return_expr(txn, rhs, row, guard)?;
2871 compare_values(&lv, *op, &rv)
2872 }
2873 WithExpr::IsNull(e) => Some(matches!(
2875 self.eval_return_expr(txn, e, row, guard)?,
2876 Value::Null
2877 )),
2878 WithExpr::Bare(ReturnExpr::PatternPredicate(pattern)) => {
2889 Some(self.eval_pattern_predicate_exists(txn, pattern, row, guard)?)
2890 }
2891 WithExpr::Bare(e) => self.eval_return_expr_bool3(txn, e, row, guard)?,
2892 })
2893 }
2894
2895 fn eval_pattern_predicate_exists(
2913 &self,
2914 txn: Txn,
2915 pattern: &Pattern,
2916 row: &BindingRow,
2917 guard: &ExecutionGuard<'_>,
2918 ) -> Result<bool, QueryError> {
2919 let carried_vars: HashSet<String> = row.keys().cloned().collect();
2920 let plan = apply_index_seeks(build_match_plan(pattern, &None, &carried_vars)?, txn)?;
2921 let found =
2922 self.eval_plan_with_limit(txn, &plan, std::slice::from_ref(row), guard, Some(1))?;
2923 Ok(!found.is_empty())
2924 }
2925
2926 fn eval_exists_subquery(
2936 &self,
2937 txn: Txn,
2938 stmt: &Statement,
2939 row: &BindingRow,
2940 guard: &ExecutionGuard<'_>,
2941 ) -> Result<bool, QueryError> {
2942 let Statement::Match {
2943 clauses,
2944 tail,
2945 order_by,
2946 skip,
2947 limit,
2948 } = stmt
2949 else {
2950 unreachable!(
2951 "semantic::validate_statement only allows Statement::Match inside exists {{}}"
2952 )
2953 };
2954 let skip = self.resolve_skip_limit(txn, skip.as_deref(), "SKIP", guard)?;
2955 let limit = self.resolve_skip_limit(txn, limit.as_deref(), "LIMIT", guard)?;
2956 let result = self.execute_match_seeded(
2957 txn,
2958 clauses,
2959 tail,
2960 ResultModifiers {
2961 order_by,
2962 skip,
2963 limit,
2964 },
2965 Some(row),
2966 guard,
2967 )?;
2968 Ok(!result.rows.is_empty())
2969 }
2970
2971 fn eval_optional_part(
2990 &self,
2991 txn: Txn,
2992 plan: &LogicalPlan,
2993 outer_rows: &[BindingRow],
2994 new_vars: &HashSet<String>,
2995 guard: &ExecutionGuard<'_>,
2996 ) -> Result<Vec<BindingRow>, QueryError> {
2997 let tagged: Vec<BindingRow> = outer_rows
2998 .iter()
2999 .enumerate()
3000 .map(|(i, row)| {
3001 let mut r = row.clone();
3002 r.insert(
3003 OPTIONAL_SEED_IDX_KEY.to_string(),
3004 Binding::Value(PropertyValue::Int(i as i64)),
3005 );
3006 r
3007 })
3008 .collect();
3009 guard.check_intermediate_rows(tagged.len())?;
3010 let results = self.eval_plan(txn, plan, &tagged, guard)?;
3011 let mut by_idx: HashMap<i64, Vec<BindingRow>> = HashMap::new();
3012 for mut row in results {
3013 let idx = match row.remove(OPTIONAL_SEED_IDX_KEY) {
3014 Some(Binding::Value(PropertyValue::Int(i))) => i,
3015 other => unreachable!(
3016 "__seed_idx tagged internally as Binding::Value(Int), got {other:?}"
3017 ),
3018 };
3019 by_idx.entry(idx).or_default().push(row);
3020 }
3021 let mut out = Vec::with_capacity(outer_rows.len());
3022 for (i, outer_row) in outer_rows.iter().enumerate() {
3023 match by_idx.remove(&(i as i64)) {
3024 Some(matches) => out.extend(matches),
3025 None => {
3026 let mut padded = outer_row.clone();
3027 for var in new_vars {
3028 padded.insert(var.clone(), Binding::Value(PropertyValue::Null));
3029 }
3030 out.push(padded);
3031 }
3032 }
3033 guard.check_intermediate_rows(out.len())?;
3034 }
3035 Ok(out)
3036 }
3037
3038 fn eval_plan(
3039 &self,
3040 txn: Txn,
3041 plan: &LogicalPlan,
3042 seed: &[BindingRow],
3043 guard: &ExecutionGuard<'_>,
3044 ) -> Result<Vec<BindingRow>, QueryError> {
3045 self.eval_plan_with_limit(txn, plan, seed, guard, None)
3046 }
3047
3048 fn eval_plan_with_limit(
3049 &self,
3050 txn: Txn,
3051 plan: &LogicalPlan,
3052 seed: &[BindingRow],
3053 guard: &ExecutionGuard<'_>,
3054 limit: Option<usize>,
3055 ) -> Result<Vec<BindingRow>, QueryError> {
3056 let stream = self.stream_plan(txn, plan, seed, guard, limit);
3057 match limit {
3058 Some(limit) => stream.take(limit).collect(),
3059 None => stream.collect(),
3060 }
3061 }
3062
3063 fn stream_plan<'s>(
3068 &'s self,
3069 txn: Txn<'s>,
3070 plan: &'s LogicalPlan,
3071 seed: &'s [BindingRow],
3072 guard: &'s ExecutionGuard<'_>,
3073 scan_limit: Option<usize>,
3074 ) -> RowStream<'s> {
3075 match plan {
3076 LogicalPlan::Seed { var } => {
3077 debug_assert!(
3078 seed.first().is_none_or(|row| row.contains_key(var)),
3079 "Seed{{var: {var:?}}} planned for a var not present in the carried-forward rows"
3080 );
3081 Self::count_stream(Box::new(seed.iter().cloned().map(Ok)), guard)
3082 }
3083 LogicalPlan::AllNodesScan { var } => {
3084 self.stream_scan(txn, var, None, seed, guard, scan_limit)
3085 }
3086 LogicalPlan::NodeByLabelScan { var, label } => {
3087 self.stream_scan(txn, var, Some(label), seed, guard, scan_limit)
3088 }
3089 LogicalPlan::IndexSeek {
3090 var,
3091 label,
3092 prop,
3093 value,
3094 } => self.stream_index_seek(
3095 txn,
3096 IndexSeekSpec {
3097 var,
3098 label,
3099 prop,
3100 value,
3101 },
3102 seed,
3103 guard,
3104 scan_limit,
3105 ),
3106 LogicalPlan::Expand {
3107 input,
3108 from_var,
3109 to_var,
3110 rel_var,
3111 rel_labels,
3112 direction,
3113 } => {
3114 let mut input = self.stream_plan(txn, input, seed, guard, None);
3115 let mut current: Option<(BindingRow, std::vec::IntoIter<AdjEntry>)> = None;
3116 let mut done = false;
3117 let stream = std::iter::from_fn(move || loop {
3118 if done {
3119 return None;
3120 }
3121 if let Some((row, entries)) = &mut current {
3122 if let Some(entry) = entries.next() {
3123 if let Err(error) = guard.relationship_expansion() {
3124 done = true;
3125 return Some(Err(error));
3126 }
3127 let mut new_row = row.clone();
3128 new_row.insert(to_var.clone(), Binding::Node(entry.other));
3129 if let Some(rel_var) = rel_var {
3130 new_row.insert(rel_var.clone(), Binding::Edge(entry.edge_id));
3131 }
3132 return Some(Ok(new_row));
3133 }
3134 current = None;
3135 }
3136
3137 let row = match input.next()? {
3138 Ok(row) => row,
3139 Err(error) => {
3140 done = true;
3141 return Some(Err(error));
3142 }
3143 };
3144 let from_id = match row.get(from_var) {
3145 Some(Binding::Node(id)) => *id,
3146 Some(Binding::Value(PropertyValue::Null)) => continue,
3150 _ => {
3151 done = true;
3152 return Some(Err(QueryError::UnboundVariable(from_var.clone())));
3153 }
3154 };
3155 match neighbors_for_direction(txn, from_id, *direction, rel_labels) {
3156 Ok(entries) => current = Some((row, entries.into_iter())),
3157 Err(error) => {
3158 done = true;
3159 return Some(Err(error));
3160 }
3161 }
3162 });
3163 Self::count_stream(Box::new(stream), guard)
3164 }
3165 LogicalPlan::VarExpand {
3166 input,
3167 from_var,
3168 to_var,
3169 rel_labels,
3170 direction,
3171 min_hops,
3172 max_hops,
3173 exclude_edge_vars,
3174 exclude_edge_sets,
3175 exclude_edge_var,
3176 path_segment_var,
3177 rel_list_var,
3178 rel_props,
3179 } => {
3180 let mut input = self.stream_plan(txn, input, seed, guard, None);
3181 let mut pending = Vec::new().into_iter();
3182 let mut done = false;
3183 let stream = std::iter::from_fn(move || loop {
3184 if done {
3185 return None;
3186 }
3187 if let Some(row) = pending.next() {
3188 return Some(Ok(row));
3189 }
3190 let row = match input.next()? {
3191 Ok(row) => row,
3192 Err(error) => {
3193 done = true;
3194 return Some(Err(error));
3195 }
3196 };
3197 match self.expand_variable_row(
3198 txn,
3199 row,
3200 VarExpandSpec {
3201 from_var,
3202 to_var,
3203 rel_labels,
3204 direction: *direction,
3205 min_hops: *min_hops,
3206 max_hops: *max_hops,
3207 exclude_edge_vars,
3208 exclude_edge_sets,
3209 exclude_edge_var,
3210 path_segment_var: path_segment_var.as_deref(),
3211 rel_list_var: rel_list_var.as_deref(),
3212 rel_props,
3213 },
3214 guard,
3215 ) {
3216 Ok(rows) => pending = rows.into_iter(),
3217 Err(error) => {
3218 done = true;
3219 return Some(Err(error));
3220 }
3221 }
3222 });
3223 Self::count_stream(Box::new(stream), guard)
3224 }
3225 LogicalPlan::MatchRelList {
3226 input,
3227 from_var,
3228 to_var,
3229 rel_list_var,
3230 rel_labels,
3231 direction,
3232 min_hops,
3233 max_hops,
3234 } => {
3235 let mut input = self.stream_plan(txn, input, seed, guard, None);
3236 let mut done = false;
3237 let stream = std::iter::from_fn(move || loop {
3238 if done {
3239 return None;
3240 }
3241 let row = match input.next()? {
3242 Ok(row) => row,
3243 Err(error) => {
3244 done = true;
3245 return Some(Err(error));
3246 }
3247 };
3248 match self.match_bound_rel_list_row(
3249 row,
3250 MatchRelListSpec {
3251 from_var,
3252 to_var,
3253 rel_list_var,
3254 rel_labels,
3255 direction: *direction,
3256 min_hops: *min_hops,
3257 max_hops: *max_hops,
3258 },
3259 ) {
3260 Ok(Some(row)) => return Some(Ok(row)),
3261 Ok(None) => continue,
3262 Err(error) => {
3263 done = true;
3264 return Some(Err(error));
3265 }
3266 }
3267 });
3268 Self::count_stream(Box::new(stream), guard)
3269 }
3270 LogicalPlan::Filter { input, predicate } => {
3271 let mut input = self.stream_plan(txn, input, seed, guard, None);
3272 let mut done = false;
3273 let stream = std::iter::from_fn(move || loop {
3274 if done {
3275 return None;
3276 }
3277 let row = match input.next()? {
3278 Ok(row) => row,
3279 Err(error) => {
3280 done = true;
3281 return Some(Err(error));
3282 }
3283 };
3284 if let Err(error) = guard.checkpoint() {
3285 done = true;
3286 return Some(Err(error));
3287 }
3288 match self.eval_expr(txn, predicate, &row, guard) {
3289 Ok(Some(true)) => return Some(Ok(row)),
3290 Ok(_) => continue,
3291 Err(error) => {
3292 done = true;
3293 return Some(Err(error));
3294 }
3295 }
3296 });
3297 Self::count_stream(Box::new(stream), guard)
3298 }
3299 }
3300 }
3301
3302 fn count_stream<'s>(mut stream: RowStream<'s>, guard: &'s ExecutionGuard<'_>) -> RowStream<'s> {
3303 let mut produced = 0usize;
3304 let mut done = false;
3305 Box::new(std::iter::from_fn(move || {
3306 if done {
3307 return None;
3308 }
3309 let item = stream.next()?;
3310 if item.is_ok() {
3311 produced = match produced.checked_add(1) {
3312 Some(produced) => produced,
3313 None => {
3314 done = true;
3315 return Some(Err(QueryError::ResourceLimit(
3316 "stream row counter overflow".into(),
3317 )));
3318 }
3319 };
3320 if let Err(error) = guard.check_intermediate_rows(produced) {
3321 done = true;
3322 return Some(Err(error));
3323 }
3324 } else {
3325 done = true;
3326 }
3327 Some(item)
3328 }))
3329 }
3330
3331 fn stream_scan<'s>(
3332 &'s self,
3333 txn: Txn<'s>,
3334 var: &'s str,
3335 label: Option<&'s str>,
3336 seed: &'s [BindingRow],
3337 guard: &'s ExecutionGuard<'_>,
3338 row_limit: Option<usize>,
3339 ) -> RowStream<'s> {
3340 let mut initialized = false;
3341 let mut node_ids = Vec::new();
3342 let mut seed_index = 0usize;
3343 let mut node_index = 0usize;
3344 let mut done = false;
3345 let stream = std::iter::from_fn(move || {
3346 if done || seed.is_empty() {
3347 return None;
3348 }
3349 if !initialized {
3350 initialized = true;
3351 let budget_node_limit = guard.options.max_intermediate_rows.map(|max_rows| {
3352 max_rows
3353 .checked_div(seed.len())
3354 .unwrap_or(0)
3355 .saturating_add(1)
3356 });
3357 let storage_limit = match (row_limit, budget_node_limit) {
3358 (Some(a), Some(b)) => Some(a.min(b)),
3359 (Some(a), None) => Some(a),
3360 (None, Some(b)) => Some(b),
3361 (None, None) => None,
3362 };
3363 let storage_limit = storage_limit.unwrap_or(usize::MAX);
3364 match GraphStore::all_node_ids_limited_in_txn(txn, label, storage_limit) {
3365 Ok(ids) => node_ids = ids,
3366 Err(error) => {
3367 done = true;
3368 return Some(Err(error.into()));
3369 }
3370 }
3371 }
3372 if node_ids.is_empty() || seed_index >= seed.len() {
3373 return None;
3374 }
3375 if let Err(error) = guard.checkpoint() {
3376 done = true;
3377 return Some(Err(error));
3378 }
3379 let mut row = seed[seed_index].clone();
3380 row.insert(var.to_string(), Binding::Node(node_ids[node_index]));
3381 node_index += 1;
3382 if node_index == node_ids.len() {
3383 node_index = 0;
3384 seed_index += 1;
3385 }
3386 Some(Ok(row))
3387 });
3388 Self::count_stream(Box::new(stream), guard)
3389 }
3390
3391 fn stream_index_seek<'s>(
3400 &'s self,
3401 txn: Txn<'s>,
3402 spec: IndexSeekSpec<'s>,
3403 seed: &'s [BindingRow],
3404 guard: &'s ExecutionGuard<'_>,
3405 row_limit: Option<usize>,
3406 ) -> RowStream<'s> {
3407 let mut initialized = false;
3408 let mut node_ids: Vec<NodeId> = Vec::new();
3409 let mut seed_index = 0usize;
3410 let mut node_index = 0usize;
3411 let mut done = false;
3412 let stream = std::iter::from_fn(move || {
3413 if done || seed.is_empty() {
3414 return None;
3415 }
3416 if !initialized {
3417 initialized = true;
3418 let budget_node_limit = guard.options.max_intermediate_rows.map(|max_rows| {
3419 max_rows
3420 .checked_div(seed.len())
3421 .unwrap_or(0)
3422 .saturating_add(1)
3423 });
3424 let storage_limit = match (row_limit, budget_node_limit) {
3425 (Some(a), Some(b)) => Some(a.min(b)),
3426 (Some(a), None) => Some(a),
3427 (None, Some(b)) => Some(b),
3428 (None, None) => None,
3429 };
3430 let result = match storage_limit {
3431 Some(limit) => GraphStore::lookup_by_index_limited_in_txn(
3432 txn, spec.label, spec.prop, spec.value, limit,
3433 ),
3434 None => {
3435 GraphStore::lookup_by_index_in_txn(txn, spec.label, spec.prop, spec.value)
3436 }
3437 };
3438 match result {
3439 Ok(ids) => node_ids = ids,
3440 Err(error) => {
3441 done = true;
3442 return Some(Err(error.into()));
3443 }
3444 }
3445 }
3446 if node_ids.is_empty() || seed_index >= seed.len() {
3447 return None;
3448 }
3449 if let Err(error) = guard.checkpoint() {
3450 done = true;
3451 return Some(Err(error));
3452 }
3453 let mut row = seed[seed_index].clone();
3454 row.insert(spec.var.to_string(), Binding::Node(node_ids[node_index]));
3455 node_index += 1;
3456 if node_index == node_ids.len() {
3457 node_index = 0;
3458 seed_index += 1;
3459 }
3460 Some(Ok(row))
3461 });
3462 Self::count_stream(Box::new(stream), guard)
3463 }
3464
3465 fn expand_variable_row(
3466 &self,
3467 txn: Txn,
3468 row: BindingRow,
3469 spec: VarExpandSpec<'_>,
3470 guard: &ExecutionGuard<'_>,
3471 ) -> Result<Vec<BindingRow>, QueryError> {
3472 let start_id = match row.get(spec.from_var) {
3473 Some(Binding::Node(id)) => *id,
3474 Some(Binding::Value(PropertyValue::Null)) => return Ok(Vec::new()),
3475 _ => return Err(QueryError::UnboundVariable(spec.from_var.to_string())),
3476 };
3477 let mut out = Vec::new();
3478 if spec.min_hops == 0 {
3479 let mut new_row = row.clone();
3480 new_row.insert(spec.to_var.to_string(), Binding::Node(start_id));
3481 if let Some(path_segment_var) = spec.path_segment_var {
3482 new_row.insert(path_segment_var.to_string(), Binding::Path(Vec::new()));
3483 }
3484 if let Some(rel_list_var) = spec.rel_list_var {
3485 new_row.insert(rel_list_var.to_string(), Binding::List(Vec::new()));
3486 }
3487 new_row.insert(spec.exclude_edge_var.to_string(), Binding::Path(Vec::new()));
3488 out.push(new_row);
3489 }
3490 let rel_props = spec
3497 .rel_props
3498 .iter()
3499 .map(|(key, expr)| {
3500 let value = self.eval_return_expr(txn, expr, &row, guard)?;
3501 Ok::<_, QueryError>((key.as_str(), value_to_property_value(&value)))
3502 })
3503 .collect::<Result<Vec<_>, _>>()?;
3504 let unbounded = spec.max_hops.is_none();
3505 let effective_max = spec.max_hops.unwrap_or(VAR_EXPAND_DEPTH_CAP);
3506 let seed_used_edges: HashSet<EdgeId> = spec
3517 .exclude_edge_vars
3518 .iter()
3519 .filter_map(|v| match row.get(v) {
3520 Some(Binding::Edge(id)) => Some(*id),
3521 _ => None,
3522 })
3523 .chain(spec.exclude_edge_sets.iter().flat_map(|v| {
3524 match row.get(v) {
3525 Some(Binding::Path(segment)) => segment
3526 .iter()
3527 .filter_map(|p| match p {
3528 PathBinding::Edge(id) => Some(*id),
3529 PathBinding::Node(_) => None,
3530 })
3531 .collect::<Vec<_>>(),
3532 _ => Vec::new(),
3533 }
3534 }))
3535 .collect();
3536 let mut frontier = vec![(start_id, seed_used_edges, Vec::<PathBinding>::new())];
3543 let mut depth = 0u32;
3544 while depth < effective_max && !frontier.is_empty() {
3545 depth += 1;
3546 let mut next_frontier = Vec::new();
3547 for (node, used_edges, segment) in frontier {
3548 for entry in neighbors_for_direction(txn, node, spec.direction, spec.rel_labels)? {
3549 guard.relationship_expansion()?;
3550 if used_edges.contains(&entry.edge_id) {
3551 continue;
3552 }
3553 if !rel_props.is_empty() {
3554 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(
3555 txn,
3556 entry.edge_id,
3557 )?)?;
3558 let matches = rel_props
3559 .iter()
3560 .all(|(key, expected)| edge.props.get(*key) == Some(expected));
3561 if !matches {
3562 continue;
3563 }
3564 }
3565 let mut next_used_edges = used_edges.clone();
3566 next_used_edges.insert(entry.edge_id);
3567 let mut next_segment = segment.clone();
3568 next_segment.push(PathBinding::Edge(entry.edge_id));
3569 next_segment.push(PathBinding::Node(entry.other));
3570 next_frontier.push((entry.other, next_used_edges, next_segment.clone()));
3571 guard.check_intermediate_rows(next_frontier.len())?;
3572 if depth >= spec.min_hops {
3573 let mut new_row = row.clone();
3574 new_row.insert(spec.to_var.to_string(), Binding::Node(entry.other));
3575 if let Some(path_segment_var) = spec.path_segment_var {
3576 new_row.insert(
3577 path_segment_var.to_string(),
3578 Binding::Path(next_segment.clone()),
3579 );
3580 }
3581 if let Some(rel_list_var) = spec.rel_list_var {
3582 let edges = segment_edges_to_list(txn, &next_segment)?;
3583 new_row.insert(rel_list_var.to_string(), edges);
3584 }
3585 new_row.insert(
3586 spec.exclude_edge_var.to_string(),
3587 Binding::Path(next_segment.clone()),
3588 );
3589 out.push(new_row);
3590 guard.check_intermediate_rows(out.len())?;
3591 }
3592 }
3593 }
3594 frontier = next_frontier;
3595 if depth == effective_max && unbounded && !frontier.is_empty() {
3596 return Err(QueryError::ResourceLimit(format!(
3597 "variable-length traversal exceeded the safety depth cap ({VAR_EXPAND_DEPTH_CAP} \
3598 hops) — likely a cyclic graph or unexpectedly large fanout; narrow the pattern or \
3599 add an explicit upper bound (e.g. *0..10)"
3600 )));
3601 }
3602 }
3603 Ok(out)
3604 }
3605
3606 fn match_bound_rel_list_row(
3615 &self,
3616 row: BindingRow,
3617 spec: MatchRelListSpec<'_>,
3618 ) -> Result<Option<BindingRow>, QueryError> {
3619 let start_id = match row.get(spec.from_var) {
3620 Some(Binding::Node(id)) => *id,
3621 Some(Binding::Value(PropertyValue::Null)) => return Ok(None),
3622 _ => return Err(QueryError::UnboundVariable(spec.from_var.to_string())),
3623 };
3624 let edges: Vec<&Edge> = match row.get(spec.rel_list_var) {
3625 Some(Binding::List(items)) => items
3626 .iter()
3627 .map(|v| match v {
3628 Value::Edge(e) => Ok(e),
3629 other => Err(QueryError::Type(format!(
3630 "'{}' must be a list of relationships, found {other:?} in it",
3631 spec.rel_list_var
3632 ))),
3633 })
3634 .collect::<Result<_, _>>()?,
3635 Some(Binding::Value(PropertyValue::Null)) => return Ok(None),
3636 _ => return Err(QueryError::UnboundVariable(spec.rel_list_var.to_string())),
3637 };
3638 let hops = edges.len() as u32;
3639 if hops < spec.min_hops || spec.max_hops.is_some_and(|max| hops > max) {
3640 return Ok(None);
3641 }
3642 if !spec.rel_labels.is_empty() && edges.iter().any(|e| !spec.rel_labels.contains(&e.label))
3643 {
3644 return Ok(None);
3645 }
3646 let mut current = start_id;
3647 for edge in &edges {
3648 let next = match spec.direction {
3649 ExpandDirection::Out if edge.src == current => edge.dst,
3650 ExpandDirection::In if edge.dst == current => edge.src,
3651 ExpandDirection::Either if edge.src == current => edge.dst,
3652 ExpandDirection::Either if edge.dst == current => edge.src,
3653 _ => return Ok(None),
3654 };
3655 current = next;
3656 }
3657 let mut new_row = row.clone();
3658 new_row.insert(spec.to_var.to_string(), Binding::Node(current));
3659 Ok(Some(new_row))
3660 }
3661
3662 fn eval_expr(
3667 &self,
3668 txn: Txn,
3669 expr: &Expr,
3670 row: &BindingRow,
3671 guard: &ExecutionGuard<'_>,
3672 ) -> Result<Option<bool>, QueryError> {
3673 Ok(match expr {
3674 Expr::And(l, r) => and3(
3675 self.eval_expr(txn, l, row, guard)?,
3676 self.eval_expr(txn, r, row, guard)?,
3677 ),
3678 Expr::Or(l, r) => or3(
3679 self.eval_expr(txn, l, row, guard)?,
3680 self.eval_expr(txn, r, row, guard)?,
3681 ),
3682 Expr::Not(e) => self.eval_expr(txn, e, row, guard)?.map(|b| !b),
3683 Expr::Compare(pa, op, lit) => {
3684 let prop_value = self.lookup_prop(txn, pa, row)?;
3685 compare(&prop_value, *op, lit)
3686 }
3687 Expr::PropCompare(left, op, right) => {
3688 let a = self.lookup_prop(txn, left, row)?;
3689 let b = self.lookup_prop(txn, right, row)?;
3690 compare_property_pair_opt(&a, *op, &b)
3691 }
3692 Expr::IsNull(pa) => Some(matches!(
3696 self.lookup_prop(txn, pa, row)?,
3697 None | Some(PropertyValue::Null)
3698 )),
3699 Expr::HasLabel(var, label) => {
3700 let binding = row
3701 .get(var)
3702 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
3703 let Binding::Node(id) = binding else {
3704 return Err(QueryError::UnboundVariable(var.clone()));
3705 };
3706 let node = GraphStore::get_node_in_txn(txn, *id)?;
3707 Some(node.is_some_and(|n| n.labels.iter().any(|l| l == label)))
3708 }
3709 Expr::VarEq(a, b) => {
3710 let ba = row
3711 .get(a)
3712 .ok_or_else(|| QueryError::UnboundVariable(a.clone()))?;
3713 let bb = row
3714 .get(b)
3715 .ok_or_else(|| QueryError::UnboundVariable(b.clone()))?;
3716 Some(match (ba, bb) {
3717 (Binding::Node(x), Binding::Node(y)) => x == y,
3718 (Binding::Edge(x), Binding::Edge(y)) => x == y,
3719 _ => false,
3727 })
3728 }
3729 Expr::GeneralCompare(lhs, op, rhs) => {
3730 let lv = self.eval_return_expr(txn, lhs, row, guard)?;
3731 let rv = self.eval_return_expr(txn, rhs, row, guard)?;
3732 compare_values(&lv, *op, &rv)
3733 }
3734 Expr::GeneralIsNull(e) => Some(matches!(
3735 self.eval_return_expr(txn, e, row, guard)?,
3736 Value::Null
3737 )),
3738 Expr::GeneralBare(e) => self.eval_return_expr_bool3(txn, e, row, guard)?,
3739 Expr::Pattern(pattern) => {
3755 Some(self.eval_pattern_predicate_exists(txn, pattern, row, guard)?)
3756 }
3757 Expr::Exists {
3763 pattern,
3764 where_clause,
3765 } => {
3766 let carried_vars: HashSet<String> = row.keys().cloned().collect();
3767 let wc: Option<Expr> = where_clause.as_deref().cloned();
3768 let plan = apply_index_seeks(build_match_plan(pattern, &wc, &carried_vars)?, txn)?;
3769 let found = self.eval_plan_with_limit(
3770 txn,
3771 &plan,
3772 std::slice::from_ref(row),
3773 guard,
3774 Some(1),
3775 )?;
3776 Some(!found.is_empty())
3777 }
3778 Expr::ExistsSubquery(stmt) => Some(self.eval_exists_subquery(txn, stmt, row, guard)?),
3783 Expr::EdgeNotInSet {
3792 edge_var,
3793 edge_set_var,
3794 } => {
3795 let Some(Binding::Edge(edge_id)) = row.get(edge_var) else {
3796 return Err(QueryError::UnboundVariable(edge_var.clone()));
3797 };
3798 let Some(Binding::Path(segment)) = row.get(edge_set_var) else {
3799 return Err(QueryError::UnboundVariable(edge_set_var.clone()));
3800 };
3801 Some(
3802 !segment
3803 .iter()
3804 .any(|elem| matches!(elem, PathBinding::Edge(id) if id == edge_id)),
3805 )
3806 }
3807 })
3808 }
3809
3810 fn lookup_prop(
3811 &self,
3812 txn: Txn,
3813 pa: &PropAccess,
3814 row: &BindingRow,
3815 ) -> Result<Option<PropertyValue>, QueryError> {
3816 let binding = row
3817 .get(&pa.var)
3818 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
3819 match binding {
3820 Binding::Node(id) => {
3828 let node = deleted_entity_access(GraphStore::get_node_in_txn(txn, *id)?)?;
3829 Ok(node.props.get(&pa.prop).cloned())
3830 }
3831 Binding::Edge(id) => {
3832 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(txn, *id)?)?;
3833 Ok(edge.props.get(&pa.prop).cloned())
3834 }
3835 Binding::Value(_) | Binding::List(_) | Binding::Map(_) => Ok(None),
3849 Binding::Path(_) => Err(QueryError::Type(format!(
3855 "'{}' is a path — property access requires a node, relationship, or map",
3856 pa.var
3857 ))),
3858 }
3859 }
3860
3861 fn lookup_prop_value(
3882 &self,
3883 txn: Txn,
3884 pa: &PropAccess,
3885 row: &BindingRow,
3886 ) -> Result<Value, QueryError> {
3887 match row.get(&pa.var) {
3888 Some(Binding::Map(m)) => Ok(m.get(&pa.prop).cloned().unwrap_or(Value::Null)),
3889 Some(Binding::Value(PropertyValue::Null)) => Ok(Value::Null),
3890 Some(Binding::Value(pv)) => match temporal_component(pv, &pa.prop) {
3891 Some(component) => Ok(Value::Property(component)),
3892 None if is_temporal_property_value(pv) => Ok(Value::Null),
3893 None => Err(QueryError::Type(format!(
3894 "'{}' can't have properties accessed on it -- property access requires a \
3895 node, relationship, map, or temporal value",
3896 pa.var
3897 ))),
3898 },
3899 Some(Binding::List(_)) => Err(QueryError::Type(format!(
3900 "'{}' can't have properties accessed on it -- property access requires a node, \
3901 relationship, map, or temporal value, not a list",
3902 pa.var
3903 ))),
3904 Some(_) => Ok(match self.lookup_prop(txn, pa, row)? {
3905 Some(PropertyValue::Null) | None => Value::Null,
3906 Some(pv) => property_value_to_value(pv),
3907 }),
3908 None => Err(QueryError::UnboundVariable(pa.var.clone())),
3909 }
3910 }
3911
3912 fn materialize_return(
3913 &self,
3914 txn: Txn,
3915 items: &[ReturnItem],
3916 rows: &[BindingRow],
3917 distinct: bool,
3918 guard: &ExecutionGuard<'_>,
3919 ) -> Result<QueryResult, QueryError> {
3920 let columns = items
3921 .iter()
3922 .enumerate()
3923 .map(|(i, item)| {
3924 item.alias
3925 .clone()
3926 .unwrap_or_else(|| default_column_name(&item.expr, i))
3927 })
3928 .collect();
3929 let mut out_rows = if !has_aggregate(items) {
3930 let mut out_rows = Vec::with_capacity(rows.len());
3931 for row in rows {
3932 let mut out_row = Vec::with_capacity(items.len());
3933 for item in items {
3934 out_row.push(self.eval_return_expr(txn, &item.expr, row, guard)?);
3935 }
3936 out_rows.push(out_row);
3937 }
3938 out_rows
3939 } else {
3940 validate_return_items(items)?;
3941 let grouped = self.resolve_grouped_rows(txn, items, rows, guard)?;
3942 grouped
3943 .into_iter()
3944 .map(|bindings| {
3945 bindings
3946 .iter()
3947 .map(|b| self.binding_to_value(txn, b))
3948 .collect::<Result<Vec<_>, _>>()
3949 })
3950 .collect::<Result<Vec<_>, _>>()?
3951 };
3952 if distinct {
3953 out_rows = dedup_rows(out_rows)?;
3954 }
3955 Ok(QueryResult {
3956 columns,
3957 rows: out_rows,
3958 })
3959 }
3960
3961 fn materialize_aggregating_return_with_order(
3985 &self,
3986 txn: Txn,
3987 items: &[ReturnItem],
3988 rows: &[BindingRow],
3989 order_by: &[(ReturnExpr, SortDir)],
3990 skip_limit: (Option<i64>, Option<i64>),
3991 guard: &ExecutionGuard<'_>,
3992 ) -> Result<QueryResult, QueryError> {
3993 let (skip, limit) = skip_limit;
3994 enum OrderKeySource {
3995 RealColumn(usize),
3996 Extra(usize),
3997 }
3998 let mut extra_exprs: Vec<ReturnExpr> = Vec::new();
3999 let order_by_source: Vec<OrderKeySource> = order_by
4000 .iter()
4001 .map(|(expr, _)| {
4002 match items
4003 .iter()
4004 .enumerate()
4005 .position(|(i, it)| item_matches_leaf(expr, i, it))
4006 {
4007 Some(i) => OrderKeySource::RealColumn(i),
4008 None => {
4009 let idx = extra_exprs.len();
4010 extra_exprs.push(expr.clone());
4011 OrderKeySource::Extra(idx)
4012 }
4013 }
4014 })
4015 .collect();
4016 let extended_items: Vec<ReturnItem> = items
4017 .iter()
4018 .cloned()
4019 .chain(
4020 extra_exprs
4021 .into_iter()
4022 .map(|expr| ReturnItem { expr, alias: None }),
4023 )
4024 .collect();
4025 validate_return_items(&extended_items)?;
4026 let grouped = self.resolve_grouped_rows(txn, &extended_items, rows, guard)?;
4027 let columns: Vec<String> = items
4028 .iter()
4029 .enumerate()
4030 .map(|(i, item)| {
4031 item.alias
4032 .clone()
4033 .unwrap_or_else(|| default_column_name(&item.expr, i))
4034 })
4035 .collect();
4036 let real_len = items.len();
4037 let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(grouped.len());
4038 for bindings in grouped {
4039 let values: Vec<Value> = bindings
4040 .iter()
4041 .map(|b| self.binding_to_value(txn, b))
4042 .collect::<Result<Vec<_>, _>>()?;
4043 let (real, extra) = values.split_at(real_len);
4044 let keys: Vec<Value> = order_by_source
4045 .iter()
4046 .map(|src| match src {
4047 OrderKeySource::RealColumn(i) => real[*i].clone(),
4048 OrderKeySource::Extra(k) => extra[*k].clone(),
4049 })
4050 .collect();
4051 keyed.push((keys, real.to_vec()));
4052 }
4053 let rows = top_k_by(keyed, order_by, skip, limit)
4054 .into_iter()
4055 .map(|(_, row)| row)
4056 .collect();
4057 Ok(QueryResult { columns, rows })
4058 }
4059
4060 fn resolve_skip_limit(
4069 &self,
4070 txn: Txn,
4071 expr: Option<&ReturnExpr>,
4072 clause: &str,
4073 guard: &ExecutionGuard<'_>,
4074 ) -> Result<Option<i64>, QueryError> {
4075 let Some(expr) = expr else {
4076 return Ok(None);
4077 };
4078 let value = self.eval_return_expr(txn, expr, &BindingRow::new(), guard)?;
4079 let n = match value {
4080 Value::Literal(Literal::Int(n)) | Value::Property(PropertyValue::Int(n)) => n,
4081 _ => {
4082 return Err(QueryError::Semantic(format!(
4083 "{clause} must evaluate to an integer"
4084 )));
4085 }
4086 };
4087 if n < 0 {
4088 return Err(QueryError::Semantic(format!("{clause} can't be negative")));
4089 }
4090 Ok(Some(n))
4091 }
4092
4093 fn eval_return_expr(
4094 &self,
4095 txn: Txn,
4096 expr: &ReturnExpr,
4097 row: &BindingRow,
4098 guard: &ExecutionGuard<'_>,
4099 ) -> Result<Value, QueryError> {
4100 match expr {
4101 ReturnExpr::Var(var) => {
4102 let binding = row
4103 .get(var)
4104 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4105 self.binding_to_value(txn, binding)
4106 }
4107 ReturnExpr::Prop(pa) => self.lookup_prop_value(txn, pa, row),
4108 ReturnExpr::PropOf(base, prop) => {
4109 let v = self.eval_return_expr(txn, base, row, guard)?;
4110 property_of_value(&v, prop)
4111 }
4112 ReturnExpr::Lit(lit) => Ok(match lit {
4113 Literal::Null => Value::Null,
4114 other => Value::Literal(other.clone()),
4115 }),
4116 ReturnExpr::Call { name, args, .. } => {
4117 if is_aggregate_name(name) {
4125 return Err(QueryError::Semantic(format!(
4126 "aggregate function '{name}' can only be used as a return item's top-level expression"
4127 )));
4128 }
4129 let lower = name.to_ascii_lowercase();
4130 if lower == "type" {
4131 return self.eval_type_call(txn, args.first(), row, guard);
4138 }
4139 let arg_values = args
4140 .iter()
4141 .map(|a| self.eval_return_expr(txn, a, row, guard))
4142 .collect::<Result<Vec<_>, _>>()?;
4143 if lower == "startnode" || lower == "endnode" {
4144 return self.start_or_end_node(txn, &lower, arg_values.first());
4145 }
4146 call_builtin(name, &arg_values, self.now_snapshot())
4147 }
4148 ReturnExpr::CountStar => Err(QueryError::Semantic(
4149 "count(*) can only be used as a return item's top-level expression".into(),
4150 )),
4151 ReturnExpr::Case { test, whens, else_ } => {
4152 let test_value = match test {
4153 Some(t) => Some(self.eval_return_expr(txn, t, row, guard)?),
4154 None => None,
4155 };
4156 for (when, then) in whens {
4157 let when_value = self.eval_return_expr(txn, when, row, guard)?;
4158 let matched = match &test_value {
4164 Some(tv) => value_eq(tv, &when_value),
4165 None => matches!(when_value, Value::Literal(Literal::Bool(true))),
4166 };
4167 if matched {
4168 return self.eval_return_expr(txn, then, row, guard);
4169 }
4170 }
4171 match else_ {
4172 Some(e) => self.eval_return_expr(txn, e, row, guard),
4173 None => Ok(Value::Null),
4174 }
4175 }
4176 ReturnExpr::Arith(l, op, r) => {
4177 let lv = self.eval_return_expr(txn, l, row, guard)?;
4178 let rv = self.eval_return_expr(txn, r, row, guard)?;
4179 apply_arith(*op, &lv, &rv)
4180 }
4181 ReturnExpr::Neg(e) => {
4182 let v = self.eval_return_expr(txn, e, row, guard)?;
4183 apply_neg(&v)
4184 }
4185 ReturnExpr::ListLit(items) => Ok(Value::List(
4186 items
4187 .iter()
4188 .map(|item| self.eval_return_expr(txn, item, row, guard))
4189 .collect::<Result<Vec<_>, _>>()?,
4190 )),
4191 ReturnExpr::Index(base, index) => {
4192 let base_v = self.eval_return_expr(txn, base, row, guard)?;
4193 let index_v = self.eval_return_expr(txn, index, row, guard)?;
4194 apply_index(&base_v, &index_v)
4195 }
4196 ReturnExpr::Slice(base, start, end) => {
4197 let base_v = self.eval_return_expr(txn, base, row, guard)?;
4198 let start_v = start
4199 .as_deref()
4200 .map(|s| self.eval_return_expr(txn, s, row, guard))
4201 .transpose()?;
4202 let end_v = end
4203 .as_deref()
4204 .map(|e| self.eval_return_expr(txn, e, row, guard))
4205 .transpose()?;
4206 apply_slice(&base_v, start_v.as_ref(), end_v.as_ref())
4207 }
4208 ReturnExpr::ListComp {
4209 var,
4210 source,
4211 where_clause,
4212 project,
4213 } => {
4214 let source_v = self.eval_return_expr(txn, source, row, guard)?;
4215 let items = match source_v {
4216 Value::List(items) => items,
4217 Value::Null => return Ok(Value::Null),
4218 other => {
4219 return Err(QueryError::Type(format!(
4220 "list comprehension source must be a list, got {other:?}"
4221 )))
4222 }
4223 };
4224 let mut result = Vec::with_capacity(items.len());
4225 for item in items {
4226 let mut scoped_row = row.clone();
4230 scoped_row.insert(var.clone(), value_to_binding_restore(&item));
4231 let keep = match where_clause {
4232 Some(w) => {
4233 self.eval_return_expr_bool3(txn, w, &scoped_row, guard)? == Some(true)
4234 }
4235 None => true,
4236 };
4237 if !keep {
4238 continue;
4239 }
4240 result.push(match project {
4241 Some(p) => self.eval_return_expr(txn, p, &scoped_row, guard)?,
4242 None => item,
4243 });
4244 }
4245 Ok(Value::List(result))
4246 }
4247 ReturnExpr::Quantifier {
4248 kind,
4249 var,
4250 source,
4251 where_clause,
4252 } => {
4253 let source_v = self.eval_return_expr(txn, source, row, guard)?;
4254 let items = match source_v {
4255 Value::List(items) => items,
4256 Value::Null => return Ok(Value::Null),
4257 other => {
4258 return Err(QueryError::Type(format!(
4259 "quantifier source must be a list, got {other:?}"
4260 )))
4261 }
4262 };
4263 let mut preds = Vec::with_capacity(items.len());
4264 for item in &items {
4265 let mut scoped_row = row.clone();
4266 scoped_row.insert(var.clone(), value_to_binding_restore(item));
4267 preds.push(match where_clause {
4268 Some(w) => self.eval_return_expr_bool3(txn, w, &scoped_row, guard)?,
4269 None => item_truthy(item),
4270 });
4271 }
4272 Ok(match eval_quantifier(*kind, &preds) {
4273 Some(b) => Value::Literal(Literal::Bool(b)),
4274 None => Value::Null,
4275 })
4276 }
4277 ReturnExpr::MapLit(entries) => {
4278 let mut map = BTreeMap::new();
4279 for (k, v) in entries {
4280 map.insert(k.clone(), self.eval_return_expr(txn, v, row, guard)?);
4281 }
4282 Ok(Value::Map(map))
4283 }
4284 ReturnExpr::And(l, r) => Ok(bool3_to_value(and3(
4285 self.eval_return_expr_bool3(txn, l, row, guard)?,
4286 self.eval_return_expr_bool3(txn, r, row, guard)?,
4287 ))),
4288 ReturnExpr::Or(l, r) => Ok(bool3_to_value(or3(
4289 self.eval_return_expr_bool3(txn, l, row, guard)?,
4290 self.eval_return_expr_bool3(txn, r, row, guard)?,
4291 ))),
4292 ReturnExpr::Xor(l, r) => Ok(bool3_to_value(xor3(
4293 self.eval_return_expr_bool3(txn, l, row, guard)?,
4294 self.eval_return_expr_bool3(txn, r, row, guard)?,
4295 ))),
4296 ReturnExpr::Not(e) => Ok(bool3_to_value(
4297 self.eval_return_expr_bool3(txn, e, row, guard)?.map(|b| !b),
4298 )),
4299 ReturnExpr::Compare(l, op, r) => {
4300 let lv = self.eval_return_expr(txn, l, row, guard)?;
4301 let rv = self.eval_return_expr(txn, r, row, guard)?;
4302 Ok(bool3_to_value(compare_values(&lv, *op, &rv)))
4303 }
4304 ReturnExpr::IsNull(e) => {
4305 let v = self.eval_return_expr(txn, e, row, guard)?;
4306 Ok(Value::Literal(Literal::Bool(matches!(v, Value::Null))))
4307 }
4308 ReturnExpr::In(needle, haystack) => {
4309 let nv = self.eval_return_expr(txn, needle, row, guard)?;
4310 let hv = self.eval_return_expr(txn, haystack, row, guard)?;
4311 Ok(bool3_to_value(list_membership_ternary(&nv, &hv)?))
4312 }
4313 ReturnExpr::HasLabel(var, labels) => {
4314 let binding = row
4315 .get(var)
4316 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4317 match binding {
4318 Binding::Node(id) => {
4319 let node = deleted_entity_access(GraphStore::get_node_in_txn(txn, *id)?)?;
4320 Ok(Value::Literal(Literal::Bool(
4321 labels.iter().all(|l| node.labels.contains(l)),
4322 )))
4323 }
4324 Binding::Edge(id) => {
4333 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(txn, *id)?)?;
4334 Ok(Value::Literal(Literal::Bool(
4335 labels.iter().all(|l| edge.label == *l),
4336 )))
4337 }
4338 Binding::Value(PropertyValue::Null) => Ok(Value::Null),
4339 other => Err(QueryError::Type(format!(
4340 "'{var}' isn't a node or relationship — (n:Label) needs one, got {other:?}"
4341 ))),
4342 }
4343 }
4344 ReturnExpr::PatternPredicate(_) => Err(QueryError::Semantic(
4345 "a pattern predicate (`(n)-->()` etc) can only be used inside WHERE".into(),
4346 )),
4347 ReturnExpr::PatternComprehension {
4348 path_var,
4349 pattern,
4350 where_clause,
4351 projection,
4352 } => self.eval_pattern_comprehension(
4353 txn,
4354 PatternComprehensionSpec {
4355 path_var,
4356 pattern,
4357 where_clause,
4358 projection,
4359 },
4360 row,
4361 guard,
4362 ),
4363 ReturnExpr::ExistsPattern { .. } | ReturnExpr::ExistsSubquery(_) => Err(
4364 QueryError::Semantic("an exists {} subquery can only be used inside WHERE".into()),
4365 ),
4366 }
4367 }
4368
4369 fn eval_pattern_comprehension(
4390 &self,
4391 txn: Txn,
4392 spec: PatternComprehensionSpec<'_>,
4393 row: &BindingRow,
4394 guard: &ExecutionGuard<'_>,
4395 ) -> Result<Value, QueryError> {
4396 let PatternComprehensionSpec {
4397 path_var,
4398 pattern,
4399 where_clause,
4400 projection,
4401 } = spec;
4402 if path_var.is_some() {
4403 validate_named_path_pattern(pattern)?;
4404 }
4405 let carried_vars: HashSet<String> = row.keys().cloned().collect();
4406 let (named_pattern, synthesized) = match path_var {
4407 Some(_) => name_pattern_for_path(pattern),
4408 None => (pattern.clone(), HashSet::new()),
4409 };
4410 let wc: Option<Expr> = where_clause.as_deref().cloned();
4411 let plan = apply_index_seeks(build_match_plan(&named_pattern, &wc, &carried_vars)?, txn)?;
4412 let rows = self.eval_plan_with_limit(txn, &plan, std::slice::from_ref(row), guard, None)?;
4413 let mut out = Vec::with_capacity(rows.len());
4414 for mut r in rows {
4415 if let Some(pv) = path_var {
4416 let path_binding = assemble_path(&named_pattern, &r);
4417 for key in &synthesized {
4418 r.remove(key);
4419 }
4420 r.insert(pv.clone(), path_binding);
4421 }
4422 out.push(self.eval_return_expr(txn, projection, &r, guard)?);
4423 }
4424 Ok(Value::List(out))
4425 }
4426
4427 fn eval_return_expr_bool3(
4432 &self,
4433 txn: Txn,
4434 expr: &ReturnExpr,
4435 row: &BindingRow,
4436 guard: &ExecutionGuard<'_>,
4437 ) -> Result<Option<bool>, QueryError> {
4438 value_to_bool3(&self.eval_return_expr(txn, expr, row, guard)?)
4439 }
4440
4441 fn delete_targets(
4459 &self,
4460 txn: Txn,
4461 write_txn: &WriteTransaction,
4462 targets: &[ReturnExpr],
4463 rows: &[BindingRow],
4464 detach: bool,
4465 guard: &ExecutionGuard<'_>,
4466 ) -> Result<(), QueryError> {
4467 let mut deleted_edges = HashSet::new();
4468 let mut pending_nodes = HashSet::new();
4469 for row in rows {
4470 for target in targets {
4471 if let ReturnExpr::Var(name) = target {
4486 let binding = row
4487 .get(name)
4488 .ok_or_else(|| QueryError::UnboundVariable(name.clone()))?;
4489 delete_binding(
4490 txn,
4491 binding,
4492 write_txn,
4493 &mut deleted_edges,
4494 &mut pending_nodes,
4495 guard,
4496 )?;
4497 } else {
4498 let value = self.eval_return_expr(txn, target, row, guard)?;
4499 delete_value(
4500 &value,
4501 write_txn,
4502 &mut deleted_edges,
4503 &mut pending_nodes,
4504 guard,
4505 )?;
4506 }
4507 }
4508 }
4509 for id in pending_nodes {
4510 GraphStore::delete_node_in_txn(write_txn, id, detach)?;
4511 }
4512 Ok(())
4513 }
4514
4515 fn materialize_delete(
4528 &self,
4529 txn: Txn,
4530 targets: &[ReturnExpr],
4531 rows: &[BindingRow],
4532 detach: bool,
4533 ret: &Option<ReturnTail>,
4534 guard: &ExecutionGuard<'_>,
4535 ) -> Result<QueryResult, QueryError> {
4536 let write_txn = require_write_txn(txn);
4537 self.delete_targets(txn, write_txn, targets, rows, detach, guard)?;
4538 let result = match ret {
4539 Some(rt) => self.materialize_return(txn, &rt.items, rows, rt.distinct, guard)?,
4540 None => QueryResult {
4541 columns: vec![],
4542 rows: vec![],
4543 },
4544 };
4545 Ok(result)
4546 }
4547
4548 fn materialize_set(
4549 &self,
4550 txn: Txn,
4551 items: &[SetItem],
4552 rows: &[BindingRow],
4553 ret: &Option<ReturnTail>,
4554 guard: &ExecutionGuard<'_>,
4555 ) -> Result<QueryResult, QueryError> {
4556 let write_txn = require_write_txn(txn);
4557 for row in rows {
4558 for item in items {
4559 self.apply_set_item(txn, write_txn, row, item, guard)?;
4560 }
4561 }
4562 match ret {
4563 Some(rt) => self.materialize_return(txn, &rt.items, rows, rt.distinct, guard),
4564 None => Ok(QueryResult {
4565 columns: vec![],
4566 rows: vec![],
4567 }),
4568 }
4569 }
4570
4571 fn materialize_remove(
4572 &self,
4573 txn: Txn,
4574 items: &[RemoveItem],
4575 rows: &[BindingRow],
4576 ret: &Option<ReturnTail>,
4577 guard: &ExecutionGuard<'_>,
4578 ) -> Result<QueryResult, QueryError> {
4579 let write_txn = require_write_txn(txn);
4580 for row in rows {
4581 for item in items {
4582 apply_remove_item(write_txn, row, item)?;
4583 }
4584 }
4585 match ret {
4586 Some(rt) => self.materialize_return(txn, &rt.items, rows, rt.distinct, guard),
4587 None => Ok(QueryResult {
4588 columns: vec![],
4589 rows: vec![],
4590 }),
4591 }
4592 }
4593
4594 fn materialize_union(
4607 &self,
4608 txn: Txn,
4609 parts: &[Statement],
4610 all: bool,
4611 guard: &ExecutionGuard<'_>,
4612 ) -> Result<QueryResult, QueryError> {
4613 let mut combined: Option<QueryResult> = None;
4614 for part in parts {
4615 let Statement::Match {
4616 clauses,
4617 tail,
4618 order_by,
4619 skip,
4620 limit,
4621 } = part
4622 else {
4623 unreachable!(
4624 "union_stmt parts are always Statement::Match -- see parser::parse_union_stmt"
4625 )
4626 };
4627 let skip = self.resolve_skip_limit(txn, skip.as_deref(), "SKIP", guard)?;
4628 let limit = self.resolve_skip_limit(txn, limit.as_deref(), "LIMIT", guard)?;
4629 let result = self.execute_match(
4630 txn,
4631 clauses,
4632 tail,
4633 ResultModifiers {
4634 order_by,
4635 skip,
4636 limit,
4637 },
4638 guard,
4639 )?;
4640 combined = Some(match combined {
4641 None => result,
4642 Some(mut acc) => {
4643 if acc.columns != result.columns {
4644 return Err(QueryError::Semantic(format!(
4645 "UNION requires every part to return the same columns -- got {:?} \
4646 and {:?}",
4647 acc.columns, result.columns
4648 )));
4649 }
4650 acc.rows.extend(result.rows);
4651 acc
4652 }
4653 });
4654 guard.check_intermediate_rows(combined.as_ref().map(|r| r.rows.len()).unwrap_or(0))?;
4655 }
4656 let mut result = combined.expect("union_stmt grammar guarantees at least 2 parts");
4657 if !all {
4658 result.rows = dedup_rows(result.rows)?;
4659 }
4660 Ok(result)
4661 }
4662
4663 fn apply_set_item(
4664 &self,
4665 txn: Txn,
4666 write_txn: &WriteTransaction,
4667 row: &BindingRow,
4668 item: &SetItem,
4669 guard: &ExecutionGuard<'_>,
4670 ) -> Result<(), QueryError> {
4671 match item {
4672 SetItem::Prop(pa, expr) => {
4673 let binding = row
4674 .get(&pa.var)
4675 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
4676 if matches!(binding, Binding::Value(PropertyValue::Null)) {
4682 return Ok(());
4683 }
4684 let node_id = if let Binding::Node(id) = binding {
4685 Some(*id)
4686 } else {
4687 None
4688 };
4689 let edge_id = if let Binding::Edge(id) = binding {
4690 Some(*id)
4691 } else {
4692 None
4693 };
4694 if node_id.is_none() && edge_id.is_none() {
4695 return Err(QueryError::UnboundVariable(format!(
4696 "'{}' is a WITH-projected scalar, not a node/edge — SET needs a graph binding",
4697 pa.var
4698 )));
4699 }
4700 let value = self.eval_return_expr(txn, expr, row, guard)?;
4701 if matches!(value, Value::Null) {
4716 if let Some(id) = node_id {
4717 GraphStore::remove_node_prop_in_txn(write_txn, id, &pa.prop)?;
4718 }
4719 if let Some(id) = edge_id {
4720 GraphStore::remove_edge_prop_in_txn(write_txn, id, &pa.prop)?;
4721 }
4722 } else {
4723 let pv = value_to_storable_property(&value).ok_or_else(|| {
4724 QueryError::Type(format!(
4725 "property '{}' can't be stored -- MarsDB's node/edge properties are limited \
4726 to null/bool/int/float/string/date/duration; a list/map/node/edge/path value \
4727 (got {value:?}) isn't storable",
4728 pa.prop
4729 ))
4730 })?;
4731 if let Some(id) = node_id {
4732 GraphStore::set_node_prop_in_txn(write_txn, id, &pa.prop, pv.clone())?;
4733 }
4734 if let Some(id) = edge_id {
4735 GraphStore::set_edge_prop_in_txn(write_txn, id, &pa.prop, pv)?;
4736 }
4737 }
4738 }
4739 SetItem::Labels(var, labels) => {
4740 let binding = row
4741 .get(var)
4742 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4743 match binding {
4744 Binding::Node(id) => {
4745 for label in labels {
4746 GraphStore::add_node_label_in_txn(write_txn, *id, label)?;
4747 }
4748 }
4749 Binding::Value(PropertyValue::Null) => {}
4751 _ => {
4752 return Err(QueryError::UnboundVariable(format!(
4753 "'{var}' isn't a node — SET can only add labels to a node"
4754 )))
4755 }
4756 }
4757 }
4758 SetItem::MapAssign { var, value, merge } => {
4759 let binding = row
4760 .get(var)
4761 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4762 if matches!(binding, Binding::Value(PropertyValue::Null)) {
4764 return Ok(());
4765 }
4766 let node_id = if let Binding::Node(id) = binding {
4767 Some(*id)
4768 } else {
4769 None
4770 };
4771 let edge_id = if let Binding::Edge(id) = binding {
4772 Some(*id)
4773 } else {
4774 None
4775 };
4776 if node_id.is_none() && edge_id.is_none() {
4777 return Err(QueryError::UnboundVariable(format!(
4778 "'{var}' is a WITH-projected scalar, not a node/edge — SET needs a graph binding"
4779 )));
4780 }
4781 let map_value = self.eval_return_expr(txn, value, row, guard)?;
4782 let entries = match map_value {
4788 Value::Map(entries) => entries,
4789 Value::Node(n) => n
4790 .props
4791 .into_iter()
4792 .map(|(k, v)| (k, property_value_to_value(v)))
4793 .collect(),
4794 Value::Edge(e) => e
4795 .props
4796 .into_iter()
4797 .map(|(k, v)| (k, property_value_to_value(v)))
4798 .collect(),
4799 other => {
4800 return Err(QueryError::Type(format!(
4801 "SET {var} = ...{} needs a map, node, or relationship, got {other:?}",
4802 if *merge { " (+=)" } else { "" }
4803 )))
4804 }
4805 };
4806 if !merge {
4812 let existing_keys: Vec<String> = if let Some(id) = node_id {
4813 deleted_entity_access(GraphStore::get_node_in_txn(txn, id)?)?
4814 .props
4815 .into_keys()
4816 .collect()
4817 } else {
4818 deleted_entity_access(GraphStore::get_edge_in_txn(
4819 txn,
4820 edge_id.expect("node_id or edge_id is Some, checked above"),
4821 )?)?
4822 .props
4823 .into_keys()
4824 .collect()
4825 };
4826 for key in existing_keys {
4827 if let Some(id) = node_id {
4828 GraphStore::remove_node_prop_in_txn(write_txn, id, &key)?;
4829 }
4830 if let Some(id) = edge_id {
4831 GraphStore::remove_edge_prop_in_txn(write_txn, id, &key)?;
4832 }
4833 }
4834 }
4835 for (key, entry_value) in entries {
4840 if matches!(entry_value, Value::Null) {
4841 if let Some(id) = node_id {
4842 GraphStore::remove_node_prop_in_txn(write_txn, id, &key)?;
4843 }
4844 if let Some(id) = edge_id {
4845 GraphStore::remove_edge_prop_in_txn(write_txn, id, &key)?;
4846 }
4847 continue;
4848 }
4849 let pv = value_to_storable_property(&entry_value).ok_or_else(|| {
4850 QueryError::Type(format!(
4851 "property '{key}' can't be stored -- MarsDB's node/edge properties are \
4852 limited to null/bool/int/float/string/date/duration/list; a map/node/\
4853 edge/path value (got {entry_value:?}) isn't storable"
4854 ))
4855 })?;
4856 if let Some(id) = node_id {
4857 GraphStore::set_node_prop_in_txn(write_txn, id, &key, pv.clone())?;
4858 }
4859 if let Some(id) = edge_id {
4860 GraphStore::set_edge_prop_in_txn(write_txn, id, &key, pv)?;
4861 }
4862 }
4863 }
4864 }
4865 Ok(())
4866 }
4867}
4868
4869fn record_and_delete_edge(
4884 txn: Txn,
4885 write_txn: &WriteTransaction,
4886 id: EdgeId,
4887 guard: &ExecutionGuard<'_>,
4888) -> Result<(), QueryError> {
4889 if let Some(edge) = GraphStore::get_edge_in_txn(txn, id)? {
4890 guard.record_deleted_edge_type(id, edge.label);
4891 }
4892 GraphStore::delete_edge_in_txn(write_txn, id)?;
4893 Ok(())
4894}
4895
4896fn delete_binding(
4897 txn: Txn,
4898 binding: &Binding,
4899 write_txn: &WriteTransaction,
4900 deleted_edges: &mut HashSet<EdgeId>,
4901 pending_nodes: &mut HashSet<NodeId>,
4902 guard: &ExecutionGuard<'_>,
4903) -> Result<(), QueryError> {
4904 match binding {
4905 Binding::Node(id) => {
4906 pending_nodes.insert(*id);
4907 }
4908 Binding::Edge(id) => {
4909 if deleted_edges.insert(*id) {
4910 record_and_delete_edge(txn, write_txn, *id, guard)?;
4911 }
4912 }
4913 Binding::Path(elems) => {
4914 for elem in elems {
4915 if let PathBinding::Edge(id) = elem {
4916 if deleted_edges.insert(*id) {
4917 record_and_delete_edge(txn, write_txn, *id, guard)?;
4918 }
4919 }
4920 }
4921 for elem in elems {
4922 if let PathBinding::Node(id) = elem {
4923 pending_nodes.insert(*id);
4924 }
4925 }
4926 }
4927 Binding::Value(PropertyValue::Null) => {}
4931 Binding::Value(_) | Binding::List(_) | Binding::Map(_) => {
4932 return Err(QueryError::Type(
4933 "DELETE needs a node, relationship, or path, not a scalar/list/map".into(),
4934 ))
4935 }
4936 }
4937 Ok(())
4938}
4939
4940fn delete_value(
4951 value: &Value,
4952 write_txn: &WriteTransaction,
4953 deleted_edges: &mut HashSet<EdgeId>,
4954 pending_nodes: &mut HashSet<NodeId>,
4955 guard: &ExecutionGuard<'_>,
4956) -> Result<(), QueryError> {
4957 match value {
4958 Value::Node(n) => {
4959 pending_nodes.insert(n.id);
4960 }
4961 Value::Edge(e) => {
4962 if deleted_edges.insert(e.id) {
4963 guard.record_deleted_edge_type(e.id, e.label.clone());
4964 GraphStore::delete_edge_in_txn(write_txn, e.id)?;
4965 }
4966 }
4967 Value::Path(elems) => {
4968 for elem in elems {
4969 if let PathElem::Edge(e) = elem {
4970 if deleted_edges.insert(e.id) {
4971 guard.record_deleted_edge_type(e.id, e.label.clone());
4972 GraphStore::delete_edge_in_txn(write_txn, e.id)?;
4973 }
4974 }
4975 }
4976 for elem in elems {
4977 if let PathElem::Node(n) = elem {
4978 pending_nodes.insert(n.id);
4979 }
4980 }
4981 }
4982 Value::Null => {}
4983 other => {
4984 return Err(QueryError::Type(format!(
4985 "DELETE needs a node, relationship, or path, got {other:?}"
4986 )))
4987 }
4988 }
4989 Ok(())
4990}
4991
4992fn apply_remove_item(
4993 write_txn: &WriteTransaction,
4994 row: &BindingRow,
4995 item: &RemoveItem,
4996) -> Result<(), QueryError> {
4997 match item {
4998 RemoveItem::Prop(pa) => {
4999 let binding = row
5000 .get(&pa.var)
5001 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
5002 match binding {
5003 Binding::Node(id) => {
5004 GraphStore::remove_node_prop_in_txn(write_txn, *id, &pa.prop)?;
5005 }
5006 Binding::Edge(id) => {
5007 GraphStore::remove_edge_prop_in_txn(write_txn, *id, &pa.prop)?;
5008 }
5009 Binding::Value(PropertyValue::Null) => {}
5013 Binding::Value(_) | Binding::List(_) | Binding::Map(_) | Binding::Path(_) => {
5014 return Err(QueryError::UnboundVariable(format!(
5015 "'{}' is a WITH-projected scalar, not a node/edge — REMOVE needs a graph binding",
5016 pa.var
5017 )))
5018 }
5019 }
5020 }
5021 RemoveItem::Labels(var, labels) => {
5022 let binding = row
5023 .get(var)
5024 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
5025 match binding {
5026 Binding::Node(id) => {
5027 for label in labels {
5028 GraphStore::remove_node_label_in_txn(write_txn, *id, label)?;
5029 }
5030 }
5031 Binding::Value(PropertyValue::Null) => {}
5035 _ => {
5036 return Err(QueryError::UnboundVariable(format!(
5037 "'{var}' isn't a node — REMOVE can only remove labels from a node"
5038 )))
5039 }
5040 }
5041 }
5042 }
5043 Ok(())
5044}
5045
5046fn tail_is_distinct_return(tail: &Option<Tail>) -> bool {
5054 match tail {
5055 Some(Tail::Return(_, distinct)) | Some(Tail::ReturnStar(distinct)) => *distinct,
5056 Some(Tail::Delete(_, ret))
5057 | Some(Tail::DetachDelete(_, ret))
5058 | Some(Tail::Set(_, ret))
5059 | Some(Tail::Remove(_, ret))
5060 | Some(Tail::Create(_, ret)) => ret.as_ref().is_some_and(|rt| rt.distinct),
5061 None => false,
5062 }
5063}
5064
5065pub fn is_read_only(stmt: &Statement) -> bool {
5083 if let Statement::Union { parts, .. } = stmt {
5084 return parts.iter().all(is_read_only);
5085 }
5086 let Statement::Match {
5087 tail: Some(Tail::Return(_, _)) | Some(Tail::ReturnStar(_)),
5088 clauses,
5089 ..
5090 } = stmt
5091 else {
5092 return false;
5093 };
5094 !clauses.iter().any(|c| {
5095 matches!(
5096 c,
5097 QueryClause::Merge(_)
5098 | QueryClause::Set(_)
5099 | QueryClause::Delete { .. }
5100 | QueryClause::Remove(_)
5101 | QueryClause::Create(_)
5102 | QueryClause::Call(_)
5109 )
5110 })
5111}
5112
5113fn require_write_txn(txn: Txn<'_>) -> &WriteTransaction {
5121 let Txn::Write(write_txn) = txn else {
5122 unreachable!(
5123 "materialize_delete/materialize_set/QueryClause::Set only reached via the \
5124 write-dispatch path in Executor::execute — is_read_only(stmt) is false for any \
5125 statement with one of these, so execute always opens a WriteTransaction for them"
5126 )
5127 };
5128 write_txn
5129}
5130
5131fn default_column_name(expr: &ReturnExpr, idx: usize) -> String {
5132 match expr {
5133 ReturnExpr::Var(v) => v.clone(),
5134 ReturnExpr::Prop(pa) => format!("{}.{}", pa.var, pa.prop),
5135 ReturnExpr::Lit(_) => format!("col{idx}"),
5136 ReturnExpr::Call { name, .. } => format!("{name}(...)"),
5137 ReturnExpr::CountStar => "count(*)".to_string(),
5138 ReturnExpr::Case { .. } => format!("case{idx}"),
5139 ReturnExpr::Arith(..) | ReturnExpr::Neg(..) => format!("col{idx}"),
5140 ReturnExpr::ListLit(..)
5141 | ReturnExpr::Index(..)
5142 | ReturnExpr::PropOf(..)
5143 | ReturnExpr::Slice(..)
5144 | ReturnExpr::ListComp { .. }
5145 | ReturnExpr::Quantifier { .. }
5146 | ReturnExpr::MapLit(..)
5147 | ReturnExpr::And(..)
5148 | ReturnExpr::Or(..)
5149 | ReturnExpr::Xor(..)
5150 | ReturnExpr::Not(..)
5151 | ReturnExpr::Compare(..)
5152 | ReturnExpr::IsNull(..)
5153 | ReturnExpr::In(..)
5154 | ReturnExpr::HasLabel(..)
5155 | ReturnExpr::PatternPredicate(..)
5156 | ReturnExpr::PatternComprehension { .. }
5157 | ReturnExpr::ExistsPattern { .. }
5158 | ReturnExpr::ExistsSubquery(_) => format!("col{idx}"),
5159 }
5160}
5161
5162pub(crate) fn with_item_output_name((i, item): (usize, &ReturnItem)) -> String {
5167 item.alias
5168 .clone()
5169 .unwrap_or_else(|| default_column_name(&item.expr, i))
5170}
5171
5172fn collect_agg_nodes<'a>(expr: &'a ReturnExpr, out: &mut Vec<&'a ReturnExpr>) {
5189 match expr {
5190 ReturnExpr::CountStar => out.push(expr),
5191 ReturnExpr::Call { name, args, .. } => {
5192 if is_aggregate_name(name) {
5193 out.push(expr);
5194 } else {
5195 for arg in args {
5196 collect_agg_nodes(arg, out);
5197 }
5198 }
5199 }
5200 ReturnExpr::Case { test, whens, else_ } => {
5201 if let Some(t) = test.as_deref() {
5202 collect_agg_nodes(t, out);
5203 }
5204 for (w, t) in whens {
5205 collect_agg_nodes(w, out);
5206 collect_agg_nodes(t, out);
5207 }
5208 if let Some(e) = else_.as_deref() {
5209 collect_agg_nodes(e, out);
5210 }
5211 }
5212 ReturnExpr::Arith(l, _, r) => {
5213 collect_agg_nodes(l, out);
5214 collect_agg_nodes(r, out);
5215 }
5216 ReturnExpr::Neg(e) => collect_agg_nodes(e, out),
5217 ReturnExpr::ListLit(items) => {
5218 for item in items {
5219 collect_agg_nodes(item, out);
5220 }
5221 }
5222 ReturnExpr::Index(base, index) => {
5223 collect_agg_nodes(base, out);
5224 collect_agg_nodes(index, out);
5225 }
5226 ReturnExpr::PropOf(base, _) => collect_agg_nodes(base, out),
5227 ReturnExpr::Slice(base, start, end) => {
5228 collect_agg_nodes(base, out);
5229 if let Some(s) = start.as_deref() {
5230 collect_agg_nodes(s, out);
5231 }
5232 if let Some(e) = end.as_deref() {
5233 collect_agg_nodes(e, out);
5234 }
5235 }
5236 ReturnExpr::ListComp {
5239 source, project, ..
5240 } => {
5241 collect_agg_nodes(source, out);
5242 if let Some(p) = project.as_deref() {
5243 collect_agg_nodes(p, out);
5244 }
5245 }
5246 ReturnExpr::Quantifier { source, .. } => collect_agg_nodes(source, out),
5247 ReturnExpr::MapLit(entries) => {
5248 for (_, v) in entries {
5249 collect_agg_nodes(v, out);
5250 }
5251 }
5252 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5253 collect_agg_nodes(l, out);
5254 collect_agg_nodes(r, out);
5255 }
5256 ReturnExpr::Not(e) => collect_agg_nodes(e, out),
5257 ReturnExpr::Compare(l, _, r) => {
5258 collect_agg_nodes(l, out);
5259 collect_agg_nodes(r, out);
5260 }
5261 ReturnExpr::IsNull(e) => collect_agg_nodes(e, out),
5262 ReturnExpr::In(needle, haystack) => {
5263 collect_agg_nodes(needle, out);
5264 collect_agg_nodes(haystack, out);
5265 }
5266 ReturnExpr::Var(_)
5267 | ReturnExpr::Prop(_)
5268 | ReturnExpr::Lit(_)
5269 | ReturnExpr::HasLabel(..)
5270 | ReturnExpr::PatternPredicate(..)
5271 | ReturnExpr::PatternComprehension { .. }
5272 | ReturnExpr::ExistsPattern { .. }
5273 | ReturnExpr::ExistsSubquery(_) => {}
5274 }
5275}
5276
5277pub(crate) fn contains_aggregate(expr: &ReturnExpr) -> bool {
5278 match expr {
5279 ReturnExpr::CountStar => true,
5280 ReturnExpr::Call { name, args, .. } => {
5281 is_aggregate_name(name) || args.iter().any(contains_aggregate)
5282 }
5283 ReturnExpr::Case { test, whens, else_ } => {
5284 test.as_deref().is_some_and(contains_aggregate)
5285 || whens
5286 .iter()
5287 .any(|(w, t)| contains_aggregate(w) || contains_aggregate(t))
5288 || else_.as_deref().is_some_and(contains_aggregate)
5289 }
5290 ReturnExpr::Arith(l, _, r) => contains_aggregate(l) || contains_aggregate(r),
5291 ReturnExpr::Neg(e) => contains_aggregate(e),
5292 ReturnExpr::ListLit(items) => items.iter().any(contains_aggregate),
5293 ReturnExpr::Index(base, index) => contains_aggregate(base) || contains_aggregate(index),
5294 ReturnExpr::PropOf(base, _) => contains_aggregate(base),
5295 ReturnExpr::Slice(base, start, end) => {
5296 contains_aggregate(base)
5297 || start.as_deref().is_some_and(contains_aggregate)
5298 || end.as_deref().is_some_and(contains_aggregate)
5299 }
5300 ReturnExpr::ListComp {
5305 source, project, ..
5306 } => contains_aggregate(source) || project.as_deref().is_some_and(contains_aggregate),
5307 ReturnExpr::Quantifier { source, .. } => contains_aggregate(source),
5308 ReturnExpr::MapLit(entries) => entries.iter().any(|(_, v)| contains_aggregate(v)),
5309 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5310 contains_aggregate(l) || contains_aggregate(r)
5311 }
5312 ReturnExpr::Not(e) => contains_aggregate(e),
5313 ReturnExpr::Compare(l, _, r) => contains_aggregate(l) || contains_aggregate(r),
5314 ReturnExpr::IsNull(e) => contains_aggregate(e),
5315 ReturnExpr::In(needle, haystack) => {
5316 contains_aggregate(needle) || contains_aggregate(haystack)
5317 }
5318 ReturnExpr::Var(_)
5319 | ReturnExpr::Prop(_)
5320 | ReturnExpr::Lit(_)
5321 | ReturnExpr::HasLabel(..)
5322 | ReturnExpr::PatternPredicate(..)
5323 | ReturnExpr::PatternComprehension { .. }
5330 | ReturnExpr::ExistsPattern { .. }
5331 | ReturnExpr::ExistsSubquery(_) => false,
5332 }
5333}
5334
5335pub(crate) fn has_aggregate(items: &[ReturnItem]) -> bool {
5341 items.iter().any(|item| contains_aggregate(&item.expr))
5352}
5353
5354fn contains_rand_call(expr: &ReturnExpr) -> bool {
5360 match expr {
5361 ReturnExpr::Call { name, args, .. } => {
5362 name.eq_ignore_ascii_case("rand") || args.iter().any(contains_rand_call)
5363 }
5364 ReturnExpr::Case { test, whens, else_ } => {
5365 test.as_deref().is_some_and(contains_rand_call)
5366 || whens
5367 .iter()
5368 .any(|(w, t)| contains_rand_call(w) || contains_rand_call(t))
5369 || else_.as_deref().is_some_and(contains_rand_call)
5370 }
5371 ReturnExpr::Arith(l, _, r) => contains_rand_call(l) || contains_rand_call(r),
5372 ReturnExpr::Neg(e) => contains_rand_call(e),
5373 ReturnExpr::ListLit(items) => items.iter().any(contains_rand_call),
5374 ReturnExpr::Index(base, index) => contains_rand_call(base) || contains_rand_call(index),
5375 ReturnExpr::PropOf(base, _) => contains_rand_call(base),
5376 ReturnExpr::Slice(base, start, end) => {
5377 contains_rand_call(base)
5378 || start.as_deref().is_some_and(contains_rand_call)
5379 || end.as_deref().is_some_and(contains_rand_call)
5380 }
5381 ReturnExpr::ListComp {
5382 source, project, ..
5383 } => contains_rand_call(source) || project.as_deref().is_some_and(contains_rand_call),
5384 ReturnExpr::Quantifier { source, .. } => contains_rand_call(source),
5385 ReturnExpr::MapLit(entries) => entries.iter().any(|(_, v)| contains_rand_call(v)),
5386 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5387 contains_rand_call(l) || contains_rand_call(r)
5388 }
5389 ReturnExpr::Not(e) => contains_rand_call(e),
5390 ReturnExpr::Compare(l, _, r) => contains_rand_call(l) || contains_rand_call(r),
5391 ReturnExpr::IsNull(e) => contains_rand_call(e),
5392 ReturnExpr::In(needle, haystack) => {
5393 contains_rand_call(needle) || contains_rand_call(haystack)
5394 }
5395 ReturnExpr::CountStar
5396 | ReturnExpr::Var(_)
5397 | ReturnExpr::Prop(_)
5398 | ReturnExpr::Lit(_)
5399 | ReturnExpr::HasLabel(..)
5400 | ReturnExpr::PatternPredicate(..)
5401 | ReturnExpr::PatternComprehension { .. }
5405 | ReturnExpr::ExistsPattern { .. }
5406 | ReturnExpr::ExistsSubquery(_) => false,
5407 }
5408}
5409
5410pub(crate) fn return_star_items(
5426 names: impl Iterator<Item = String>,
5427) -> Result<Vec<ReturnItem>, QueryError> {
5428 let names: Vec<String> = names.collect();
5429 if names.is_empty() {
5430 return Err(QueryError::Semantic(
5431 "RETURN * needs at least one variable in scope".into(),
5432 ));
5433 }
5434 Ok(star_items(names))
5435}
5436
5437pub(crate) fn with_star_items(names: impl Iterator<Item = String>) -> Vec<ReturnItem> {
5441 star_items(names.collect())
5442}
5443
5444fn star_items(mut names: Vec<String>) -> Vec<ReturnItem> {
5445 names.sort();
5446 names
5447 .into_iter()
5448 .map(|name| ReturnItem {
5449 expr: ReturnExpr::Var(name),
5450 alias: None,
5451 })
5452 .collect()
5453}
5454
5455pub(crate) fn validate_return_items(items: &[ReturnItem]) -> Result<(), QueryError> {
5476 for item in items {
5477 if contains_aggregate(&item.expr) {
5478 validate_composed_expr(&item.expr, items)?;
5479 }
5480 }
5481 Ok(())
5482}
5483
5484pub(crate) fn item_matches_leaf(expr: &ReturnExpr, index: usize, item: &ReturnItem) -> bool {
5495 item.expr == *expr
5496 || matches!(expr, ReturnExpr::Var(name) if *name == with_item_output_name((index, item)))
5497}
5498
5499pub(crate) fn validate_composed_expr(
5500 expr: &ReturnExpr,
5501 items: &[ReturnItem],
5502) -> Result<(), QueryError> {
5503 if matches!(expr, ReturnExpr::CountStar) {
5504 return Ok(());
5505 }
5506 if let ReturnExpr::Call { name, args, .. } = expr {
5507 if is_aggregate_name(name) {
5508 let expected_args = if is_percentile_name(name) { 2 } else { 1 };
5512 if args.len() != expected_args {
5513 return Err(QueryError::Semantic(if expected_args == 2 {
5514 format!("{name}() takes exactly two arguments (the value, then the percentile)")
5515 } else {
5516 format!(
5517 "{name}() takes exactly one argument (use count(*) for a row count with no argument)"
5518 )
5519 }));
5520 }
5521 for arg in args {
5522 if contains_aggregate(arg) {
5523 return Err(QueryError::Semantic(format!(
5524 "aggregate function '{name}' can't take another aggregate as an argument"
5525 )));
5526 }
5527 if contains_rand_call(arg) {
5535 return Err(QueryError::Semantic(format!(
5536 "aggregate function '{name}' can't take a non-deterministic expression \
5537 (e.g. rand()) as an argument"
5538 )));
5539 }
5540 }
5541 return Ok(());
5542 }
5543 }
5544 if matches!(expr, ReturnExpr::Var(_) | ReturnExpr::Prop(_)) {
5545 let is_grouping_key = items
5546 .iter()
5547 .enumerate()
5548 .any(|(i, it)| item_matches_leaf(expr, i, it) && !contains_aggregate(&it.expr));
5549 return if is_grouping_key {
5550 Ok(())
5551 } else {
5552 Err(QueryError::Semantic(format!(
5553 "{expr:?} is used alongside an aggregate function but isn't itself one of this \
5554 RETURN/WITH's own items -- once any item aggregates, every other value used \
5555 with it must be listed as its own explicit grouping key"
5556 )))
5557 };
5558 }
5559 match expr {
5566 ReturnExpr::Case { test, whens, else_ } => {
5567 if let Some(t) = test.as_deref() {
5568 validate_composed_expr(t, items)?;
5569 }
5570 for (w, t) in whens {
5571 validate_composed_expr(w, items)?;
5572 validate_composed_expr(t, items)?;
5573 }
5574 if let Some(e) = else_.as_deref() {
5575 validate_composed_expr(e, items)?;
5576 }
5577 }
5578 ReturnExpr::Call { args, .. } => {
5579 for arg in args {
5580 validate_composed_expr(arg, items)?;
5581 }
5582 }
5583 ReturnExpr::Arith(l, _, r) => {
5584 validate_composed_expr(l, items)?;
5585 validate_composed_expr(r, items)?;
5586 }
5587 ReturnExpr::Neg(e) => validate_composed_expr(e, items)?,
5588 ReturnExpr::ListLit(list_items) => {
5589 for item in list_items {
5590 validate_composed_expr(item, items)?;
5591 }
5592 }
5593 ReturnExpr::Index(base, index) => {
5594 validate_composed_expr(base, items)?;
5595 validate_composed_expr(index, items)?;
5596 }
5597 ReturnExpr::PropOf(base, _) => validate_composed_expr(base, items)?,
5598 ReturnExpr::Slice(base, start, end) => {
5599 validate_composed_expr(base, items)?;
5600 if let Some(s) = start.as_deref() {
5601 validate_composed_expr(s, items)?;
5602 }
5603 if let Some(e) = end.as_deref() {
5604 validate_composed_expr(e, items)?;
5605 }
5606 }
5607 ReturnExpr::ListComp {
5623 source,
5624 project,
5625 where_clause,
5626 ..
5627 } => {
5628 if project.as_deref().is_some_and(contains_aggregate) {
5629 return Err(QueryError::Semantic(
5630 "an aggregate function can't be used inside a list comprehension's projection"
5631 .into(),
5632 ));
5633 }
5634 validate_composed_expr(source, items)?;
5635 let _ = where_clause;
5638 }
5639 ReturnExpr::Quantifier { source, .. } => validate_composed_expr(source, items)?,
5640 ReturnExpr::MapLit(entries) => {
5641 for (_, v) in entries {
5642 validate_composed_expr(v, items)?;
5643 }
5644 }
5645 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5646 validate_composed_expr(l, items)?;
5647 validate_composed_expr(r, items)?;
5648 }
5649 ReturnExpr::Not(e) => validate_composed_expr(e, items)?,
5650 ReturnExpr::Compare(l, _, r) => {
5651 validate_composed_expr(l, items)?;
5652 validate_composed_expr(r, items)?;
5653 }
5654 ReturnExpr::IsNull(e) => validate_composed_expr(e, items)?,
5655 ReturnExpr::In(needle, haystack) => {
5656 validate_composed_expr(needle, items)?;
5657 validate_composed_expr(haystack, items)?;
5658 }
5659 ReturnExpr::CountStar
5660 | ReturnExpr::Var(_)
5661 | ReturnExpr::Prop(_)
5662 | ReturnExpr::Lit(_)
5663 | ReturnExpr::HasLabel(..)
5664 | ReturnExpr::PatternPredicate(..)
5665 | ReturnExpr::PatternComprehension { .. }
5666 | ReturnExpr::ExistsPattern { .. }
5667 | ReturnExpr::ExistsSubquery(_) => {}
5668 }
5669 Ok(())
5670}
5671
5672pub(crate) fn validate_order_by_composed_expr(
5685 expr: &ReturnExpr,
5686 items: &[ReturnItem],
5687) -> Result<(), QueryError> {
5688 validate_composed_expr(expr, items)?;
5689 let mut agg_nodes = Vec::new();
5690 collect_agg_nodes(expr, &mut agg_nodes);
5691 for node in agg_nodes {
5692 let matches_item = items
5693 .iter()
5694 .enumerate()
5695 .any(|(i, it)| item_matches_leaf(node, i, it));
5696 if !matches_item {
5697 return Err(QueryError::Semantic(
5698 "ORDER BY aggregate does not match any RETURN/WITH item".into(),
5699 ));
5700 }
5701 }
5702 Ok(())
5703}
5704
5705fn binding_hash_key(b: &Binding) -> Result<HashKey, QueryError> {
5712 Ok(match b {
5713 Binding::Node(id) => HashKey::Node(*id),
5714 Binding::Edge(id) => HashKey::Edge(*id),
5715 Binding::Value(pv) => property_value_hash_key(pv),
5716 Binding::List(items) => HashKey::List(
5717 items
5718 .iter()
5719 .map(value_hash_key)
5720 .collect::<Result<Vec<_>, _>>()?,
5721 ),
5722 Binding::Path(elems) => HashKey::List(
5730 elems
5731 .iter()
5732 .map(|e| match e {
5733 PathBinding::Node(id) => HashKey::Node(*id),
5734 PathBinding::Edge(id) => HashKey::Edge(*id),
5735 })
5736 .collect(),
5737 ),
5738 Binding::Map(m) => HashKey::List(
5742 m.iter()
5743 .map(|(k, v)| -> Result<HashKey, QueryError> {
5744 Ok(HashKey::List(vec![
5745 HashKey::Str(k.clone()),
5746 value_hash_key(v)?,
5747 ]))
5748 })
5749 .collect::<Result<Vec<_>, _>>()?,
5750 ),
5751 })
5752}
5753
5754fn project_call_row(
5764 sig: &ProcedureSignature,
5765 proc_row: &[Value],
5766 yield_items: &CallYield,
5767) -> Result<Vec<Value>, QueryError> {
5768 match yield_items {
5769 CallYield::Star => Ok(proc_row.to_vec()),
5770 CallYield::Items(items, _) => items
5771 .iter()
5772 .map(|(name, _)| {
5773 let idx = sig.outputs.iter().position(|o| o == name).ok_or_else(|| {
5774 QueryError::Semantic(format!(
5775 "'{name}' isn't a declared output of this procedure"
5776 ))
5777 })?;
5778 Ok(proc_row[idx].clone())
5779 })
5780 .collect(),
5781 }
5782}
5783
5784fn value_matches_declared_type(value: &Value, declared: &str) -> bool {
5796 if matches!(value, Value::Null) {
5797 return true;
5798 }
5799 let is_int = matches!(
5800 value,
5801 Value::Literal(Literal::Int(_)) | Value::Property(PropertyValue::Int(_))
5802 );
5803 let is_float = matches!(
5804 value,
5805 Value::Literal(Literal::Float(_)) | Value::Property(PropertyValue::Float(_))
5806 );
5807 match declared.trim_end_matches('?').to_ascii_uppercase().as_str() {
5808 "INTEGER" => is_int,
5809 "FLOAT" | "NUMBER" => is_int || is_float,
5810 "STRING" => matches!(
5811 value,
5812 Value::Literal(Literal::String(_)) | Value::Property(PropertyValue::String(_))
5813 ),
5814 "BOOLEAN" => matches!(
5815 value,
5816 Value::Literal(Literal::Bool(_)) | Value::Property(PropertyValue::Bool(_))
5817 ),
5818 _ => true,
5819 }
5820}
5821
5822fn value_to_binding(v: Value) -> Binding {
5832 match v {
5833 Value::List(items) => Binding::List(items),
5834 Value::Map(m) => Binding::Map(m),
5835 other => Binding::Value(value_to_property_value(&other)),
5836 }
5837}
5838
5839fn value_to_binding_restore(v: &Value) -> Binding {
5847 match v {
5848 Value::Node(n) => Binding::Node(n.id),
5849 Value::Edge(e) => Binding::Edge(e.id),
5850 Value::Property(pv) => Binding::Value(pv.clone()),
5851 Value::Literal(lit) => Binding::Value(literal_to_value(lit)),
5852 Value::List(items) => Binding::List(items.clone()),
5853 Value::Map(m) => Binding::Map(m.clone()),
5854 Value::Path(elems) => Binding::Path(elems.iter().map(path_elem_to_binding).collect()),
5855 Value::Null => Binding::Value(PropertyValue::Null),
5856 }
5857}
5858
5859fn path_elem_to_binding(elem: &PathElem) -> PathBinding {
5860 match elem {
5861 PathElem::Node(n) => PathBinding::Node(n.id),
5862 PathElem::Edge(e) => PathBinding::Edge(e.id),
5863 }
5864}
5865
5866fn name_pattern_for_path(pattern: &Pattern) -> (Pattern, HashSet<String>) {
5880 fn fresh(counter: &mut usize, synthesized: &mut HashSet<String>) -> String {
5881 *counter += 1;
5882 let name = format!("__path_elem{counter}");
5883 synthesized.insert(name.clone());
5884 name
5885 }
5886 let mut counter = 0usize;
5887 let mut synthesized = HashSet::new();
5888 let mut start = pattern.start.clone();
5889 if start.var.is_none() {
5890 start.var = Some(fresh(&mut counter, &mut synthesized));
5891 }
5892 let hops = pattern
5893 .hops
5894 .iter()
5895 .map(|(rel, node)| {
5896 let mut rel = rel.clone();
5897 if rel.hop_range.is_some() {
5898 rel.rel_list_var = rel.var.take();
5911 rel.var = Some(fresh(&mut counter, &mut synthesized));
5912 rel.capture_path_segment = true;
5913 } else if rel.var.is_none() {
5914 rel.var = Some(fresh(&mut counter, &mut synthesized));
5915 }
5916 let mut node = node.clone();
5917 if node.var.is_none() {
5918 node.var = Some(fresh(&mut counter, &mut synthesized));
5919 }
5920 (rel, node)
5921 })
5922 .collect();
5923 (Pattern { start, hops }, synthesized)
5924}
5925
5926fn assemble_path(pattern: &Pattern, row: &BindingRow) -> Binding {
5936 let Some(start_id) = path_node_id(pattern.start.var.as_deref(), row) else {
5937 return Binding::Value(PropertyValue::Null);
5938 };
5939 let mut elems = vec![PathBinding::Node(start_id)];
5940 for (rel, node) in &pattern.hops {
5941 if rel.capture_path_segment {
5942 let Some(Binding::Path(segment)) = rel.var.as_deref().and_then(|v| row.get(v)) else {
5949 return Binding::Value(PropertyValue::Null);
5950 };
5951 elems.extend(segment.iter().cloned());
5952 continue;
5953 }
5954 let Some(edge_id) = path_edge_id(rel.var.as_deref(), row) else {
5955 return Binding::Value(PropertyValue::Null);
5956 };
5957 let Some(node_id) = path_node_id(node.var.as_deref(), row) else {
5958 return Binding::Value(PropertyValue::Null);
5959 };
5960 elems.push(PathBinding::Edge(edge_id));
5961 elems.push(PathBinding::Node(node_id));
5962 }
5963 Binding::Path(elems)
5964}
5965
5966fn segment_edges_to_list(txn: Txn, segment: &[PathBinding]) -> Result<Binding, QueryError> {
5972 let edges = segment
5973 .iter()
5974 .filter_map(|elem| match elem {
5975 PathBinding::Edge(id) => Some(*id),
5976 PathBinding::Node(_) => None,
5977 })
5978 .map(|id| {
5979 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(txn, id)?)?;
5980 Ok(Value::Edge(edge))
5981 })
5982 .collect::<Result<Vec<_>, QueryError>>()?;
5983 Ok(Binding::List(edges))
5984}
5985
5986fn path_node_id(var: Option<&str>, row: &BindingRow) -> Option<NodeId> {
5987 match var.and_then(|v| row.get(v)) {
5988 Some(Binding::Node(id)) => Some(*id),
5989 _ => None,
5990 }
5991}
5992
5993fn path_edge_id(var: Option<&str>, row: &BindingRow) -> Option<EdgeId> {
5994 match var.and_then(|v| row.get(v)) {
5995 Some(Binding::Edge(id)) => Some(*id),
5996 _ => None,
5997 }
5998}
5999
6000fn require_bound_node(row: &BindingRow, var: &str) -> Result<NodeId, QueryError> {
6001 match row.get(var) {
6002 Some(Binding::Node(id)) => Ok(*id),
6003 _ => Err(QueryError::UnboundVariable(format!(
6004 "'{var}' must already be bound to a node before shortestPath() — match it in a preceding MATCH"
6005 ))),
6006 }
6007}
6008
6009fn reconstruct_path(
6015 parent: &HashMap<NodeId, (NodeId, EdgeId)>,
6016 start: NodeId,
6017 end: NodeId,
6018) -> Vec<PathBinding> {
6019 let mut hops = Vec::new();
6020 let mut current = end;
6021 while current != start {
6022 let (prev, edge_id) = parent[¤t];
6023 hops.push((edge_id, current));
6024 current = prev;
6025 }
6026 hops.reverse();
6027 let mut elems = vec![PathBinding::Node(start)];
6028 for (edge_id, node) in hops {
6029 elems.push(PathBinding::Edge(edge_id));
6030 elems.push(PathBinding::Node(node));
6031 }
6032 elems
6033}
6034
6035fn value_to_property_value(v: &Value) -> PropertyValue {
6048 match v {
6049 Value::Null => PropertyValue::Null,
6050 Value::Property(pv) => pv.clone(),
6051 Value::Literal(lit) => literal_to_value(lit),
6052 Value::List(items) => {
6053 PropertyValue::List(items.iter().map(value_to_property_value).collect())
6054 }
6055 Value::Node(_) | Value::Edge(_) | Value::Map(_) | Value::Path(_) => PropertyValue::Null,
6056 }
6057}
6058
6059fn value_to_storable_property(v: &Value) -> Option<PropertyValue> {
6074 match v {
6075 Value::Null => Some(PropertyValue::Null),
6076 Value::Property(pv) => Some(pv.clone()),
6077 Value::Literal(lit) => Some(literal_to_value(lit)),
6078 Value::List(items) => Some(PropertyValue::List(
6079 items
6080 .iter()
6081 .map(value_to_storable_property)
6082 .collect::<Option<Vec<_>>>()?,
6083 )),
6084 Value::Node(_) | Value::Edge(_) | Value::Map(_) | Value::Path(_) => None,
6085 }
6086}
6087
6088fn property_value_to_value(pv: PropertyValue) -> Value {
6101 match pv {
6102 PropertyValue::Null => Value::Null,
6103 PropertyValue::List(items) => {
6104 Value::List(items.into_iter().map(property_value_to_value).collect())
6105 }
6106 other => Value::Property(other),
6107 }
6108}
6109
6110fn deleted_entity_access<T>(record: Option<T>) -> Result<T, QueryError> {
6121 record.ok_or_else(|| {
6122 QueryError::UnboundVariable(
6123 "refers to a node/relationship that no longer exists — it was deleted earlier in this statement".into(),
6124 )
6125 })
6126}
6127
6128pub(crate) fn literal_to_value(lit: &Literal) -> PropertyValue {
6129 match lit {
6130 Literal::Int(i) => PropertyValue::Int(*i),
6131 Literal::Float(f) => PropertyValue::Float(*f),
6132 Literal::String(s) => PropertyValue::String(s.clone()),
6133 Literal::Bool(b) => PropertyValue::Bool(*b),
6134 Literal::Null => PropertyValue::Null,
6135 Literal::Param(name) => {
6136 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
6137 }
6138 }
6139}
6140
6141fn tag_merge_created(mut row: BindingRow, created: bool) -> BindingRow {
6142 row.insert(
6143 MERGE_CREATED_KEY.to_string(),
6144 Binding::Value(PropertyValue::Bool(created)),
6145 );
6146 row
6147}
6148
6149fn neighbors_for_direction(
6162 txn: Txn,
6163 node: NodeId,
6164 direction: ExpandDirection,
6165 rel_labels: &[String],
6166) -> Result<Vec<AdjEntry>, QueryError> {
6167 let dirs: &[Direction] = match direction {
6168 ExpandDirection::Out => &[Direction::Out],
6169 ExpandDirection::In => &[Direction::In],
6170 ExpandDirection::Either => &[Direction::Out, Direction::In],
6171 };
6172 let mut out = Vec::new();
6173 let mut seen: HashSet<EdgeId> = HashSet::new();
6174 let label_filters: Vec<Option<&str>> = if rel_labels.is_empty() {
6175 vec![None]
6176 } else {
6177 rel_labels.iter().map(|l| Some(l.as_str())).collect()
6178 };
6179 for label in label_filters {
6180 for &dir in dirs {
6181 for entry in GraphStore::neighbors_in_txn(txn, node, dir, label)? {
6182 if seen.insert(entry.edge_id) {
6183 out.push(entry);
6184 }
6185 }
6186 }
6187 }
6188 Ok(out)
6189}
6190
6191fn compare(prop: &Option<PropertyValue>, op: CompareOp, lit: &Literal) -> Option<bool> {
6200 let Some(prop) = prop else { return None };
6201 if matches!(prop, PropertyValue::Null) || matches!(lit, Literal::Null) {
6202 return None;
6203 }
6204 compare_property_pair(prop, op, &literal_to_value(lit))
6205}
6206
6207fn compare_property_pair_opt(
6211 a: &Option<PropertyValue>,
6212 op: CompareOp,
6213 b: &Option<PropertyValue>,
6214) -> Option<bool> {
6215 let (Some(a), Some(b)) = (a, b) else {
6216 return None;
6217 };
6218 if matches!(a, PropertyValue::Null) || matches!(b, PropertyValue::Null) {
6219 return None;
6220 }
6221 compare_property_pair(a, op, b)
6222}
6223
6224fn compare_property_pair(a: &PropertyValue, op: CompareOp, b: &PropertyValue) -> Option<bool> {
6238 match (a, b) {
6239 (PropertyValue::Int(a), PropertyValue::Int(b)) => Some(cmp_ord(op, *a, *b)),
6240 (PropertyValue::Int(a), PropertyValue::Float(b)) => Some(cmp_f64(op, *a as f64, *b)),
6241 (PropertyValue::Float(a), PropertyValue::Float(b)) => Some(cmp_f64(op, *a, *b)),
6242 (PropertyValue::Float(a), PropertyValue::Int(b)) => Some(cmp_f64(op, *a, *b as f64)),
6243 (PropertyValue::String(a), PropertyValue::String(b)) => Some(match op {
6244 CompareOp::StartsWith => a.starts_with(b.as_str()),
6245 CompareOp::EndsWith => a.ends_with(b.as_str()),
6246 CompareOp::Contains => a.contains(b.as_str()),
6247 _ => cmp_ord(op, a.as_str(), b.as_str()),
6248 }),
6249 (PropertyValue::Bool(a), PropertyValue::Bool(b)) => Some(cmp_ord(op, *a, *b)),
6254 (PropertyValue::Date(a), PropertyValue::Date(b)) => Some(cmp_ord(op, *a, *b)),
6261 (PropertyValue::LocalTime(a), PropertyValue::LocalTime(b)) => Some(cmp_ord(op, *a, *b)),
6262 (
6265 PropertyValue::Time {
6266 nanos_of_day: na,
6267 offset_seconds: oa,
6268 },
6269 PropertyValue::Time {
6270 nanos_of_day: nb,
6271 offset_seconds: ob,
6272 },
6273 ) => Some(cmp_ord(
6274 op,
6275 na - *oa as i64 * 1_000_000_000,
6276 nb - *ob as i64 * 1_000_000_000,
6277 )),
6278 (
6279 PropertyValue::LocalDateTime {
6280 epoch_seconds: sa,
6281 nanos: na,
6282 },
6283 PropertyValue::LocalDateTime {
6284 epoch_seconds: sb,
6285 nanos: nb,
6286 },
6287 ) => Some(cmp_ord(op, (*sa, *na), (*sb, *nb))),
6288 (
6291 PropertyValue::DateTime {
6292 epoch_seconds: sa,
6293 nanos: na,
6294 ..
6295 },
6296 PropertyValue::DateTime {
6297 epoch_seconds: sb,
6298 nanos: nb,
6299 ..
6300 },
6301 ) => Some(cmp_ord(op, (*sa, *na), (*sb, *nb))),
6302 (PropertyValue::Duration { .. }, PropertyValue::Duration { .. }) => match op {
6308 CompareOp::Eq => Some(a == b),
6309 CompareOp::Ne => Some(a != b),
6310 _ => None,
6311 },
6312 _ => match op {
6313 CompareOp::Eq => Some(false),
6314 CompareOp::Ne => Some(true),
6315 CompareOp::StartsWith
6323 | CompareOp::EndsWith
6324 | CompareOp::Contains
6325 | CompareOp::Lt
6326 | CompareOp::Le
6327 | CompareOp::Gt
6328 | CompareOp::Ge => None,
6329 },
6330 }
6331}
6332
6333fn value_to_bool3(v: &Value) -> Result<Option<bool>, QueryError> {
6337 match v {
6338 Value::Null => Ok(None),
6339 Value::Literal(Literal::Bool(b)) | Value::Property(PropertyValue::Bool(b)) => Ok(Some(*b)),
6340 other => Err(QueryError::Type(format!(
6341 "expected a boolean, got {other:?}"
6342 ))),
6343 }
6344}
6345
6346fn bool3_to_value(b: Option<bool>) -> Value {
6347 match b {
6348 Some(b) => Value::Literal(Literal::Bool(b)),
6349 None => Value::Null,
6350 }
6351}
6352
6353fn and3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
6357 match (a, b) {
6358 (Some(false), _) | (_, Some(false)) => Some(false),
6359 (Some(true), Some(true)) => Some(true),
6360 _ => None,
6361 }
6362}
6363
6364fn or3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
6366 match (a, b) {
6367 (Some(true), _) | (_, Some(true)) => Some(true),
6368 (Some(false), Some(false)) => Some(false),
6369 _ => None,
6370 }
6371}
6372
6373fn xor3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
6377 match (a, b) {
6378 (Some(a), Some(b)) => Some(a != b),
6379 _ => None,
6380 }
6381}
6382
6383fn cmp_f64(op: CompareOp, a: f64, b: f64) -> bool {
6384 match op {
6385 CompareOp::Eq => a == b,
6386 CompareOp::Ne => a != b,
6387 CompareOp::Lt => a < b,
6388 CompareOp::Le => a <= b,
6389 CompareOp::Gt => a > b,
6390 CompareOp::Ge => a >= b,
6391 CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => false,
6395 }
6396}
6397
6398fn cmp_ord<T: PartialOrd>(op: CompareOp, a: T, b: T) -> bool {
6399 match op {
6400 CompareOp::Eq => a == b,
6401 CompareOp::Ne => a != b,
6402 CompareOp::Lt => a < b,
6403 CompareOp::Le => a <= b,
6404 CompareOp::Gt => a > b,
6405 CompareOp::Ge => a >= b,
6406 CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => false,
6407 }
6408}
6409
6410pub(crate) fn value_eq(a: &Value, b: &Value) -> bool {
6421 match (a, b) {
6422 (Value::Null, Value::Null) => true,
6423 (Value::Null, _) | (_, Value::Null) => false,
6424 (Value::Property(pa), Value::Property(pb)) => property_value_eq(pa, pb),
6425 (Value::Literal(la), Value::Literal(lb)) => la == lb,
6426 (Value::Property(pa), Value::Literal(lb)) => *pa == literal_to_value(lb),
6427 (Value::Literal(la), Value::Property(pb)) => literal_to_value(la) == *pb,
6428 (Value::Node(na), Value::Node(nb)) => na.id == nb.id,
6429 (Value::Edge(ea), Value::Edge(eb)) => ea.id == eb.id,
6430 (Value::List(la), Value::List(lb)) => {
6431 la.len() == lb.len() && la.iter().zip(lb).all(|(x, y)| value_eq(x, y))
6432 }
6433 (Value::Path(pa), Value::Path(pb)) => {
6442 pa.len() == pb.len()
6443 && pa.iter().zip(pb).all(|(x, y)| match (x, y) {
6444 (PathElem::Node(na), PathElem::Node(nb)) => na.id == nb.id,
6445 (PathElem::Edge(ea), PathElem::Edge(eb)) => ea.id == eb.id,
6446 _ => false,
6447 })
6448 }
6449 _ => false,
6450 }
6451}
6452
6453fn property_value_eq(a: &PropertyValue, b: &PropertyValue) -> bool {
6460 match (a, b) {
6461 (
6462 PropertyValue::Time {
6463 nanos_of_day: na,
6464 offset_seconds: oa,
6465 },
6466 PropertyValue::Time {
6467 nanos_of_day: nb,
6468 offset_seconds: ob,
6469 },
6470 ) => na - *oa as i64 * 1_000_000_000 == nb - *ob as i64 * 1_000_000_000,
6471 (
6472 PropertyValue::DateTime {
6473 epoch_seconds: sa,
6474 nanos: na,
6475 ..
6476 },
6477 PropertyValue::DateTime {
6478 epoch_seconds: sb,
6479 nanos: nb,
6480 ..
6481 },
6482 ) => sa == sb && na == nb,
6483 _ => a == b,
6484 }
6485}
6486
6487enum ArithNum {
6491 Int(i64),
6492 Float(f64),
6493}
6494
6495fn as_arith_num(v: &Value) -> Option<ArithNum> {
6496 match v {
6497 Value::Property(PropertyValue::Int(i)) | Value::Literal(Literal::Int(i)) => {
6498 Some(ArithNum::Int(*i))
6499 }
6500 Value::Property(PropertyValue::Float(f)) | Value::Literal(Literal::Float(f)) => {
6501 Some(ArithNum::Float(*f))
6502 }
6503 _ => None,
6504 }
6505}
6506
6507fn require_int_arg(v: Option<&Value>, fn_name: &str) -> Result<i64, QueryError> {
6513 match v {
6514 Some(Value::Property(PropertyValue::Int(i))) | Some(Value::Literal(Literal::Int(i))) => {
6515 Ok(*i)
6516 }
6517 other => Err(QueryError::Type(format!(
6518 "{fn_name}() expects an integer argument, got {other:?}"
6519 ))),
6520 }
6521}
6522
6523fn as_arith_str(v: &Value) -> Option<&str> {
6524 match v {
6525 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => {
6526 Some(s.as_str())
6527 }
6528 _ => None,
6529 }
6530}
6531
6532fn apply_neg(v: &Value) -> Result<Value, QueryError> {
6538 if matches!(v, Value::Null) {
6539 return Ok(Value::Null);
6540 }
6541 Ok(match as_arith_num(v) {
6542 Some(ArithNum::Int(i)) => {
6543 Value::Property(PropertyValue::Int(i.checked_neg().ok_or_else(|| {
6544 QueryError::Type("integer arithmetic overflow".into())
6545 })?))
6546 }
6547 Some(ArithNum::Float(f)) => Value::Property(PropertyValue::Float(-f)),
6548 None => {
6549 return Err(QueryError::Type(format!(
6550 "unary minus needs a number -- got {v:?}"
6551 )))
6552 }
6553 })
6554}
6555
6556fn apply_arith(op: ArithOp, a: &Value, b: &Value) -> Result<Value, QueryError> {
6564 if matches!(a, Value::Null) || matches!(b, Value::Null) {
6565 return Ok(Value::Null);
6566 }
6567 if op == ArithOp::Add {
6568 match (a, b) {
6575 (Value::List(xs), Value::List(ys)) => {
6576 let mut combined = xs.clone();
6577 combined.extend(ys.iter().cloned());
6578 return Ok(Value::List(combined));
6579 }
6580 (Value::List(xs), scalar) => {
6581 let mut combined = xs.clone();
6582 combined.push(scalar.clone());
6583 return Ok(Value::List(combined));
6584 }
6585 (scalar, Value::List(ys)) => {
6586 let mut combined = vec![scalar.clone()];
6587 combined.extend(ys.iter().cloned());
6588 return Ok(Value::List(combined));
6589 }
6590 _ => {}
6591 }
6592 if let (Some(sa), Some(sb)) = (as_arith_str(a), as_arith_str(b)) {
6593 return Ok(Value::Property(PropertyValue::String(format!("{sa}{sb}"))));
6594 }
6595 }
6596 if let Some(result) = apply_temporal_arith(op, a, b)? {
6597 return Ok(result);
6598 }
6599 let (Some(na), Some(nb)) = (as_arith_num(a), as_arith_num(b)) else {
6600 return Err(QueryError::Type(format!(
6601 "arithmetic needs two numbers (or, for +, two strings) -- got {a:?} and {b:?}"
6602 )));
6603 };
6604 if op == ArithOp::Pow {
6609 let to_f64 = |n: ArithNum| match n {
6610 ArithNum::Int(i) => i as f64,
6611 ArithNum::Float(f) => f,
6612 };
6613 return Ok(Value::Property(PropertyValue::Float(
6614 to_f64(na).powf(to_f64(nb)),
6615 )));
6616 }
6617 Ok(match (na, nb) {
6621 (ArithNum::Int(x), ArithNum::Int(y)) => {
6622 if matches!(op, ArithOp::Div | ArithOp::Mod) && y == 0 {
6623 return Err(QueryError::Type("division by zero".into()));
6624 }
6625 let value = match op {
6626 ArithOp::Add => x.checked_add(y),
6627 ArithOp::Sub => x.checked_sub(y),
6628 ArithOp::Mul => x.checked_mul(y),
6629 ArithOp::Div => x.checked_div(y),
6630 ArithOp::Mod => x.checked_rem(y),
6631 ArithOp::Pow => unreachable!("handled above"),
6632 }
6633 .ok_or_else(|| QueryError::Type("integer arithmetic overflow".into()))?;
6634 Value::Property(PropertyValue::Int(value))
6635 }
6636 (x, y) => {
6637 let x = match x {
6638 ArithNum::Int(i) => i as f64,
6639 ArithNum::Float(f) => f,
6640 };
6641 let y = match y {
6642 ArithNum::Int(i) => i as f64,
6643 ArithNum::Float(f) => f,
6644 };
6645 Value::Property(PropertyValue::Float(match op {
6646 ArithOp::Add => x + y,
6647 ArithOp::Sub => x - y,
6648 ArithOp::Mul => x * y,
6649 ArithOp::Div => x / y,
6650 ArithOp::Mod => x % y,
6651 ArithOp::Pow => unreachable!("handled above"),
6652 }))
6653 }
6654 })
6655}
6656
6657fn as_date(v: &Value) -> Option<i32> {
6658 match v {
6659 Value::Property(PropertyValue::Date(d)) => Some(*d),
6660 _ => None,
6661 }
6662}
6663
6664fn as_duration(v: &Value) -> Option<temporal::DurationParts> {
6665 match v {
6666 Value::Property(PropertyValue::Duration {
6667 months,
6668 days,
6669 seconds,
6670 nanos,
6671 }) => Some((*months, *days, *seconds, *nanos)),
6672 _ => None,
6673 }
6674}
6675
6676fn duration_value((months, days, seconds, nanos): temporal::DurationParts) -> Value {
6677 Value::Property(PropertyValue::Duration {
6678 months,
6679 days,
6680 seconds,
6681 nanos,
6682 })
6683}
6684
6685fn as_local_time(v: &Value) -> Option<i64> {
6686 match v {
6687 Value::Property(PropertyValue::LocalTime(n)) => Some(*n),
6688 _ => None,
6689 }
6690}
6691
6692fn as_time(v: &Value) -> Option<(i64, i32)> {
6693 match v {
6694 Value::Property(PropertyValue::Time {
6695 nanos_of_day,
6696 offset_seconds,
6697 }) => Some((*nanos_of_day, *offset_seconds)),
6698 _ => None,
6699 }
6700}
6701
6702fn as_local_date_time(v: &Value) -> Option<(i64, i32)> {
6703 match v {
6704 Value::Property(PropertyValue::LocalDateTime {
6705 epoch_seconds,
6706 nanos,
6707 }) => Some((*epoch_seconds, *nanos)),
6708 _ => None,
6709 }
6710}
6711
6712fn as_date_time(v: &Value) -> Option<(i64, i32, temporal::TzId)> {
6713 match v {
6714 Value::Property(PropertyValue::DateTime {
6715 epoch_seconds,
6716 nanos,
6717 zone,
6718 }) => Some((*epoch_seconds, *nanos, tz_from_graph(zone))),
6719 _ => None,
6720 }
6721}
6722
6723fn tz_from_graph(zone: &GraphTzId) -> temporal::TzId {
6728 match zone {
6729 GraphTzId::Offset(o) => temporal::TzId::Offset(*o),
6730 GraphTzId::Named(name) => temporal::TzId::Named(name.clone()),
6731 }
6732}
6733
6734fn tz_to_graph(zone: temporal::TzId) -> GraphTzId {
6735 match zone {
6736 temporal::TzId::Offset(o) => GraphTzId::Offset(o),
6737 temporal::TzId::Named(name) => GraphTzId::Named(name),
6738 }
6739}
6740
6741fn apply_temporal_arith(op: ArithOp, a: &Value, b: &Value) -> Result<Option<Value>, QueryError> {
6755 let date_plus_duration =
6756 |d: i32, dur: temporal::DurationParts, negate: bool| -> Result<Value, QueryError> {
6757 let (months, days, seconds, nanos) = dur;
6758 temporal::add_duration_to_date(d, months, days, seconds, nanos, negate)
6759 .map(|d| Value::Property(PropertyValue::Date(d)))
6760 .ok_or_else(|| {
6761 QueryError::Type("date +/- duration produced an out-of-range date".into())
6762 })
6763 };
6764 let local_time_plus_duration = |t: i64, dur: temporal::DurationParts, negate: bool| -> Value {
6765 let (_, _, seconds, nanos) = dur;
6766 Value::Property(PropertyValue::LocalTime(temporal::add_duration_to_time(
6767 t, seconds, nanos, negate,
6768 )))
6769 };
6770 let time_plus_duration =
6771 |(t, offset): (i64, i32), dur: temporal::DurationParts, negate: bool| -> Value {
6772 let (_, _, seconds, nanos) = dur;
6773 Value::Property(PropertyValue::Time {
6774 nanos_of_day: temporal::add_duration_to_time(t, seconds, nanos, negate),
6775 offset_seconds: offset,
6776 })
6777 };
6778 let local_date_time_plus_duration = |(epoch_seconds, existing_nanos): (i64, i32),
6779 dur: temporal::DurationParts,
6780 negate: bool|
6781 -> Result<Value, QueryError> {
6782 let (months, days, seconds, nanos) = dur;
6783 temporal::add_duration_to_local_date_time(
6784 epoch_seconds,
6785 existing_nanos,
6786 months,
6787 days,
6788 seconds,
6789 nanos,
6790 negate,
6791 )
6792 .map(|(epoch_seconds, nanos)| {
6793 Value::Property(PropertyValue::LocalDateTime {
6794 epoch_seconds,
6795 nanos,
6796 })
6797 })
6798 .ok_or_else(|| {
6799 QueryError::Type("local date-time +/- duration produced an out-of-range value".into())
6800 })
6801 };
6802 let date_time_plus_duration =
6810 |(epoch_seconds, existing_nanos, zone): (i64, i32, temporal::TzId),
6811 dur: temporal::DurationParts,
6812 negate: bool|
6813 -> Result<Value, QueryError> {
6814 let (months, days, seconds, nanos) = dur;
6815 let offset_seconds = temporal::resolve_offset(&zone, epoch_seconds);
6816 temporal::add_duration_to_local_date_time(
6817 epoch_seconds + offset_seconds as i64,
6818 existing_nanos,
6819 months,
6820 days,
6821 seconds,
6822 nanos,
6823 negate,
6824 )
6825 .map(|(local_epoch_seconds, nanos)| {
6826 Value::Property(PropertyValue::DateTime {
6827 epoch_seconds: local_epoch_seconds - offset_seconds as i64,
6828 nanos,
6829 zone: tz_to_graph(zone),
6830 })
6831 })
6832 .ok_or_else(|| {
6833 QueryError::Type("date-time +/- duration produced an out-of-range value".into())
6834 })
6835 };
6836 Ok(match op {
6837 ArithOp::Add => {
6838 if let (Some(d), Some(dur)) = (as_date(a), as_duration(b)) {
6839 Some(date_plus_duration(d, dur, false)?)
6840 } else if let (Some(dur), Some(d)) = (as_duration(a), as_date(b)) {
6841 Some(date_plus_duration(d, dur, false)?)
6842 } else if let (Some(t), Some(dur)) = (as_local_time(a), as_duration(b)) {
6843 Some(local_time_plus_duration(t, dur, false))
6844 } else if let (Some(dur), Some(t)) = (as_duration(a), as_local_time(b)) {
6845 Some(local_time_plus_duration(t, dur, false))
6846 } else if let (Some(t), Some(dur)) = (as_time(a), as_duration(b)) {
6847 Some(time_plus_duration(t, dur, false))
6848 } else if let (Some(dur), Some(t)) = (as_duration(a), as_time(b)) {
6849 Some(time_plus_duration(t, dur, false))
6850 } else if let (Some(dt), Some(dur)) = (as_local_date_time(a), as_duration(b)) {
6851 Some(local_date_time_plus_duration(dt, dur, false)?)
6852 } else if let (Some(dur), Some(dt)) = (as_duration(a), as_local_date_time(b)) {
6853 Some(local_date_time_plus_duration(dt, dur, false)?)
6854 } else if let (Some(dt), Some(dur)) = (as_date_time(a), as_duration(b)) {
6855 Some(date_time_plus_duration(dt, dur, false)?)
6856 } else if let (Some(dur), Some(dt)) = (as_duration(a), as_date_time(b)) {
6857 Some(date_time_plus_duration(dt, dur, false)?)
6858 } else if let (Some(x), Some(y)) = (as_duration(a), as_duration(b)) {
6859 Some(duration_value(temporal::add_duration(x, y).ok_or_else(
6860 || QueryError::Type("duration addition overflow".into()),
6861 )?))
6862 } else {
6863 None
6864 }
6865 }
6866 ArithOp::Sub => {
6867 if let (Some(d), Some(dur)) = (as_date(a), as_duration(b)) {
6868 Some(date_plus_duration(d, dur, true)?)
6869 } else if let (Some(t), Some(dur)) = (as_local_time(a), as_duration(b)) {
6870 Some(local_time_plus_duration(t, dur, true))
6871 } else if let (Some(t), Some(dur)) = (as_time(a), as_duration(b)) {
6872 Some(time_plus_duration(t, dur, true))
6873 } else if let (Some(dt), Some(dur)) = (as_local_date_time(a), as_duration(b)) {
6874 Some(local_date_time_plus_duration(dt, dur, true)?)
6875 } else if let (Some(dt), Some(dur)) = (as_date_time(a), as_duration(b)) {
6876 Some(date_time_plus_duration(dt, dur, true)?)
6877 } else if let (Some(x), Some(y)) = (as_duration(a), as_duration(b)) {
6878 Some(duration_value(temporal::sub_duration(x, y).ok_or_else(
6879 || QueryError::Type("duration subtraction overflow".into()),
6880 )?))
6881 } else {
6882 None
6883 }
6884 }
6885 ArithOp::Mul => {
6886 if let (Some(dur), Some(f)) = (as_duration(a), value_as_f64(b)) {
6887 Some(duration_value(temporal::scale_duration(dur, f)))
6888 } else if let (Some(f), Some(dur)) = (value_as_f64(a), as_duration(b)) {
6889 Some(duration_value(temporal::scale_duration(dur, f)))
6890 } else {
6891 None
6892 }
6893 }
6894 ArithOp::Div => {
6895 if let (Some(dur), Some(f)) = (as_duration(a), value_as_f64(b)) {
6896 if f == 0.0 {
6897 return Err(QueryError::Type("division by zero".into()));
6898 }
6899 Some(duration_value(temporal::scale_duration(dur, 1.0 / f)))
6900 } else {
6901 None
6902 }
6903 }
6904 ArithOp::Mod => None,
6905 ArithOp::Pow => None,
6909 })
6910}
6911
6912fn apply_index(list: &Value, index: &Value) -> Result<Value, QueryError> {
6918 if matches!(list, Value::Null) || matches!(index, Value::Null) {
6919 return Ok(Value::Null);
6920 }
6921 if let Value::Map(entries) = list {
6928 let Some(key) = as_arith_str(index) else {
6929 return Err(QueryError::Type(format!(
6930 "a map index must be a string, got {index:?}"
6931 )));
6932 };
6933 return Ok(entries.get(key).cloned().unwrap_or(Value::Null));
6934 }
6935 if matches!(list, Value::Node(_) | Value::Edge(_) | Value::Property(_)) {
6941 let Some(key) = as_arith_str(index) else {
6942 return Err(QueryError::Type(format!(
6943 "a property index must be a string, got {index:?}"
6944 )));
6945 };
6946 return property_of_value(list, key);
6947 }
6948 let Value::List(items) = list else {
6949 return Err(QueryError::Type(format!(
6950 "[] indexing needs a list or map, got {list:?}"
6951 )));
6952 };
6953 let Some(ArithNum::Int(i)) = as_arith_num(index) else {
6954 return Err(QueryError::Type(format!(
6955 "a list index must be an integer, got {index:?}"
6956 )));
6957 };
6958 let len = items.len() as i64;
6959 let i = if i < 0 { i + len } else { i };
6960 if i < 0 || i >= len {
6961 return Ok(Value::Null);
6962 }
6963 Ok(items[i as usize].clone())
6964}
6965
6966fn apply_slice(
6973 list: &Value,
6974 start: Option<&Value>,
6975 end: Option<&Value>,
6976) -> Result<Value, QueryError> {
6977 if matches!(list, Value::Null) {
6978 return Ok(Value::Null);
6979 }
6980 let Value::List(items) = list else {
6981 return Err(QueryError::Type(format!(
6982 "[..] slicing needs a list, got {list:?}"
6983 )));
6984 };
6985 let len = items.len() as i64;
6986 let clamp = |i: i64| -> i64 {
6987 let i = if i < 0 { i + len } else { i };
6988 i.clamp(0, len)
6989 };
6990 let bound_index = |v: Option<&Value>, default: i64| -> Result<Option<i64>, QueryError> {
6991 match v {
6992 None => Ok(Some(default)),
6993 Some(Value::Null) => Ok(None),
6994 Some(other) => match as_arith_num(other) {
6995 Some(ArithNum::Int(i)) => Ok(Some(clamp(i))),
6996 _ => Err(QueryError::Type(format!(
6997 "a slice bound must be an integer, got {other:?}"
6998 ))),
6999 },
7000 }
7001 };
7002 let (Some(start_idx), Some(end_idx)) = (bound_index(start, 0)?, bound_index(end, len)?) else {
7006 return Ok(Value::Null);
7007 };
7008 if start_idx >= end_idx {
7009 return Ok(Value::List(Vec::new()));
7010 }
7011 Ok(Value::List(
7012 items[start_idx as usize..end_idx as usize].to_vec(),
7013 ))
7014}
7015
7016fn call_builtin(
7017 name: &str,
7018 args: &[Value],
7019 now: temporal::NowSnapshot,
7020) -> Result<Value, QueryError> {
7021 match name.to_ascii_lowercase().as_str() {
7022 "coalesce" => Ok(args
7023 .iter()
7024 .find(|v| !matches!(v, Value::Null))
7025 .cloned()
7026 .unwrap_or(Value::Null)),
7027 "tointeger" => match args.first() {
7028 Some(v) => to_integer(v),
7029 None => Ok(Value::Null),
7030 },
7031 "tostring" => match args.first() {
7032 Some(v) => to_string_value(v),
7033 None => Ok(Value::Null),
7034 },
7035 "date" => date_builtin(args, now),
7036 "date.transaction" | "date.statement" | "date.realtime" => Ok(now_or_null(args, || {
7037 Value::Property(PropertyValue::Date(now.epoch_day))
7038 })),
7039 "duration" => duration_builtin(args),
7040 "localtime" => local_time_builtin(args, now),
7041 "localtime.transaction" | "localtime.statement" | "localtime.realtime" => {
7042 Ok(now_or_null(args, || {
7043 Value::Property(PropertyValue::LocalTime(now.nanos_of_day))
7044 }))
7045 }
7046 "time" => time_builtin(args, now),
7047 "time.transaction" | "time.statement" | "time.realtime" => {
7048 Ok(now_or_null(args, || {
7050 Value::Property(PropertyValue::Time {
7051 nanos_of_day: now.nanos_of_day,
7052 offset_seconds: 0,
7053 })
7054 }))
7055 }
7056 "localdatetime" => local_date_time_builtin(args, now),
7057 "localdatetime.transaction" | "localdatetime.statement" | "localdatetime.realtime" => {
7058 Ok(now_or_null(args, || {
7059 Value::Property(PropertyValue::LocalDateTime {
7060 epoch_seconds: now.epoch_seconds,
7061 nanos: now.nanos,
7062 })
7063 }))
7064 }
7065 "datetime" => date_time_builtin(args, now),
7066 "datetime.transaction" | "datetime.statement" | "datetime.realtime" => {
7067 Ok(now_or_null(args, || {
7069 Value::Property(PropertyValue::DateTime {
7070 epoch_seconds: now.epoch_seconds,
7071 nanos: now.nanos,
7072 zone: GraphTzId::Offset(0),
7073 })
7074 }))
7075 }
7076 "datetime.fromepoch" => {
7077 let seconds = require_int_arg(args.first(), "datetime.fromepoch")?;
7078 let nanos = require_int_arg(args.get(1), "datetime.fromepoch")?;
7079 Ok(Value::Property(PropertyValue::DateTime {
7080 epoch_seconds: seconds,
7081 nanos: nanos as i32,
7082 zone: GraphTzId::Offset(0),
7083 }))
7084 }
7085 "datetime.fromepochmillis" => {
7086 let millis = require_int_arg(args.first(), "datetime.fromepochmillis")?;
7087 Ok(Value::Property(PropertyValue::DateTime {
7088 epoch_seconds: millis.div_euclid(1000),
7089 nanos: (millis.rem_euclid(1000) * 1_000_000) as i32,
7090 zone: GraphTzId::Offset(0),
7091 }))
7092 }
7093 "duration.between" => {
7094 duration_between_builtin("duration.between", args, temporal::duration_between)
7095 }
7096 "duration.inmonths" => {
7097 duration_between_builtin("duration.inMonths", args, temporal::duration_in_months)
7098 }
7099 "duration.indays" => {
7100 duration_between_builtin("duration.inDays", args, temporal::duration_in_days)
7101 }
7102 "duration.inseconds" => {
7103 duration_between_builtin("duration.inSeconds", args, temporal::duration_in_seconds)
7104 }
7105 "date.truncate" => date_truncate_builtin(args),
7106 "localtime.truncate" => local_time_truncate_builtin(args),
7107 "time.truncate" => time_truncate_builtin(args),
7108 "localdatetime.truncate" => local_date_time_truncate_builtin(args),
7109 "datetime.truncate" => date_time_truncate_builtin(args),
7110 "length" => Ok(match args.first() {
7115 Some(Value::Path(elems)) => {
7116 Value::Property(PropertyValue::Int(((elems.len().max(1) - 1) / 2) as i64))
7117 }
7118 Some(Value::Null) | None => Value::Null,
7119 Some(other) => {
7120 return Err(QueryError::Type(format!(
7121 "length() expects a path, got {other:?}"
7122 )))
7123 }
7124 }),
7125 "keys" => keys_builtin(args.first()),
7126 "labels" => labels_builtin(args.first()),
7127 "type" => type_builtin(args.first()),
7128 "properties" => properties_builtin(args.first()),
7129 "id" => id_builtin(args.first()),
7130 "size" => size_builtin(args.first()),
7131 "nodes" => nodes_builtin(args.first()),
7132 "relationships" => relationships_builtin(args.first()),
7133 "head" => list_edge_builtin(args.first(), "head", |items| items.first().cloned()),
7134 "last" => list_edge_builtin(args.first(), "last", |items| items.last().cloned()),
7135 "tail" => match args.first() {
7136 Some(Value::List(items)) => Ok(Value::List(
7137 items.iter().skip(1).cloned().collect::<Vec<_>>(),
7138 )),
7139 Some(Value::Null) | None => Ok(Value::Null),
7140 Some(other) => Err(QueryError::Type(format!(
7141 "tail() expects a list, got {other:?}"
7142 ))),
7143 },
7144 "range" => range_builtin(args),
7145 "exists" => Ok(Value::Literal(Literal::Bool(!matches!(
7146 args.first(),
7147 None | Some(Value::Null)
7148 )))),
7149 "toupper" | "upper" => string_transform(args.first(), "toUpper", str::to_uppercase),
7150 "tolower" | "lower" => string_transform(args.first(), "toLower", str::to_lowercase),
7151 "trim" => string_transform(args.first(), "trim", |s| s.trim().to_string()),
7152 "ltrim" => string_transform(args.first(), "ltrim", |s| s.trim_start().to_string()),
7153 "rtrim" => string_transform(args.first(), "rtrim", |s| s.trim_end().to_string()),
7154 "reverse" => reverse_builtin(args.first()),
7155 "replace" => replace_builtin(args),
7156 "split" => split_builtin(args),
7157 "substring" => substring_builtin(args),
7158 "left" => left_right_builtin(args, true),
7159 "right" => left_right_builtin(args, false),
7160 "tofloat" => match args.first() {
7161 Some(v) => to_float(v),
7162 None => Ok(Value::Null),
7163 },
7164 "toboolean" => match args.first() {
7165 Some(v) => to_boolean(v),
7166 None => Ok(Value::Null),
7167 },
7168 "abs" => match args.first() {
7169 Some(Value::Property(PropertyValue::Int(i)))
7170 | Some(Value::Literal(Literal::Int(i))) => {
7171 Ok(Value::Property(PropertyValue::Int(i.abs())))
7172 }
7173 Some(Value::Null) | None => Ok(Value::Null),
7174 Some(other) => match value_as_f64(other) {
7175 Some(f) => Ok(Value::Property(PropertyValue::Float(f.abs()))),
7176 None => Err(QueryError::Type(format!(
7177 "abs() expects a number, got {other:?}"
7178 ))),
7179 },
7180 },
7181 "ceil" => float_math_fn(args.first(), "ceil", f64::ceil),
7182 "floor" => float_math_fn(args.first(), "floor", f64::floor),
7183 "round" => float_math_fn(args.first(), "round", f64::round),
7184 "sqrt" => float_math_fn(args.first(), "sqrt", f64::sqrt),
7185 "sign" => match args.first() {
7186 Some(Value::Null) | None => Ok(Value::Null),
7187 Some(other) => match value_as_f64(other) {
7188 Some(f) => Ok(Value::Property(PropertyValue::Int(if f > 0.0 {
7189 1
7190 } else if f < 0.0 {
7191 -1
7192 } else {
7193 0
7194 }))),
7195 None => Err(QueryError::Type(format!(
7196 "sign() expects a number, got {other:?}"
7197 ))),
7198 },
7199 },
7200 "rand" => Ok(Value::Property(PropertyValue::Float(rand_f64()))),
7201 other => Err(QueryError::Semantic(format!("unknown function: {other}"))),
7202 }
7203}
7204
7205fn rand_f64() -> f64 {
7214 use std::collections::hash_map::RandomState;
7215 use std::hash::{BuildHasher, Hasher};
7216 use std::sync::atomic::{AtomicU64, Ordering};
7217 static COUNTER: AtomicU64 = AtomicU64::new(0);
7218 let mut hasher = RandomState::new().build_hasher();
7219 hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
7220 let bits = hasher.finish();
7221 (bits >> 11) as f64 / (1u64 << 53) as f64
7222}
7223
7224fn keys_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7225 Ok(match arg {
7226 Some(Value::Node(n)) => Value::List(
7227 n.props
7228 .keys()
7229 .map(|k| Value::Property(PropertyValue::String(k.clone())))
7230 .collect(),
7231 ),
7232 Some(Value::Edge(e)) => Value::List(
7233 e.props
7234 .keys()
7235 .map(|k| Value::Property(PropertyValue::String(k.clone())))
7236 .collect(),
7237 ),
7238 Some(Value::Map(m)) => Value::List(
7239 m.keys()
7240 .map(|k| Value::Property(PropertyValue::String(k.clone())))
7241 .collect(),
7242 ),
7243 Some(Value::Null) | None => Value::Null,
7244 Some(other) => {
7245 return Err(QueryError::Type(format!(
7246 "keys() expects a node, relationship, or map, got {other:?}"
7247 )))
7248 }
7249 })
7250}
7251
7252fn labels_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7253 Ok(match arg {
7254 Some(Value::Node(n)) => Value::List(
7255 n.labels
7256 .iter()
7257 .map(|l| Value::Property(PropertyValue::String(l.clone())))
7258 .collect(),
7259 ),
7260 Some(Value::Null) | None => Value::Null,
7261 Some(other) => {
7262 return Err(QueryError::Type(format!(
7263 "labels() expects a node, got {other:?}"
7264 )))
7265 }
7266 })
7267}
7268
7269fn type_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7270 Ok(match arg {
7271 Some(Value::Edge(e)) => Value::Property(PropertyValue::String(e.label.clone())),
7272 Some(Value::Null) | None => Value::Null,
7273 Some(other) => {
7274 return Err(QueryError::Type(format!(
7275 "type() expects a relationship, got {other:?}"
7276 )))
7277 }
7278 })
7279}
7280
7281fn properties_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7282 Ok(match arg {
7283 Some(Value::Node(n)) => Value::Map(
7284 n.props
7285 .iter()
7286 .map(|(k, v)| (k.clone(), property_value_to_value(v.clone())))
7287 .collect(),
7288 ),
7289 Some(Value::Edge(e)) => Value::Map(
7290 e.props
7291 .iter()
7292 .map(|(k, v)| (k.clone(), property_value_to_value(v.clone())))
7293 .collect(),
7294 ),
7295 Some(Value::Map(m)) => Value::Map(m.clone()),
7296 Some(Value::Null) | None => Value::Null,
7297 Some(other) => {
7298 return Err(QueryError::Type(format!(
7299 "properties() expects a node, relationship, or map, got {other:?}"
7300 )))
7301 }
7302 })
7303}
7304
7305fn id_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7306 Ok(match arg {
7307 Some(Value::Node(n)) => Value::Property(PropertyValue::Int(n.id.0 as i64)),
7308 Some(Value::Edge(e)) => Value::Property(PropertyValue::Int(e.id.0 as i64)),
7309 Some(Value::Null) | None => Value::Null,
7310 Some(other) => {
7311 return Err(QueryError::Type(format!(
7312 "id() expects a node or relationship, got {other:?}"
7313 )))
7314 }
7315 })
7316}
7317
7318fn size_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7319 Ok(match arg {
7320 Some(Value::List(items)) => Value::Property(PropertyValue::Int(items.len() as i64)),
7321 Some(Value::Null) | None => Value::Null,
7322 Some(other) => match as_arith_str(other) {
7323 Some(s) => Value::Property(PropertyValue::Int(s.chars().count() as i64)),
7324 None => {
7325 return Err(QueryError::Type(format!(
7326 "size() expects a list or string, got {other:?}"
7327 )))
7328 }
7329 },
7330 })
7331}
7332
7333fn nodes_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7334 Ok(match arg {
7335 Some(Value::Path(elems)) => Value::List(
7336 elems
7337 .iter()
7338 .filter_map(|e| match e {
7339 PathElem::Node(n) => Some(Value::Node(n.clone())),
7340 PathElem::Edge(_) => None,
7341 })
7342 .collect(),
7343 ),
7344 Some(Value::Null) | None => Value::Null,
7345 Some(other) => {
7346 return Err(QueryError::Type(format!(
7347 "nodes() expects a path, got {other:?}"
7348 )))
7349 }
7350 })
7351}
7352
7353fn relationships_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7354 Ok(match arg {
7355 Some(Value::Path(elems)) => Value::List(
7356 elems
7357 .iter()
7358 .filter_map(|e| match e {
7359 PathElem::Edge(e) => Some(Value::Edge(e.clone())),
7360 PathElem::Node(_) => None,
7361 })
7362 .collect(),
7363 ),
7364 Some(Value::Null) | None => Value::Null,
7365 Some(other) => {
7366 return Err(QueryError::Type(format!(
7367 "relationships() expects a path, got {other:?}"
7368 )))
7369 }
7370 })
7371}
7372
7373fn list_edge_builtin(
7377 arg: Option<&Value>,
7378 fn_name: &str,
7379 pick: impl Fn(&[Value]) -> Option<Value>,
7380) -> Result<Value, QueryError> {
7381 Ok(match arg {
7382 Some(Value::List(items)) => pick(items).unwrap_or(Value::Null),
7383 Some(Value::Null) | None => Value::Null,
7384 Some(other) => {
7385 return Err(QueryError::Type(format!(
7386 "{fn_name}() expects a list, got {other:?}"
7387 )))
7388 }
7389 })
7390}
7391
7392fn range_builtin(args: &[Value]) -> Result<Value, QueryError> {
7398 let int_arg = |v: &Value, which: &str| -> Result<i64, QueryError> {
7399 value_as_i64(v).ok_or_else(|| {
7400 QueryError::Type(format!("range()'s {which} must be an integer, got {v:?}"))
7401 })
7402 };
7403 let start = int_arg(
7404 args.first()
7405 .ok_or_else(|| QueryError::Semantic("range() requires at least 2 arguments".into()))?,
7406 "start",
7407 )?;
7408 let end = int_arg(
7409 args.get(1)
7410 .ok_or_else(|| QueryError::Semantic("range() requires at least 2 arguments".into()))?,
7411 "end",
7412 )?;
7413 let step = match args.get(2) {
7414 Some(v) => int_arg(v, "step")?,
7415 None => 1,
7416 };
7417 if step == 0 {
7418 return Err(QueryError::Type("range()'s step can't be 0".into()));
7419 }
7420 let mut out = Vec::new();
7421 let mut i = start;
7422 if step > 0 {
7423 while i <= end {
7424 out.push(Value::Property(PropertyValue::Int(i)));
7425 i += step;
7426 }
7427 } else {
7428 while i >= end {
7429 out.push(Value::Property(PropertyValue::Int(i)));
7430 i += step;
7431 }
7432 }
7433 Ok(Value::List(out))
7434}
7435
7436fn string_transform(
7437 arg: Option<&Value>,
7438 fn_name: &str,
7439 f: impl FnOnce(&str) -> String,
7440) -> Result<Value, QueryError> {
7441 Ok(match arg {
7442 Some(Value::Null) | None => Value::Null,
7443 Some(other) => match as_arith_str(other) {
7444 Some(s) => Value::Property(PropertyValue::String(f(s))),
7445 None => {
7446 return Err(QueryError::Type(format!(
7447 "{fn_name}() expects a string, got {other:?}"
7448 )))
7449 }
7450 },
7451 })
7452}
7453
7454fn reverse_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7455 Ok(match arg {
7456 Some(Value::Null) | None => Value::Null,
7457 Some(Value::List(items)) => Value::List(items.iter().rev().cloned().collect()),
7458 Some(other) => match as_arith_str(other) {
7459 Some(s) => Value::Property(PropertyValue::String(s.chars().rev().collect())),
7460 None => {
7461 return Err(QueryError::Type(format!(
7462 "reverse() expects a string or list, got {other:?}"
7463 )))
7464 }
7465 },
7466 })
7467}
7468
7469fn replace_str_arg<'a>(v: &'a Value, which: &str) -> Result<&'a str, QueryError> {
7473 as_arith_str(v)
7474 .ok_or_else(|| QueryError::Type(format!("replace()'s {which} must be a string, got {v:?}")))
7475}
7476
7477fn replace_builtin(args: &[Value]) -> Result<Value, QueryError> {
7478 if args.iter().any(|v| matches!(v, Value::Null)) {
7479 return Ok(Value::Null);
7480 }
7481 let original = replace_str_arg(
7482 args.first()
7483 .ok_or_else(|| QueryError::Semantic("replace() requires 3 arguments".into()))?,
7484 "original",
7485 )?;
7486 let search = replace_str_arg(
7487 args.get(1)
7488 .ok_or_else(|| QueryError::Semantic("replace() requires 3 arguments".into()))?,
7489 "search",
7490 )?;
7491 let replacement = replace_str_arg(
7492 args.get(2)
7493 .ok_or_else(|| QueryError::Semantic("replace() requires 3 arguments".into()))?,
7494 "replacement",
7495 )?;
7496 Ok(Value::Property(PropertyValue::String(
7497 original.replace(search, replacement),
7498 )))
7499}
7500
7501fn split_builtin(args: &[Value]) -> Result<Value, QueryError> {
7502 if args.iter().any(|v| matches!(v, Value::Null)) {
7503 return Ok(Value::Null);
7504 }
7505 let s = args
7506 .first()
7507 .and_then(as_arith_str)
7508 .ok_or_else(|| QueryError::Type("split()'s first argument must be a string".into()))?;
7509 let delim = args
7510 .get(1)
7511 .and_then(as_arith_str)
7512 .ok_or_else(|| QueryError::Type("split()'s second argument must be a string".into()))?;
7513 let parts = if delim.is_empty() {
7514 s.split("").filter(|p| !p.is_empty()).collect::<Vec<_>>()
7515 } else {
7516 s.split(delim).collect::<Vec<_>>()
7517 };
7518 Ok(Value::List(
7519 parts
7520 .into_iter()
7521 .map(|p| Value::Property(PropertyValue::String(p.to_string())))
7522 .collect(),
7523 ))
7524}
7525
7526fn substring_builtin(args: &[Value]) -> Result<Value, QueryError> {
7532 if matches!(args.first(), Some(Value::Null)) {
7533 return Ok(Value::Null);
7534 }
7535 let s = args
7536 .first()
7537 .and_then(as_arith_str)
7538 .ok_or_else(|| QueryError::Type("substring()'s first argument must be a string".into()))?;
7539 let chars: Vec<char> = s.chars().collect();
7540 let start = args
7541 .get(1)
7542 .and_then(value_as_i64)
7543 .ok_or_else(|| QueryError::Type("substring()'s start must be an integer".into()))?
7544 .max(0) as usize;
7545 let start = start.min(chars.len());
7546 let end = match args.get(2) {
7547 Some(v) => {
7548 let len = value_as_i64(v)
7549 .ok_or_else(|| QueryError::Type("substring()'s length must be an integer".into()))?
7550 .max(0) as usize;
7551 (start + len).min(chars.len())
7552 }
7553 None => chars.len(),
7554 };
7555 Ok(Value::Property(PropertyValue::String(
7556 chars[start..end].iter().collect(),
7557 )))
7558}
7559
7560fn left_right_builtin(args: &[Value], from_left: bool) -> Result<Value, QueryError> {
7563 if matches!(args.first(), Some(Value::Null)) {
7564 return Ok(Value::Null);
7565 }
7566 let fn_name = if from_left { "left" } else { "right" };
7567 let s = args.first().and_then(as_arith_str).ok_or_else(|| {
7568 QueryError::Type(format!("{fn_name}()'s first argument must be a string"))
7569 })?;
7570 let n = args
7571 .get(1)
7572 .and_then(value_as_i64)
7573 .ok_or_else(|| {
7574 QueryError::Type(format!("{fn_name}()'s second argument must be an integer"))
7575 })?
7576 .max(0) as usize;
7577 let chars: Vec<char> = s.chars().collect();
7578 let n = n.min(chars.len());
7579 let slice = if from_left {
7580 &chars[..n]
7581 } else {
7582 &chars[chars.len() - n..]
7583 };
7584 Ok(Value::Property(PropertyValue::String(
7585 slice.iter().collect(),
7586 )))
7587}
7588
7589fn float_math_fn(
7590 arg: Option<&Value>,
7591 fn_name: &str,
7592 f: impl FnOnce(f64) -> f64,
7593) -> Result<Value, QueryError> {
7594 Ok(match arg {
7595 Some(Value::Null) | None => Value::Null,
7596 Some(other) => match value_as_f64(other) {
7597 Some(x) => Value::Property(PropertyValue::Float(f(x))),
7598 None => {
7599 return Err(QueryError::Type(format!(
7600 "{fn_name}() expects a number, got {other:?}"
7601 )))
7602 }
7603 },
7604 })
7605}
7606
7607fn to_float(v: &Value) -> Result<Value, QueryError> {
7608 Ok(match v {
7609 Value::Property(PropertyValue::Int(i)) => Value::Property(PropertyValue::Float(*i as f64)),
7610 Value::Property(PropertyValue::Float(f)) => Value::Property(PropertyValue::Float(*f)),
7611 Value::Literal(Literal::Int(i)) => Value::Property(PropertyValue::Float(*i as f64)),
7612 Value::Literal(Literal::Float(f)) => Value::Property(PropertyValue::Float(*f)),
7613 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => {
7614 match s.trim().parse::<f64>() {
7615 Ok(f) => Value::Property(PropertyValue::Float(f)),
7616 Err(_) => Value::Null,
7617 }
7618 }
7619 Value::Property(PropertyValue::Null) | Value::Literal(Literal::Null) | Value::Null => {
7620 Value::Null
7621 }
7622 Value::Literal(Literal::Param(name)) => {
7623 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7624 }
7625 other => {
7630 return Err(QueryError::Type(format!(
7631 "toFloat() cannot convert {other:?} to a float"
7632 )))
7633 }
7634 })
7635}
7636
7637fn to_boolean(v: &Value) -> Result<Value, QueryError> {
7638 Ok(match v {
7639 Value::Property(PropertyValue::Bool(b)) | Value::Literal(Literal::Bool(b)) => {
7640 Value::Literal(Literal::Bool(*b))
7641 }
7642 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => {
7643 match s.trim().to_ascii_lowercase().as_str() {
7644 "true" => Value::Literal(Literal::Bool(true)),
7645 "false" => Value::Literal(Literal::Bool(false)),
7646 _ => Value::Null,
7647 }
7648 }
7649 Value::Property(PropertyValue::Null) | Value::Literal(Literal::Null) | Value::Null => {
7650 Value::Null
7651 }
7652 Value::Literal(Literal::Param(name)) => {
7653 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7654 }
7655 other => {
7656 return Err(QueryError::Type(format!(
7657 "toBoolean() cannot convert {other:?} to a boolean"
7658 )))
7659 }
7660 })
7661}
7662
7663fn item_truthy(v: &Value) -> Option<bool> {
7670 match v {
7671 Value::Null => None,
7672 Value::Literal(Literal::Bool(b)) | Value::Property(PropertyValue::Bool(b)) => Some(*b),
7673 _ => Some(false),
7674 }
7675}
7676
7677fn eval_quantifier(kind: QuantifierKind, preds: &[Option<bool>]) -> Option<bool> {
7687 let true_count = preds.iter().filter(|p| **p == Some(true)).count();
7688 let any_false = preds.contains(&Some(false));
7689 let any_null = preds.iter().any(|p| p.is_none());
7690 match kind {
7691 QuantifierKind::Any => {
7692 if true_count > 0 {
7693 Some(true)
7694 } else if any_null {
7695 None
7696 } else {
7697 Some(false)
7698 }
7699 }
7700 QuantifierKind::None => {
7701 if true_count > 0 {
7702 Some(false)
7703 } else if any_null {
7704 None
7705 } else {
7706 Some(true)
7707 }
7708 }
7709 QuantifierKind::All => {
7710 if any_false {
7711 Some(false)
7712 } else if any_null {
7713 None
7714 } else {
7715 Some(true)
7716 }
7717 }
7718 QuantifierKind::Single => {
7719 if true_count >= 2 {
7720 Some(false)
7721 } else if any_null {
7722 None
7723 } else {
7724 Some(true_count == 1)
7725 }
7726 }
7727 }
7728}
7729
7730fn to_integer(v: &Value) -> Result<Value, QueryError> {
7731 let as_str_parse = |s: &str| match s.trim().parse::<i64>() {
7737 Ok(i) => Value::Property(PropertyValue::Int(i)),
7738 Err(_) => match s.trim().parse::<f64>() {
7739 Ok(f) => Value::Property(PropertyValue::Int(f as i64)),
7740 Err(_) => Value::Null,
7741 },
7742 };
7743 Ok(match v {
7744 Value::Property(PropertyValue::Int(i)) => Value::Property(PropertyValue::Int(*i)),
7745 Value::Property(PropertyValue::Float(f)) => Value::Property(PropertyValue::Int(*f as i64)),
7746 Value::Property(PropertyValue::String(s)) => as_str_parse(s),
7747 Value::Literal(Literal::Int(i)) => Value::Property(PropertyValue::Int(*i)),
7748 Value::Literal(Literal::Float(f)) => Value::Property(PropertyValue::Int(*f as i64)),
7749 Value::Literal(Literal::String(s)) => as_str_parse(s),
7750 Value::Property(PropertyValue::Bool(_) | PropertyValue::Null)
7751 | Value::Literal(Literal::Bool(_) | Literal::Null)
7752 | Value::Null => Value::Null,
7753 Value::Literal(Literal::Param(name)) => {
7754 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7755 }
7756 Value::Property(
7761 PropertyValue::Date(_)
7762 | PropertyValue::Duration { .. }
7763 | PropertyValue::LocalTime(_)
7764 | PropertyValue::Time { .. }
7765 | PropertyValue::LocalDateTime { .. }
7766 | PropertyValue::DateTime { .. }
7767 | PropertyValue::List(_)
7768 | PropertyValue::Map(_),
7769 )
7770 | Value::Node(_)
7771 | Value::Edge(_)
7772 | Value::List(_)
7773 | Value::Map(_)
7774 | Value::Path(_) => {
7775 return Err(QueryError::Type(format!(
7776 "toInteger() cannot convert {v:?} to an integer"
7777 )))
7778 }
7779 })
7780}
7781
7782fn to_string_value(v: &Value) -> Result<Value, QueryError> {
7789 let s = match v {
7790 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => s.clone(),
7791 Value::Property(PropertyValue::Int(i)) | Value::Literal(Literal::Int(i)) => i.to_string(),
7792 Value::Property(PropertyValue::Float(f)) | Value::Literal(Literal::Float(f)) => {
7793 f.to_string()
7794 }
7795 Value::Property(PropertyValue::Bool(b)) | Value::Literal(Literal::Bool(b)) => b.to_string(),
7796 Value::Property(PropertyValue::Date(d)) => temporal::format_date(*d),
7797 Value::Property(PropertyValue::Duration {
7798 months,
7799 days,
7800 seconds,
7801 nanos,
7802 }) => temporal::format_duration(*months, *days, *seconds, *nanos),
7803 Value::Property(PropertyValue::LocalTime(nanos_of_day)) => {
7804 temporal::format_local_time(*nanos_of_day)
7805 }
7806 Value::Property(PropertyValue::Time {
7807 nanos_of_day,
7808 offset_seconds,
7809 }) => temporal::format_time(*nanos_of_day, *offset_seconds),
7810 Value::Property(PropertyValue::LocalDateTime {
7811 epoch_seconds,
7812 nanos,
7813 }) => temporal::format_local_date_time(*epoch_seconds, *nanos),
7814 Value::Property(PropertyValue::DateTime {
7815 epoch_seconds,
7816 nanos,
7817 zone,
7818 }) => temporal::format_date_time(*epoch_seconds, *nanos, &tz_from_graph(zone)),
7819 Value::Property(PropertyValue::Null) | Value::Literal(Literal::Null) | Value::Null => {
7820 return Ok(Value::Null);
7821 }
7822 Value::Literal(Literal::Param(name)) => {
7823 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7824 }
7825 Value::Property(PropertyValue::List(_) | PropertyValue::Map(_))
7826 | Value::Node(_)
7827 | Value::Edge(_)
7828 | Value::List(_)
7829 | Value::Map(_)
7830 | Value::Path(_) => {
7831 return Err(QueryError::Type(format!(
7832 "toString() cannot convert {v:?} to a string"
7833 )))
7834 }
7835 };
7836 Ok(Value::Property(PropertyValue::String(s)))
7837}
7838
7839fn now_or_null(args: &[Value], now_value: impl FnOnce() -> Value) -> Value {
7862 if matches!(args.first(), Some(Value::Null)) {
7863 Value::Null
7864 } else {
7865 now_value()
7866 }
7867}
7868
7869fn date_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
7870 if args.len() > 1 {
7871 return Err(QueryError::Semantic(format!(
7872 "date() expects zero or one argument, got {}",
7873 args.len()
7874 )));
7875 }
7876 let Some(arg) = args.first() else {
7877 return Ok(Value::Property(PropertyValue::Date(now.epoch_day)));
7878 };
7879 if matches!(arg, Value::Null) {
7880 return Ok(Value::Null);
7881 }
7882 if let Value::Property(PropertyValue::Date(d)) = arg {
7883 return Ok(Value::Property(PropertyValue::Date(*d)));
7884 }
7885 if matches!(
7889 arg,
7890 Value::Property(PropertyValue::LocalDateTime { .. } | PropertyValue::DateTime { .. })
7891 ) {
7892 let epoch_day = extract_date_base_epoch_day("date() argument", arg)?;
7893 return Ok(Value::Property(PropertyValue::Date(epoch_day)));
7894 }
7895 if let Some(s) = as_arith_str(arg) {
7896 let d = temporal::parse_date(s).ok_or_else(|| {
7897 QueryError::Type(format!(
7898 "'{s}' isn't a date string MarsDB can parse -- only the calendar forms YYYY-MM-DD/YYYYMMDD/\
7899 YYYY-MM/YYYYMM/YYYY, week-date forms YYYY-Www[-D]/YYYYWww[D], and ordinal-date forms \
7900 YYYY-DDD/YYYYDDD are supported"
7901 ))
7902 })?;
7903 return Ok(Value::Property(PropertyValue::Date(d)));
7904 }
7905 if let Value::Map(m) = arg {
7906 return Ok(Value::Property(PropertyValue::Date(date_from_map(m)?)));
7907 }
7908 Err(QueryError::Type(format!(
7909 "date() doesn't support this argument: {arg:?}"
7910 )))
7911}
7912
7913fn extract_date_base_epoch_day(key: &str, v: &Value) -> Result<i32, QueryError> {
7924 match v {
7925 Value::Property(PropertyValue::Date(d)) => Ok(*d),
7926 Value::Property(PropertyValue::LocalDateTime { epoch_seconds, .. }) => {
7927 Ok(temporal::split_epoch_seconds(*epoch_seconds).0)
7928 }
7929 Value::Property(PropertyValue::DateTime {
7930 epoch_seconds,
7931 zone,
7932 ..
7933 }) => {
7934 let offset_seconds = temporal::resolve_offset(&tz_from_graph(zone), *epoch_seconds);
7935 Ok(temporal::split_epoch_seconds(epoch_seconds + offset_seconds as i64).0)
7936 }
7937 other => Err(QueryError::Type(format!(
7938 "'{key}' must be a Date, LocalDateTime, or DateTime, got {other:?}"
7939 ))),
7940 }
7941}
7942
7943type ClockBase = (i64, i64, i64, i64, Option<(temporal::TzId, i32)>);
7957
7958fn extract_time_base(key: &str, v: &Value) -> Result<ClockBase, QueryError> {
7959 let hms_nanos = |nanos_of_day: i64| {
7960 (
7961 temporal::local_time_component(nanos_of_day, "hour").unwrap(),
7962 temporal::local_time_component(nanos_of_day, "minute").unwrap(),
7963 temporal::local_time_component(nanos_of_day, "second").unwrap(),
7964 temporal::local_time_component(nanos_of_day, "nanosecond").unwrap(),
7965 )
7966 };
7967 match v {
7968 Value::Property(PropertyValue::LocalTime(n)) => {
7969 let (h, m, s, ns) = hms_nanos(*n);
7970 Ok((h, m, s, ns, None))
7971 }
7972 Value::Property(PropertyValue::Time {
7973 nanos_of_day,
7974 offset_seconds,
7975 }) => {
7976 let (h, m, s, ns) = hms_nanos(*nanos_of_day);
7977 Ok((
7978 h,
7979 m,
7980 s,
7981 ns,
7982 Some((temporal::TzId::Offset(*offset_seconds), *offset_seconds)),
7983 ))
7984 }
7985 Value::Property(PropertyValue::LocalDateTime {
7986 epoch_seconds,
7987 nanos,
7988 }) => {
7989 let (_, nanos_of_day) = temporal::split_epoch_seconds(*epoch_seconds);
7990 let (h, m, s, _) = hms_nanos(nanos_of_day);
7991 Ok((h, m, s, *nanos as i64, None))
7992 }
7993 Value::Property(PropertyValue::DateTime {
7994 epoch_seconds,
7995 nanos,
7996 zone,
7997 }) => {
7998 let tz = tz_from_graph(zone);
7999 let offset_seconds = temporal::resolve_offset(&tz, *epoch_seconds);
8000 let local = epoch_seconds + offset_seconds as i64;
8001 let (_, nanos_of_day) = temporal::split_epoch_seconds(local);
8002 let (h, m, s, _) = hms_nanos(nanos_of_day);
8003 Ok((h, m, s, *nanos as i64, Some((tz, offset_seconds))))
8004 }
8005 other => Err(QueryError::Type(format!(
8006 "'{key}' must be a LocalTime, Time, LocalDateTime, or DateTime, got {other:?}"
8007 ))),
8008 }
8009}
8010
8011const DATE_ALLOWED_KEYS: &[&str] = &[
8012 "year",
8013 "month",
8014 "day",
8015 "week",
8016 "dayOfWeek",
8017 "ordinalDay",
8018 "quarter",
8019 "dayOfQuarter",
8020 "date",
8021];
8022
8023fn date_from_map(m: &BTreeMap<String, Value>) -> Result<i32, QueryError> {
8024 let (year, month, day) = calendar_fields_from_map("date", m, DATE_ALLOWED_KEYS)?;
8025 temporal::epoch_day_from_ymd(year, month, day).ok_or_else(|| {
8026 QueryError::Type(format!(
8027 "{year:04}-{month:02}-{day:02} isn't a valid calendar date"
8028 ))
8029 })
8030}
8031
8032fn calendar_fields_from_map(
8046 caller: &str,
8047 m: &BTreeMap<String, Value>,
8048 allowed: &[&str],
8049) -> Result<(i32, u32, u32), QueryError> {
8050 if let Some(bad) = m.keys().find(|k| !allowed.contains(&k.as_str())) {
8051 return Err(QueryError::Type(format!(
8052 "{caller}({{...}}) key '{bad}' isn't a recognized field"
8053 )));
8054 }
8055 let int_field = |key: &str, value: &Value| {
8056 value_as_i64(value).ok_or_else(|| {
8057 QueryError::Type(format!("{caller}({{...}})'s '{key}' must be an integer"))
8058 })
8059 };
8060 let base_epoch_day = m
8061 .get("date")
8062 .map(|v| ("date", v))
8063 .or_else(|| m.get("datetime").map(|v| ("datetime", v)))
8064 .map(|(k, v)| extract_date_base_epoch_day(k, v))
8065 .transpose()?;
8066 let epoch_day_from_component =
8067 |prop: &str| base_epoch_day.map(|ed| temporal::date_component(ed, prop).unwrap());
8068
8069 if m.contains_key("week") || m.contains_key("dayOfWeek") {
8070 let week_year = match m.get("year") {
8071 Some(v) => i32::try_from(int_field("year", v)?).map_err(|_| {
8072 QueryError::Type(format!("{caller}({{...}})'s 'year' is out of range"))
8073 })?,
8074 None => i32::try_from(epoch_day_from_component("weekYear").ok_or_else(|| {
8075 QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
8076 })?)
8077 .unwrap(),
8078 };
8079 let week = match m.get("week") {
8080 Some(v) => u32::try_from(int_field("week", v)?).map_err(|_| {
8081 QueryError::Type(format!("{caller}({{...}})'s 'week' is out of range"))
8082 })?,
8083 None => u32::try_from(epoch_day_from_component("week").ok_or_else(|| {
8084 QueryError::Type(format!("{caller}({{...}}) requires a 'week' key"))
8085 })?)
8086 .unwrap(),
8087 };
8088 let day_of_week = match m.get("dayOfWeek") {
8089 Some(v) => int_field("dayOfWeek", v)?,
8090 None => epoch_day_from_component("dayOfWeek").unwrap_or(1),
8091 };
8092 let epoch_day = temporal::epoch_day_from_week_fields(week_year, week, day_of_week)
8093 .ok_or_else(|| {
8094 QueryError::Type(format!(
8095 "{caller}({{...}}) has an out-of-range week-date field"
8096 ))
8097 })?;
8098 return Ok((
8099 temporal::date_component(epoch_day, "year").unwrap() as i32,
8100 temporal::date_component(epoch_day, "month").unwrap() as u32,
8101 temporal::date_component(epoch_day, "day").unwrap() as u32,
8102 ));
8103 }
8104
8105 if m.contains_key("ordinalDay") {
8106 let year = match m.get("year") {
8107 Some(v) => i32::try_from(int_field("year", v)?).map_err(|_| {
8108 QueryError::Type(format!("{caller}({{...}})'s 'year' is out of range"))
8109 })?,
8110 None => i32::try_from(epoch_day_from_component("year").ok_or_else(|| {
8111 QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
8112 })?)
8113 .unwrap(),
8114 };
8115 let ordinal_raw = int_field("ordinalDay", m.get("ordinalDay").unwrap())?;
8116 let ordinal_day = u32::try_from(ordinal_raw).map_err(|_| {
8117 QueryError::Type(format!("{caller}({{...}})'s 'ordinalDay' is out of range"))
8118 })?;
8119 let epoch_day =
8120 temporal::epoch_day_from_ordinal_fields(year, ordinal_day).ok_or_else(|| {
8121 QueryError::Type(format!(
8122 "{caller}({{...}}) has an out-of-range ordinalDay field"
8123 ))
8124 })?;
8125 return Ok((
8126 year,
8127 temporal::date_component(epoch_day, "month").unwrap() as u32,
8128 temporal::date_component(epoch_day, "day").unwrap() as u32,
8129 ));
8130 }
8131
8132 if m.contains_key("quarter") || m.contains_key("dayOfQuarter") {
8133 let year = match m.get("year") {
8134 Some(v) => i32::try_from(int_field("year", v)?).map_err(|_| {
8135 QueryError::Type(format!("{caller}({{...}})'s 'year' is out of range"))
8136 })?,
8137 None => i32::try_from(epoch_day_from_component("year").ok_or_else(|| {
8138 QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
8139 })?)
8140 .unwrap(),
8141 };
8142 let quarter = match m.get("quarter") {
8143 Some(v) => u32::try_from(int_field("quarter", v)?).map_err(|_| {
8144 QueryError::Type(format!("{caller}({{...}})'s 'quarter' is out of range"))
8145 })?,
8146 None => u32::try_from(epoch_day_from_component("quarter").ok_or_else(|| {
8147 QueryError::Type(format!("{caller}({{...}}) requires a 'quarter' key"))
8148 })?)
8149 .unwrap(),
8150 };
8151 let day_of_quarter = match m.get("dayOfQuarter") {
8152 Some(v) => int_field("dayOfQuarter", v)?,
8153 None => epoch_day_from_component("dayOfQuarter").unwrap_or(1),
8154 };
8155 let epoch_day = temporal::epoch_day_from_quarter_fields(year, quarter, day_of_quarter)
8156 .ok_or_else(|| {
8157 QueryError::Type(format!(
8158 "{caller}({{...}}) has an out-of-range quarter-date field"
8159 ))
8160 })?;
8161 return Ok((
8162 year,
8163 temporal::date_component(epoch_day, "month").unwrap() as u32,
8164 temporal::date_component(epoch_day, "day").unwrap() as u32,
8165 ));
8166 }
8167
8168 let year_raw = match m.get("year") {
8169 Some(v) => int_field("year", v)?,
8170 None => epoch_day_from_component("year")
8171 .ok_or_else(|| QueryError::Type(format!("{caller}({{...}}) requires a 'year' key")))?,
8172 };
8173 let year = i32::try_from(year_raw).map_err(|_| {
8174 QueryError::Type(format!(
8175 "{caller}({{...}})'s 'year' is out of range: {year_raw}"
8176 ))
8177 })?;
8178 let month_raw = match m.get("month") {
8179 Some(v) => int_field("month", v)?,
8180 None => epoch_day_from_component("month").unwrap_or(1),
8181 };
8182 let month = u32::try_from(month_raw).map_err(|_| {
8183 QueryError::Type(format!(
8184 "{caller}({{...}})'s 'month' is out of range: {month_raw}"
8185 ))
8186 })?;
8187 let day_raw = match m.get("day") {
8188 Some(v) => int_field("day", v)?,
8189 None => epoch_day_from_component("day").unwrap_or(1),
8190 };
8191 let day = u32::try_from(day_raw).map_err(|_| {
8192 QueryError::Type(format!(
8193 "{caller}({{...}})'s 'day' is out of range: {day_raw}"
8194 ))
8195 })?;
8196 Ok((year, month, day))
8197}
8198
8199fn duration_builtin(args: &[Value]) -> Result<Value, QueryError> {
8205 if args.len() != 1 {
8206 return Err(QueryError::Semantic(format!(
8207 "duration() expects exactly one argument, got {}",
8208 args.len()
8209 )));
8210 }
8211 let arg = &args[0];
8212 if matches!(arg, Value::Null) {
8213 return Ok(Value::Null);
8214 }
8215 let (months, days, seconds, nanos) = if let Some(s) = as_arith_str(arg) {
8216 temporal::parse_duration(s).ok_or_else(|| {
8217 QueryError::Type(format!(
8218 "'{s}' isn't a duration string MarsDB can parse -- only ISO-8601 'PnYnMnWnDTnHnMnS' text is \
8219 supported, not the alternate combined date-time duration syntax"
8220 ))
8221 })?
8222 } else if let Value::Map(m) = arg {
8223 temporal::normalize_duration(duration_fields_from_map(m)?)
8224 } else {
8225 return Err(QueryError::Type(format!(
8226 "duration() doesn't support this argument: {arg:?}"
8227 )));
8228 };
8229 Ok(Value::Property(PropertyValue::Duration {
8230 months,
8231 days,
8232 seconds,
8233 nanos,
8234 }))
8235}
8236
8237fn duration_fields_from_map(
8238 m: &BTreeMap<String, Value>,
8239) -> Result<temporal::DurationFields, QueryError> {
8240 const ALLOWED: &[&str] = &[
8241 "years",
8242 "months",
8243 "weeks",
8244 "days",
8245 "hours",
8246 "minutes",
8247 "seconds",
8248 "milliseconds",
8249 "microseconds",
8250 "nanoseconds",
8251 ];
8252 if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
8253 return Err(QueryError::Type(format!(
8254 "duration({{...}}) key '{bad}' isn't a recognized duration unit"
8255 )));
8256 }
8257 let field = |key: &str| -> Result<f64, QueryError> {
8258 match m.get(key) {
8259 None => Ok(0.0),
8260 Some(v) => value_as_f64(v).ok_or_else(|| {
8261 QueryError::Type(format!("duration({{...}})'s '{key}' must be a number"))
8262 }),
8263 }
8264 };
8265 Ok(temporal::DurationFields {
8266 years: field("years")?,
8267 months: field("months")?,
8268 weeks: field("weeks")?,
8269 days: field("days")?,
8270 hours: field("hours")?,
8271 minutes: field("minutes")?,
8272 seconds: field("seconds")?,
8273 milliseconds: field("milliseconds")?,
8274 microseconds: field("microseconds")?,
8275 nanoseconds: field("nanoseconds")?,
8276 })
8277}
8278
8279fn sub_second_nanos_from_map(
8298 base_fraction_ns: i64,
8299 m: &BTreeMap<String, Value>,
8300) -> Result<i64, QueryError> {
8301 let base_ms = base_fraction_ns / 1_000_000;
8302 let base_us = (base_fraction_ns / 1_000) % 1000;
8303 let base_ns = base_fraction_ns % 1000;
8304 let ms = int_field(m, "millisecond", base_ms)?;
8305 let us = int_field(m, "microsecond", base_us)?;
8306 let ns = int_field(m, "nanosecond", base_ns)?;
8307 Ok(ms * 1_000_000 + us * 1_000 + ns)
8308}
8309
8310fn int_field(m: &BTreeMap<String, Value>, key: &str, default: i64) -> Result<i64, QueryError> {
8311 match m.get(key) {
8312 None => Ok(default),
8313 Some(v) => {
8314 value_as_i64(v).ok_or_else(|| QueryError::Type(format!("'{key}' must be an integer")))
8315 }
8316 }
8317}
8318
8319fn clock_fields_from_map(
8350 m: &BTreeMap<String, Value>,
8351 epoch_day: Option<i32>,
8352) -> Result<ClockBase, QueryError> {
8353 let (base_h, base_m, base_s, base_ns, base_zone) = if let Some(v) = m.get("time") {
8354 extract_time_base("time", v)?
8355 } else if let Some(v) = m.get("datetime") {
8356 extract_time_base("datetime", v)?
8357 } else {
8358 (0, 0, 0, 0, None)
8359 };
8360 let has_explicit_timezone = m.contains_key("timezone");
8361 let effective_zone = match m.get("timezone") {
8362 Some(v) => Some(timezone_value_to_tzid(v)?),
8363 None => base_zone.as_ref().map(|(tz, _)| tz.clone()),
8368 };
8369 let base_nanos_of_day =
8379 base_h * 3_600_000_000_000 + base_m * 60_000_000_000 + base_s * 1_000_000_000 + base_ns;
8380 let (base_h, base_m, base_s, base_ns, effective_offset) = if has_explicit_timezone {
8381 let from_offset = match base_zone.as_ref() {
8389 Some((temporal::TzId::Offset(o), _)) => Some(*o),
8390 Some((zone @ temporal::TzId::Named(_), resolved)) => Some(match epoch_day {
8391 Some(ed) => temporal::resolve_offset(
8392 zone,
8393 temporal::combine_epoch_day_and_nanos_of_day(ed, base_nanos_of_day),
8394 ),
8395 None => *resolved,
8396 }),
8397 None => None,
8398 };
8399 let to_offset = match (from_offset, effective_zone.as_ref(), epoch_day) {
8400 (Some(_), Some(temporal::TzId::Offset(to)), _) => Some(*to),
8401 (Some(from), Some(zone @ temporal::TzId::Named(_)), Some(ed)) => {
8402 let approx_epoch_seconds =
8403 temporal::combine_epoch_day_and_nanos_of_day(ed, base_nanos_of_day)
8404 - from as i64;
8405 Some(temporal::resolve_offset(zone, approx_epoch_seconds))
8406 }
8407 _ => None,
8408 };
8409 match (from_offset, to_offset) {
8410 (Some(from), Some(to)) if from != to => {
8411 let shifted = (base_nanos_of_day + (to - from) as i64 * 1_000_000_000)
8412 .rem_euclid(86_400_000_000_000);
8413 (
8414 shifted / 3_600_000_000_000,
8415 (shifted / 60_000_000_000) % 60,
8416 (shifted / 1_000_000_000) % 60,
8417 shifted % 1_000_000_000,
8418 to_offset.unwrap_or(0),
8419 )
8420 }
8421 _ => (base_h, base_m, base_s, base_ns, to_offset.unwrap_or(0)),
8422 }
8423 } else {
8424 (
8430 base_h,
8431 base_m,
8432 base_s,
8433 base_ns,
8434 base_zone.as_ref().map_or(0, |(_, o)| *o),
8435 )
8436 };
8437 Ok((
8438 int_field(m, "hour", base_h)?,
8439 int_field(m, "minute", base_m)?,
8440 int_field(m, "second", base_s)?,
8441 sub_second_nanos_from_map(base_ns, m)?,
8442 effective_zone.map(|z| (z, effective_offset)),
8443 ))
8444}
8445
8446fn local_time_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
8452 if args.len() > 1 {
8453 return Err(QueryError::Semantic(format!(
8454 "localtime() expects zero or one argument, got {}",
8455 args.len()
8456 )));
8457 }
8458 let Some(arg) = args.first() else {
8459 return Ok(Value::Property(PropertyValue::LocalTime(now.nanos_of_day)));
8460 };
8461 if matches!(arg, Value::Null) {
8462 return Ok(Value::Null);
8463 }
8464 if let Value::Property(PropertyValue::LocalTime(t)) = arg {
8465 return Ok(Value::Property(PropertyValue::LocalTime(*t)));
8466 }
8467 if matches!(
8471 arg,
8472 Value::Property(
8473 PropertyValue::Time { .. }
8474 | PropertyValue::LocalDateTime { .. }
8475 | PropertyValue::DateTime { .. }
8476 )
8477 ) {
8478 let (hour, minute, second, nanos, _) = extract_time_base("localtime() argument", arg)?;
8479 let t = temporal::local_time_nanos_from_fields(hour, minute, second, nanos).ok_or_else(
8480 || QueryError::Type("localtime() argument has an out-of-range field".into()),
8481 )?;
8482 return Ok(Value::Property(PropertyValue::LocalTime(t)));
8483 }
8484 if let Some(s) = as_arith_str(arg) {
8485 let t = temporal::parse_local_time(s).ok_or_else(|| {
8486 QueryError::Type(format!("'{s}' isn't a local time string MarsDB can parse"))
8487 })?;
8488 return Ok(Value::Property(PropertyValue::LocalTime(t)));
8489 }
8490 if let Value::Map(m) = arg {
8491 const ALLOWED: &[&str] = &[
8492 "hour",
8493 "minute",
8494 "second",
8495 "millisecond",
8496 "microsecond",
8497 "nanosecond",
8498 "time",
8499 ];
8500 if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
8501 return Err(QueryError::Type(format!(
8502 "localtime({{...}}) key '{bad}' isn't a recognized field"
8503 )));
8504 }
8505 let (hour, minute, second, nanos, _) = clock_fields_from_map(m, None)?;
8506 let t = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
8507 .ok_or_else(|| QueryError::Type("localtime({...}) has an out-of-range field".into()))?;
8508 return Ok(Value::Property(PropertyValue::LocalTime(t)));
8509 }
8510 Err(QueryError::Type(format!(
8511 "localtime() doesn't support this argument: {arg:?}"
8512 )))
8513}
8514
8515fn time_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
8522 if args.len() > 1 {
8523 return Err(QueryError::Semantic(format!(
8524 "time() expects zero or one argument, got {}",
8525 args.len()
8526 )));
8527 }
8528 let Some(arg) = args.first() else {
8529 return Ok(Value::Property(PropertyValue::Time {
8530 nanos_of_day: now.nanos_of_day,
8531 offset_seconds: 0,
8532 }));
8533 };
8534 if matches!(arg, Value::Null) {
8535 return Ok(Value::Null);
8536 }
8537 if let Value::Property(PropertyValue::Time {
8538 nanos_of_day,
8539 offset_seconds,
8540 }) = arg
8541 {
8542 return Ok(Value::Property(PropertyValue::Time {
8543 nanos_of_day: *nanos_of_day,
8544 offset_seconds: *offset_seconds,
8545 }));
8546 }
8547 if matches!(
8553 arg,
8554 Value::Property(
8555 PropertyValue::LocalTime(_)
8556 | PropertyValue::LocalDateTime { .. }
8557 | PropertyValue::DateTime { .. }
8558 )
8559 ) {
8560 let (hour, minute, second, nanos, zone) = extract_time_base("time() argument", arg)?;
8561 let nanos_of_day = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
8562 .ok_or_else(|| QueryError::Type("time() argument has an out-of-range field".into()))?;
8563 return Ok(Value::Property(PropertyValue::Time {
8564 nanos_of_day,
8565 offset_seconds: zone.map_or(0, |(_, o)| o),
8571 }));
8572 }
8573 if let Some(s) = as_arith_str(arg) {
8574 if s.contains('[') {
8575 return Err(QueryError::Type(
8576 "time('...'): named timezones (e.g. '[Europe/Stockholm]') aren't supported, only a fixed UTC \
8577 offset like '+01:00'"
8578 .into(),
8579 ));
8580 }
8581 let (nanos_of_day, offset_seconds) = temporal::parse_time(s).ok_or_else(|| {
8582 QueryError::Type(format!("'{s}' isn't a time string MarsDB can parse"))
8583 })?;
8584 return Ok(Value::Property(PropertyValue::Time {
8585 nanos_of_day,
8586 offset_seconds,
8587 }));
8588 }
8589 if let Value::Map(m) = arg {
8590 const ALLOWED: &[&str] = &[
8591 "hour",
8592 "minute",
8593 "second",
8594 "millisecond",
8595 "microsecond",
8596 "nanosecond",
8597 "timezone",
8598 "time",
8599 ];
8600 if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
8601 return Err(QueryError::Type(format!(
8602 "time({{...}}) key '{bad}' isn't a recognized field"
8603 )));
8604 }
8605 let (hour, minute, second, nanos, zone) = clock_fields_from_map(m, None)?;
8606 let offset_seconds = match zone {
8607 None => 0,
8608 Some((_, o)) if !m.contains_key("timezone") => o,
8617 Some((temporal::TzId::Offset(o), _)) => o,
8618 Some((temporal::TzId::Named(name), _)) => {
8619 return Err(QueryError::Type(format!(
8620 "'timezone': '{name}' looks like a named timezone (e.g. 'Europe/Stockholm') -- TIME has \
8621 no calendar date to resolve a named zone's DST-dependent offset against, only a fixed \
8622 UTC offset like '+01:00' is supported"
8623 )));
8624 }
8625 };
8626 let nanos_of_day = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
8627 .ok_or_else(|| QueryError::Type("time({...}) has an out-of-range field".into()))?;
8628 return Ok(Value::Property(PropertyValue::Time {
8629 nanos_of_day,
8630 offset_seconds,
8631 }));
8632 }
8633 Err(QueryError::Type(format!(
8634 "time() doesn't support this argument: {arg:?}"
8635 )))
8636}
8637
8638fn timezone_value_to_tzid(v: &Value) -> Result<temporal::TzId, QueryError> {
8646 let s = as_arith_str(v).ok_or_else(|| {
8647 QueryError::Type(
8648 "'timezone' must be a string offset or IANA zone name, e.g. '+01:00' or \
8649 'Europe/Stockholm'"
8650 .into(),
8651 )
8652 })?;
8653 if let Some(offset) = temporal::parse_offset_seconds(s) {
8654 return Ok(temporal::TzId::Offset(offset));
8655 }
8656 if temporal::parse_timezone_name(s).is_some() {
8657 return Ok(temporal::TzId::Named(s.to_string()));
8658 }
8659 Err(QueryError::Type(format!(
8660 "'timezone': '{s}' isn't a valid UTC offset or a recognized IANA zone name"
8661 )))
8662}
8663
8664fn local_date_time_builtin(
8668 args: &[Value],
8669 now: temporal::NowSnapshot,
8670) -> Result<Value, QueryError> {
8671 if args.len() > 1 {
8672 return Err(QueryError::Semantic(format!(
8673 "localdatetime() expects zero or one argument, got {}",
8674 args.len()
8675 )));
8676 }
8677 let Some(arg) = args.first() else {
8678 return Ok(Value::Property(PropertyValue::LocalDateTime {
8679 epoch_seconds: now.epoch_seconds,
8680 nanos: now.nanos,
8681 }));
8682 };
8683 if matches!(arg, Value::Null) {
8684 return Ok(Value::Null);
8685 }
8686 if let Value::Property(PropertyValue::LocalDateTime {
8687 epoch_seconds,
8688 nanos,
8689 }) = arg
8690 {
8691 return Ok(Value::Property(PropertyValue::LocalDateTime {
8692 epoch_seconds: *epoch_seconds,
8693 nanos: *nanos,
8694 }));
8695 }
8696 if matches!(arg, Value::Property(PropertyValue::DateTime { .. })) {
8700 let epoch_day = extract_date_base_epoch_day("localdatetime() argument", arg)?;
8701 let year = temporal::date_component(epoch_day, "year").unwrap() as i32;
8702 let month = temporal::date_component(epoch_day, "month").unwrap() as u32;
8703 let day = temporal::date_component(epoch_day, "day").unwrap() as u32;
8704 let (hour, minute, second, nanos, _) = extract_time_base("localdatetime() argument", arg)?;
8705 let (epoch_seconds, nanos) =
8706 temporal::local_date_time_from_fields(temporal::CalendarDateTime {
8707 year,
8708 month,
8709 day,
8710 hour,
8711 minute,
8712 second,
8713 nanos,
8714 })
8715 .ok_or_else(|| {
8716 QueryError::Type("localdatetime() argument has an out-of-range field".into())
8717 })?;
8718 return Ok(Value::Property(PropertyValue::LocalDateTime {
8719 epoch_seconds,
8720 nanos,
8721 }));
8722 }
8723 if let Some(s) = as_arith_str(arg) {
8724 let (epoch_seconds, nanos) = temporal::parse_local_date_time(s).ok_or_else(|| {
8725 QueryError::Type(format!(
8726 "'{s}' isn't a local date-time string MarsDB can parse"
8727 ))
8728 })?;
8729 return Ok(Value::Property(PropertyValue::LocalDateTime {
8730 epoch_seconds,
8731 nanos,
8732 }));
8733 }
8734 if let Value::Map(m) = arg {
8735 let (year, month, day) =
8736 calendar_fields_from_map("localdatetime", m, DATE_TIME_ALLOWED_KEYS)?;
8737 let (hour, minute, second, nanos, _) = clock_fields_from_map(m, None)?;
8738 let (epoch_seconds, nanos) =
8739 temporal::local_date_time_from_fields(temporal::CalendarDateTime {
8740 year,
8741 month,
8742 day,
8743 hour,
8744 minute,
8745 second,
8746 nanos,
8747 })
8748 .ok_or_else(|| {
8749 QueryError::Type("localdatetime({...}) has an out-of-range field".into())
8750 })?;
8751 return Ok(Value::Property(PropertyValue::LocalDateTime {
8752 epoch_seconds,
8753 nanos,
8754 }));
8755 }
8756 Err(QueryError::Type(format!(
8757 "localdatetime() doesn't support this argument: {arg:?}"
8758 )))
8759}
8760
8761const DATE_TIME_ALLOWED_KEYS: &[&str] = &[
8762 "year",
8763 "month",
8764 "day",
8765 "week",
8766 "dayOfWeek",
8767 "ordinalDay",
8768 "quarter",
8769 "dayOfQuarter",
8770 "hour",
8771 "minute",
8772 "second",
8773 "millisecond",
8774 "microsecond",
8775 "nanosecond",
8776 "timezone",
8777 "date",
8778 "time",
8779 "datetime",
8780];
8781
8782fn date_time_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
8789 if args.len() > 1 {
8790 return Err(QueryError::Semantic(format!(
8791 "datetime() expects zero or one argument, got {}",
8792 args.len()
8793 )));
8794 }
8795 let Some(arg) = args.first() else {
8796 return Ok(Value::Property(PropertyValue::DateTime {
8797 epoch_seconds: now.epoch_seconds,
8798 nanos: now.nanos,
8799 zone: GraphTzId::Offset(0),
8800 }));
8801 };
8802 if matches!(arg, Value::Null) {
8803 return Ok(Value::Null);
8804 }
8805 if let Value::Property(PropertyValue::DateTime {
8806 epoch_seconds,
8807 nanos,
8808 zone,
8809 }) = arg
8810 {
8811 return Ok(Value::Property(PropertyValue::DateTime {
8812 epoch_seconds: *epoch_seconds,
8813 nanos: *nanos,
8814 zone: zone.clone(),
8815 }));
8816 }
8817 if let Value::Property(PropertyValue::LocalDateTime {
8821 epoch_seconds,
8822 nanos,
8823 }) = arg
8824 {
8825 return Ok(Value::Property(PropertyValue::DateTime {
8826 epoch_seconds: *epoch_seconds,
8827 nanos: *nanos,
8828 zone: GraphTzId::Offset(0),
8829 }));
8830 }
8831 if let Some(s) = as_arith_str(arg) {
8832 let (epoch_seconds, nanos, zone) = temporal::parse_date_time(s).ok_or_else(|| {
8833 QueryError::Type(format!("'{s}' isn't a date-time string MarsDB can parse"))
8834 })?;
8835 return Ok(Value::Property(PropertyValue::DateTime {
8836 epoch_seconds,
8837 nanos,
8838 zone: tz_to_graph(zone),
8839 }));
8840 }
8841 if let Value::Map(m) = arg {
8842 let (year, month, day) = calendar_fields_from_map("datetime", m, DATE_TIME_ALLOWED_KEYS)?;
8843 let epoch_day = temporal::epoch_day_from_ymd(year, month, day);
8844 let (hour, minute, second, nanos, zone) = clock_fields_from_map(m, epoch_day)?;
8845 let zone = zone.map_or(temporal::TzId::Offset(0), |(z, _)| z);
8846 let (epoch_seconds, nanos) = temporal::date_time_from_fields(
8847 temporal::CalendarDateTime {
8848 year,
8849 month,
8850 day,
8851 hour,
8852 minute,
8853 second,
8854 nanos,
8855 },
8856 &zone,
8857 )
8858 .ok_or_else(|| QueryError::Type("datetime({...}) has an out-of-range field".into()))?;
8859 return Ok(Value::Property(PropertyValue::DateTime {
8860 epoch_seconds,
8861 nanos,
8862 zone: tz_to_graph(zone),
8863 }));
8864 }
8865 Err(QueryError::Type(format!(
8866 "datetime() doesn't support this argument: {arg:?}"
8867 )))
8868}
8869
8870fn between_operand(name: &str, v: &Value) -> Result<BetweenOperand, QueryError> {
8881 match v {
8882 Value::Property(PropertyValue::Date(d)) => Ok((Some(*d), None, None)),
8883 Value::Property(PropertyValue::LocalTime(n)) => Ok((None, Some(*n), None)),
8884 Value::Property(PropertyValue::Time {
8885 nanos_of_day,
8886 offset_seconds,
8887 }) => Ok((
8888 None,
8889 Some(*nanos_of_day),
8890 Some(temporal::TzId::Offset(*offset_seconds)),
8891 )),
8892 Value::Property(PropertyValue::LocalDateTime {
8893 epoch_seconds,
8894 nanos,
8895 }) => {
8896 let (d, n) = temporal::split_epoch_seconds(*epoch_seconds);
8897 Ok((Some(d), Some(n + *nanos as i64), None))
8898 }
8899 Value::Property(PropertyValue::DateTime {
8900 epoch_seconds,
8901 nanos,
8902 zone,
8903 }) => {
8904 let tz = tz_from_graph(zone);
8905 let offset_seconds = temporal::resolve_offset(&tz, *epoch_seconds);
8906 let local = epoch_seconds + offset_seconds as i64;
8907 let (d, n) = temporal::split_epoch_seconds(local);
8908 Ok((Some(d), Some(n + *nanos as i64), Some(tz)))
8909 }
8910 other => Err(QueryError::Type(format!(
8911 "{name}() needs a Date, LocalTime, Time, LocalDateTime, or DateTime, got {other:?}"
8912 ))),
8913 }
8914}
8915
8916type BetweenOperand = (Option<i32>, Option<i64>, Option<temporal::TzId>);
8918
8919type BetweenFn = fn(
8924 Option<i32>,
8925 Option<i64>,
8926 Option<&temporal::TzId>,
8927 Option<i32>,
8928 Option<i64>,
8929 Option<&temporal::TzId>,
8930) -> temporal::DurationParts;
8931
8932fn duration_between_builtin(name: &str, args: &[Value], f: BetweenFn) -> Result<Value, QueryError> {
8937 if args.len() != 2 {
8938 return Err(QueryError::Semantic(format!(
8939 "{name}() expects exactly two arguments, got {}",
8940 args.len()
8941 )));
8942 }
8943 if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
8944 return Ok(Value::Null);
8945 }
8946 let (a_date, a_time, a_zone) = between_operand(name, &args[0])?;
8947 let (b_date, b_time, b_zone) = between_operand(name, &args[1])?;
8948 Ok(duration_value(f(
8949 a_date,
8950 a_time,
8951 a_zone.as_ref(),
8952 b_date,
8953 b_time,
8954 b_zone.as_ref(),
8955 )))
8956}
8957
8958type TruncateArgs<'a> = (&'a str, &'a Value, Option<&'a BTreeMap<String, Value>>);
8963
8964fn parse_truncate_args<'a>(name: &str, args: &'a [Value]) -> Result<TruncateArgs<'a>, QueryError> {
8965 if args.len() < 2 || args.len() > 3 {
8966 return Err(QueryError::Semantic(format!(
8967 "{name}() expects 2 or 3 arguments, got {}",
8968 args.len()
8969 )));
8970 }
8971 let unit = as_arith_str(&args[0]).ok_or_else(|| {
8972 QueryError::Type(format!("{name}()'s first argument must be a unit string"))
8973 })?;
8974 let map = match args.get(2) {
8975 None | Some(Value::Null) => None,
8976 Some(Value::Map(m)) => Some(m),
8977 Some(other) => {
8978 return Err(QueryError::Type(format!(
8979 "{name}()'s third argument must be a map, got {other:?}"
8980 )))
8981 }
8982 };
8983 Ok((unit, &args[1], map))
8984}
8985
8986fn apply_date_overrides(
8995 base_epoch_day: i32,
8996 map: Option<&BTreeMap<String, Value>>,
8997) -> Result<i32, QueryError> {
8998 let base_y = temporal::date_component(base_epoch_day, "year").unwrap();
8999 let base_m = temporal::date_component(base_epoch_day, "month").unwrap();
9000 let base_d = temporal::date_component(base_epoch_day, "day").unwrap();
9001 let Some(m) = map else {
9002 return Ok(base_epoch_day);
9003 };
9004 let year_raw = int_field(m, "year", base_y)?;
9005 let year = i32::try_from(year_raw)
9006 .map_err(|_| QueryError::Type(format!("'year' is out of range: {year_raw}")))?;
9007 let month_raw = int_field(m, "month", base_m)?;
9008 let month = u32::try_from(month_raw)
9009 .map_err(|_| QueryError::Type(format!("'month' is out of range: {month_raw}")))?;
9010 let day_raw = int_field(m, "day", base_d)?;
9011 let day = u32::try_from(day_raw)
9012 .map_err(|_| QueryError::Type(format!("'day' is out of range: {day_raw}")))?;
9013 let result = temporal::epoch_day_from_ymd(year, month, day).ok_or_else(|| {
9014 QueryError::Type(format!(
9015 "{year:04}-{month:02}-{day:02} isn't a valid calendar date"
9016 ))
9017 })?;
9018 match m.get("dayOfWeek") {
9019 None => Ok(result),
9020 Some(v) => {
9021 let dow = value_as_i64(v)
9022 .ok_or_else(|| QueryError::Type("'dayOfWeek' must be an integer".into()))?;
9023 temporal::set_iso_weekday(result, dow).ok_or_else(|| {
9024 QueryError::Type(format!(
9025 "'dayOfWeek' must be 1..7 (Monday..Sunday), got {dow}"
9026 ))
9027 })
9028 }
9029 }
9030}
9031
9032fn apply_time_overrides(
9037 base_nanos_of_day: i64,
9038 map: Option<&BTreeMap<String, Value>>,
9039) -> Result<i64, QueryError> {
9040 let base_h = temporal::local_time_component(base_nanos_of_day, "hour").unwrap();
9041 let base_min = temporal::local_time_component(base_nanos_of_day, "minute").unwrap();
9042 let base_s = temporal::local_time_component(base_nanos_of_day, "second").unwrap();
9043 let base_ns = temporal::local_time_component(base_nanos_of_day, "nanosecond").unwrap();
9044 let Some(m) = map else {
9045 return Ok(base_nanos_of_day);
9046 };
9047 let nanos = sub_second_nanos_from_map(base_ns, m)?;
9048 let hour = int_field(m, "hour", base_h)?;
9049 let minute = int_field(m, "minute", base_min)?;
9050 let second = int_field(m, "second", base_s)?;
9051 temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
9052 .ok_or_else(|| QueryError::Type("truncate(...)'s map has an out-of-range field".into()))
9053}
9054
9055fn validate_truncate_map_keys(
9062 name: &str,
9063 map: Option<&BTreeMap<String, Value>>,
9064 allowed: &[&str],
9065) -> Result<(), QueryError> {
9066 let Some(m) = map else { return Ok(()) };
9067 if let Some(bad) = m.keys().find(|k| !allowed.contains(&k.as_str())) {
9068 return Err(QueryError::Type(format!(
9069 "{name}(...)'s map has an unrecognized field '{bad}'"
9070 )));
9071 }
9072 Ok(())
9073}
9074
9075fn date_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9076 let (unit, other, map) = parse_truncate_args("date.truncate", args)?;
9077 validate_truncate_map_keys("date.truncate", map, &["year", "month", "day", "dayOfWeek"])?;
9078 if matches!(other, Value::Null) {
9079 return Ok(Value::Null);
9080 }
9081 let (base_date, _, _) = between_operand("date.truncate", other)?;
9082 let base_date = base_date.ok_or_else(|| {
9083 QueryError::Type(
9084 "date.truncate() needs a value with a calendar date (Date, LocalDateTime, or DateTime)"
9085 .into(),
9086 )
9087 })?;
9088 let truncated = temporal::truncate_date_unit(base_date, unit).ok_or_else(|| {
9089 QueryError::Type(format!(
9090 "date.truncate(): '{unit}' isn't a recognized date unit"
9091 ))
9092 })?;
9093 Ok(Value::Property(PropertyValue::Date(apply_date_overrides(
9094 truncated, map,
9095 )?)))
9096}
9097
9098const TIME_TRUNCATE_MAP_KEYS: &[&str] = &[
9099 "hour",
9100 "minute",
9101 "second",
9102 "millisecond",
9103 "microsecond",
9104 "nanosecond",
9105];
9106
9107fn local_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9108 let (unit, other, map) = parse_truncate_args("localtime.truncate", args)?;
9109 validate_truncate_map_keys("localtime.truncate", map, TIME_TRUNCATE_MAP_KEYS)?;
9110 if matches!(other, Value::Null) {
9111 return Ok(Value::Null);
9112 }
9113 let (_, base_time, _) = between_operand("localtime.truncate", other)?;
9114 let base_time = base_time.ok_or_else(|| {
9115 QueryError::Type(
9116 "localtime.truncate() needs a value with a time-of-day (LocalTime, Time, \
9117 LocalDateTime, or DateTime)"
9118 .into(),
9119 )
9120 })?;
9121 let truncated = temporal::truncate_time_unit(base_time, unit).ok_or_else(|| {
9122 QueryError::Type(format!(
9123 "localtime.truncate(): '{unit}' isn't a recognized time unit"
9124 ))
9125 })?;
9126 Ok(Value::Property(PropertyValue::LocalTime(
9127 apply_time_overrides(truncated, map)?,
9128 )))
9129}
9130
9131fn time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9132 let (unit, other, map) = parse_truncate_args("time.truncate", args)?;
9133 validate_truncate_map_keys(
9134 "time.truncate",
9135 map,
9136 &[
9137 "hour",
9138 "minute",
9139 "second",
9140 "millisecond",
9141 "microsecond",
9142 "nanosecond",
9143 "timezone",
9144 ],
9145 )?;
9146 if matches!(other, Value::Null) {
9147 return Ok(Value::Null);
9148 }
9149 let (_, base_time, base_offset) = between_operand("time.truncate", other)?;
9150 let base_time = base_time.ok_or_else(|| {
9151 QueryError::Type(
9152 "time.truncate() needs a value with a time-of-day (LocalTime, Time, LocalDateTime, \
9153 or DateTime)"
9154 .into(),
9155 )
9156 })?;
9157 let truncated = temporal::truncate_time_unit(base_time, unit).ok_or_else(|| {
9158 QueryError::Type(format!(
9159 "time.truncate(): '{unit}' isn't a recognized time unit"
9160 ))
9161 })?;
9162 let nanos_of_day = apply_time_overrides(truncated, map)?;
9163 let offset_seconds = match map.and_then(|m| m.get("timezone")) {
9164 Some(v) => match timezone_value_to_tzid(v)? {
9165 temporal::TzId::Offset(o) => o,
9166 temporal::TzId::Named(name) => {
9167 return Err(QueryError::Type(format!(
9168 "'timezone': '{name}' looks like a named timezone (e.g. 'Europe/Stockholm') -- TIME has \
9169 no calendar date to resolve a named zone's DST-dependent offset against, only a fixed \
9170 UTC offset like '+01:00' is supported"
9171 )));
9172 }
9173 },
9174 None => match base_offset {
9175 Some(temporal::TzId::Offset(o)) => o,
9176 _ => 0,
9177 },
9178 };
9179 Ok(Value::Property(PropertyValue::Time {
9180 nanos_of_day,
9181 offset_seconds,
9182 }))
9183}
9184
9185fn truncate_date_time(base_date: i32, base_time: i64, unit: &str) -> Option<(i32, i64)> {
9193 if let Some(d) = temporal::truncate_date_unit(base_date, unit) {
9194 Some((d, 0))
9195 } else {
9196 temporal::truncate_time_unit(base_time, unit).map(|t| (base_date, t))
9197 }
9198}
9199
9200fn local_date_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9201 let (unit, other, map) = parse_truncate_args("localdatetime.truncate", args)?;
9202 validate_truncate_map_keys(
9203 "localdatetime.truncate",
9204 map,
9205 &[
9206 "year",
9207 "month",
9208 "day",
9209 "dayOfWeek",
9210 "hour",
9211 "minute",
9212 "second",
9213 "millisecond",
9214 "microsecond",
9215 "nanosecond",
9216 ],
9217 )?;
9218 if matches!(other, Value::Null) {
9219 return Ok(Value::Null);
9220 }
9221 let (base_date, base_time, _) = between_operand("localdatetime.truncate", other)?;
9222 let base_date = base_date.ok_or_else(|| {
9223 QueryError::Type(
9224 "localdatetime.truncate() needs a value with a calendar date (Date, LocalDateTime, \
9225 or DateTime)"
9226 .into(),
9227 )
9228 })?;
9229 let (trunc_date, trunc_time) = truncate_date_time(base_date, base_time.unwrap_or(0), unit)
9230 .ok_or_else(|| {
9231 QueryError::Type(format!(
9232 "localdatetime.truncate(): '{unit}' isn't a recognized unit"
9233 ))
9234 })?;
9235 let final_date = apply_date_overrides(trunc_date, map)?;
9236 let final_time = apply_time_overrides(trunc_time, map)?;
9237 let (epoch_seconds, nanos) = temporal::combine_date_and_time(final_date, final_time);
9238 Ok(Value::Property(PropertyValue::LocalDateTime {
9239 epoch_seconds,
9240 nanos,
9241 }))
9242}
9243
9244fn date_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9245 let (unit, other, map) = parse_truncate_args("datetime.truncate", args)?;
9246 validate_truncate_map_keys(
9247 "datetime.truncate",
9248 map,
9249 &[
9250 "year",
9251 "month",
9252 "day",
9253 "dayOfWeek",
9254 "hour",
9255 "minute",
9256 "second",
9257 "millisecond",
9258 "microsecond",
9259 "nanosecond",
9260 "timezone",
9261 ],
9262 )?;
9263 if matches!(other, Value::Null) {
9264 return Ok(Value::Null);
9265 }
9266 let (base_date, base_time, base_offset) = between_operand("datetime.truncate", other)?;
9267 let base_date = base_date.ok_or_else(|| {
9268 QueryError::Type(
9269 "datetime.truncate() needs a value with a calendar date (Date, LocalDateTime, or \
9270 DateTime)"
9271 .into(),
9272 )
9273 })?;
9274 let (trunc_date, trunc_time) = truncate_date_time(base_date, base_time.unwrap_or(0), unit)
9275 .ok_or_else(|| {
9276 QueryError::Type(format!(
9277 "datetime.truncate(): '{unit}' isn't a recognized unit"
9278 ))
9279 })?;
9280 let final_date = apply_date_overrides(trunc_date, map)?;
9281 let final_time = apply_time_overrides(trunc_time, map)?;
9282 let zone = match map.and_then(|m| m.get("timezone")) {
9283 Some(v) => timezone_value_to_tzid(v)?,
9284 None => base_offset.unwrap_or(temporal::TzId::Offset(0)),
9285 };
9286 let calendar = temporal::CalendarDateTime {
9287 year: temporal::date_component(final_date, "year").unwrap() as i32,
9288 month: temporal::date_component(final_date, "month").unwrap() as u32,
9289 day: temporal::date_component(final_date, "day").unwrap() as u32,
9290 hour: temporal::local_time_component(final_time, "hour").unwrap(),
9291 minute: temporal::local_time_component(final_time, "minute").unwrap(),
9292 second: temporal::local_time_component(final_time, "second").unwrap(),
9293 nanos: temporal::local_time_component(final_time, "nanosecond").unwrap(),
9294 };
9295 let (epoch_seconds, nanos) =
9296 temporal::date_time_from_fields(calendar, &zone).ok_or_else(|| {
9297 QueryError::Type("datetime.truncate() produced an out-of-range value".into())
9298 })?;
9299 Ok(Value::Property(PropertyValue::DateTime {
9300 epoch_seconds,
9301 nanos,
9302 zone: tz_to_graph(zone),
9303 }))
9304}
9305
9306fn value_as_i64(v: &Value) -> Option<i64> {
9307 match v {
9308 Value::Property(PropertyValue::Int(i)) | Value::Literal(Literal::Int(i)) => Some(*i),
9309 _ => None,
9310 }
9311}
9312
9313fn value_as_f64(v: &Value) -> Option<f64> {
9314 match as_arith_num(v)? {
9315 ArithNum::Int(i) => Some(i as f64),
9316 ArithNum::Float(f) => Some(f),
9317 }
9318}
9319
9320fn is_temporal_property_value(pv: &PropertyValue) -> bool {
9335 matches!(
9336 pv,
9337 PropertyValue::Date(_)
9338 | PropertyValue::Duration { .. }
9339 | PropertyValue::LocalTime(_)
9340 | PropertyValue::Time { .. }
9341 | PropertyValue::LocalDateTime { .. }
9342 | PropertyValue::DateTime { .. }
9343 )
9344}
9345
9346fn property_of_value(v: &Value, prop: &str) -> Result<Value, QueryError> {
9356 match v {
9357 Value::Node(n) => Ok(n
9358 .props
9359 .get(prop)
9360 .cloned()
9361 .map(property_value_to_value)
9362 .unwrap_or(Value::Null)),
9363 Value::Edge(e) => Ok(e
9364 .props
9365 .get(prop)
9366 .cloned()
9367 .map(property_value_to_value)
9368 .unwrap_or(Value::Null)),
9369 Value::Map(m) => Ok(m.get(prop).cloned().unwrap_or(Value::Null)),
9370 Value::Null => Ok(Value::Null),
9371 Value::Property(PropertyValue::Null) => Ok(Value::Null),
9372 Value::Property(pv) => match temporal_component(pv, prop) {
9373 Some(component) => Ok(Value::Property(component)),
9374 None if is_temporal_property_value(pv) => Ok(Value::Null),
9375 None => Err(QueryError::Type(
9376 "property access requires a node, relationship, map, or temporal value".into(),
9377 )),
9378 },
9379 Value::List(_) | Value::Path(_) => Err(QueryError::Type(
9380 "property access requires a node, relationship, map, or temporal value, not a list \
9381 or path"
9382 .into(),
9383 )),
9384 Value::Literal(_) => Err(QueryError::Type(
9385 "property access requires a node, relationship, map, or temporal value".into(),
9386 )),
9387 }
9388}
9389
9390fn temporal_component(pv: &PropertyValue, prop: &str) -> Option<PropertyValue> {
9391 match pv {
9392 PropertyValue::Date(d) => temporal::date_component(*d, prop).map(PropertyValue::Int),
9393 PropertyValue::Duration {
9394 months,
9395 days,
9396 seconds,
9397 nanos,
9398 } => temporal::duration_component(*months, *days, *seconds, *nanos, prop)
9399 .map(PropertyValue::Int),
9400 PropertyValue::LocalTime(nanos_of_day) => {
9401 temporal::local_time_component(*nanos_of_day, prop).map(PropertyValue::Int)
9402 }
9403 PropertyValue::Time {
9404 nanos_of_day,
9405 offset_seconds,
9406 } => time_component(*nanos_of_day, *offset_seconds, prop),
9407 PropertyValue::LocalDateTime {
9408 epoch_seconds,
9409 nanos,
9410 } => date_time_component(*epoch_seconds, *nanos, None, prop),
9411 PropertyValue::DateTime {
9412 epoch_seconds,
9413 nanos,
9414 zone,
9415 } => date_time_component(*epoch_seconds, *nanos, Some(&tz_from_graph(zone)), prop),
9416 _ => None,
9417 }
9418}
9419
9420fn time_component(nanos_of_day: i64, offset_seconds: i32, prop: &str) -> Option<PropertyValue> {
9424 match prop {
9425 "timezone" | "offset" => Some(PropertyValue::String(temporal::format_offset(
9426 offset_seconds,
9427 ))),
9428 "offsetSeconds" => Some(PropertyValue::Int(offset_seconds as i64)),
9429 "offsetMinutes" => Some(PropertyValue::Int(offset_seconds as i64 / 60)),
9430 _ => temporal::local_time_component(nanos_of_day, prop).map(PropertyValue::Int),
9431 }
9432}
9433
9434fn date_time_component(
9449 epoch_seconds: i64,
9450 nanos: i32,
9451 zone: Option<&temporal::TzId>,
9452 prop: &str,
9453) -> Option<PropertyValue> {
9454 if let Some(zone) = zone {
9455 let offset_seconds = temporal::resolve_offset(zone, epoch_seconds);
9456 match prop {
9457 "timezone" => {
9464 let text = match zone {
9465 temporal::TzId::Named(name) => name.clone(),
9466 temporal::TzId::Offset(_) => temporal::format_offset(offset_seconds),
9467 };
9468 return Some(PropertyValue::String(text));
9469 }
9470 "offset" => {
9471 return Some(PropertyValue::String(temporal::format_offset(
9472 offset_seconds,
9473 )))
9474 }
9475 "offsetSeconds" => return Some(PropertyValue::Int(offset_seconds as i64)),
9476 "offsetMinutes" => return Some(PropertyValue::Int(offset_seconds as i64 / 60)),
9477 "epochSeconds" => return Some(PropertyValue::Int(epoch_seconds)),
9478 "epochMillis" => {
9479 return Some(PropertyValue::Int(
9480 temporal::epoch_seconds_and_millis(epoch_seconds, nanos).1,
9481 ))
9482 }
9483 _ => {}
9484 }
9485 }
9486 let offset_seconds = zone.map_or(0, |z| temporal::resolve_offset(z, epoch_seconds));
9487 let local_epoch_seconds = epoch_seconds + offset_seconds as i64;
9488 temporal::date_time_calendar_component(local_epoch_seconds, prop)
9489 .or_else(|| temporal::date_time_clock_component(local_epoch_seconds, nanos, prop))
9490 .map(PropertyValue::Int)
9491}
9492
9493fn apply_order_by(
9498 rows: Vec<Vec<Value>>,
9499 columns: &[String],
9500 order_by: &[(ReturnExpr, SortDir)],
9501 items: Option<&[ReturnItem]>,
9502 skip: Option<i64>,
9503 limit: Option<i64>,
9504) -> Result<Vec<Vec<Value>>, QueryError> {
9505 let order_by_col: Vec<Option<usize>> = order_by
9517 .iter()
9518 .map(|(expr, _)| {
9519 columns
9520 .iter()
9521 .position(|c| *c == default_column_name(expr, 0))
9522 .or_else(|| {
9523 items.and_then(|items| items.iter().position(|item| item.expr == *expr))
9524 })
9525 })
9526 .collect();
9527 let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(rows.len());
9528 for row in rows {
9529 let row_map: HashMap<String, Value> =
9530 columns.iter().cloned().zip(row.iter().cloned()).collect();
9531 let keys = order_by
9532 .iter()
9533 .zip(&order_by_col)
9534 .map(|((expr, _), col)| match col {
9535 Some(i) => Ok(row[*i].clone()),
9536 None => eval_projected_expr(expr, &row_map),
9537 })
9538 .collect::<Result<Vec<_>, _>>()?;
9539 keyed.push((keys, row));
9540 }
9541 Ok(top_k_by(keyed, order_by, skip, limit)
9542 .into_iter()
9543 .map(|(_, row)| row)
9544 .collect())
9545}
9546
9547fn eval_projected_expr(
9553 expr: &ReturnExpr,
9554 row: &HashMap<String, Value>,
9555) -> Result<Value, QueryError> {
9556 match expr {
9557 ReturnExpr::Var(name) => row
9558 .get(name)
9559 .cloned()
9560 .ok_or_else(|| QueryError::UnboundVariable(name.clone())),
9561 ReturnExpr::Prop(pa) => {
9562 let base = row
9563 .get(&pa.var)
9564 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
9565 match base {
9566 Value::Map(m) => Ok(m.get(&pa.prop).cloned().unwrap_or(Value::Null)),
9567 Value::Node(n) => Ok(match n.props.get(&pa.prop).cloned() {
9568 Some(PropertyValue::Null) | None => Value::Null,
9569 Some(v) => property_value_to_value(v),
9570 }),
9571 Value::Edge(e) => Ok(match e.props.get(&pa.prop).cloned() {
9572 Some(PropertyValue::Null) | None => Value::Null,
9573 Some(v) => property_value_to_value(v),
9574 }),
9575 Value::Property(pv) => Ok(match temporal_component(pv, &pa.prop) {
9580 Some(component) => Value::Property(component),
9581 None => Value::Null,
9582 }),
9583 _ => Ok(Value::Null),
9584 }
9585 }
9586 ReturnExpr::PropOf(base, prop) => {
9587 let v = eval_projected_expr(base, row)?;
9588 property_of_value(&v, prop)
9589 }
9590 ReturnExpr::Lit(lit) => Ok(match lit {
9591 Literal::Null => Value::Null,
9592 other => Value::Literal(other.clone()),
9593 }),
9594 ReturnExpr::Call { name, args, .. } => {
9595 if is_aggregate_name(name) {
9602 return Err(QueryError::Semantic(format!(
9603 "aggregate function '{name}' can only be used as a return item's top-level expression"
9604 )));
9605 }
9606 let arg_values = args
9607 .iter()
9608 .map(|a| eval_projected_expr(a, row))
9609 .collect::<Result<Vec<_>, _>>()?;
9610 call_builtin(name, &arg_values, temporal::capture_now())
9618 }
9619 ReturnExpr::CountStar => Err(QueryError::Semantic(
9620 "count(*) can only be used as a return item's top-level expression".into(),
9621 )),
9622 ReturnExpr::Case { test, whens, else_ } => {
9623 let test_value = match test {
9624 Some(t) => Some(eval_projected_expr(t, row)?),
9625 None => None,
9626 };
9627 for (when, then) in whens {
9628 let when_value = eval_projected_expr(when, row)?;
9629 let matched = match &test_value {
9630 Some(tv) => value_eq(tv, &when_value),
9631 None => matches!(when_value, Value::Literal(Literal::Bool(true))),
9632 };
9633 if matched {
9634 return eval_projected_expr(then, row);
9635 }
9636 }
9637 match else_ {
9638 Some(e) => eval_projected_expr(e, row),
9639 None => Ok(Value::Null),
9640 }
9641 }
9642 ReturnExpr::Arith(l, op, r) => {
9643 let lv = eval_projected_expr(l, row)?;
9644 let rv = eval_projected_expr(r, row)?;
9645 apply_arith(*op, &lv, &rv)
9646 }
9647 ReturnExpr::Neg(e) => {
9648 let v = eval_projected_expr(e, row)?;
9649 apply_neg(&v)
9650 }
9651 ReturnExpr::ListLit(items) => Ok(Value::List(
9652 items
9653 .iter()
9654 .map(|item| eval_projected_expr(item, row))
9655 .collect::<Result<Vec<_>, _>>()?,
9656 )),
9657 ReturnExpr::Index(base, index) => {
9658 let base_v = eval_projected_expr(base, row)?;
9659 let index_v = eval_projected_expr(index, row)?;
9660 apply_index(&base_v, &index_v)
9661 }
9662 ReturnExpr::Slice(base, start, end) => {
9663 let base_v = eval_projected_expr(base, row)?;
9664 let start_v = start
9665 .as_deref()
9666 .map(|s| eval_projected_expr(s, row))
9667 .transpose()?;
9668 let end_v = end
9669 .as_deref()
9670 .map(|e| eval_projected_expr(e, row))
9671 .transpose()?;
9672 apply_slice(&base_v, start_v.as_ref(), end_v.as_ref())
9673 }
9674 ReturnExpr::ListComp {
9675 var,
9676 source,
9677 where_clause,
9678 project,
9679 } => {
9680 let source_v = eval_projected_expr(source, row)?;
9681 let items = match source_v {
9682 Value::List(items) => items,
9683 Value::Null => return Ok(Value::Null),
9684 other => {
9685 return Err(QueryError::Type(format!(
9686 "list comprehension source must be a list, got {other:?}"
9687 )))
9688 }
9689 };
9690 let mut result = Vec::with_capacity(items.len());
9691 for item in items {
9692 let mut scoped_row = row.clone();
9693 scoped_row.insert(var.clone(), item.clone());
9694 let keep = match where_clause {
9695 Some(w) => value_to_bool3(&eval_projected_expr(w, &scoped_row)?)? == Some(true),
9696 None => true,
9697 };
9698 if !keep {
9699 continue;
9700 }
9701 result.push(match project {
9702 Some(p) => eval_projected_expr(p, &scoped_row)?,
9703 None => item,
9704 });
9705 }
9706 Ok(Value::List(result))
9707 }
9708 ReturnExpr::Quantifier {
9709 kind,
9710 var,
9711 source,
9712 where_clause,
9713 } => {
9714 let source_v = eval_projected_expr(source, row)?;
9715 let items = match source_v {
9716 Value::List(items) => items,
9717 Value::Null => return Ok(Value::Null),
9718 other => {
9719 return Err(QueryError::Type(format!(
9720 "quantifier source must be a list, got {other:?}"
9721 )))
9722 }
9723 };
9724 let mut preds = Vec::with_capacity(items.len());
9725 for item in &items {
9726 let mut scoped_row = row.clone();
9727 scoped_row.insert(var.clone(), item.clone());
9728 preds.push(match where_clause {
9729 Some(w) => value_to_bool3(&eval_projected_expr(w, &scoped_row)?)?,
9730 None => item_truthy(item),
9731 });
9732 }
9733 Ok(match eval_quantifier(*kind, &preds) {
9734 Some(b) => Value::Literal(Literal::Bool(b)),
9735 None => Value::Null,
9736 })
9737 }
9738 ReturnExpr::MapLit(entries) => {
9739 let mut map = BTreeMap::new();
9740 for (k, v) in entries {
9741 map.insert(k.clone(), eval_projected_expr(v, row)?);
9742 }
9743 Ok(Value::Map(map))
9744 }
9745 ReturnExpr::And(l, r) => Ok(bool3_to_value(and3(
9746 value_to_bool3(&eval_projected_expr(l, row)?)?,
9747 value_to_bool3(&eval_projected_expr(r, row)?)?,
9748 ))),
9749 ReturnExpr::Or(l, r) => Ok(bool3_to_value(or3(
9750 value_to_bool3(&eval_projected_expr(l, row)?)?,
9751 value_to_bool3(&eval_projected_expr(r, row)?)?,
9752 ))),
9753 ReturnExpr::Xor(l, r) => Ok(bool3_to_value(xor3(
9754 value_to_bool3(&eval_projected_expr(l, row)?)?,
9755 value_to_bool3(&eval_projected_expr(r, row)?)?,
9756 ))),
9757 ReturnExpr::Not(e) => Ok(bool3_to_value(
9758 value_to_bool3(&eval_projected_expr(e, row)?)?.map(|b| !b),
9759 )),
9760 ReturnExpr::Compare(l, op, r) => {
9761 let lv = eval_projected_expr(l, row)?;
9762 let rv = eval_projected_expr(r, row)?;
9763 Ok(bool3_to_value(compare_values(&lv, *op, &rv)))
9764 }
9765 ReturnExpr::IsNull(e) => Ok(Value::Literal(Literal::Bool(matches!(
9766 eval_projected_expr(e, row)?,
9767 Value::Null
9768 )))),
9769 ReturnExpr::In(needle, haystack) => {
9770 let nv = eval_projected_expr(needle, row)?;
9771 let hv = eval_projected_expr(haystack, row)?;
9772 Ok(bool3_to_value(list_membership_ternary(&nv, &hv)?))
9773 }
9774 ReturnExpr::HasLabel(var, labels) => {
9775 let binding = row
9776 .get(var)
9777 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
9778 match binding {
9779 Value::Node(n) => Ok(Value::Literal(Literal::Bool(
9780 labels.iter().all(|l| n.labels.contains(l)),
9781 ))),
9782 Value::Null => Ok(Value::Null),
9783 other => Err(QueryError::Type(format!(
9784 "'{var}' isn't a node — (n:Label) needs a node binding, got {other:?}"
9785 ))),
9786 }
9787 }
9788 ReturnExpr::PatternPredicate(_) => Err(QueryError::Semantic(
9789 "a pattern predicate (`(n)-->()` etc) can only be used inside WHERE".into(),
9790 )),
9791 ReturnExpr::PatternComprehension { .. } => Err(QueryError::Semantic(
9801 "a pattern comprehension can only be used in RETURN/WITH position, or as an ORDER BY \
9802 key that repeats one of their items verbatim"
9803 .into(),
9804 )),
9805 ReturnExpr::ExistsPattern { .. } | ReturnExpr::ExistsSubquery(_) => Err(
9806 QueryError::Semantic("an exists {} subquery can only be used inside WHERE".into()),
9807 ),
9808 }
9809}
9810
9811fn dedup_rows(rows: Vec<Vec<Value>>) -> Result<Vec<Vec<Value>>, QueryError> {
9819 let mut seen: HashSet<Vec<HashKey>> = HashSet::with_capacity(rows.len());
9820 let mut out = Vec::with_capacity(rows.len());
9821 for row in rows {
9822 let key = row
9823 .iter()
9824 .map(value_hash_key)
9825 .collect::<Result<Vec<_>, _>>()?;
9826 if seen.insert(key) {
9827 out.push(row);
9828 }
9829 }
9830 Ok(out)
9831}
9832
9833fn dedup_binding_rows(
9840 items: &[ReturnItem],
9841 rows: Vec<BindingRow>,
9842) -> Result<Vec<BindingRow>, QueryError> {
9843 let names: Vec<String> = items
9844 .iter()
9845 .enumerate()
9846 .map(with_item_output_name)
9847 .collect();
9848 let mut seen: HashSet<Vec<HashKey>> = HashSet::with_capacity(rows.len());
9849 let mut out = Vec::with_capacity(rows.len());
9850 for row in rows {
9851 let key = names
9852 .iter()
9853 .map(|name| {
9854 binding_hash_key(row.get(name).unwrap_or_else(|| {
9855 panic!("DISTINCT row missing its own projected column '{name}'")
9856 }))
9857 })
9858 .collect::<Result<Vec<_>, _>>()?;
9859 if seen.insert(key) {
9860 out.push(row);
9861 }
9862 }
9863 Ok(out)
9864}
9865
9866fn top_k_by<T>(
9883 mut keyed: Vec<(Vec<Value>, T)>,
9884 order_by: &[(ReturnExpr, SortDir)],
9885 skip: Option<i64>,
9886 limit: Option<i64>,
9887) -> Vec<(Vec<Value>, T)> {
9888 let cmp = |a: &(Vec<Value>, T), b: &(Vec<Value>, T)| -> std::cmp::Ordering {
9889 for (i, (_, dir)) in order_by.iter().enumerate() {
9890 let ord = compare_with_dir(&a.0[i], &b.0[i], *dir);
9891 if ord != std::cmp::Ordering::Equal {
9892 return ord;
9893 }
9894 }
9895 std::cmp::Ordering::Equal
9896 };
9897 let skip_n = skip.unwrap_or(0).max(0) as usize;
9898 match limit {
9899 Some(n) => {
9900 let k = skip_n + n.max(0) as usize;
9901 if k == 0 {
9902 keyed.clear();
9903 } else if k < keyed.len() {
9904 keyed.select_nth_unstable_by(k - 1, cmp);
9905 keyed.truncate(k);
9906 keyed.sort_by(cmp);
9907 } else {
9908 keyed.sort_by(cmp);
9909 }
9910 }
9911 None => keyed.sort_by(cmp),
9912 }
9913 if skip_n > 0 {
9914 keyed.drain(0..skip_n.min(keyed.len()));
9915 }
9916 keyed
9917}
9918
9919fn compare_with_dir(a: &Value, b: &Value, dir: SortDir) -> std::cmp::Ordering {
9928 let ord = compare_non_null(a, b);
9929 if dir == SortDir::Desc {
9930 ord.reverse()
9931 } else {
9932 ord
9933 }
9934}
9935
9936fn cmp_f64_nan_greatest(x: f64, y: f64) -> std::cmp::Ordering {
9949 use std::cmp::Ordering;
9950 match (x.is_nan(), y.is_nan()) {
9951 (true, true) => Ordering::Equal,
9952 (true, false) => Ordering::Greater,
9953 (false, true) => Ordering::Less,
9954 (false, false) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
9955 }
9956}
9957
9958fn compare_non_null(a: &Value, b: &Value) -> std::cmp::Ordering {
9959 use std::cmp::Ordering;
9960 if let (Value::List(_), Value::List(_)) = (a, b) {
9969 return list_cmp_asc(a, b);
9970 }
9971 let pa = value_to_comparable(a);
9972 let pb = value_to_comparable(b);
9973 match (pa, pb) {
9974 (Some(PropertyValue::Int(x)), Some(PropertyValue::Int(y))) => x.cmp(&y),
9975 (Some(PropertyValue::Int(x)), Some(PropertyValue::Float(y))) => {
9976 cmp_f64_nan_greatest(x as f64, y)
9977 }
9978 (Some(PropertyValue::Float(x)), Some(PropertyValue::Int(y))) => {
9979 cmp_f64_nan_greatest(x, y as f64)
9980 }
9981 (Some(PropertyValue::Float(x)), Some(PropertyValue::Float(y))) => {
9982 cmp_f64_nan_greatest(x, y)
9983 }
9984 (Some(PropertyValue::String(x)), Some(PropertyValue::String(y))) => x.cmp(&y),
9985 (Some(PropertyValue::Bool(x)), Some(PropertyValue::Bool(y))) => x.cmp(&y),
9986 (Some(PropertyValue::Date(x)), Some(PropertyValue::Date(y))) => x.cmp(&y),
9987 (Some(PropertyValue::LocalTime(x)), Some(PropertyValue::LocalTime(y))) => x.cmp(&y),
9988 (
9989 Some(PropertyValue::Time {
9990 nanos_of_day: x,
9991 offset_seconds: ox,
9992 }),
9993 Some(PropertyValue::Time {
9994 nanos_of_day: y,
9995 offset_seconds: oy,
9996 }),
9997 ) => (x - ox as i64 * 1_000_000_000).cmp(&(y - oy as i64 * 1_000_000_000)),
9998 (
9999 Some(PropertyValue::LocalDateTime {
10000 epoch_seconds: xs,
10001 nanos: xn,
10002 }),
10003 Some(PropertyValue::LocalDateTime {
10004 epoch_seconds: ys,
10005 nanos: yn,
10006 }),
10007 ) => (xs, xn).cmp(&(ys, yn)),
10008 (
10009 Some(PropertyValue::DateTime {
10010 epoch_seconds: xs,
10011 nanos: xn,
10012 ..
10013 }),
10014 Some(PropertyValue::DateTime {
10015 epoch_seconds: ys,
10016 nanos: yn,
10017 ..
10018 }),
10019 ) => (xs, xn).cmp(&(ys, yn)),
10020 _ => match (type_rank(a), type_rank(b)) {
10028 (Some(ra), Some(rb)) if ra != rb => ra.cmp(&rb),
10029 _ => Ordering::Equal,
10030 },
10031 }
10032}
10033
10034fn type_rank(v: &Value) -> Option<u8> {
10063 match v {
10064 Value::Map(_) => Some(0),
10065 Value::Node(_) => Some(1),
10066 Value::Edge(_) => Some(2),
10067 Value::List(_) => Some(3),
10068 Value::Path(_) => Some(4),
10069 Value::Literal(Literal::String(_)) | Value::Property(PropertyValue::String(_)) => Some(5),
10070 Value::Literal(Literal::Bool(_)) | Value::Property(PropertyValue::Bool(_)) => Some(6),
10071 Value::Literal(Literal::Int(_))
10072 | Value::Property(PropertyValue::Int(_))
10073 | Value::Literal(Literal::Float(_))
10074 | Value::Property(PropertyValue::Float(_)) => Some(7),
10075 Value::Property(PropertyValue::Date(_)) => Some(8),
10076 Value::Property(PropertyValue::LocalTime(_)) => Some(9),
10077 Value::Property(PropertyValue::Time { .. }) => Some(10),
10078 Value::Property(PropertyValue::LocalDateTime { .. }) => Some(11),
10079 Value::Property(PropertyValue::DateTime { .. }) => Some(12),
10080 Value::Null | Value::Literal(Literal::Null) | Value::Property(PropertyValue::Null) => {
10081 Some(13)
10082 }
10083 _ => None,
10084 }
10085}
10086
10087fn list_cmp_asc(a: &Value, b: &Value) -> std::cmp::Ordering {
10099 use std::cmp::Ordering;
10100 let a_null = matches!(a, Value::Null);
10101 let b_null = matches!(b, Value::Null);
10102 match (a_null, b_null) {
10103 (true, true) => return Ordering::Equal,
10104 (true, false) => return Ordering::Greater,
10105 (false, true) => return Ordering::Less,
10106 (false, false) => {}
10107 }
10108 if let (Value::List(xs), Value::List(ys)) = (a, b) {
10109 for (x, y) in xs.iter().zip(ys) {
10110 match list_cmp_asc(x, y) {
10111 Ordering::Equal => continue,
10112 other => return other,
10113 }
10114 }
10115 return xs.len().cmp(&ys.len());
10116 }
10117 compare_non_null(a, b)
10118}
10119
10120fn value_to_comparable(v: &Value) -> Option<PropertyValue> {
10121 match v {
10122 Value::Property(pv) => Some(pv.clone()),
10123 Value::Literal(lit) => Some(literal_to_value(lit)),
10124 _ => None,
10125 }
10126}
10127
10128pub(crate) fn comparable_ordering(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
10144 if let (Value::List(_), Value::List(_)) = (a, b) {
10145 return Some(list_cmp_asc(a, b));
10146 }
10147 let (pa, pb) = match (value_to_comparable(a), value_to_comparable(b)) {
10148 (Some(pa), Some(pb)) => (pa, pb),
10149 _ => {
10150 return match (type_rank(a), type_rank(b)) {
10151 (Some(ra), Some(rb)) if ra != rb => Some(ra.cmp(&rb)),
10155 _ => None,
10167 };
10168 }
10169 };
10170 Some(match (pa, pb) {
10171 (PropertyValue::Int(x), PropertyValue::Int(y)) => x.cmp(&y),
10172 (PropertyValue::Int(x), PropertyValue::Float(y)) => cmp_f64_nan_greatest(x as f64, y),
10173 (PropertyValue::Float(x), PropertyValue::Int(y)) => cmp_f64_nan_greatest(x, y as f64),
10174 (PropertyValue::Float(x), PropertyValue::Float(y)) => cmp_f64_nan_greatest(x, y),
10175 (PropertyValue::String(x), PropertyValue::String(y)) => x.cmp(&y),
10176 (PropertyValue::Bool(x), PropertyValue::Bool(y)) => x.cmp(&y),
10177 (PropertyValue::Date(x), PropertyValue::Date(y)) => x.cmp(&y),
10182 (PropertyValue::LocalTime(x), PropertyValue::LocalTime(y)) => x.cmp(&y),
10183 (
10184 PropertyValue::Time {
10185 nanos_of_day: x,
10186 offset_seconds: ox,
10187 },
10188 PropertyValue::Time {
10189 nanos_of_day: y,
10190 offset_seconds: oy,
10191 },
10192 ) => (x - ox as i64 * 1_000_000_000).cmp(&(y - oy as i64 * 1_000_000_000)),
10193 (
10194 PropertyValue::LocalDateTime {
10195 epoch_seconds: xs,
10196 nanos: xn,
10197 },
10198 PropertyValue::LocalDateTime {
10199 epoch_seconds: ys,
10200 nanos: yn,
10201 },
10202 ) => (xs, xn).cmp(&(ys, yn)),
10203 (
10204 PropertyValue::DateTime {
10205 epoch_seconds: xs,
10206 nanos: xn,
10207 ..
10208 },
10209 PropertyValue::DateTime {
10210 epoch_seconds: ys,
10211 nanos: yn,
10212 ..
10213 },
10214 ) => (xs, xn).cmp(&(ys, yn)),
10215 _ => return None,
10216 })
10217}
10218
10219fn compare_values(a: &Value, op: CompareOp, b: &Value) -> Option<bool> {
10228 if matches!(a, Value::Null) || matches!(b, Value::Null) {
10229 return None;
10230 }
10231 match op {
10232 CompareOp::Eq => value_equal_ternary(a, b),
10233 CompareOp::Ne => value_equal_ternary(a, b).map(|eq| !eq),
10234 CompareOp::Lt => ordered_compare(a, b, |o| o == std::cmp::Ordering::Less),
10235 CompareOp::Le => ordered_compare(a, b, |o| o != std::cmp::Ordering::Greater),
10236 CompareOp::Gt => ordered_compare(a, b, |o| o == std::cmp::Ordering::Greater),
10237 CompareOp::Ge => ordered_compare(a, b, |o| o != std::cmp::Ordering::Less),
10238 CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => {
10239 let (Some(s), Some(p)) = (as_arith_str(a), as_arith_str(b)) else {
10240 return None;
10241 };
10242 Some(match op {
10243 CompareOp::StartsWith => s.starts_with(p),
10244 CompareOp::EndsWith => s.ends_with(p),
10245 CompareOp::Contains => s.contains(p),
10246 _ => unreachable!("only StartsWith/EndsWith/Contains reach this arm"),
10247 })
10248 }
10249 }
10250}
10251
10252fn ordered_compare(
10264 a: &Value,
10265 b: &Value,
10266 pred: impl Fn(std::cmp::Ordering) -> bool,
10267) -> Option<bool> {
10268 if let (Some(x), Some(y)) = (value_as_f64(a), value_as_f64(b)) {
10269 return Some(x.partial_cmp(&y).map(pred).unwrap_or(false));
10270 }
10271 value_partial_cmp(a, b).map(pred)
10272}
10273
10274fn value_partial_cmp(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
10290 use std::cmp::Ordering;
10291 if matches!(a, Value::Null) || matches!(b, Value::Null) {
10292 return None;
10293 }
10294 if let (Value::List(xs), Value::List(ys)) = (a, b) {
10295 for (x, y) in xs.iter().zip(ys) {
10296 match value_partial_cmp(x, y) {
10297 Some(Ordering::Equal) => continue,
10298 other => return other,
10299 }
10300 }
10301 return Some(xs.len().cmp(&ys.len()));
10302 }
10303 if value_to_comparable(a).is_none() || value_to_comparable(b).is_none() {
10317 return None;
10318 }
10319 comparable_ordering(a, b)
10320}
10321
10322fn value_equal_ternary(a: &Value, b: &Value) -> Option<bool> {
10340 match (a, b) {
10341 (Value::Null, _) | (_, Value::Null) => None,
10342 (Value::List(xs), Value::List(ys)) => {
10343 if xs.len() != ys.len() {
10344 return Some(false);
10345 }
10346 fold_ternary_eq(xs.iter().zip(ys).map(|(x, y)| value_equal_ternary(x, y)))
10347 }
10348 (Value::Map(x), Value::Map(y)) => {
10349 if !x.keys().eq(y.keys()) {
10350 return Some(false);
10351 }
10352 fold_ternary_eq(x.iter().map(|(k, xv)| value_equal_ternary(xv, &y[k])))
10353 }
10354 _ => Some(values_equal_numeric_aware(a, b)),
10355 }
10356}
10357
10358fn list_membership_ternary(needle: &Value, haystack: &Value) -> Result<Option<bool>, QueryError> {
10371 match haystack {
10372 Value::Null => Ok(None),
10373 Value::List(items) => {
10374 let mut saw_unknown = false;
10375 for item in items {
10376 match value_equal_ternary(needle, item) {
10377 Some(true) => return Ok(Some(true)),
10378 Some(false) => {}
10379 None => saw_unknown = true,
10380 }
10381 }
10382 Ok(if saw_unknown { None } else { Some(false) })
10383 }
10384 other => Err(QueryError::Type(format!(
10385 "IN requires a list on the right-hand side, got {other:?}"
10386 ))),
10387 }
10388}
10389
10390fn fold_ternary_eq(mut results: impl Iterator<Item = Option<bool>>) -> Option<bool> {
10396 let mut saw_unknown = false;
10397 for r in results.by_ref() {
10398 match r {
10399 Some(false) => return Some(false),
10400 Some(true) => {}
10401 None => saw_unknown = true,
10402 }
10403 }
10404 if saw_unknown {
10405 None
10406 } else {
10407 Some(true)
10408 }
10409}
10410
10411fn values_equal_numeric_aware(a: &Value, b: &Value) -> bool {
10420 match (as_arith_num(a), as_arith_num(b)) {
10421 (Some(ArithNum::Int(x)), Some(ArithNum::Int(y))) => x == y,
10422 (Some(ArithNum::Int(x)), Some(ArithNum::Float(y)))
10423 | (Some(ArithNum::Float(y)), Some(ArithNum::Int(x))) => x as f64 == y,
10424 (Some(ArithNum::Float(x)), Some(ArithNum::Float(y))) => x == y,
10425 _ => value_eq(a, b),
10426 }
10427}