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, IndexSeekValue, 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 IndexSeekValue,
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>(
3412 &'s self,
3413 txn: Txn<'s>,
3414 spec: IndexSeekSpec<'s>,
3415 seed: &'s [BindingRow],
3416 guard: &'s ExecutionGuard<'_>,
3417 row_limit: Option<usize>,
3418 ) -> RowStream<'s> {
3419 let budget_node_limit = guard.options.max_intermediate_rows.map(|max_rows| {
3420 max_rows
3421 .checked_div(seed.len().max(1))
3422 .unwrap_or(0)
3423 .saturating_add(1)
3424 });
3425 let storage_limit = match (row_limit, budget_node_limit) {
3426 (Some(a), Some(b)) => Some(a.min(b)),
3427 (Some(a), None) => Some(a),
3428 (None, Some(b)) => Some(b),
3429 (None, None) => None,
3430 };
3431 let lookup = move |value: &PropertyValue| -> Result<Vec<NodeId>, QueryError> {
3432 match storage_limit {
3433 Some(limit) => GraphStore::lookup_by_index_limited_in_txn(
3434 txn, spec.label, spec.prop, value, limit,
3435 )
3436 .map_err(Into::into),
3437 None => GraphStore::lookup_by_index_in_txn(txn, spec.label, spec.prop, value)
3438 .map_err(Into::into),
3439 }
3440 };
3441 match spec.value {
3442 IndexSeekValue::Fixed(value) => {
3446 let mut node_ids: Option<Vec<NodeId>> = None;
3447 let mut seed_index = 0usize;
3448 let mut node_index = 0usize;
3449 let mut done = false;
3450 let stream = std::iter::from_fn(move || {
3451 if done || seed.is_empty() {
3452 return None;
3453 }
3454 let ids = match &node_ids {
3455 Some(ids) => ids,
3456 None => match lookup(value) {
3457 Ok(ids) => node_ids.insert(ids),
3458 Err(error) => {
3459 done = true;
3460 return Some(Err(error));
3461 }
3462 },
3463 };
3464 if ids.is_empty() || seed_index >= seed.len() {
3465 return None;
3466 }
3467 if let Err(error) = guard.checkpoint() {
3468 done = true;
3469 return Some(Err(error));
3470 }
3471 let mut row = seed[seed_index].clone();
3472 row.insert(spec.var.to_string(), Binding::Node(ids[node_index]));
3473 node_index += 1;
3474 if node_index == ids.len() {
3475 node_index = 0;
3476 seed_index += 1;
3477 }
3478 Some(Ok(row))
3479 });
3480 Self::count_stream(Box::new(stream), guard)
3481 }
3482 IndexSeekValue::RowExpr(expr) => {
3487 let mut node_ids: Vec<NodeId> = Vec::new();
3488 let mut seed_index = 0usize;
3489 let mut node_index = 0usize;
3490 let mut done = false;
3491 let stream = std::iter::from_fn(move || loop {
3492 if done || seed_index >= seed.len() {
3493 return None;
3494 }
3495 if node_index == 0 {
3496 let evaluated =
3497 match self.eval_return_expr(txn, expr, &seed[seed_index], guard) {
3498 Ok(v) => v,
3499 Err(error) => {
3500 done = true;
3501 return Some(Err(error));
3502 }
3503 };
3504 let value = value_to_property_value(&evaluated);
3505 if matches!(value, PropertyValue::Null) {
3511 seed_index += 1;
3512 continue;
3513 }
3514 node_ids = match lookup(&value) {
3515 Ok(ids) => ids,
3516 Err(error) => {
3517 done = true;
3518 return Some(Err(error));
3519 }
3520 };
3521 if node_ids.is_empty() {
3522 seed_index += 1;
3523 continue;
3524 }
3525 }
3526 if let Err(error) = guard.checkpoint() {
3527 done = true;
3528 return Some(Err(error));
3529 }
3530 let mut row = seed[seed_index].clone();
3531 row.insert(spec.var.to_string(), Binding::Node(node_ids[node_index]));
3532 node_index += 1;
3533 if node_index == node_ids.len() {
3534 node_index = 0;
3535 seed_index += 1;
3536 }
3537 return Some(Ok(row));
3538 });
3539 Self::count_stream(Box::new(stream), guard)
3540 }
3541 }
3542 }
3543
3544 fn expand_variable_row(
3545 &self,
3546 txn: Txn,
3547 row: BindingRow,
3548 spec: VarExpandSpec<'_>,
3549 guard: &ExecutionGuard<'_>,
3550 ) -> Result<Vec<BindingRow>, QueryError> {
3551 let start_id = match row.get(spec.from_var) {
3552 Some(Binding::Node(id)) => *id,
3553 Some(Binding::Value(PropertyValue::Null)) => return Ok(Vec::new()),
3554 _ => return Err(QueryError::UnboundVariable(spec.from_var.to_string())),
3555 };
3556 let mut out = Vec::new();
3557 if spec.min_hops == 0 {
3558 let mut new_row = row.clone();
3559 new_row.insert(spec.to_var.to_string(), Binding::Node(start_id));
3560 if let Some(path_segment_var) = spec.path_segment_var {
3561 new_row.insert(path_segment_var.to_string(), Binding::Path(Vec::new()));
3562 }
3563 if let Some(rel_list_var) = spec.rel_list_var {
3564 new_row.insert(rel_list_var.to_string(), Binding::List(Vec::new()));
3565 }
3566 new_row.insert(spec.exclude_edge_var.to_string(), Binding::Path(Vec::new()));
3567 out.push(new_row);
3568 }
3569 let rel_props = spec
3576 .rel_props
3577 .iter()
3578 .map(|(key, expr)| {
3579 let value = self.eval_return_expr(txn, expr, &row, guard)?;
3580 Ok::<_, QueryError>((key.as_str(), value_to_property_value(&value)))
3581 })
3582 .collect::<Result<Vec<_>, _>>()?;
3583 let unbounded = spec.max_hops.is_none();
3584 let effective_max = spec.max_hops.unwrap_or(VAR_EXPAND_DEPTH_CAP);
3585 let seed_used_edges: HashSet<EdgeId> = spec
3596 .exclude_edge_vars
3597 .iter()
3598 .filter_map(|v| match row.get(v) {
3599 Some(Binding::Edge(id)) => Some(*id),
3600 _ => None,
3601 })
3602 .chain(spec.exclude_edge_sets.iter().flat_map(|v| {
3603 match row.get(v) {
3604 Some(Binding::Path(segment)) => segment
3605 .iter()
3606 .filter_map(|p| match p {
3607 PathBinding::Edge(id) => Some(*id),
3608 PathBinding::Node(_) => None,
3609 })
3610 .collect::<Vec<_>>(),
3611 _ => Vec::new(),
3612 }
3613 }))
3614 .collect();
3615 let mut frontier = vec![(start_id, seed_used_edges, Vec::<PathBinding>::new())];
3622 let mut depth = 0u32;
3623 while depth < effective_max && !frontier.is_empty() {
3624 depth += 1;
3625 let mut next_frontier = Vec::new();
3626 for (node, used_edges, segment) in frontier {
3627 for entry in neighbors_for_direction(txn, node, spec.direction, spec.rel_labels)? {
3628 guard.relationship_expansion()?;
3629 if used_edges.contains(&entry.edge_id) {
3630 continue;
3631 }
3632 if !rel_props.is_empty() {
3633 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(
3634 txn,
3635 entry.edge_id,
3636 )?)?;
3637 let matches = rel_props
3638 .iter()
3639 .all(|(key, expected)| edge.props.get(*key) == Some(expected));
3640 if !matches {
3641 continue;
3642 }
3643 }
3644 let mut next_used_edges = used_edges.clone();
3645 next_used_edges.insert(entry.edge_id);
3646 let mut next_segment = segment.clone();
3647 next_segment.push(PathBinding::Edge(entry.edge_id));
3648 next_segment.push(PathBinding::Node(entry.other));
3649 next_frontier.push((entry.other, next_used_edges, next_segment.clone()));
3650 guard.check_intermediate_rows(next_frontier.len())?;
3651 if depth >= spec.min_hops {
3652 let mut new_row = row.clone();
3653 new_row.insert(spec.to_var.to_string(), Binding::Node(entry.other));
3654 if let Some(path_segment_var) = spec.path_segment_var {
3655 new_row.insert(
3656 path_segment_var.to_string(),
3657 Binding::Path(next_segment.clone()),
3658 );
3659 }
3660 if let Some(rel_list_var) = spec.rel_list_var {
3661 let edges = segment_edges_to_list(txn, &next_segment)?;
3662 new_row.insert(rel_list_var.to_string(), edges);
3663 }
3664 new_row.insert(
3665 spec.exclude_edge_var.to_string(),
3666 Binding::Path(next_segment.clone()),
3667 );
3668 out.push(new_row);
3669 guard.check_intermediate_rows(out.len())?;
3670 }
3671 }
3672 }
3673 frontier = next_frontier;
3674 if depth == effective_max && unbounded && !frontier.is_empty() {
3675 return Err(QueryError::ResourceLimit(format!(
3676 "variable-length traversal exceeded the safety depth cap ({VAR_EXPAND_DEPTH_CAP} \
3677 hops) — likely a cyclic graph or unexpectedly large fanout; narrow the pattern or \
3678 add an explicit upper bound (e.g. *0..10)"
3679 )));
3680 }
3681 }
3682 Ok(out)
3683 }
3684
3685 fn match_bound_rel_list_row(
3694 &self,
3695 row: BindingRow,
3696 spec: MatchRelListSpec<'_>,
3697 ) -> Result<Option<BindingRow>, QueryError> {
3698 let start_id = match row.get(spec.from_var) {
3699 Some(Binding::Node(id)) => *id,
3700 Some(Binding::Value(PropertyValue::Null)) => return Ok(None),
3701 _ => return Err(QueryError::UnboundVariable(spec.from_var.to_string())),
3702 };
3703 let edges: Vec<&Edge> = match row.get(spec.rel_list_var) {
3704 Some(Binding::List(items)) => items
3705 .iter()
3706 .map(|v| match v {
3707 Value::Edge(e) => Ok(e),
3708 other => Err(QueryError::Type(format!(
3709 "'{}' must be a list of relationships, found {other:?} in it",
3710 spec.rel_list_var
3711 ))),
3712 })
3713 .collect::<Result<_, _>>()?,
3714 Some(Binding::Value(PropertyValue::Null)) => return Ok(None),
3715 _ => return Err(QueryError::UnboundVariable(spec.rel_list_var.to_string())),
3716 };
3717 let hops = edges.len() as u32;
3718 if hops < spec.min_hops || spec.max_hops.is_some_and(|max| hops > max) {
3719 return Ok(None);
3720 }
3721 if !spec.rel_labels.is_empty() && edges.iter().any(|e| !spec.rel_labels.contains(&e.label))
3722 {
3723 return Ok(None);
3724 }
3725 let mut current = start_id;
3726 for edge in &edges {
3727 let next = match spec.direction {
3728 ExpandDirection::Out if edge.src == current => edge.dst,
3729 ExpandDirection::In if edge.dst == current => edge.src,
3730 ExpandDirection::Either if edge.src == current => edge.dst,
3731 ExpandDirection::Either if edge.dst == current => edge.src,
3732 _ => return Ok(None),
3733 };
3734 current = next;
3735 }
3736 let mut new_row = row.clone();
3737 new_row.insert(spec.to_var.to_string(), Binding::Node(current));
3738 Ok(Some(new_row))
3739 }
3740
3741 fn eval_expr(
3746 &self,
3747 txn: Txn,
3748 expr: &Expr,
3749 row: &BindingRow,
3750 guard: &ExecutionGuard<'_>,
3751 ) -> Result<Option<bool>, QueryError> {
3752 Ok(match expr {
3753 Expr::And(l, r) => and3(
3754 self.eval_expr(txn, l, row, guard)?,
3755 self.eval_expr(txn, r, row, guard)?,
3756 ),
3757 Expr::Or(l, r) => or3(
3758 self.eval_expr(txn, l, row, guard)?,
3759 self.eval_expr(txn, r, row, guard)?,
3760 ),
3761 Expr::Not(e) => self.eval_expr(txn, e, row, guard)?.map(|b| !b),
3762 Expr::Compare(pa, op, lit) => {
3763 let prop_value = self.lookup_prop(txn, pa, row)?;
3764 compare(&prop_value, *op, lit)
3765 }
3766 Expr::PropCompare(left, op, right) => {
3767 let a = self.lookup_prop(txn, left, row)?;
3768 let b = self.lookup_prop(txn, right, row)?;
3769 compare_property_pair_opt(&a, *op, &b)
3770 }
3771 Expr::IsNull(pa) => Some(matches!(
3775 self.lookup_prop(txn, pa, row)?,
3776 None | Some(PropertyValue::Null)
3777 )),
3778 Expr::HasLabel(var, label) => {
3779 let binding = row
3780 .get(var)
3781 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
3782 let Binding::Node(id) = binding else {
3783 return Err(QueryError::UnboundVariable(var.clone()));
3784 };
3785 let node = GraphStore::get_node_in_txn(txn, *id)?;
3786 Some(node.is_some_and(|n| n.labels.iter().any(|l| l == label)))
3787 }
3788 Expr::VarEq(a, b) => {
3789 let ba = row
3790 .get(a)
3791 .ok_or_else(|| QueryError::UnboundVariable(a.clone()))?;
3792 let bb = row
3793 .get(b)
3794 .ok_or_else(|| QueryError::UnboundVariable(b.clone()))?;
3795 Some(match (ba, bb) {
3796 (Binding::Node(x), Binding::Node(y)) => x == y,
3797 (Binding::Edge(x), Binding::Edge(y)) => x == y,
3798 _ => false,
3806 })
3807 }
3808 Expr::GeneralCompare(lhs, op, rhs) => {
3809 let lv = self.eval_return_expr(txn, lhs, row, guard)?;
3810 let rv = self.eval_return_expr(txn, rhs, row, guard)?;
3811 compare_values(&lv, *op, &rv)
3812 }
3813 Expr::GeneralIsNull(e) => Some(matches!(
3814 self.eval_return_expr(txn, e, row, guard)?,
3815 Value::Null
3816 )),
3817 Expr::GeneralBare(e) => self.eval_return_expr_bool3(txn, e, row, guard)?,
3818 Expr::Pattern(pattern) => {
3834 Some(self.eval_pattern_predicate_exists(txn, pattern, row, guard)?)
3835 }
3836 Expr::Exists {
3842 pattern,
3843 where_clause,
3844 } => {
3845 let carried_vars: HashSet<String> = row.keys().cloned().collect();
3846 let wc: Option<Expr> = where_clause.as_deref().cloned();
3847 let plan = apply_index_seeks(build_match_plan(pattern, &wc, &carried_vars)?, txn)?;
3848 let found = self.eval_plan_with_limit(
3849 txn,
3850 &plan,
3851 std::slice::from_ref(row),
3852 guard,
3853 Some(1),
3854 )?;
3855 Some(!found.is_empty())
3856 }
3857 Expr::ExistsSubquery(stmt) => Some(self.eval_exists_subquery(txn, stmt, row, guard)?),
3862 Expr::EdgeNotInSet {
3871 edge_var,
3872 edge_set_var,
3873 } => {
3874 let Some(Binding::Edge(edge_id)) = row.get(edge_var) else {
3875 return Err(QueryError::UnboundVariable(edge_var.clone()));
3876 };
3877 let Some(Binding::Path(segment)) = row.get(edge_set_var) else {
3878 return Err(QueryError::UnboundVariable(edge_set_var.clone()));
3879 };
3880 Some(
3881 !segment
3882 .iter()
3883 .any(|elem| matches!(elem, PathBinding::Edge(id) if id == edge_id)),
3884 )
3885 }
3886 })
3887 }
3888
3889 fn lookup_prop(
3890 &self,
3891 txn: Txn,
3892 pa: &PropAccess,
3893 row: &BindingRow,
3894 ) -> Result<Option<PropertyValue>, QueryError> {
3895 let binding = row
3896 .get(&pa.var)
3897 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
3898 match binding {
3899 Binding::Node(id) => {
3907 let node = deleted_entity_access(GraphStore::get_node_in_txn(txn, *id)?)?;
3908 Ok(node.props.get(&pa.prop).cloned())
3909 }
3910 Binding::Edge(id) => {
3911 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(txn, *id)?)?;
3912 Ok(edge.props.get(&pa.prop).cloned())
3913 }
3914 Binding::Value(_) | Binding::List(_) | Binding::Map(_) => Ok(None),
3928 Binding::Path(_) => Err(QueryError::Type(format!(
3934 "'{}' is a path — property access requires a node, relationship, or map",
3935 pa.var
3936 ))),
3937 }
3938 }
3939
3940 fn lookup_prop_value(
3961 &self,
3962 txn: Txn,
3963 pa: &PropAccess,
3964 row: &BindingRow,
3965 ) -> Result<Value, QueryError> {
3966 match row.get(&pa.var) {
3967 Some(Binding::Map(m)) => Ok(m.get(&pa.prop).cloned().unwrap_or(Value::Null)),
3968 Some(Binding::Value(PropertyValue::Null)) => Ok(Value::Null),
3969 Some(Binding::Value(pv)) => match temporal_component(pv, &pa.prop) {
3970 Some(component) => Ok(Value::Property(component)),
3971 None if is_temporal_property_value(pv) => Ok(Value::Null),
3972 None => Err(QueryError::Type(format!(
3973 "'{}' can't have properties accessed on it -- property access requires a \
3974 node, relationship, map, or temporal value",
3975 pa.var
3976 ))),
3977 },
3978 Some(Binding::List(_)) => Err(QueryError::Type(format!(
3979 "'{}' can't have properties accessed on it -- property access requires a node, \
3980 relationship, map, or temporal value, not a list",
3981 pa.var
3982 ))),
3983 Some(_) => Ok(match self.lookup_prop(txn, pa, row)? {
3984 Some(PropertyValue::Null) | None => Value::Null,
3985 Some(pv) => property_value_to_value(pv),
3986 }),
3987 None => Err(QueryError::UnboundVariable(pa.var.clone())),
3988 }
3989 }
3990
3991 fn materialize_return(
3992 &self,
3993 txn: Txn,
3994 items: &[ReturnItem],
3995 rows: &[BindingRow],
3996 distinct: bool,
3997 guard: &ExecutionGuard<'_>,
3998 ) -> Result<QueryResult, QueryError> {
3999 let columns = items
4000 .iter()
4001 .enumerate()
4002 .map(|(i, item)| {
4003 item.alias
4004 .clone()
4005 .unwrap_or_else(|| default_column_name(&item.expr, i))
4006 })
4007 .collect();
4008 let mut out_rows = if !has_aggregate(items) {
4009 let mut out_rows = Vec::with_capacity(rows.len());
4010 for row in rows {
4011 let mut out_row = Vec::with_capacity(items.len());
4012 for item in items {
4013 out_row.push(self.eval_return_expr(txn, &item.expr, row, guard)?);
4014 }
4015 out_rows.push(out_row);
4016 }
4017 out_rows
4018 } else {
4019 validate_return_items(items)?;
4020 let grouped = self.resolve_grouped_rows(txn, items, rows, guard)?;
4021 grouped
4022 .into_iter()
4023 .map(|bindings| {
4024 bindings
4025 .iter()
4026 .map(|b| self.binding_to_value(txn, b))
4027 .collect::<Result<Vec<_>, _>>()
4028 })
4029 .collect::<Result<Vec<_>, _>>()?
4030 };
4031 if distinct {
4032 out_rows = dedup_rows(out_rows)?;
4033 }
4034 Ok(QueryResult {
4035 columns,
4036 rows: out_rows,
4037 })
4038 }
4039
4040 fn materialize_aggregating_return_with_order(
4064 &self,
4065 txn: Txn,
4066 items: &[ReturnItem],
4067 rows: &[BindingRow],
4068 order_by: &[(ReturnExpr, SortDir)],
4069 skip_limit: (Option<i64>, Option<i64>),
4070 guard: &ExecutionGuard<'_>,
4071 ) -> Result<QueryResult, QueryError> {
4072 let (skip, limit) = skip_limit;
4073 enum OrderKeySource {
4074 RealColumn(usize),
4075 Extra(usize),
4076 }
4077 let mut extra_exprs: Vec<ReturnExpr> = Vec::new();
4078 let order_by_source: Vec<OrderKeySource> = order_by
4079 .iter()
4080 .map(|(expr, _)| {
4081 match items
4082 .iter()
4083 .enumerate()
4084 .position(|(i, it)| item_matches_leaf(expr, i, it))
4085 {
4086 Some(i) => OrderKeySource::RealColumn(i),
4087 None => {
4088 let idx = extra_exprs.len();
4089 extra_exprs.push(expr.clone());
4090 OrderKeySource::Extra(idx)
4091 }
4092 }
4093 })
4094 .collect();
4095 let extended_items: Vec<ReturnItem> = items
4096 .iter()
4097 .cloned()
4098 .chain(
4099 extra_exprs
4100 .into_iter()
4101 .map(|expr| ReturnItem { expr, alias: None }),
4102 )
4103 .collect();
4104 validate_return_items(&extended_items)?;
4105 let grouped = self.resolve_grouped_rows(txn, &extended_items, rows, guard)?;
4106 let columns: Vec<String> = items
4107 .iter()
4108 .enumerate()
4109 .map(|(i, item)| {
4110 item.alias
4111 .clone()
4112 .unwrap_or_else(|| default_column_name(&item.expr, i))
4113 })
4114 .collect();
4115 let real_len = items.len();
4116 let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(grouped.len());
4117 for bindings in grouped {
4118 let values: Vec<Value> = bindings
4119 .iter()
4120 .map(|b| self.binding_to_value(txn, b))
4121 .collect::<Result<Vec<_>, _>>()?;
4122 let (real, extra) = values.split_at(real_len);
4123 let keys: Vec<Value> = order_by_source
4124 .iter()
4125 .map(|src| match src {
4126 OrderKeySource::RealColumn(i) => real[*i].clone(),
4127 OrderKeySource::Extra(k) => extra[*k].clone(),
4128 })
4129 .collect();
4130 keyed.push((keys, real.to_vec()));
4131 }
4132 let rows = top_k_by(keyed, order_by, skip, limit)
4133 .into_iter()
4134 .map(|(_, row)| row)
4135 .collect();
4136 Ok(QueryResult { columns, rows })
4137 }
4138
4139 fn resolve_skip_limit(
4148 &self,
4149 txn: Txn,
4150 expr: Option<&ReturnExpr>,
4151 clause: &str,
4152 guard: &ExecutionGuard<'_>,
4153 ) -> Result<Option<i64>, QueryError> {
4154 let Some(expr) = expr else {
4155 return Ok(None);
4156 };
4157 let value = self.eval_return_expr(txn, expr, &BindingRow::new(), guard)?;
4158 let n = match value {
4159 Value::Literal(Literal::Int(n)) | Value::Property(PropertyValue::Int(n)) => n,
4160 _ => {
4161 return Err(QueryError::Semantic(format!(
4162 "{clause} must evaluate to an integer"
4163 )));
4164 }
4165 };
4166 if n < 0 {
4167 return Err(QueryError::Semantic(format!("{clause} can't be negative")));
4168 }
4169 Ok(Some(n))
4170 }
4171
4172 fn eval_return_expr(
4173 &self,
4174 txn: Txn,
4175 expr: &ReturnExpr,
4176 row: &BindingRow,
4177 guard: &ExecutionGuard<'_>,
4178 ) -> Result<Value, QueryError> {
4179 match expr {
4180 ReturnExpr::Var(var) => {
4181 let binding = row
4182 .get(var)
4183 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4184 self.binding_to_value(txn, binding)
4185 }
4186 ReturnExpr::Prop(pa) => self.lookup_prop_value(txn, pa, row),
4187 ReturnExpr::PropOf(base, prop) => {
4188 let v = self.eval_return_expr(txn, base, row, guard)?;
4189 property_of_value(&v, prop)
4190 }
4191 ReturnExpr::Lit(lit) => Ok(match lit {
4192 Literal::Null => Value::Null,
4193 other => Value::Literal(other.clone()),
4194 }),
4195 ReturnExpr::Call { name, args, .. } => {
4196 if is_aggregate_name(name) {
4204 return Err(QueryError::Semantic(format!(
4205 "aggregate function '{name}' can only be used as a return item's top-level expression"
4206 )));
4207 }
4208 let lower = name.to_ascii_lowercase();
4209 if lower == "type" {
4210 return self.eval_type_call(txn, args.first(), row, guard);
4217 }
4218 let arg_values = args
4219 .iter()
4220 .map(|a| self.eval_return_expr(txn, a, row, guard))
4221 .collect::<Result<Vec<_>, _>>()?;
4222 if lower == "startnode" || lower == "endnode" {
4223 return self.start_or_end_node(txn, &lower, arg_values.first());
4224 }
4225 call_builtin(name, &arg_values, self.now_snapshot())
4226 }
4227 ReturnExpr::CountStar => Err(QueryError::Semantic(
4228 "count(*) can only be used as a return item's top-level expression".into(),
4229 )),
4230 ReturnExpr::Case { test, whens, else_ } => {
4231 let test_value = match test {
4232 Some(t) => Some(self.eval_return_expr(txn, t, row, guard)?),
4233 None => None,
4234 };
4235 for (when, then) in whens {
4236 let when_value = self.eval_return_expr(txn, when, row, guard)?;
4237 let matched = match &test_value {
4243 Some(tv) => value_eq(tv, &when_value),
4244 None => matches!(when_value, Value::Literal(Literal::Bool(true))),
4245 };
4246 if matched {
4247 return self.eval_return_expr(txn, then, row, guard);
4248 }
4249 }
4250 match else_ {
4251 Some(e) => self.eval_return_expr(txn, e, row, guard),
4252 None => Ok(Value::Null),
4253 }
4254 }
4255 ReturnExpr::Arith(l, op, r) => {
4256 let lv = self.eval_return_expr(txn, l, row, guard)?;
4257 let rv = self.eval_return_expr(txn, r, row, guard)?;
4258 apply_arith(*op, &lv, &rv)
4259 }
4260 ReturnExpr::Neg(e) => {
4261 let v = self.eval_return_expr(txn, e, row, guard)?;
4262 apply_neg(&v)
4263 }
4264 ReturnExpr::ListLit(items) => Ok(Value::List(
4265 items
4266 .iter()
4267 .map(|item| self.eval_return_expr(txn, item, row, guard))
4268 .collect::<Result<Vec<_>, _>>()?,
4269 )),
4270 ReturnExpr::Index(base, index) => {
4271 let base_v = self.eval_return_expr(txn, base, row, guard)?;
4272 let index_v = self.eval_return_expr(txn, index, row, guard)?;
4273 apply_index(&base_v, &index_v)
4274 }
4275 ReturnExpr::Slice(base, start, end) => {
4276 let base_v = self.eval_return_expr(txn, base, row, guard)?;
4277 let start_v = start
4278 .as_deref()
4279 .map(|s| self.eval_return_expr(txn, s, row, guard))
4280 .transpose()?;
4281 let end_v = end
4282 .as_deref()
4283 .map(|e| self.eval_return_expr(txn, e, row, guard))
4284 .transpose()?;
4285 apply_slice(&base_v, start_v.as_ref(), end_v.as_ref())
4286 }
4287 ReturnExpr::ListComp {
4288 var,
4289 source,
4290 where_clause,
4291 project,
4292 } => {
4293 let source_v = self.eval_return_expr(txn, source, row, guard)?;
4294 let items = match source_v {
4295 Value::List(items) => items,
4296 Value::Null => return Ok(Value::Null),
4297 other => {
4298 return Err(QueryError::Type(format!(
4299 "list comprehension source must be a list, got {other:?}"
4300 )))
4301 }
4302 };
4303 let mut result = Vec::with_capacity(items.len());
4304 for item in items {
4305 let mut scoped_row = row.clone();
4309 scoped_row.insert(var.clone(), value_to_binding_restore(&item));
4310 let keep = match where_clause {
4311 Some(w) => {
4312 self.eval_return_expr_bool3(txn, w, &scoped_row, guard)? == Some(true)
4313 }
4314 None => true,
4315 };
4316 if !keep {
4317 continue;
4318 }
4319 result.push(match project {
4320 Some(p) => self.eval_return_expr(txn, p, &scoped_row, guard)?,
4321 None => item,
4322 });
4323 }
4324 Ok(Value::List(result))
4325 }
4326 ReturnExpr::Quantifier {
4327 kind,
4328 var,
4329 source,
4330 where_clause,
4331 } => {
4332 let source_v = self.eval_return_expr(txn, source, row, guard)?;
4333 let items = match source_v {
4334 Value::List(items) => items,
4335 Value::Null => return Ok(Value::Null),
4336 other => {
4337 return Err(QueryError::Type(format!(
4338 "quantifier source must be a list, got {other:?}"
4339 )))
4340 }
4341 };
4342 let mut preds = Vec::with_capacity(items.len());
4343 for item in &items {
4344 let mut scoped_row = row.clone();
4345 scoped_row.insert(var.clone(), value_to_binding_restore(item));
4346 preds.push(match where_clause {
4347 Some(w) => self.eval_return_expr_bool3(txn, w, &scoped_row, guard)?,
4348 None => item_truthy(item),
4349 });
4350 }
4351 Ok(match eval_quantifier(*kind, &preds) {
4352 Some(b) => Value::Literal(Literal::Bool(b)),
4353 None => Value::Null,
4354 })
4355 }
4356 ReturnExpr::MapLit(entries) => {
4357 let mut map = BTreeMap::new();
4358 for (k, v) in entries {
4359 map.insert(k.clone(), self.eval_return_expr(txn, v, row, guard)?);
4360 }
4361 Ok(Value::Map(map))
4362 }
4363 ReturnExpr::And(l, r) => Ok(bool3_to_value(and3(
4364 self.eval_return_expr_bool3(txn, l, row, guard)?,
4365 self.eval_return_expr_bool3(txn, r, row, guard)?,
4366 ))),
4367 ReturnExpr::Or(l, r) => Ok(bool3_to_value(or3(
4368 self.eval_return_expr_bool3(txn, l, row, guard)?,
4369 self.eval_return_expr_bool3(txn, r, row, guard)?,
4370 ))),
4371 ReturnExpr::Xor(l, r) => Ok(bool3_to_value(xor3(
4372 self.eval_return_expr_bool3(txn, l, row, guard)?,
4373 self.eval_return_expr_bool3(txn, r, row, guard)?,
4374 ))),
4375 ReturnExpr::Not(e) => Ok(bool3_to_value(
4376 self.eval_return_expr_bool3(txn, e, row, guard)?.map(|b| !b),
4377 )),
4378 ReturnExpr::Compare(l, op, r) => {
4379 let lv = self.eval_return_expr(txn, l, row, guard)?;
4380 let rv = self.eval_return_expr(txn, r, row, guard)?;
4381 Ok(bool3_to_value(compare_values(&lv, *op, &rv)))
4382 }
4383 ReturnExpr::IsNull(e) => {
4384 let v = self.eval_return_expr(txn, e, row, guard)?;
4385 Ok(Value::Literal(Literal::Bool(matches!(v, Value::Null))))
4386 }
4387 ReturnExpr::In(needle, haystack) => {
4388 let nv = self.eval_return_expr(txn, needle, row, guard)?;
4389 let hv = self.eval_return_expr(txn, haystack, row, guard)?;
4390 Ok(bool3_to_value(list_membership_ternary(&nv, &hv)?))
4391 }
4392 ReturnExpr::HasLabel(var, labels) => {
4393 let binding = row
4394 .get(var)
4395 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4396 match binding {
4397 Binding::Node(id) => {
4398 let node = deleted_entity_access(GraphStore::get_node_in_txn(txn, *id)?)?;
4399 Ok(Value::Literal(Literal::Bool(
4400 labels.iter().all(|l| node.labels.contains(l)),
4401 )))
4402 }
4403 Binding::Edge(id) => {
4412 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(txn, *id)?)?;
4413 Ok(Value::Literal(Literal::Bool(
4414 labels.iter().all(|l| edge.label == *l),
4415 )))
4416 }
4417 Binding::Value(PropertyValue::Null) => Ok(Value::Null),
4418 other => Err(QueryError::Type(format!(
4419 "'{var}' isn't a node or relationship — (n:Label) needs one, got {other:?}"
4420 ))),
4421 }
4422 }
4423 ReturnExpr::PatternPredicate(_) => Err(QueryError::Semantic(
4424 "a pattern predicate (`(n)-->()` etc) can only be used inside WHERE".into(),
4425 )),
4426 ReturnExpr::PatternComprehension {
4427 path_var,
4428 pattern,
4429 where_clause,
4430 projection,
4431 } => self.eval_pattern_comprehension(
4432 txn,
4433 PatternComprehensionSpec {
4434 path_var,
4435 pattern,
4436 where_clause,
4437 projection,
4438 },
4439 row,
4440 guard,
4441 ),
4442 ReturnExpr::ExistsPattern { .. } | ReturnExpr::ExistsSubquery(_) => Err(
4443 QueryError::Semantic("an exists {} subquery can only be used inside WHERE".into()),
4444 ),
4445 }
4446 }
4447
4448 fn eval_pattern_comprehension(
4469 &self,
4470 txn: Txn,
4471 spec: PatternComprehensionSpec<'_>,
4472 row: &BindingRow,
4473 guard: &ExecutionGuard<'_>,
4474 ) -> Result<Value, QueryError> {
4475 let PatternComprehensionSpec {
4476 path_var,
4477 pattern,
4478 where_clause,
4479 projection,
4480 } = spec;
4481 if path_var.is_some() {
4482 validate_named_path_pattern(pattern)?;
4483 }
4484 let carried_vars: HashSet<String> = row.keys().cloned().collect();
4485 let (named_pattern, synthesized) = match path_var {
4486 Some(_) => name_pattern_for_path(pattern),
4487 None => (pattern.clone(), HashSet::new()),
4488 };
4489 let wc: Option<Expr> = where_clause.as_deref().cloned();
4490 let plan = apply_index_seeks(build_match_plan(&named_pattern, &wc, &carried_vars)?, txn)?;
4491 let rows = self.eval_plan_with_limit(txn, &plan, std::slice::from_ref(row), guard, None)?;
4492 let mut out = Vec::with_capacity(rows.len());
4493 for mut r in rows {
4494 if let Some(pv) = path_var {
4495 let path_binding = assemble_path(&named_pattern, &r);
4496 for key in &synthesized {
4497 r.remove(key);
4498 }
4499 r.insert(pv.clone(), path_binding);
4500 }
4501 out.push(self.eval_return_expr(txn, projection, &r, guard)?);
4502 }
4503 Ok(Value::List(out))
4504 }
4505
4506 fn eval_return_expr_bool3(
4511 &self,
4512 txn: Txn,
4513 expr: &ReturnExpr,
4514 row: &BindingRow,
4515 guard: &ExecutionGuard<'_>,
4516 ) -> Result<Option<bool>, QueryError> {
4517 value_to_bool3(&self.eval_return_expr(txn, expr, row, guard)?)
4518 }
4519
4520 fn delete_targets(
4538 &self,
4539 txn: Txn,
4540 write_txn: &WriteTransaction,
4541 targets: &[ReturnExpr],
4542 rows: &[BindingRow],
4543 detach: bool,
4544 guard: &ExecutionGuard<'_>,
4545 ) -> Result<(), QueryError> {
4546 let mut deleted_edges = HashSet::new();
4547 let mut pending_nodes = HashSet::new();
4548 for row in rows {
4549 for target in targets {
4550 if let ReturnExpr::Var(name) = target {
4565 let binding = row
4566 .get(name)
4567 .ok_or_else(|| QueryError::UnboundVariable(name.clone()))?;
4568 delete_binding(
4569 txn,
4570 binding,
4571 write_txn,
4572 &mut deleted_edges,
4573 &mut pending_nodes,
4574 guard,
4575 )?;
4576 } else {
4577 let value = self.eval_return_expr(txn, target, row, guard)?;
4578 delete_value(
4579 &value,
4580 write_txn,
4581 &mut deleted_edges,
4582 &mut pending_nodes,
4583 guard,
4584 )?;
4585 }
4586 }
4587 }
4588 for id in pending_nodes {
4589 GraphStore::delete_node_in_txn(write_txn, id, detach)?;
4590 }
4591 Ok(())
4592 }
4593
4594 fn materialize_delete(
4607 &self,
4608 txn: Txn,
4609 targets: &[ReturnExpr],
4610 rows: &[BindingRow],
4611 detach: bool,
4612 ret: &Option<ReturnTail>,
4613 guard: &ExecutionGuard<'_>,
4614 ) -> Result<QueryResult, QueryError> {
4615 let write_txn = require_write_txn(txn);
4616 self.delete_targets(txn, write_txn, targets, rows, detach, guard)?;
4617 let result = match ret {
4618 Some(rt) => self.materialize_return(txn, &rt.items, rows, rt.distinct, guard)?,
4619 None => QueryResult {
4620 columns: vec![],
4621 rows: vec![],
4622 },
4623 };
4624 Ok(result)
4625 }
4626
4627 fn materialize_set(
4628 &self,
4629 txn: Txn,
4630 items: &[SetItem],
4631 rows: &[BindingRow],
4632 ret: &Option<ReturnTail>,
4633 guard: &ExecutionGuard<'_>,
4634 ) -> Result<QueryResult, QueryError> {
4635 let write_txn = require_write_txn(txn);
4636 for row in rows {
4637 for item in items {
4638 self.apply_set_item(txn, write_txn, row, item, guard)?;
4639 }
4640 }
4641 match ret {
4642 Some(rt) => self.materialize_return(txn, &rt.items, rows, rt.distinct, guard),
4643 None => Ok(QueryResult {
4644 columns: vec![],
4645 rows: vec![],
4646 }),
4647 }
4648 }
4649
4650 fn materialize_remove(
4651 &self,
4652 txn: Txn,
4653 items: &[RemoveItem],
4654 rows: &[BindingRow],
4655 ret: &Option<ReturnTail>,
4656 guard: &ExecutionGuard<'_>,
4657 ) -> Result<QueryResult, QueryError> {
4658 let write_txn = require_write_txn(txn);
4659 for row in rows {
4660 for item in items {
4661 apply_remove_item(write_txn, row, item)?;
4662 }
4663 }
4664 match ret {
4665 Some(rt) => self.materialize_return(txn, &rt.items, rows, rt.distinct, guard),
4666 None => Ok(QueryResult {
4667 columns: vec![],
4668 rows: vec![],
4669 }),
4670 }
4671 }
4672
4673 fn materialize_union(
4686 &self,
4687 txn: Txn,
4688 parts: &[Statement],
4689 all: bool,
4690 guard: &ExecutionGuard<'_>,
4691 ) -> Result<QueryResult, QueryError> {
4692 let mut combined: Option<QueryResult> = None;
4693 for part in parts {
4694 let Statement::Match {
4695 clauses,
4696 tail,
4697 order_by,
4698 skip,
4699 limit,
4700 } = part
4701 else {
4702 unreachable!(
4703 "union_stmt parts are always Statement::Match -- see parser::parse_union_stmt"
4704 )
4705 };
4706 let skip = self.resolve_skip_limit(txn, skip.as_deref(), "SKIP", guard)?;
4707 let limit = self.resolve_skip_limit(txn, limit.as_deref(), "LIMIT", guard)?;
4708 let result = self.execute_match(
4709 txn,
4710 clauses,
4711 tail,
4712 ResultModifiers {
4713 order_by,
4714 skip,
4715 limit,
4716 },
4717 guard,
4718 )?;
4719 combined = Some(match combined {
4720 None => result,
4721 Some(mut acc) => {
4722 if acc.columns != result.columns {
4723 return Err(QueryError::Semantic(format!(
4724 "UNION requires every part to return the same columns -- got {:?} \
4725 and {:?}",
4726 acc.columns, result.columns
4727 )));
4728 }
4729 acc.rows.extend(result.rows);
4730 acc
4731 }
4732 });
4733 guard.check_intermediate_rows(combined.as_ref().map(|r| r.rows.len()).unwrap_or(0))?;
4734 }
4735 let mut result = combined.expect("union_stmt grammar guarantees at least 2 parts");
4736 if !all {
4737 result.rows = dedup_rows(result.rows)?;
4738 }
4739 Ok(result)
4740 }
4741
4742 fn apply_set_item(
4743 &self,
4744 txn: Txn,
4745 write_txn: &WriteTransaction,
4746 row: &BindingRow,
4747 item: &SetItem,
4748 guard: &ExecutionGuard<'_>,
4749 ) -> Result<(), QueryError> {
4750 match item {
4751 SetItem::Prop(pa, expr) => {
4752 let binding = row
4753 .get(&pa.var)
4754 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
4755 if matches!(binding, Binding::Value(PropertyValue::Null)) {
4761 return Ok(());
4762 }
4763 let node_id = if let Binding::Node(id) = binding {
4764 Some(*id)
4765 } else {
4766 None
4767 };
4768 let edge_id = if let Binding::Edge(id) = binding {
4769 Some(*id)
4770 } else {
4771 None
4772 };
4773 if node_id.is_none() && edge_id.is_none() {
4774 return Err(QueryError::UnboundVariable(format!(
4775 "'{}' is a WITH-projected scalar, not a node/edge — SET needs a graph binding",
4776 pa.var
4777 )));
4778 }
4779 let value = self.eval_return_expr(txn, expr, row, guard)?;
4780 if matches!(value, Value::Null) {
4795 if let Some(id) = node_id {
4796 GraphStore::remove_node_prop_in_txn(write_txn, id, &pa.prop)?;
4797 }
4798 if let Some(id) = edge_id {
4799 GraphStore::remove_edge_prop_in_txn(write_txn, id, &pa.prop)?;
4800 }
4801 } else {
4802 let pv = value_to_storable_property(&value).ok_or_else(|| {
4803 QueryError::Type(format!(
4804 "property '{}' can't be stored -- MarsDB's node/edge properties are limited \
4805 to null/bool/int/float/string/date/duration; a list/map/node/edge/path value \
4806 (got {value:?}) isn't storable",
4807 pa.prop
4808 ))
4809 })?;
4810 if let Some(id) = node_id {
4811 GraphStore::set_node_prop_in_txn(write_txn, id, &pa.prop, pv.clone())?;
4812 }
4813 if let Some(id) = edge_id {
4814 GraphStore::set_edge_prop_in_txn(write_txn, id, &pa.prop, pv)?;
4815 }
4816 }
4817 }
4818 SetItem::Labels(var, labels) => {
4819 let binding = row
4820 .get(var)
4821 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4822 match binding {
4823 Binding::Node(id) => {
4824 for label in labels {
4825 GraphStore::add_node_label_in_txn(write_txn, *id, label)?;
4826 }
4827 }
4828 Binding::Value(PropertyValue::Null) => {}
4830 _ => {
4831 return Err(QueryError::UnboundVariable(format!(
4832 "'{var}' isn't a node — SET can only add labels to a node"
4833 )))
4834 }
4835 }
4836 }
4837 SetItem::MapAssign { var, value, merge } => {
4838 let binding = row
4839 .get(var)
4840 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
4841 if matches!(binding, Binding::Value(PropertyValue::Null)) {
4843 return Ok(());
4844 }
4845 let node_id = if let Binding::Node(id) = binding {
4846 Some(*id)
4847 } else {
4848 None
4849 };
4850 let edge_id = if let Binding::Edge(id) = binding {
4851 Some(*id)
4852 } else {
4853 None
4854 };
4855 if node_id.is_none() && edge_id.is_none() {
4856 return Err(QueryError::UnboundVariable(format!(
4857 "'{var}' is a WITH-projected scalar, not a node/edge — SET needs a graph binding"
4858 )));
4859 }
4860 let map_value = self.eval_return_expr(txn, value, row, guard)?;
4861 let entries = match map_value {
4867 Value::Map(entries) => entries,
4868 Value::Node(n) => n
4869 .props
4870 .into_iter()
4871 .map(|(k, v)| (k, property_value_to_value(v)))
4872 .collect(),
4873 Value::Edge(e) => e
4874 .props
4875 .into_iter()
4876 .map(|(k, v)| (k, property_value_to_value(v)))
4877 .collect(),
4878 other => {
4879 return Err(QueryError::Type(format!(
4880 "SET {var} = ...{} needs a map, node, or relationship, got {other:?}",
4881 if *merge { " (+=)" } else { "" }
4882 )))
4883 }
4884 };
4885 if !merge {
4891 let existing_keys: Vec<String> = if let Some(id) = node_id {
4892 deleted_entity_access(GraphStore::get_node_in_txn(txn, id)?)?
4893 .props
4894 .into_keys()
4895 .collect()
4896 } else {
4897 deleted_entity_access(GraphStore::get_edge_in_txn(
4898 txn,
4899 edge_id.expect("node_id or edge_id is Some, checked above"),
4900 )?)?
4901 .props
4902 .into_keys()
4903 .collect()
4904 };
4905 for key in existing_keys {
4906 if let Some(id) = node_id {
4907 GraphStore::remove_node_prop_in_txn(write_txn, id, &key)?;
4908 }
4909 if let Some(id) = edge_id {
4910 GraphStore::remove_edge_prop_in_txn(write_txn, id, &key)?;
4911 }
4912 }
4913 }
4914 for (key, entry_value) in entries {
4919 if matches!(entry_value, Value::Null) {
4920 if let Some(id) = node_id {
4921 GraphStore::remove_node_prop_in_txn(write_txn, id, &key)?;
4922 }
4923 if let Some(id) = edge_id {
4924 GraphStore::remove_edge_prop_in_txn(write_txn, id, &key)?;
4925 }
4926 continue;
4927 }
4928 let pv = value_to_storable_property(&entry_value).ok_or_else(|| {
4929 QueryError::Type(format!(
4930 "property '{key}' can't be stored -- MarsDB's node/edge properties are \
4931 limited to null/bool/int/float/string/date/duration/list; a map/node/\
4932 edge/path value (got {entry_value:?}) isn't storable"
4933 ))
4934 })?;
4935 if let Some(id) = node_id {
4936 GraphStore::set_node_prop_in_txn(write_txn, id, &key, pv.clone())?;
4937 }
4938 if let Some(id) = edge_id {
4939 GraphStore::set_edge_prop_in_txn(write_txn, id, &key, pv)?;
4940 }
4941 }
4942 }
4943 }
4944 Ok(())
4945 }
4946}
4947
4948fn record_and_delete_edge(
4963 txn: Txn,
4964 write_txn: &WriteTransaction,
4965 id: EdgeId,
4966 guard: &ExecutionGuard<'_>,
4967) -> Result<(), QueryError> {
4968 if let Some(edge) = GraphStore::get_edge_in_txn(txn, id)? {
4969 guard.record_deleted_edge_type(id, edge.label);
4970 }
4971 GraphStore::delete_edge_in_txn(write_txn, id)?;
4972 Ok(())
4973}
4974
4975fn delete_binding(
4976 txn: Txn,
4977 binding: &Binding,
4978 write_txn: &WriteTransaction,
4979 deleted_edges: &mut HashSet<EdgeId>,
4980 pending_nodes: &mut HashSet<NodeId>,
4981 guard: &ExecutionGuard<'_>,
4982) -> Result<(), QueryError> {
4983 match binding {
4984 Binding::Node(id) => {
4985 pending_nodes.insert(*id);
4986 }
4987 Binding::Edge(id) => {
4988 if deleted_edges.insert(*id) {
4989 record_and_delete_edge(txn, write_txn, *id, guard)?;
4990 }
4991 }
4992 Binding::Path(elems) => {
4993 for elem in elems {
4994 if let PathBinding::Edge(id) = elem {
4995 if deleted_edges.insert(*id) {
4996 record_and_delete_edge(txn, write_txn, *id, guard)?;
4997 }
4998 }
4999 }
5000 for elem in elems {
5001 if let PathBinding::Node(id) = elem {
5002 pending_nodes.insert(*id);
5003 }
5004 }
5005 }
5006 Binding::Value(PropertyValue::Null) => {}
5010 Binding::Value(_) | Binding::List(_) | Binding::Map(_) => {
5011 return Err(QueryError::Type(
5012 "DELETE needs a node, relationship, or path, not a scalar/list/map".into(),
5013 ))
5014 }
5015 }
5016 Ok(())
5017}
5018
5019fn delete_value(
5030 value: &Value,
5031 write_txn: &WriteTransaction,
5032 deleted_edges: &mut HashSet<EdgeId>,
5033 pending_nodes: &mut HashSet<NodeId>,
5034 guard: &ExecutionGuard<'_>,
5035) -> Result<(), QueryError> {
5036 match value {
5037 Value::Node(n) => {
5038 pending_nodes.insert(n.id);
5039 }
5040 Value::Edge(e) => {
5041 if deleted_edges.insert(e.id) {
5042 guard.record_deleted_edge_type(e.id, e.label.clone());
5043 GraphStore::delete_edge_in_txn(write_txn, e.id)?;
5044 }
5045 }
5046 Value::Path(elems) => {
5047 for elem in elems {
5048 if let PathElem::Edge(e) = elem {
5049 if deleted_edges.insert(e.id) {
5050 guard.record_deleted_edge_type(e.id, e.label.clone());
5051 GraphStore::delete_edge_in_txn(write_txn, e.id)?;
5052 }
5053 }
5054 }
5055 for elem in elems {
5056 if let PathElem::Node(n) = elem {
5057 pending_nodes.insert(n.id);
5058 }
5059 }
5060 }
5061 Value::Null => {}
5062 other => {
5063 return Err(QueryError::Type(format!(
5064 "DELETE needs a node, relationship, or path, got {other:?}"
5065 )))
5066 }
5067 }
5068 Ok(())
5069}
5070
5071fn apply_remove_item(
5072 write_txn: &WriteTransaction,
5073 row: &BindingRow,
5074 item: &RemoveItem,
5075) -> Result<(), QueryError> {
5076 match item {
5077 RemoveItem::Prop(pa) => {
5078 let binding = row
5079 .get(&pa.var)
5080 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
5081 match binding {
5082 Binding::Node(id) => {
5083 GraphStore::remove_node_prop_in_txn(write_txn, *id, &pa.prop)?;
5084 }
5085 Binding::Edge(id) => {
5086 GraphStore::remove_edge_prop_in_txn(write_txn, *id, &pa.prop)?;
5087 }
5088 Binding::Value(PropertyValue::Null) => {}
5092 Binding::Value(_) | Binding::List(_) | Binding::Map(_) | Binding::Path(_) => {
5093 return Err(QueryError::UnboundVariable(format!(
5094 "'{}' is a WITH-projected scalar, not a node/edge — REMOVE needs a graph binding",
5095 pa.var
5096 )))
5097 }
5098 }
5099 }
5100 RemoveItem::Labels(var, labels) => {
5101 let binding = row
5102 .get(var)
5103 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
5104 match binding {
5105 Binding::Node(id) => {
5106 for label in labels {
5107 GraphStore::remove_node_label_in_txn(write_txn, *id, label)?;
5108 }
5109 }
5110 Binding::Value(PropertyValue::Null) => {}
5114 _ => {
5115 return Err(QueryError::UnboundVariable(format!(
5116 "'{var}' isn't a node — REMOVE can only remove labels from a node"
5117 )))
5118 }
5119 }
5120 }
5121 }
5122 Ok(())
5123}
5124
5125fn tail_is_distinct_return(tail: &Option<Tail>) -> bool {
5133 match tail {
5134 Some(Tail::Return(_, distinct)) | Some(Tail::ReturnStar(distinct)) => *distinct,
5135 Some(Tail::Delete(_, ret))
5136 | Some(Tail::DetachDelete(_, ret))
5137 | Some(Tail::Set(_, ret))
5138 | Some(Tail::Remove(_, ret))
5139 | Some(Tail::Create(_, ret)) => ret.as_ref().is_some_and(|rt| rt.distinct),
5140 None => false,
5141 }
5142}
5143
5144pub fn is_read_only(stmt: &Statement) -> bool {
5162 if let Statement::Union { parts, .. } = stmt {
5163 return parts.iter().all(is_read_only);
5164 }
5165 let Statement::Match {
5166 tail: Some(Tail::Return(_, _)) | Some(Tail::ReturnStar(_)),
5167 clauses,
5168 ..
5169 } = stmt
5170 else {
5171 return false;
5172 };
5173 !clauses.iter().any(|c| {
5174 matches!(
5175 c,
5176 QueryClause::Merge(_)
5177 | QueryClause::Set(_)
5178 | QueryClause::Delete { .. }
5179 | QueryClause::Remove(_)
5180 | QueryClause::Create(_)
5181 | QueryClause::Call(_)
5188 )
5189 })
5190}
5191
5192fn require_write_txn(txn: Txn<'_>) -> &WriteTransaction {
5200 let Txn::Write(write_txn) = txn else {
5201 unreachable!(
5202 "materialize_delete/materialize_set/QueryClause::Set only reached via the \
5203 write-dispatch path in Executor::execute — is_read_only(stmt) is false for any \
5204 statement with one of these, so execute always opens a WriteTransaction for them"
5205 )
5206 };
5207 write_txn
5208}
5209
5210fn default_column_name(expr: &ReturnExpr, idx: usize) -> String {
5211 match expr {
5212 ReturnExpr::Var(v) => v.clone(),
5213 ReturnExpr::Prop(pa) => format!("{}.{}", pa.var, pa.prop),
5214 ReturnExpr::Lit(_) => format!("col{idx}"),
5215 ReturnExpr::Call { name, .. } => format!("{name}(...)"),
5216 ReturnExpr::CountStar => "count(*)".to_string(),
5217 ReturnExpr::Case { .. } => format!("case{idx}"),
5218 ReturnExpr::Arith(..) | ReturnExpr::Neg(..) => format!("col{idx}"),
5219 ReturnExpr::ListLit(..)
5220 | ReturnExpr::Index(..)
5221 | ReturnExpr::PropOf(..)
5222 | ReturnExpr::Slice(..)
5223 | ReturnExpr::ListComp { .. }
5224 | ReturnExpr::Quantifier { .. }
5225 | ReturnExpr::MapLit(..)
5226 | ReturnExpr::And(..)
5227 | ReturnExpr::Or(..)
5228 | ReturnExpr::Xor(..)
5229 | ReturnExpr::Not(..)
5230 | ReturnExpr::Compare(..)
5231 | ReturnExpr::IsNull(..)
5232 | ReturnExpr::In(..)
5233 | ReturnExpr::HasLabel(..)
5234 | ReturnExpr::PatternPredicate(..)
5235 | ReturnExpr::PatternComprehension { .. }
5236 | ReturnExpr::ExistsPattern { .. }
5237 | ReturnExpr::ExistsSubquery(_) => format!("col{idx}"),
5238 }
5239}
5240
5241pub(crate) fn with_item_output_name((i, item): (usize, &ReturnItem)) -> String {
5246 item.alias
5247 .clone()
5248 .unwrap_or_else(|| default_column_name(&item.expr, i))
5249}
5250
5251fn collect_agg_nodes<'a>(expr: &'a ReturnExpr, out: &mut Vec<&'a ReturnExpr>) {
5268 match expr {
5269 ReturnExpr::CountStar => out.push(expr),
5270 ReturnExpr::Call { name, args, .. } => {
5271 if is_aggregate_name(name) {
5272 out.push(expr);
5273 } else {
5274 for arg in args {
5275 collect_agg_nodes(arg, out);
5276 }
5277 }
5278 }
5279 ReturnExpr::Case { test, whens, else_ } => {
5280 if let Some(t) = test.as_deref() {
5281 collect_agg_nodes(t, out);
5282 }
5283 for (w, t) in whens {
5284 collect_agg_nodes(w, out);
5285 collect_agg_nodes(t, out);
5286 }
5287 if let Some(e) = else_.as_deref() {
5288 collect_agg_nodes(e, out);
5289 }
5290 }
5291 ReturnExpr::Arith(l, _, r) => {
5292 collect_agg_nodes(l, out);
5293 collect_agg_nodes(r, out);
5294 }
5295 ReturnExpr::Neg(e) => collect_agg_nodes(e, out),
5296 ReturnExpr::ListLit(items) => {
5297 for item in items {
5298 collect_agg_nodes(item, out);
5299 }
5300 }
5301 ReturnExpr::Index(base, index) => {
5302 collect_agg_nodes(base, out);
5303 collect_agg_nodes(index, out);
5304 }
5305 ReturnExpr::PropOf(base, _) => collect_agg_nodes(base, out),
5306 ReturnExpr::Slice(base, start, end) => {
5307 collect_agg_nodes(base, out);
5308 if let Some(s) = start.as_deref() {
5309 collect_agg_nodes(s, out);
5310 }
5311 if let Some(e) = end.as_deref() {
5312 collect_agg_nodes(e, out);
5313 }
5314 }
5315 ReturnExpr::ListComp {
5318 source, project, ..
5319 } => {
5320 collect_agg_nodes(source, out);
5321 if let Some(p) = project.as_deref() {
5322 collect_agg_nodes(p, out);
5323 }
5324 }
5325 ReturnExpr::Quantifier { source, .. } => collect_agg_nodes(source, out),
5326 ReturnExpr::MapLit(entries) => {
5327 for (_, v) in entries {
5328 collect_agg_nodes(v, out);
5329 }
5330 }
5331 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5332 collect_agg_nodes(l, out);
5333 collect_agg_nodes(r, out);
5334 }
5335 ReturnExpr::Not(e) => collect_agg_nodes(e, out),
5336 ReturnExpr::Compare(l, _, r) => {
5337 collect_agg_nodes(l, out);
5338 collect_agg_nodes(r, out);
5339 }
5340 ReturnExpr::IsNull(e) => collect_agg_nodes(e, out),
5341 ReturnExpr::In(needle, haystack) => {
5342 collect_agg_nodes(needle, out);
5343 collect_agg_nodes(haystack, out);
5344 }
5345 ReturnExpr::Var(_)
5346 | ReturnExpr::Prop(_)
5347 | ReturnExpr::Lit(_)
5348 | ReturnExpr::HasLabel(..)
5349 | ReturnExpr::PatternPredicate(..)
5350 | ReturnExpr::PatternComprehension { .. }
5351 | ReturnExpr::ExistsPattern { .. }
5352 | ReturnExpr::ExistsSubquery(_) => {}
5353 }
5354}
5355
5356pub(crate) fn contains_aggregate(expr: &ReturnExpr) -> bool {
5357 match expr {
5358 ReturnExpr::CountStar => true,
5359 ReturnExpr::Call { name, args, .. } => {
5360 is_aggregate_name(name) || args.iter().any(contains_aggregate)
5361 }
5362 ReturnExpr::Case { test, whens, else_ } => {
5363 test.as_deref().is_some_and(contains_aggregate)
5364 || whens
5365 .iter()
5366 .any(|(w, t)| contains_aggregate(w) || contains_aggregate(t))
5367 || else_.as_deref().is_some_and(contains_aggregate)
5368 }
5369 ReturnExpr::Arith(l, _, r) => contains_aggregate(l) || contains_aggregate(r),
5370 ReturnExpr::Neg(e) => contains_aggregate(e),
5371 ReturnExpr::ListLit(items) => items.iter().any(contains_aggregate),
5372 ReturnExpr::Index(base, index) => contains_aggregate(base) || contains_aggregate(index),
5373 ReturnExpr::PropOf(base, _) => contains_aggregate(base),
5374 ReturnExpr::Slice(base, start, end) => {
5375 contains_aggregate(base)
5376 || start.as_deref().is_some_and(contains_aggregate)
5377 || end.as_deref().is_some_and(contains_aggregate)
5378 }
5379 ReturnExpr::ListComp {
5384 source, project, ..
5385 } => contains_aggregate(source) || project.as_deref().is_some_and(contains_aggregate),
5386 ReturnExpr::Quantifier { source, .. } => contains_aggregate(source),
5387 ReturnExpr::MapLit(entries) => entries.iter().any(|(_, v)| contains_aggregate(v)),
5388 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5389 contains_aggregate(l) || contains_aggregate(r)
5390 }
5391 ReturnExpr::Not(e) => contains_aggregate(e),
5392 ReturnExpr::Compare(l, _, r) => contains_aggregate(l) || contains_aggregate(r),
5393 ReturnExpr::IsNull(e) => contains_aggregate(e),
5394 ReturnExpr::In(needle, haystack) => {
5395 contains_aggregate(needle) || contains_aggregate(haystack)
5396 }
5397 ReturnExpr::Var(_)
5398 | ReturnExpr::Prop(_)
5399 | ReturnExpr::Lit(_)
5400 | ReturnExpr::HasLabel(..)
5401 | ReturnExpr::PatternPredicate(..)
5402 | ReturnExpr::PatternComprehension { .. }
5409 | ReturnExpr::ExistsPattern { .. }
5410 | ReturnExpr::ExistsSubquery(_) => false,
5411 }
5412}
5413
5414pub(crate) fn has_aggregate(items: &[ReturnItem]) -> bool {
5420 items.iter().any(|item| contains_aggregate(&item.expr))
5431}
5432
5433fn contains_rand_call(expr: &ReturnExpr) -> bool {
5439 match expr {
5440 ReturnExpr::Call { name, args, .. } => {
5441 name.eq_ignore_ascii_case("rand") || args.iter().any(contains_rand_call)
5442 }
5443 ReturnExpr::Case { test, whens, else_ } => {
5444 test.as_deref().is_some_and(contains_rand_call)
5445 || whens
5446 .iter()
5447 .any(|(w, t)| contains_rand_call(w) || contains_rand_call(t))
5448 || else_.as_deref().is_some_and(contains_rand_call)
5449 }
5450 ReturnExpr::Arith(l, _, r) => contains_rand_call(l) || contains_rand_call(r),
5451 ReturnExpr::Neg(e) => contains_rand_call(e),
5452 ReturnExpr::ListLit(items) => items.iter().any(contains_rand_call),
5453 ReturnExpr::Index(base, index) => contains_rand_call(base) || contains_rand_call(index),
5454 ReturnExpr::PropOf(base, _) => contains_rand_call(base),
5455 ReturnExpr::Slice(base, start, end) => {
5456 contains_rand_call(base)
5457 || start.as_deref().is_some_and(contains_rand_call)
5458 || end.as_deref().is_some_and(contains_rand_call)
5459 }
5460 ReturnExpr::ListComp {
5461 source, project, ..
5462 } => contains_rand_call(source) || project.as_deref().is_some_and(contains_rand_call),
5463 ReturnExpr::Quantifier { source, .. } => contains_rand_call(source),
5464 ReturnExpr::MapLit(entries) => entries.iter().any(|(_, v)| contains_rand_call(v)),
5465 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5466 contains_rand_call(l) || contains_rand_call(r)
5467 }
5468 ReturnExpr::Not(e) => contains_rand_call(e),
5469 ReturnExpr::Compare(l, _, r) => contains_rand_call(l) || contains_rand_call(r),
5470 ReturnExpr::IsNull(e) => contains_rand_call(e),
5471 ReturnExpr::In(needle, haystack) => {
5472 contains_rand_call(needle) || contains_rand_call(haystack)
5473 }
5474 ReturnExpr::CountStar
5475 | ReturnExpr::Var(_)
5476 | ReturnExpr::Prop(_)
5477 | ReturnExpr::Lit(_)
5478 | ReturnExpr::HasLabel(..)
5479 | ReturnExpr::PatternPredicate(..)
5480 | ReturnExpr::PatternComprehension { .. }
5484 | ReturnExpr::ExistsPattern { .. }
5485 | ReturnExpr::ExistsSubquery(_) => false,
5486 }
5487}
5488
5489pub(crate) fn return_star_items(
5505 names: impl Iterator<Item = String>,
5506) -> Result<Vec<ReturnItem>, QueryError> {
5507 let names: Vec<String> = names.collect();
5508 if names.is_empty() {
5509 return Err(QueryError::Semantic(
5510 "RETURN * needs at least one variable in scope".into(),
5511 ));
5512 }
5513 Ok(star_items(names))
5514}
5515
5516pub(crate) fn with_star_items(names: impl Iterator<Item = String>) -> Vec<ReturnItem> {
5520 star_items(names.collect())
5521}
5522
5523fn star_items(mut names: Vec<String>) -> Vec<ReturnItem> {
5524 names.sort();
5525 names
5526 .into_iter()
5527 .map(|name| ReturnItem {
5528 expr: ReturnExpr::Var(name),
5529 alias: None,
5530 })
5531 .collect()
5532}
5533
5534pub(crate) fn validate_return_items(items: &[ReturnItem]) -> Result<(), QueryError> {
5555 for item in items {
5556 if contains_aggregate(&item.expr) {
5557 validate_composed_expr(&item.expr, items)?;
5558 }
5559 }
5560 Ok(())
5561}
5562
5563pub(crate) fn item_matches_leaf(expr: &ReturnExpr, index: usize, item: &ReturnItem) -> bool {
5574 item.expr == *expr
5575 || matches!(expr, ReturnExpr::Var(name) if *name == with_item_output_name((index, item)))
5576}
5577
5578pub(crate) fn validate_composed_expr(
5579 expr: &ReturnExpr,
5580 items: &[ReturnItem],
5581) -> Result<(), QueryError> {
5582 if matches!(expr, ReturnExpr::CountStar) {
5583 return Ok(());
5584 }
5585 if let ReturnExpr::Call { name, args, .. } = expr {
5586 if is_aggregate_name(name) {
5587 let expected_args = if is_percentile_name(name) { 2 } else { 1 };
5591 if args.len() != expected_args {
5592 return Err(QueryError::Semantic(if expected_args == 2 {
5593 format!("{name}() takes exactly two arguments (the value, then the percentile)")
5594 } else {
5595 format!(
5596 "{name}() takes exactly one argument (use count(*) for a row count with no argument)"
5597 )
5598 }));
5599 }
5600 for arg in args {
5601 if contains_aggregate(arg) {
5602 return Err(QueryError::Semantic(format!(
5603 "aggregate function '{name}' can't take another aggregate as an argument"
5604 )));
5605 }
5606 if contains_rand_call(arg) {
5614 return Err(QueryError::Semantic(format!(
5615 "aggregate function '{name}' can't take a non-deterministic expression \
5616 (e.g. rand()) as an argument"
5617 )));
5618 }
5619 }
5620 return Ok(());
5621 }
5622 }
5623 if matches!(expr, ReturnExpr::Var(_) | ReturnExpr::Prop(_)) {
5624 let is_grouping_key = items
5625 .iter()
5626 .enumerate()
5627 .any(|(i, it)| item_matches_leaf(expr, i, it) && !contains_aggregate(&it.expr));
5628 return if is_grouping_key {
5629 Ok(())
5630 } else {
5631 Err(QueryError::Semantic(format!(
5632 "{expr:?} is used alongside an aggregate function but isn't itself one of this \
5633 RETURN/WITH's own items -- once any item aggregates, every other value used \
5634 with it must be listed as its own explicit grouping key"
5635 )))
5636 };
5637 }
5638 match expr {
5645 ReturnExpr::Case { test, whens, else_ } => {
5646 if let Some(t) = test.as_deref() {
5647 validate_composed_expr(t, items)?;
5648 }
5649 for (w, t) in whens {
5650 validate_composed_expr(w, items)?;
5651 validate_composed_expr(t, items)?;
5652 }
5653 if let Some(e) = else_.as_deref() {
5654 validate_composed_expr(e, items)?;
5655 }
5656 }
5657 ReturnExpr::Call { args, .. } => {
5658 for arg in args {
5659 validate_composed_expr(arg, items)?;
5660 }
5661 }
5662 ReturnExpr::Arith(l, _, r) => {
5663 validate_composed_expr(l, items)?;
5664 validate_composed_expr(r, items)?;
5665 }
5666 ReturnExpr::Neg(e) => validate_composed_expr(e, items)?,
5667 ReturnExpr::ListLit(list_items) => {
5668 for item in list_items {
5669 validate_composed_expr(item, items)?;
5670 }
5671 }
5672 ReturnExpr::Index(base, index) => {
5673 validate_composed_expr(base, items)?;
5674 validate_composed_expr(index, items)?;
5675 }
5676 ReturnExpr::PropOf(base, _) => validate_composed_expr(base, items)?,
5677 ReturnExpr::Slice(base, start, end) => {
5678 validate_composed_expr(base, items)?;
5679 if let Some(s) = start.as_deref() {
5680 validate_composed_expr(s, items)?;
5681 }
5682 if let Some(e) = end.as_deref() {
5683 validate_composed_expr(e, items)?;
5684 }
5685 }
5686 ReturnExpr::ListComp {
5702 source,
5703 project,
5704 where_clause,
5705 ..
5706 } => {
5707 if project.as_deref().is_some_and(contains_aggregate) {
5708 return Err(QueryError::Semantic(
5709 "an aggregate function can't be used inside a list comprehension's projection"
5710 .into(),
5711 ));
5712 }
5713 validate_composed_expr(source, items)?;
5714 let _ = where_clause;
5717 }
5718 ReturnExpr::Quantifier { source, .. } => validate_composed_expr(source, items)?,
5719 ReturnExpr::MapLit(entries) => {
5720 for (_, v) in entries {
5721 validate_composed_expr(v, items)?;
5722 }
5723 }
5724 ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
5725 validate_composed_expr(l, items)?;
5726 validate_composed_expr(r, items)?;
5727 }
5728 ReturnExpr::Not(e) => validate_composed_expr(e, items)?,
5729 ReturnExpr::Compare(l, _, r) => {
5730 validate_composed_expr(l, items)?;
5731 validate_composed_expr(r, items)?;
5732 }
5733 ReturnExpr::IsNull(e) => validate_composed_expr(e, items)?,
5734 ReturnExpr::In(needle, haystack) => {
5735 validate_composed_expr(needle, items)?;
5736 validate_composed_expr(haystack, items)?;
5737 }
5738 ReturnExpr::CountStar
5739 | ReturnExpr::Var(_)
5740 | ReturnExpr::Prop(_)
5741 | ReturnExpr::Lit(_)
5742 | ReturnExpr::HasLabel(..)
5743 | ReturnExpr::PatternPredicate(..)
5744 | ReturnExpr::PatternComprehension { .. }
5745 | ReturnExpr::ExistsPattern { .. }
5746 | ReturnExpr::ExistsSubquery(_) => {}
5747 }
5748 Ok(())
5749}
5750
5751pub(crate) fn validate_order_by_composed_expr(
5764 expr: &ReturnExpr,
5765 items: &[ReturnItem],
5766) -> Result<(), QueryError> {
5767 validate_composed_expr(expr, items)?;
5768 let mut agg_nodes = Vec::new();
5769 collect_agg_nodes(expr, &mut agg_nodes);
5770 for node in agg_nodes {
5771 let matches_item = items
5772 .iter()
5773 .enumerate()
5774 .any(|(i, it)| item_matches_leaf(node, i, it));
5775 if !matches_item {
5776 return Err(QueryError::Semantic(
5777 "ORDER BY aggregate does not match any RETURN/WITH item".into(),
5778 ));
5779 }
5780 }
5781 Ok(())
5782}
5783
5784fn binding_hash_key(b: &Binding) -> Result<HashKey, QueryError> {
5791 Ok(match b {
5792 Binding::Node(id) => HashKey::Node(*id),
5793 Binding::Edge(id) => HashKey::Edge(*id),
5794 Binding::Value(pv) => property_value_hash_key(pv),
5795 Binding::List(items) => HashKey::List(
5796 items
5797 .iter()
5798 .map(value_hash_key)
5799 .collect::<Result<Vec<_>, _>>()?,
5800 ),
5801 Binding::Path(elems) => HashKey::List(
5809 elems
5810 .iter()
5811 .map(|e| match e {
5812 PathBinding::Node(id) => HashKey::Node(*id),
5813 PathBinding::Edge(id) => HashKey::Edge(*id),
5814 })
5815 .collect(),
5816 ),
5817 Binding::Map(m) => HashKey::List(
5821 m.iter()
5822 .map(|(k, v)| -> Result<HashKey, QueryError> {
5823 Ok(HashKey::List(vec![
5824 HashKey::Str(k.clone()),
5825 value_hash_key(v)?,
5826 ]))
5827 })
5828 .collect::<Result<Vec<_>, _>>()?,
5829 ),
5830 })
5831}
5832
5833fn project_call_row(
5843 sig: &ProcedureSignature,
5844 proc_row: &[Value],
5845 yield_items: &CallYield,
5846) -> Result<Vec<Value>, QueryError> {
5847 match yield_items {
5848 CallYield::Star => Ok(proc_row.to_vec()),
5849 CallYield::Items(items, _) => items
5850 .iter()
5851 .map(|(name, _)| {
5852 let idx = sig.outputs.iter().position(|o| o == name).ok_or_else(|| {
5853 QueryError::Semantic(format!(
5854 "'{name}' isn't a declared output of this procedure"
5855 ))
5856 })?;
5857 Ok(proc_row[idx].clone())
5858 })
5859 .collect(),
5860 }
5861}
5862
5863fn value_matches_declared_type(value: &Value, declared: &str) -> bool {
5875 if matches!(value, Value::Null) {
5876 return true;
5877 }
5878 let is_int = matches!(
5879 value,
5880 Value::Literal(Literal::Int(_)) | Value::Property(PropertyValue::Int(_))
5881 );
5882 let is_float = matches!(
5883 value,
5884 Value::Literal(Literal::Float(_)) | Value::Property(PropertyValue::Float(_))
5885 );
5886 match declared.trim_end_matches('?').to_ascii_uppercase().as_str() {
5887 "INTEGER" => is_int,
5888 "FLOAT" | "NUMBER" => is_int || is_float,
5889 "STRING" => matches!(
5890 value,
5891 Value::Literal(Literal::String(_)) | Value::Property(PropertyValue::String(_))
5892 ),
5893 "BOOLEAN" => matches!(
5894 value,
5895 Value::Literal(Literal::Bool(_)) | Value::Property(PropertyValue::Bool(_))
5896 ),
5897 _ => true,
5898 }
5899}
5900
5901fn value_to_binding(v: Value) -> Binding {
5911 match v {
5912 Value::List(items) => Binding::List(items),
5913 Value::Map(m) => Binding::Map(m),
5914 other => Binding::Value(value_to_property_value(&other)),
5915 }
5916}
5917
5918fn value_to_binding_restore(v: &Value) -> Binding {
5926 match v {
5927 Value::Node(n) => Binding::Node(n.id),
5928 Value::Edge(e) => Binding::Edge(e.id),
5929 Value::Property(pv) => Binding::Value(pv.clone()),
5930 Value::Literal(lit) => Binding::Value(literal_to_value(lit)),
5931 Value::List(items) => Binding::List(items.clone()),
5932 Value::Map(m) => Binding::Map(m.clone()),
5933 Value::Path(elems) => Binding::Path(elems.iter().map(path_elem_to_binding).collect()),
5934 Value::Null => Binding::Value(PropertyValue::Null),
5935 }
5936}
5937
5938fn path_elem_to_binding(elem: &PathElem) -> PathBinding {
5939 match elem {
5940 PathElem::Node(n) => PathBinding::Node(n.id),
5941 PathElem::Edge(e) => PathBinding::Edge(e.id),
5942 }
5943}
5944
5945fn name_pattern_for_path(pattern: &Pattern) -> (Pattern, HashSet<String>) {
5959 fn fresh(counter: &mut usize, synthesized: &mut HashSet<String>) -> String {
5960 *counter += 1;
5961 let name = format!("__path_elem{counter}");
5962 synthesized.insert(name.clone());
5963 name
5964 }
5965 let mut counter = 0usize;
5966 let mut synthesized = HashSet::new();
5967 let mut start = pattern.start.clone();
5968 if start.var.is_none() {
5969 start.var = Some(fresh(&mut counter, &mut synthesized));
5970 }
5971 let hops = pattern
5972 .hops
5973 .iter()
5974 .map(|(rel, node)| {
5975 let mut rel = rel.clone();
5976 if rel.hop_range.is_some() {
5977 rel.rel_list_var = rel.var.take();
5990 rel.var = Some(fresh(&mut counter, &mut synthesized));
5991 rel.capture_path_segment = true;
5992 } else if rel.var.is_none() {
5993 rel.var = Some(fresh(&mut counter, &mut synthesized));
5994 }
5995 let mut node = node.clone();
5996 if node.var.is_none() {
5997 node.var = Some(fresh(&mut counter, &mut synthesized));
5998 }
5999 (rel, node)
6000 })
6001 .collect();
6002 (Pattern { start, hops }, synthesized)
6003}
6004
6005fn assemble_path(pattern: &Pattern, row: &BindingRow) -> Binding {
6015 let Some(start_id) = path_node_id(pattern.start.var.as_deref(), row) else {
6016 return Binding::Value(PropertyValue::Null);
6017 };
6018 let mut elems = vec![PathBinding::Node(start_id)];
6019 for (rel, node) in &pattern.hops {
6020 if rel.capture_path_segment {
6021 let Some(Binding::Path(segment)) = rel.var.as_deref().and_then(|v| row.get(v)) else {
6028 return Binding::Value(PropertyValue::Null);
6029 };
6030 elems.extend(segment.iter().cloned());
6031 continue;
6032 }
6033 let Some(edge_id) = path_edge_id(rel.var.as_deref(), row) else {
6034 return Binding::Value(PropertyValue::Null);
6035 };
6036 let Some(node_id) = path_node_id(node.var.as_deref(), row) else {
6037 return Binding::Value(PropertyValue::Null);
6038 };
6039 elems.push(PathBinding::Edge(edge_id));
6040 elems.push(PathBinding::Node(node_id));
6041 }
6042 Binding::Path(elems)
6043}
6044
6045fn segment_edges_to_list(txn: Txn, segment: &[PathBinding]) -> Result<Binding, QueryError> {
6051 let edges = segment
6052 .iter()
6053 .filter_map(|elem| match elem {
6054 PathBinding::Edge(id) => Some(*id),
6055 PathBinding::Node(_) => None,
6056 })
6057 .map(|id| {
6058 let edge = deleted_entity_access(GraphStore::get_edge_in_txn(txn, id)?)?;
6059 Ok(Value::Edge(edge))
6060 })
6061 .collect::<Result<Vec<_>, QueryError>>()?;
6062 Ok(Binding::List(edges))
6063}
6064
6065fn path_node_id(var: Option<&str>, row: &BindingRow) -> Option<NodeId> {
6066 match var.and_then(|v| row.get(v)) {
6067 Some(Binding::Node(id)) => Some(*id),
6068 _ => None,
6069 }
6070}
6071
6072fn path_edge_id(var: Option<&str>, row: &BindingRow) -> Option<EdgeId> {
6073 match var.and_then(|v| row.get(v)) {
6074 Some(Binding::Edge(id)) => Some(*id),
6075 _ => None,
6076 }
6077}
6078
6079fn require_bound_node(row: &BindingRow, var: &str) -> Result<NodeId, QueryError> {
6080 match row.get(var) {
6081 Some(Binding::Node(id)) => Ok(*id),
6082 _ => Err(QueryError::UnboundVariable(format!(
6083 "'{var}' must already be bound to a node before shortestPath() — match it in a preceding MATCH"
6084 ))),
6085 }
6086}
6087
6088fn reconstruct_path(
6094 parent: &HashMap<NodeId, (NodeId, EdgeId)>,
6095 start: NodeId,
6096 end: NodeId,
6097) -> Vec<PathBinding> {
6098 let mut hops = Vec::new();
6099 let mut current = end;
6100 while current != start {
6101 let (prev, edge_id) = parent[¤t];
6102 hops.push((edge_id, current));
6103 current = prev;
6104 }
6105 hops.reverse();
6106 let mut elems = vec![PathBinding::Node(start)];
6107 for (edge_id, node) in hops {
6108 elems.push(PathBinding::Edge(edge_id));
6109 elems.push(PathBinding::Node(node));
6110 }
6111 elems
6112}
6113
6114fn value_to_property_value(v: &Value) -> PropertyValue {
6127 match v {
6128 Value::Null => PropertyValue::Null,
6129 Value::Property(pv) => pv.clone(),
6130 Value::Literal(lit) => literal_to_value(lit),
6131 Value::List(items) => {
6132 PropertyValue::List(items.iter().map(value_to_property_value).collect())
6133 }
6134 Value::Node(_) | Value::Edge(_) | Value::Map(_) | Value::Path(_) => PropertyValue::Null,
6135 }
6136}
6137
6138fn value_to_storable_property(v: &Value) -> Option<PropertyValue> {
6153 match v {
6154 Value::Null => Some(PropertyValue::Null),
6155 Value::Property(pv) => Some(pv.clone()),
6156 Value::Literal(lit) => Some(literal_to_value(lit)),
6157 Value::List(items) => Some(PropertyValue::List(
6158 items
6159 .iter()
6160 .map(value_to_storable_property)
6161 .collect::<Option<Vec<_>>>()?,
6162 )),
6163 Value::Node(_) | Value::Edge(_) | Value::Map(_) | Value::Path(_) => None,
6164 }
6165}
6166
6167fn property_value_to_value(pv: PropertyValue) -> Value {
6180 match pv {
6181 PropertyValue::Null => Value::Null,
6182 PropertyValue::List(items) => {
6183 Value::List(items.into_iter().map(property_value_to_value).collect())
6184 }
6185 other => Value::Property(other),
6186 }
6187}
6188
6189fn deleted_entity_access<T>(record: Option<T>) -> Result<T, QueryError> {
6200 record.ok_or_else(|| {
6201 QueryError::UnboundVariable(
6202 "refers to a node/relationship that no longer exists — it was deleted earlier in this statement".into(),
6203 )
6204 })
6205}
6206
6207pub(crate) fn literal_to_value(lit: &Literal) -> PropertyValue {
6208 match lit {
6209 Literal::Int(i) => PropertyValue::Int(*i),
6210 Literal::Float(f) => PropertyValue::Float(*f),
6211 Literal::String(s) => PropertyValue::String(s.clone()),
6212 Literal::Bool(b) => PropertyValue::Bool(*b),
6213 Literal::Null => PropertyValue::Null,
6214 Literal::Param(name) => {
6215 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
6216 }
6217 }
6218}
6219
6220fn tag_merge_created(mut row: BindingRow, created: bool) -> BindingRow {
6221 row.insert(
6222 MERGE_CREATED_KEY.to_string(),
6223 Binding::Value(PropertyValue::Bool(created)),
6224 );
6225 row
6226}
6227
6228fn neighbors_for_direction(
6241 txn: Txn,
6242 node: NodeId,
6243 direction: ExpandDirection,
6244 rel_labels: &[String],
6245) -> Result<Vec<AdjEntry>, QueryError> {
6246 let dirs: &[Direction] = match direction {
6247 ExpandDirection::Out => &[Direction::Out],
6248 ExpandDirection::In => &[Direction::In],
6249 ExpandDirection::Either => &[Direction::Out, Direction::In],
6250 };
6251 let mut out = Vec::new();
6252 let mut seen: HashSet<EdgeId> = HashSet::new();
6253 let label_filters: Vec<Option<&str>> = if rel_labels.is_empty() {
6254 vec![None]
6255 } else {
6256 rel_labels.iter().map(|l| Some(l.as_str())).collect()
6257 };
6258 for label in label_filters {
6259 for &dir in dirs {
6260 for entry in GraphStore::neighbors_in_txn(txn, node, dir, label)? {
6261 if seen.insert(entry.edge_id) {
6262 out.push(entry);
6263 }
6264 }
6265 }
6266 }
6267 Ok(out)
6268}
6269
6270fn compare(prop: &Option<PropertyValue>, op: CompareOp, lit: &Literal) -> Option<bool> {
6279 let Some(prop) = prop else { return None };
6280 if matches!(prop, PropertyValue::Null) || matches!(lit, Literal::Null) {
6281 return None;
6282 }
6283 compare_property_pair(prop, op, &literal_to_value(lit))
6284}
6285
6286fn compare_property_pair_opt(
6290 a: &Option<PropertyValue>,
6291 op: CompareOp,
6292 b: &Option<PropertyValue>,
6293) -> Option<bool> {
6294 let (Some(a), Some(b)) = (a, b) else {
6295 return None;
6296 };
6297 if matches!(a, PropertyValue::Null) || matches!(b, PropertyValue::Null) {
6298 return None;
6299 }
6300 compare_property_pair(a, op, b)
6301}
6302
6303fn compare_property_pair(a: &PropertyValue, op: CompareOp, b: &PropertyValue) -> Option<bool> {
6317 match (a, b) {
6318 (PropertyValue::Int(a), PropertyValue::Int(b)) => Some(cmp_ord(op, *a, *b)),
6319 (PropertyValue::Int(a), PropertyValue::Float(b)) => Some(cmp_f64(op, *a as f64, *b)),
6320 (PropertyValue::Float(a), PropertyValue::Float(b)) => Some(cmp_f64(op, *a, *b)),
6321 (PropertyValue::Float(a), PropertyValue::Int(b)) => Some(cmp_f64(op, *a, *b as f64)),
6322 (PropertyValue::String(a), PropertyValue::String(b)) => Some(match op {
6323 CompareOp::StartsWith => a.starts_with(b.as_str()),
6324 CompareOp::EndsWith => a.ends_with(b.as_str()),
6325 CompareOp::Contains => a.contains(b.as_str()),
6326 _ => cmp_ord(op, a.as_str(), b.as_str()),
6327 }),
6328 (PropertyValue::Bool(a), PropertyValue::Bool(b)) => Some(cmp_ord(op, *a, *b)),
6333 (PropertyValue::Date(a), PropertyValue::Date(b)) => Some(cmp_ord(op, *a, *b)),
6340 (PropertyValue::LocalTime(a), PropertyValue::LocalTime(b)) => Some(cmp_ord(op, *a, *b)),
6341 (
6344 PropertyValue::Time {
6345 nanos_of_day: na,
6346 offset_seconds: oa,
6347 },
6348 PropertyValue::Time {
6349 nanos_of_day: nb,
6350 offset_seconds: ob,
6351 },
6352 ) => Some(cmp_ord(
6353 op,
6354 na - *oa as i64 * 1_000_000_000,
6355 nb - *ob as i64 * 1_000_000_000,
6356 )),
6357 (
6358 PropertyValue::LocalDateTime {
6359 epoch_seconds: sa,
6360 nanos: na,
6361 },
6362 PropertyValue::LocalDateTime {
6363 epoch_seconds: sb,
6364 nanos: nb,
6365 },
6366 ) => Some(cmp_ord(op, (*sa, *na), (*sb, *nb))),
6367 (
6370 PropertyValue::DateTime {
6371 epoch_seconds: sa,
6372 nanos: na,
6373 ..
6374 },
6375 PropertyValue::DateTime {
6376 epoch_seconds: sb,
6377 nanos: nb,
6378 ..
6379 },
6380 ) => Some(cmp_ord(op, (*sa, *na), (*sb, *nb))),
6381 (PropertyValue::Duration { .. }, PropertyValue::Duration { .. }) => match op {
6387 CompareOp::Eq => Some(a == b),
6388 CompareOp::Ne => Some(a != b),
6389 _ => None,
6390 },
6391 _ => match op {
6392 CompareOp::Eq => Some(false),
6393 CompareOp::Ne => Some(true),
6394 CompareOp::StartsWith
6402 | CompareOp::EndsWith
6403 | CompareOp::Contains
6404 | CompareOp::Lt
6405 | CompareOp::Le
6406 | CompareOp::Gt
6407 | CompareOp::Ge => None,
6408 },
6409 }
6410}
6411
6412fn value_to_bool3(v: &Value) -> Result<Option<bool>, QueryError> {
6416 match v {
6417 Value::Null => Ok(None),
6418 Value::Literal(Literal::Bool(b)) | Value::Property(PropertyValue::Bool(b)) => Ok(Some(*b)),
6419 other => Err(QueryError::Type(format!(
6420 "expected a boolean, got {other:?}"
6421 ))),
6422 }
6423}
6424
6425fn bool3_to_value(b: Option<bool>) -> Value {
6426 match b {
6427 Some(b) => Value::Literal(Literal::Bool(b)),
6428 None => Value::Null,
6429 }
6430}
6431
6432fn and3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
6436 match (a, b) {
6437 (Some(false), _) | (_, Some(false)) => Some(false),
6438 (Some(true), Some(true)) => Some(true),
6439 _ => None,
6440 }
6441}
6442
6443fn or3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
6445 match (a, b) {
6446 (Some(true), _) | (_, Some(true)) => Some(true),
6447 (Some(false), Some(false)) => Some(false),
6448 _ => None,
6449 }
6450}
6451
6452fn xor3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
6456 match (a, b) {
6457 (Some(a), Some(b)) => Some(a != b),
6458 _ => None,
6459 }
6460}
6461
6462fn cmp_f64(op: CompareOp, a: f64, b: f64) -> bool {
6463 match op {
6464 CompareOp::Eq => a == b,
6465 CompareOp::Ne => a != b,
6466 CompareOp::Lt => a < b,
6467 CompareOp::Le => a <= b,
6468 CompareOp::Gt => a > b,
6469 CompareOp::Ge => a >= b,
6470 CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => false,
6474 }
6475}
6476
6477fn cmp_ord<T: PartialOrd>(op: CompareOp, a: T, b: T) -> bool {
6478 match op {
6479 CompareOp::Eq => a == b,
6480 CompareOp::Ne => a != b,
6481 CompareOp::Lt => a < b,
6482 CompareOp::Le => a <= b,
6483 CompareOp::Gt => a > b,
6484 CompareOp::Ge => a >= b,
6485 CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => false,
6486 }
6487}
6488
6489pub(crate) fn value_eq(a: &Value, b: &Value) -> bool {
6500 match (a, b) {
6501 (Value::Null, Value::Null) => true,
6502 (Value::Null, _) | (_, Value::Null) => false,
6503 (Value::Property(pa), Value::Property(pb)) => property_value_eq(pa, pb),
6504 (Value::Literal(la), Value::Literal(lb)) => la == lb,
6505 (Value::Property(pa), Value::Literal(lb)) => *pa == literal_to_value(lb),
6506 (Value::Literal(la), Value::Property(pb)) => literal_to_value(la) == *pb,
6507 (Value::Node(na), Value::Node(nb)) => na.id == nb.id,
6508 (Value::Edge(ea), Value::Edge(eb)) => ea.id == eb.id,
6509 (Value::List(la), Value::List(lb)) => {
6510 la.len() == lb.len() && la.iter().zip(lb).all(|(x, y)| value_eq(x, y))
6511 }
6512 (Value::Path(pa), Value::Path(pb)) => {
6521 pa.len() == pb.len()
6522 && pa.iter().zip(pb).all(|(x, y)| match (x, y) {
6523 (PathElem::Node(na), PathElem::Node(nb)) => na.id == nb.id,
6524 (PathElem::Edge(ea), PathElem::Edge(eb)) => ea.id == eb.id,
6525 _ => false,
6526 })
6527 }
6528 _ => false,
6529 }
6530}
6531
6532fn property_value_eq(a: &PropertyValue, b: &PropertyValue) -> bool {
6539 match (a, b) {
6540 (
6541 PropertyValue::Time {
6542 nanos_of_day: na,
6543 offset_seconds: oa,
6544 },
6545 PropertyValue::Time {
6546 nanos_of_day: nb,
6547 offset_seconds: ob,
6548 },
6549 ) => na - *oa as i64 * 1_000_000_000 == nb - *ob as i64 * 1_000_000_000,
6550 (
6551 PropertyValue::DateTime {
6552 epoch_seconds: sa,
6553 nanos: na,
6554 ..
6555 },
6556 PropertyValue::DateTime {
6557 epoch_seconds: sb,
6558 nanos: nb,
6559 ..
6560 },
6561 ) => sa == sb && na == nb,
6562 _ => a == b,
6563 }
6564}
6565
6566enum ArithNum {
6570 Int(i64),
6571 Float(f64),
6572}
6573
6574fn as_arith_num(v: &Value) -> Option<ArithNum> {
6575 match v {
6576 Value::Property(PropertyValue::Int(i)) | Value::Literal(Literal::Int(i)) => {
6577 Some(ArithNum::Int(*i))
6578 }
6579 Value::Property(PropertyValue::Float(f)) | Value::Literal(Literal::Float(f)) => {
6580 Some(ArithNum::Float(*f))
6581 }
6582 _ => None,
6583 }
6584}
6585
6586fn require_int_arg(v: Option<&Value>, fn_name: &str) -> Result<i64, QueryError> {
6592 match v {
6593 Some(Value::Property(PropertyValue::Int(i))) | Some(Value::Literal(Literal::Int(i))) => {
6594 Ok(*i)
6595 }
6596 other => Err(QueryError::Type(format!(
6597 "{fn_name}() expects an integer argument, got {other:?}"
6598 ))),
6599 }
6600}
6601
6602fn as_arith_str(v: &Value) -> Option<&str> {
6603 match v {
6604 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => {
6605 Some(s.as_str())
6606 }
6607 _ => None,
6608 }
6609}
6610
6611fn apply_neg(v: &Value) -> Result<Value, QueryError> {
6617 if matches!(v, Value::Null) {
6618 return Ok(Value::Null);
6619 }
6620 Ok(match as_arith_num(v) {
6621 Some(ArithNum::Int(i)) => {
6622 Value::Property(PropertyValue::Int(i.checked_neg().ok_or_else(|| {
6623 QueryError::Type("integer arithmetic overflow".into())
6624 })?))
6625 }
6626 Some(ArithNum::Float(f)) => Value::Property(PropertyValue::Float(-f)),
6627 None => {
6628 return Err(QueryError::Type(format!(
6629 "unary minus needs a number -- got {v:?}"
6630 )))
6631 }
6632 })
6633}
6634
6635fn apply_arith(op: ArithOp, a: &Value, b: &Value) -> Result<Value, QueryError> {
6643 if matches!(a, Value::Null) || matches!(b, Value::Null) {
6644 return Ok(Value::Null);
6645 }
6646 if op == ArithOp::Add {
6647 match (a, b) {
6654 (Value::List(xs), Value::List(ys)) => {
6655 let mut combined = xs.clone();
6656 combined.extend(ys.iter().cloned());
6657 return Ok(Value::List(combined));
6658 }
6659 (Value::List(xs), scalar) => {
6660 let mut combined = xs.clone();
6661 combined.push(scalar.clone());
6662 return Ok(Value::List(combined));
6663 }
6664 (scalar, Value::List(ys)) => {
6665 let mut combined = vec![scalar.clone()];
6666 combined.extend(ys.iter().cloned());
6667 return Ok(Value::List(combined));
6668 }
6669 _ => {}
6670 }
6671 if let (Some(sa), Some(sb)) = (as_arith_str(a), as_arith_str(b)) {
6672 return Ok(Value::Property(PropertyValue::String(format!("{sa}{sb}"))));
6673 }
6674 }
6675 if let Some(result) = apply_temporal_arith(op, a, b)? {
6676 return Ok(result);
6677 }
6678 let (Some(na), Some(nb)) = (as_arith_num(a), as_arith_num(b)) else {
6679 return Err(QueryError::Type(format!(
6680 "arithmetic needs two numbers (or, for +, two strings) -- got {a:?} and {b:?}"
6681 )));
6682 };
6683 if op == ArithOp::Pow {
6688 let to_f64 = |n: ArithNum| match n {
6689 ArithNum::Int(i) => i as f64,
6690 ArithNum::Float(f) => f,
6691 };
6692 return Ok(Value::Property(PropertyValue::Float(
6693 to_f64(na).powf(to_f64(nb)),
6694 )));
6695 }
6696 Ok(match (na, nb) {
6700 (ArithNum::Int(x), ArithNum::Int(y)) => {
6701 if matches!(op, ArithOp::Div | ArithOp::Mod) && y == 0 {
6702 return Err(QueryError::Type("division by zero".into()));
6703 }
6704 let value = match op {
6705 ArithOp::Add => x.checked_add(y),
6706 ArithOp::Sub => x.checked_sub(y),
6707 ArithOp::Mul => x.checked_mul(y),
6708 ArithOp::Div => x.checked_div(y),
6709 ArithOp::Mod => x.checked_rem(y),
6710 ArithOp::Pow => unreachable!("handled above"),
6711 }
6712 .ok_or_else(|| QueryError::Type("integer arithmetic overflow".into()))?;
6713 Value::Property(PropertyValue::Int(value))
6714 }
6715 (x, y) => {
6716 let x = match x {
6717 ArithNum::Int(i) => i as f64,
6718 ArithNum::Float(f) => f,
6719 };
6720 let y = match y {
6721 ArithNum::Int(i) => i as f64,
6722 ArithNum::Float(f) => f,
6723 };
6724 Value::Property(PropertyValue::Float(match op {
6725 ArithOp::Add => x + y,
6726 ArithOp::Sub => x - y,
6727 ArithOp::Mul => x * y,
6728 ArithOp::Div => x / y,
6729 ArithOp::Mod => x % y,
6730 ArithOp::Pow => unreachable!("handled above"),
6731 }))
6732 }
6733 })
6734}
6735
6736fn as_date(v: &Value) -> Option<i32> {
6737 match v {
6738 Value::Property(PropertyValue::Date(d)) => Some(*d),
6739 _ => None,
6740 }
6741}
6742
6743fn as_duration(v: &Value) -> Option<temporal::DurationParts> {
6744 match v {
6745 Value::Property(PropertyValue::Duration {
6746 months,
6747 days,
6748 seconds,
6749 nanos,
6750 }) => Some((*months, *days, *seconds, *nanos)),
6751 _ => None,
6752 }
6753}
6754
6755fn duration_value((months, days, seconds, nanos): temporal::DurationParts) -> Value {
6756 Value::Property(PropertyValue::Duration {
6757 months,
6758 days,
6759 seconds,
6760 nanos,
6761 })
6762}
6763
6764fn as_local_time(v: &Value) -> Option<i64> {
6765 match v {
6766 Value::Property(PropertyValue::LocalTime(n)) => Some(*n),
6767 _ => None,
6768 }
6769}
6770
6771fn as_time(v: &Value) -> Option<(i64, i32)> {
6772 match v {
6773 Value::Property(PropertyValue::Time {
6774 nanos_of_day,
6775 offset_seconds,
6776 }) => Some((*nanos_of_day, *offset_seconds)),
6777 _ => None,
6778 }
6779}
6780
6781fn as_local_date_time(v: &Value) -> Option<(i64, i32)> {
6782 match v {
6783 Value::Property(PropertyValue::LocalDateTime {
6784 epoch_seconds,
6785 nanos,
6786 }) => Some((*epoch_seconds, *nanos)),
6787 _ => None,
6788 }
6789}
6790
6791fn as_date_time(v: &Value) -> Option<(i64, i32, temporal::TzId)> {
6792 match v {
6793 Value::Property(PropertyValue::DateTime {
6794 epoch_seconds,
6795 nanos,
6796 zone,
6797 }) => Some((*epoch_seconds, *nanos, tz_from_graph(zone))),
6798 _ => None,
6799 }
6800}
6801
6802fn tz_from_graph(zone: &GraphTzId) -> temporal::TzId {
6807 match zone {
6808 GraphTzId::Offset(o) => temporal::TzId::Offset(*o),
6809 GraphTzId::Named(name) => temporal::TzId::Named(name.clone()),
6810 }
6811}
6812
6813fn tz_to_graph(zone: temporal::TzId) -> GraphTzId {
6814 match zone {
6815 temporal::TzId::Offset(o) => GraphTzId::Offset(o),
6816 temporal::TzId::Named(name) => GraphTzId::Named(name),
6817 }
6818}
6819
6820fn apply_temporal_arith(op: ArithOp, a: &Value, b: &Value) -> Result<Option<Value>, QueryError> {
6834 let date_plus_duration =
6835 |d: i32, dur: temporal::DurationParts, negate: bool| -> Result<Value, QueryError> {
6836 let (months, days, seconds, nanos) = dur;
6837 temporal::add_duration_to_date(d, months, days, seconds, nanos, negate)
6838 .map(|d| Value::Property(PropertyValue::Date(d)))
6839 .ok_or_else(|| {
6840 QueryError::Type("date +/- duration produced an out-of-range date".into())
6841 })
6842 };
6843 let local_time_plus_duration = |t: i64, dur: temporal::DurationParts, negate: bool| -> Value {
6844 let (_, _, seconds, nanos) = dur;
6845 Value::Property(PropertyValue::LocalTime(temporal::add_duration_to_time(
6846 t, seconds, nanos, negate,
6847 )))
6848 };
6849 let time_plus_duration =
6850 |(t, offset): (i64, i32), dur: temporal::DurationParts, negate: bool| -> Value {
6851 let (_, _, seconds, nanos) = dur;
6852 Value::Property(PropertyValue::Time {
6853 nanos_of_day: temporal::add_duration_to_time(t, seconds, nanos, negate),
6854 offset_seconds: offset,
6855 })
6856 };
6857 let local_date_time_plus_duration = |(epoch_seconds, existing_nanos): (i64, i32),
6858 dur: temporal::DurationParts,
6859 negate: bool|
6860 -> Result<Value, QueryError> {
6861 let (months, days, seconds, nanos) = dur;
6862 temporal::add_duration_to_local_date_time(
6863 epoch_seconds,
6864 existing_nanos,
6865 months,
6866 days,
6867 seconds,
6868 nanos,
6869 negate,
6870 )
6871 .map(|(epoch_seconds, nanos)| {
6872 Value::Property(PropertyValue::LocalDateTime {
6873 epoch_seconds,
6874 nanos,
6875 })
6876 })
6877 .ok_or_else(|| {
6878 QueryError::Type("local date-time +/- duration produced an out-of-range value".into())
6879 })
6880 };
6881 let date_time_plus_duration =
6889 |(epoch_seconds, existing_nanos, zone): (i64, i32, temporal::TzId),
6890 dur: temporal::DurationParts,
6891 negate: bool|
6892 -> Result<Value, QueryError> {
6893 let (months, days, seconds, nanos) = dur;
6894 let offset_seconds = temporal::resolve_offset(&zone, epoch_seconds);
6895 temporal::add_duration_to_local_date_time(
6896 epoch_seconds + offset_seconds as i64,
6897 existing_nanos,
6898 months,
6899 days,
6900 seconds,
6901 nanos,
6902 negate,
6903 )
6904 .map(|(local_epoch_seconds, nanos)| {
6905 Value::Property(PropertyValue::DateTime {
6906 epoch_seconds: local_epoch_seconds - offset_seconds as i64,
6907 nanos,
6908 zone: tz_to_graph(zone),
6909 })
6910 })
6911 .ok_or_else(|| {
6912 QueryError::Type("date-time +/- duration produced an out-of-range value".into())
6913 })
6914 };
6915 Ok(match op {
6916 ArithOp::Add => {
6917 if let (Some(d), Some(dur)) = (as_date(a), as_duration(b)) {
6918 Some(date_plus_duration(d, dur, false)?)
6919 } else if let (Some(dur), Some(d)) = (as_duration(a), as_date(b)) {
6920 Some(date_plus_duration(d, dur, false)?)
6921 } else if let (Some(t), Some(dur)) = (as_local_time(a), as_duration(b)) {
6922 Some(local_time_plus_duration(t, dur, false))
6923 } else if let (Some(dur), Some(t)) = (as_duration(a), as_local_time(b)) {
6924 Some(local_time_plus_duration(t, dur, false))
6925 } else if let (Some(t), Some(dur)) = (as_time(a), as_duration(b)) {
6926 Some(time_plus_duration(t, dur, false))
6927 } else if let (Some(dur), Some(t)) = (as_duration(a), as_time(b)) {
6928 Some(time_plus_duration(t, dur, false))
6929 } else if let (Some(dt), Some(dur)) = (as_local_date_time(a), as_duration(b)) {
6930 Some(local_date_time_plus_duration(dt, dur, false)?)
6931 } else if let (Some(dur), Some(dt)) = (as_duration(a), as_local_date_time(b)) {
6932 Some(local_date_time_plus_duration(dt, dur, false)?)
6933 } else if let (Some(dt), Some(dur)) = (as_date_time(a), as_duration(b)) {
6934 Some(date_time_plus_duration(dt, dur, false)?)
6935 } else if let (Some(dur), Some(dt)) = (as_duration(a), as_date_time(b)) {
6936 Some(date_time_plus_duration(dt, dur, false)?)
6937 } else if let (Some(x), Some(y)) = (as_duration(a), as_duration(b)) {
6938 Some(duration_value(temporal::add_duration(x, y).ok_or_else(
6939 || QueryError::Type("duration addition overflow".into()),
6940 )?))
6941 } else {
6942 None
6943 }
6944 }
6945 ArithOp::Sub => {
6946 if let (Some(d), Some(dur)) = (as_date(a), as_duration(b)) {
6947 Some(date_plus_duration(d, dur, true)?)
6948 } else if let (Some(t), Some(dur)) = (as_local_time(a), as_duration(b)) {
6949 Some(local_time_plus_duration(t, dur, true))
6950 } else if let (Some(t), Some(dur)) = (as_time(a), as_duration(b)) {
6951 Some(time_plus_duration(t, dur, true))
6952 } else if let (Some(dt), Some(dur)) = (as_local_date_time(a), as_duration(b)) {
6953 Some(local_date_time_plus_duration(dt, dur, true)?)
6954 } else if let (Some(dt), Some(dur)) = (as_date_time(a), as_duration(b)) {
6955 Some(date_time_plus_duration(dt, dur, true)?)
6956 } else if let (Some(x), Some(y)) = (as_duration(a), as_duration(b)) {
6957 Some(duration_value(temporal::sub_duration(x, y).ok_or_else(
6958 || QueryError::Type("duration subtraction overflow".into()),
6959 )?))
6960 } else {
6961 None
6962 }
6963 }
6964 ArithOp::Mul => {
6965 if let (Some(dur), Some(f)) = (as_duration(a), value_as_f64(b)) {
6966 Some(duration_value(temporal::scale_duration(dur, f)))
6967 } else if let (Some(f), Some(dur)) = (value_as_f64(a), as_duration(b)) {
6968 Some(duration_value(temporal::scale_duration(dur, f)))
6969 } else {
6970 None
6971 }
6972 }
6973 ArithOp::Div => {
6974 if let (Some(dur), Some(f)) = (as_duration(a), value_as_f64(b)) {
6975 if f == 0.0 {
6976 return Err(QueryError::Type("division by zero".into()));
6977 }
6978 Some(duration_value(temporal::scale_duration(dur, 1.0 / f)))
6979 } else {
6980 None
6981 }
6982 }
6983 ArithOp::Mod => None,
6984 ArithOp::Pow => None,
6988 })
6989}
6990
6991fn apply_index(list: &Value, index: &Value) -> Result<Value, QueryError> {
6997 if matches!(list, Value::Null) || matches!(index, Value::Null) {
6998 return Ok(Value::Null);
6999 }
7000 if let Value::Map(entries) = list {
7007 let Some(key) = as_arith_str(index) else {
7008 return Err(QueryError::Type(format!(
7009 "a map index must be a string, got {index:?}"
7010 )));
7011 };
7012 return Ok(entries.get(key).cloned().unwrap_or(Value::Null));
7013 }
7014 if matches!(list, Value::Node(_) | Value::Edge(_) | Value::Property(_)) {
7020 let Some(key) = as_arith_str(index) else {
7021 return Err(QueryError::Type(format!(
7022 "a property index must be a string, got {index:?}"
7023 )));
7024 };
7025 return property_of_value(list, key);
7026 }
7027 let Value::List(items) = list else {
7028 return Err(QueryError::Type(format!(
7029 "[] indexing needs a list or map, got {list:?}"
7030 )));
7031 };
7032 let Some(ArithNum::Int(i)) = as_arith_num(index) else {
7033 return Err(QueryError::Type(format!(
7034 "a list index must be an integer, got {index:?}"
7035 )));
7036 };
7037 let len = items.len() as i64;
7038 let i = if i < 0 { i + len } else { i };
7039 if i < 0 || i >= len {
7040 return Ok(Value::Null);
7041 }
7042 Ok(items[i as usize].clone())
7043}
7044
7045fn apply_slice(
7052 list: &Value,
7053 start: Option<&Value>,
7054 end: Option<&Value>,
7055) -> Result<Value, QueryError> {
7056 if matches!(list, Value::Null) {
7057 return Ok(Value::Null);
7058 }
7059 let Value::List(items) = list else {
7060 return Err(QueryError::Type(format!(
7061 "[..] slicing needs a list, got {list:?}"
7062 )));
7063 };
7064 let len = items.len() as i64;
7065 let clamp = |i: i64| -> i64 {
7066 let i = if i < 0 { i + len } else { i };
7067 i.clamp(0, len)
7068 };
7069 let bound_index = |v: Option<&Value>, default: i64| -> Result<Option<i64>, QueryError> {
7070 match v {
7071 None => Ok(Some(default)),
7072 Some(Value::Null) => Ok(None),
7073 Some(other) => match as_arith_num(other) {
7074 Some(ArithNum::Int(i)) => Ok(Some(clamp(i))),
7075 _ => Err(QueryError::Type(format!(
7076 "a slice bound must be an integer, got {other:?}"
7077 ))),
7078 },
7079 }
7080 };
7081 let (Some(start_idx), Some(end_idx)) = (bound_index(start, 0)?, bound_index(end, len)?) else {
7085 return Ok(Value::Null);
7086 };
7087 if start_idx >= end_idx {
7088 return Ok(Value::List(Vec::new()));
7089 }
7090 Ok(Value::List(
7091 items[start_idx as usize..end_idx as usize].to_vec(),
7092 ))
7093}
7094
7095fn call_builtin(
7096 name: &str,
7097 args: &[Value],
7098 now: temporal::NowSnapshot,
7099) -> Result<Value, QueryError> {
7100 match name.to_ascii_lowercase().as_str() {
7101 "coalesce" => Ok(args
7102 .iter()
7103 .find(|v| !matches!(v, Value::Null))
7104 .cloned()
7105 .unwrap_or(Value::Null)),
7106 "tointeger" => match args.first() {
7107 Some(v) => to_integer(v),
7108 None => Ok(Value::Null),
7109 },
7110 "tostring" => match args.first() {
7111 Some(v) => to_string_value(v),
7112 None => Ok(Value::Null),
7113 },
7114 "date" => date_builtin(args, now),
7115 "date.transaction" | "date.statement" | "date.realtime" => Ok(now_or_null(args, || {
7116 Value::Property(PropertyValue::Date(now.epoch_day))
7117 })),
7118 "duration" => duration_builtin(args),
7119 "localtime" => local_time_builtin(args, now),
7120 "localtime.transaction" | "localtime.statement" | "localtime.realtime" => {
7121 Ok(now_or_null(args, || {
7122 Value::Property(PropertyValue::LocalTime(now.nanos_of_day))
7123 }))
7124 }
7125 "time" => time_builtin(args, now),
7126 "time.transaction" | "time.statement" | "time.realtime" => {
7127 Ok(now_or_null(args, || {
7129 Value::Property(PropertyValue::Time {
7130 nanos_of_day: now.nanos_of_day,
7131 offset_seconds: 0,
7132 })
7133 }))
7134 }
7135 "localdatetime" => local_date_time_builtin(args, now),
7136 "localdatetime.transaction" | "localdatetime.statement" | "localdatetime.realtime" => {
7137 Ok(now_or_null(args, || {
7138 Value::Property(PropertyValue::LocalDateTime {
7139 epoch_seconds: now.epoch_seconds,
7140 nanos: now.nanos,
7141 })
7142 }))
7143 }
7144 "datetime" => date_time_builtin(args, now),
7145 "datetime.transaction" | "datetime.statement" | "datetime.realtime" => {
7146 Ok(now_or_null(args, || {
7148 Value::Property(PropertyValue::DateTime {
7149 epoch_seconds: now.epoch_seconds,
7150 nanos: now.nanos,
7151 zone: GraphTzId::Offset(0),
7152 })
7153 }))
7154 }
7155 "datetime.fromepoch" => {
7156 let seconds = require_int_arg(args.first(), "datetime.fromepoch")?;
7157 let nanos = require_int_arg(args.get(1), "datetime.fromepoch")?;
7158 Ok(Value::Property(PropertyValue::DateTime {
7159 epoch_seconds: seconds,
7160 nanos: nanos as i32,
7161 zone: GraphTzId::Offset(0),
7162 }))
7163 }
7164 "datetime.fromepochmillis" => {
7165 let millis = require_int_arg(args.first(), "datetime.fromepochmillis")?;
7166 Ok(Value::Property(PropertyValue::DateTime {
7167 epoch_seconds: millis.div_euclid(1000),
7168 nanos: (millis.rem_euclid(1000) * 1_000_000) as i32,
7169 zone: GraphTzId::Offset(0),
7170 }))
7171 }
7172 "duration.between" => {
7173 duration_between_builtin("duration.between", args, temporal::duration_between)
7174 }
7175 "duration.inmonths" => {
7176 duration_between_builtin("duration.inMonths", args, temporal::duration_in_months)
7177 }
7178 "duration.indays" => {
7179 duration_between_builtin("duration.inDays", args, temporal::duration_in_days)
7180 }
7181 "duration.inseconds" => {
7182 duration_between_builtin("duration.inSeconds", args, temporal::duration_in_seconds)
7183 }
7184 "date.truncate" => date_truncate_builtin(args),
7185 "localtime.truncate" => local_time_truncate_builtin(args),
7186 "time.truncate" => time_truncate_builtin(args),
7187 "localdatetime.truncate" => local_date_time_truncate_builtin(args),
7188 "datetime.truncate" => date_time_truncate_builtin(args),
7189 "length" => Ok(match args.first() {
7194 Some(Value::Path(elems)) => {
7195 Value::Property(PropertyValue::Int(((elems.len().max(1) - 1) / 2) as i64))
7196 }
7197 Some(Value::Null) | None => Value::Null,
7198 Some(other) => {
7199 return Err(QueryError::Type(format!(
7200 "length() expects a path, got {other:?}"
7201 )))
7202 }
7203 }),
7204 "keys" => keys_builtin(args.first()),
7205 "labels" => labels_builtin(args.first()),
7206 "type" => type_builtin(args.first()),
7207 "properties" => properties_builtin(args.first()),
7208 "id" => id_builtin(args.first()),
7209 "size" => size_builtin(args.first()),
7210 "nodes" => nodes_builtin(args.first()),
7211 "relationships" => relationships_builtin(args.first()),
7212 "head" => list_edge_builtin(args.first(), "head", |items| items.first().cloned()),
7213 "last" => list_edge_builtin(args.first(), "last", |items| items.last().cloned()),
7214 "tail" => match args.first() {
7215 Some(Value::List(items)) => Ok(Value::List(
7216 items.iter().skip(1).cloned().collect::<Vec<_>>(),
7217 )),
7218 Some(Value::Null) | None => Ok(Value::Null),
7219 Some(other) => Err(QueryError::Type(format!(
7220 "tail() expects a list, got {other:?}"
7221 ))),
7222 },
7223 "range" => range_builtin(args),
7224 "exists" => Ok(Value::Literal(Literal::Bool(!matches!(
7225 args.first(),
7226 None | Some(Value::Null)
7227 )))),
7228 "toupper" | "upper" => string_transform(args.first(), "toUpper", str::to_uppercase),
7229 "tolower" | "lower" => string_transform(args.first(), "toLower", str::to_lowercase),
7230 "trim" => string_transform(args.first(), "trim", |s| s.trim().to_string()),
7231 "ltrim" => string_transform(args.first(), "ltrim", |s| s.trim_start().to_string()),
7232 "rtrim" => string_transform(args.first(), "rtrim", |s| s.trim_end().to_string()),
7233 "reverse" => reverse_builtin(args.first()),
7234 "replace" => replace_builtin(args),
7235 "split" => split_builtin(args),
7236 "substring" => substring_builtin(args),
7237 "left" => left_right_builtin(args, true),
7238 "right" => left_right_builtin(args, false),
7239 "tofloat" => match args.first() {
7240 Some(v) => to_float(v),
7241 None => Ok(Value::Null),
7242 },
7243 "toboolean" => match args.first() {
7244 Some(v) => to_boolean(v),
7245 None => Ok(Value::Null),
7246 },
7247 "abs" => match args.first() {
7248 Some(Value::Property(PropertyValue::Int(i)))
7249 | Some(Value::Literal(Literal::Int(i))) => {
7250 Ok(Value::Property(PropertyValue::Int(i.abs())))
7251 }
7252 Some(Value::Null) | None => Ok(Value::Null),
7253 Some(other) => match value_as_f64(other) {
7254 Some(f) => Ok(Value::Property(PropertyValue::Float(f.abs()))),
7255 None => Err(QueryError::Type(format!(
7256 "abs() expects a number, got {other:?}"
7257 ))),
7258 },
7259 },
7260 "ceil" => float_math_fn(args.first(), "ceil", f64::ceil),
7261 "floor" => float_math_fn(args.first(), "floor", f64::floor),
7262 "round" => float_math_fn(args.first(), "round", f64::round),
7263 "sqrt" => float_math_fn(args.first(), "sqrt", f64::sqrt),
7264 "sign" => match args.first() {
7265 Some(Value::Null) | None => Ok(Value::Null),
7266 Some(other) => match value_as_f64(other) {
7267 Some(f) => Ok(Value::Property(PropertyValue::Int(if f > 0.0 {
7268 1
7269 } else if f < 0.0 {
7270 -1
7271 } else {
7272 0
7273 }))),
7274 None => Err(QueryError::Type(format!(
7275 "sign() expects a number, got {other:?}"
7276 ))),
7277 },
7278 },
7279 "rand" => Ok(Value::Property(PropertyValue::Float(rand_f64()))),
7280 other => Err(QueryError::Semantic(format!("unknown function: {other}"))),
7281 }
7282}
7283
7284fn rand_f64() -> f64 {
7293 use std::collections::hash_map::RandomState;
7294 use std::hash::{BuildHasher, Hasher};
7295 use std::sync::atomic::{AtomicU64, Ordering};
7296 static COUNTER: AtomicU64 = AtomicU64::new(0);
7297 let mut hasher = RandomState::new().build_hasher();
7298 hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
7299 let bits = hasher.finish();
7300 (bits >> 11) as f64 / (1u64 << 53) as f64
7301}
7302
7303fn keys_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7304 Ok(match arg {
7305 Some(Value::Node(n)) => Value::List(
7306 n.props
7307 .keys()
7308 .map(|k| Value::Property(PropertyValue::String(k.clone())))
7309 .collect(),
7310 ),
7311 Some(Value::Edge(e)) => Value::List(
7312 e.props
7313 .keys()
7314 .map(|k| Value::Property(PropertyValue::String(k.clone())))
7315 .collect(),
7316 ),
7317 Some(Value::Map(m)) => Value::List(
7318 m.keys()
7319 .map(|k| Value::Property(PropertyValue::String(k.clone())))
7320 .collect(),
7321 ),
7322 Some(Value::Null) | None => Value::Null,
7323 Some(other) => {
7324 return Err(QueryError::Type(format!(
7325 "keys() expects a node, relationship, or map, got {other:?}"
7326 )))
7327 }
7328 })
7329}
7330
7331fn labels_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7332 Ok(match arg {
7333 Some(Value::Node(n)) => Value::List(
7334 n.labels
7335 .iter()
7336 .map(|l| Value::Property(PropertyValue::String(l.clone())))
7337 .collect(),
7338 ),
7339 Some(Value::Null) | None => Value::Null,
7340 Some(other) => {
7341 return Err(QueryError::Type(format!(
7342 "labels() expects a node, got {other:?}"
7343 )))
7344 }
7345 })
7346}
7347
7348fn type_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7349 Ok(match arg {
7350 Some(Value::Edge(e)) => Value::Property(PropertyValue::String(e.label.clone())),
7351 Some(Value::Null) | None => Value::Null,
7352 Some(other) => {
7353 return Err(QueryError::Type(format!(
7354 "type() expects a relationship, got {other:?}"
7355 )))
7356 }
7357 })
7358}
7359
7360fn properties_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7361 Ok(match arg {
7362 Some(Value::Node(n)) => Value::Map(
7363 n.props
7364 .iter()
7365 .map(|(k, v)| (k.clone(), property_value_to_value(v.clone())))
7366 .collect(),
7367 ),
7368 Some(Value::Edge(e)) => Value::Map(
7369 e.props
7370 .iter()
7371 .map(|(k, v)| (k.clone(), property_value_to_value(v.clone())))
7372 .collect(),
7373 ),
7374 Some(Value::Map(m)) => Value::Map(m.clone()),
7375 Some(Value::Null) | None => Value::Null,
7376 Some(other) => {
7377 return Err(QueryError::Type(format!(
7378 "properties() expects a node, relationship, or map, got {other:?}"
7379 )))
7380 }
7381 })
7382}
7383
7384fn id_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7385 Ok(match arg {
7386 Some(Value::Node(n)) => Value::Property(PropertyValue::Int(n.id.0 as i64)),
7387 Some(Value::Edge(e)) => Value::Property(PropertyValue::Int(e.id.0 as i64)),
7388 Some(Value::Null) | None => Value::Null,
7389 Some(other) => {
7390 return Err(QueryError::Type(format!(
7391 "id() expects a node or relationship, got {other:?}"
7392 )))
7393 }
7394 })
7395}
7396
7397fn size_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7398 Ok(match arg {
7399 Some(Value::List(items)) => Value::Property(PropertyValue::Int(items.len() as i64)),
7400 Some(Value::Null) | None => Value::Null,
7401 Some(other) => match as_arith_str(other) {
7402 Some(s) => Value::Property(PropertyValue::Int(s.chars().count() as i64)),
7403 None => {
7404 return Err(QueryError::Type(format!(
7405 "size() expects a list or string, got {other:?}"
7406 )))
7407 }
7408 },
7409 })
7410}
7411
7412fn nodes_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7413 Ok(match arg {
7414 Some(Value::Path(elems)) => Value::List(
7415 elems
7416 .iter()
7417 .filter_map(|e| match e {
7418 PathElem::Node(n) => Some(Value::Node(n.clone())),
7419 PathElem::Edge(_) => None,
7420 })
7421 .collect(),
7422 ),
7423 Some(Value::Null) | None => Value::Null,
7424 Some(other) => {
7425 return Err(QueryError::Type(format!(
7426 "nodes() expects a path, got {other:?}"
7427 )))
7428 }
7429 })
7430}
7431
7432fn relationships_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7433 Ok(match arg {
7434 Some(Value::Path(elems)) => Value::List(
7435 elems
7436 .iter()
7437 .filter_map(|e| match e {
7438 PathElem::Edge(e) => Some(Value::Edge(e.clone())),
7439 PathElem::Node(_) => None,
7440 })
7441 .collect(),
7442 ),
7443 Some(Value::Null) | None => Value::Null,
7444 Some(other) => {
7445 return Err(QueryError::Type(format!(
7446 "relationships() expects a path, got {other:?}"
7447 )))
7448 }
7449 })
7450}
7451
7452fn list_edge_builtin(
7456 arg: Option<&Value>,
7457 fn_name: &str,
7458 pick: impl Fn(&[Value]) -> Option<Value>,
7459) -> Result<Value, QueryError> {
7460 Ok(match arg {
7461 Some(Value::List(items)) => pick(items).unwrap_or(Value::Null),
7462 Some(Value::Null) | None => Value::Null,
7463 Some(other) => {
7464 return Err(QueryError::Type(format!(
7465 "{fn_name}() expects a list, got {other:?}"
7466 )))
7467 }
7468 })
7469}
7470
7471fn range_builtin(args: &[Value]) -> Result<Value, QueryError> {
7477 let int_arg = |v: &Value, which: &str| -> Result<i64, QueryError> {
7478 value_as_i64(v).ok_or_else(|| {
7479 QueryError::Type(format!("range()'s {which} must be an integer, got {v:?}"))
7480 })
7481 };
7482 let start = int_arg(
7483 args.first()
7484 .ok_or_else(|| QueryError::Semantic("range() requires at least 2 arguments".into()))?,
7485 "start",
7486 )?;
7487 let end = int_arg(
7488 args.get(1)
7489 .ok_or_else(|| QueryError::Semantic("range() requires at least 2 arguments".into()))?,
7490 "end",
7491 )?;
7492 let step = match args.get(2) {
7493 Some(v) => int_arg(v, "step")?,
7494 None => 1,
7495 };
7496 if step == 0 {
7497 return Err(QueryError::Type("range()'s step can't be 0".into()));
7498 }
7499 let mut out = Vec::new();
7500 let mut i = start;
7501 if step > 0 {
7502 while i <= end {
7503 out.push(Value::Property(PropertyValue::Int(i)));
7504 i += step;
7505 }
7506 } else {
7507 while i >= end {
7508 out.push(Value::Property(PropertyValue::Int(i)));
7509 i += step;
7510 }
7511 }
7512 Ok(Value::List(out))
7513}
7514
7515fn string_transform(
7516 arg: Option<&Value>,
7517 fn_name: &str,
7518 f: impl FnOnce(&str) -> String,
7519) -> Result<Value, QueryError> {
7520 Ok(match arg {
7521 Some(Value::Null) | None => Value::Null,
7522 Some(other) => match as_arith_str(other) {
7523 Some(s) => Value::Property(PropertyValue::String(f(s))),
7524 None => {
7525 return Err(QueryError::Type(format!(
7526 "{fn_name}() expects a string, got {other:?}"
7527 )))
7528 }
7529 },
7530 })
7531}
7532
7533fn reverse_builtin(arg: Option<&Value>) -> Result<Value, QueryError> {
7534 Ok(match arg {
7535 Some(Value::Null) | None => Value::Null,
7536 Some(Value::List(items)) => Value::List(items.iter().rev().cloned().collect()),
7537 Some(other) => match as_arith_str(other) {
7538 Some(s) => Value::Property(PropertyValue::String(s.chars().rev().collect())),
7539 None => {
7540 return Err(QueryError::Type(format!(
7541 "reverse() expects a string or list, got {other:?}"
7542 )))
7543 }
7544 },
7545 })
7546}
7547
7548fn replace_str_arg<'a>(v: &'a Value, which: &str) -> Result<&'a str, QueryError> {
7552 as_arith_str(v)
7553 .ok_or_else(|| QueryError::Type(format!("replace()'s {which} must be a string, got {v:?}")))
7554}
7555
7556fn replace_builtin(args: &[Value]) -> Result<Value, QueryError> {
7557 if args.iter().any(|v| matches!(v, Value::Null)) {
7558 return Ok(Value::Null);
7559 }
7560 let original = replace_str_arg(
7561 args.first()
7562 .ok_or_else(|| QueryError::Semantic("replace() requires 3 arguments".into()))?,
7563 "original",
7564 )?;
7565 let search = replace_str_arg(
7566 args.get(1)
7567 .ok_or_else(|| QueryError::Semantic("replace() requires 3 arguments".into()))?,
7568 "search",
7569 )?;
7570 let replacement = replace_str_arg(
7571 args.get(2)
7572 .ok_or_else(|| QueryError::Semantic("replace() requires 3 arguments".into()))?,
7573 "replacement",
7574 )?;
7575 Ok(Value::Property(PropertyValue::String(
7576 original.replace(search, replacement),
7577 )))
7578}
7579
7580fn split_builtin(args: &[Value]) -> Result<Value, QueryError> {
7581 if args.iter().any(|v| matches!(v, Value::Null)) {
7582 return Ok(Value::Null);
7583 }
7584 let s = args
7585 .first()
7586 .and_then(as_arith_str)
7587 .ok_or_else(|| QueryError::Type("split()'s first argument must be a string".into()))?;
7588 let delim = args
7589 .get(1)
7590 .and_then(as_arith_str)
7591 .ok_or_else(|| QueryError::Type("split()'s second argument must be a string".into()))?;
7592 let parts = if delim.is_empty() {
7593 s.split("").filter(|p| !p.is_empty()).collect::<Vec<_>>()
7594 } else {
7595 s.split(delim).collect::<Vec<_>>()
7596 };
7597 Ok(Value::List(
7598 parts
7599 .into_iter()
7600 .map(|p| Value::Property(PropertyValue::String(p.to_string())))
7601 .collect(),
7602 ))
7603}
7604
7605fn substring_builtin(args: &[Value]) -> Result<Value, QueryError> {
7611 if matches!(args.first(), Some(Value::Null)) {
7612 return Ok(Value::Null);
7613 }
7614 let s = args
7615 .first()
7616 .and_then(as_arith_str)
7617 .ok_or_else(|| QueryError::Type("substring()'s first argument must be a string".into()))?;
7618 let chars: Vec<char> = s.chars().collect();
7619 let start = args
7620 .get(1)
7621 .and_then(value_as_i64)
7622 .ok_or_else(|| QueryError::Type("substring()'s start must be an integer".into()))?
7623 .max(0) as usize;
7624 let start = start.min(chars.len());
7625 let end = match args.get(2) {
7626 Some(v) => {
7627 let len = value_as_i64(v)
7628 .ok_or_else(|| QueryError::Type("substring()'s length must be an integer".into()))?
7629 .max(0) as usize;
7630 (start + len).min(chars.len())
7631 }
7632 None => chars.len(),
7633 };
7634 Ok(Value::Property(PropertyValue::String(
7635 chars[start..end].iter().collect(),
7636 )))
7637}
7638
7639fn left_right_builtin(args: &[Value], from_left: bool) -> Result<Value, QueryError> {
7642 if matches!(args.first(), Some(Value::Null)) {
7643 return Ok(Value::Null);
7644 }
7645 let fn_name = if from_left { "left" } else { "right" };
7646 let s = args.first().and_then(as_arith_str).ok_or_else(|| {
7647 QueryError::Type(format!("{fn_name}()'s first argument must be a string"))
7648 })?;
7649 let n = args
7650 .get(1)
7651 .and_then(value_as_i64)
7652 .ok_or_else(|| {
7653 QueryError::Type(format!("{fn_name}()'s second argument must be an integer"))
7654 })?
7655 .max(0) as usize;
7656 let chars: Vec<char> = s.chars().collect();
7657 let n = n.min(chars.len());
7658 let slice = if from_left {
7659 &chars[..n]
7660 } else {
7661 &chars[chars.len() - n..]
7662 };
7663 Ok(Value::Property(PropertyValue::String(
7664 slice.iter().collect(),
7665 )))
7666}
7667
7668fn float_math_fn(
7669 arg: Option<&Value>,
7670 fn_name: &str,
7671 f: impl FnOnce(f64) -> f64,
7672) -> Result<Value, QueryError> {
7673 Ok(match arg {
7674 Some(Value::Null) | None => Value::Null,
7675 Some(other) => match value_as_f64(other) {
7676 Some(x) => Value::Property(PropertyValue::Float(f(x))),
7677 None => {
7678 return Err(QueryError::Type(format!(
7679 "{fn_name}() expects a number, got {other:?}"
7680 )))
7681 }
7682 },
7683 })
7684}
7685
7686fn to_float(v: &Value) -> Result<Value, QueryError> {
7687 Ok(match v {
7688 Value::Property(PropertyValue::Int(i)) => Value::Property(PropertyValue::Float(*i as f64)),
7689 Value::Property(PropertyValue::Float(f)) => Value::Property(PropertyValue::Float(*f)),
7690 Value::Literal(Literal::Int(i)) => Value::Property(PropertyValue::Float(*i as f64)),
7691 Value::Literal(Literal::Float(f)) => Value::Property(PropertyValue::Float(*f)),
7692 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => {
7693 match s.trim().parse::<f64>() {
7694 Ok(f) => Value::Property(PropertyValue::Float(f)),
7695 Err(_) => Value::Null,
7696 }
7697 }
7698 Value::Property(PropertyValue::Null) | Value::Literal(Literal::Null) | Value::Null => {
7699 Value::Null
7700 }
7701 Value::Literal(Literal::Param(name)) => {
7702 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7703 }
7704 other => {
7709 return Err(QueryError::Type(format!(
7710 "toFloat() cannot convert {other:?} to a float"
7711 )))
7712 }
7713 })
7714}
7715
7716fn to_boolean(v: &Value) -> Result<Value, QueryError> {
7717 Ok(match v {
7718 Value::Property(PropertyValue::Bool(b)) | Value::Literal(Literal::Bool(b)) => {
7719 Value::Literal(Literal::Bool(*b))
7720 }
7721 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => {
7722 match s.trim().to_ascii_lowercase().as_str() {
7723 "true" => Value::Literal(Literal::Bool(true)),
7724 "false" => Value::Literal(Literal::Bool(false)),
7725 _ => Value::Null,
7726 }
7727 }
7728 Value::Property(PropertyValue::Null) | Value::Literal(Literal::Null) | Value::Null => {
7729 Value::Null
7730 }
7731 Value::Literal(Literal::Param(name)) => {
7732 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7733 }
7734 other => {
7735 return Err(QueryError::Type(format!(
7736 "toBoolean() cannot convert {other:?} to a boolean"
7737 )))
7738 }
7739 })
7740}
7741
7742fn item_truthy(v: &Value) -> Option<bool> {
7749 match v {
7750 Value::Null => None,
7751 Value::Literal(Literal::Bool(b)) | Value::Property(PropertyValue::Bool(b)) => Some(*b),
7752 _ => Some(false),
7753 }
7754}
7755
7756fn eval_quantifier(kind: QuantifierKind, preds: &[Option<bool>]) -> Option<bool> {
7766 let true_count = preds.iter().filter(|p| **p == Some(true)).count();
7767 let any_false = preds.contains(&Some(false));
7768 let any_null = preds.iter().any(|p| p.is_none());
7769 match kind {
7770 QuantifierKind::Any => {
7771 if true_count > 0 {
7772 Some(true)
7773 } else if any_null {
7774 None
7775 } else {
7776 Some(false)
7777 }
7778 }
7779 QuantifierKind::None => {
7780 if true_count > 0 {
7781 Some(false)
7782 } else if any_null {
7783 None
7784 } else {
7785 Some(true)
7786 }
7787 }
7788 QuantifierKind::All => {
7789 if any_false {
7790 Some(false)
7791 } else if any_null {
7792 None
7793 } else {
7794 Some(true)
7795 }
7796 }
7797 QuantifierKind::Single => {
7798 if true_count >= 2 {
7799 Some(false)
7800 } else if any_null {
7801 None
7802 } else {
7803 Some(true_count == 1)
7804 }
7805 }
7806 }
7807}
7808
7809fn to_integer(v: &Value) -> Result<Value, QueryError> {
7810 let as_str_parse = |s: &str| match s.trim().parse::<i64>() {
7816 Ok(i) => Value::Property(PropertyValue::Int(i)),
7817 Err(_) => match s.trim().parse::<f64>() {
7818 Ok(f) => Value::Property(PropertyValue::Int(f as i64)),
7819 Err(_) => Value::Null,
7820 },
7821 };
7822 Ok(match v {
7823 Value::Property(PropertyValue::Int(i)) => Value::Property(PropertyValue::Int(*i)),
7824 Value::Property(PropertyValue::Float(f)) => Value::Property(PropertyValue::Int(*f as i64)),
7825 Value::Property(PropertyValue::String(s)) => as_str_parse(s),
7826 Value::Literal(Literal::Int(i)) => Value::Property(PropertyValue::Int(*i)),
7827 Value::Literal(Literal::Float(f)) => Value::Property(PropertyValue::Int(*f as i64)),
7828 Value::Literal(Literal::String(s)) => as_str_parse(s),
7829 Value::Property(PropertyValue::Bool(_) | PropertyValue::Null)
7830 | Value::Literal(Literal::Bool(_) | Literal::Null)
7831 | Value::Null => Value::Null,
7832 Value::Literal(Literal::Param(name)) => {
7833 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7834 }
7835 Value::Property(
7840 PropertyValue::Date(_)
7841 | PropertyValue::Duration { .. }
7842 | PropertyValue::LocalTime(_)
7843 | PropertyValue::Time { .. }
7844 | PropertyValue::LocalDateTime { .. }
7845 | PropertyValue::DateTime { .. }
7846 | PropertyValue::List(_)
7847 | PropertyValue::Map(_),
7848 )
7849 | Value::Node(_)
7850 | Value::Edge(_)
7851 | Value::List(_)
7852 | Value::Map(_)
7853 | Value::Path(_) => {
7854 return Err(QueryError::Type(format!(
7855 "toInteger() cannot convert {v:?} to an integer"
7856 )))
7857 }
7858 })
7859}
7860
7861fn to_string_value(v: &Value) -> Result<Value, QueryError> {
7868 let s = match v {
7869 Value::Property(PropertyValue::String(s)) | Value::Literal(Literal::String(s)) => s.clone(),
7870 Value::Property(PropertyValue::Int(i)) | Value::Literal(Literal::Int(i)) => i.to_string(),
7871 Value::Property(PropertyValue::Float(f)) | Value::Literal(Literal::Float(f)) => {
7872 f.to_string()
7873 }
7874 Value::Property(PropertyValue::Bool(b)) | Value::Literal(Literal::Bool(b)) => b.to_string(),
7875 Value::Property(PropertyValue::Date(d)) => temporal::format_date(*d),
7876 Value::Property(PropertyValue::Duration {
7877 months,
7878 days,
7879 seconds,
7880 nanos,
7881 }) => temporal::format_duration(*months, *days, *seconds, *nanos),
7882 Value::Property(PropertyValue::LocalTime(nanos_of_day)) => {
7883 temporal::format_local_time(*nanos_of_day)
7884 }
7885 Value::Property(PropertyValue::Time {
7886 nanos_of_day,
7887 offset_seconds,
7888 }) => temporal::format_time(*nanos_of_day, *offset_seconds),
7889 Value::Property(PropertyValue::LocalDateTime {
7890 epoch_seconds,
7891 nanos,
7892 }) => temporal::format_local_date_time(*epoch_seconds, *nanos),
7893 Value::Property(PropertyValue::DateTime {
7894 epoch_seconds,
7895 nanos,
7896 zone,
7897 }) => temporal::format_date_time(*epoch_seconds, *nanos, &tz_from_graph(zone)),
7898 Value::Property(PropertyValue::Null) | Value::Literal(Literal::Null) | Value::Null => {
7899 return Ok(Value::Null);
7900 }
7901 Value::Literal(Literal::Param(name)) => {
7902 unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
7903 }
7904 Value::Property(PropertyValue::List(_) | PropertyValue::Map(_))
7905 | Value::Node(_)
7906 | Value::Edge(_)
7907 | Value::List(_)
7908 | Value::Map(_)
7909 | Value::Path(_) => {
7910 return Err(QueryError::Type(format!(
7911 "toString() cannot convert {v:?} to a string"
7912 )))
7913 }
7914 };
7915 Ok(Value::Property(PropertyValue::String(s)))
7916}
7917
7918fn now_or_null(args: &[Value], now_value: impl FnOnce() -> Value) -> Value {
7941 if matches!(args.first(), Some(Value::Null)) {
7942 Value::Null
7943 } else {
7944 now_value()
7945 }
7946}
7947
7948fn date_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
7949 if args.len() > 1 {
7950 return Err(QueryError::Semantic(format!(
7951 "date() expects zero or one argument, got {}",
7952 args.len()
7953 )));
7954 }
7955 let Some(arg) = args.first() else {
7956 return Ok(Value::Property(PropertyValue::Date(now.epoch_day)));
7957 };
7958 if matches!(arg, Value::Null) {
7959 return Ok(Value::Null);
7960 }
7961 if let Value::Property(PropertyValue::Date(d)) = arg {
7962 return Ok(Value::Property(PropertyValue::Date(*d)));
7963 }
7964 if matches!(
7968 arg,
7969 Value::Property(PropertyValue::LocalDateTime { .. } | PropertyValue::DateTime { .. })
7970 ) {
7971 let epoch_day = extract_date_base_epoch_day("date() argument", arg)?;
7972 return Ok(Value::Property(PropertyValue::Date(epoch_day)));
7973 }
7974 if let Some(s) = as_arith_str(arg) {
7975 let d = temporal::parse_date(s).ok_or_else(|| {
7976 QueryError::Type(format!(
7977 "'{s}' isn't a date string MarsDB can parse -- only the calendar forms YYYY-MM-DD/YYYYMMDD/\
7978 YYYY-MM/YYYYMM/YYYY, week-date forms YYYY-Www[-D]/YYYYWww[D], and ordinal-date forms \
7979 YYYY-DDD/YYYYDDD are supported"
7980 ))
7981 })?;
7982 return Ok(Value::Property(PropertyValue::Date(d)));
7983 }
7984 if let Value::Map(m) = arg {
7985 return Ok(Value::Property(PropertyValue::Date(date_from_map(m)?)));
7986 }
7987 Err(QueryError::Type(format!(
7988 "date() doesn't support this argument: {arg:?}"
7989 )))
7990}
7991
7992fn extract_date_base_epoch_day(key: &str, v: &Value) -> Result<i32, QueryError> {
8003 match v {
8004 Value::Property(PropertyValue::Date(d)) => Ok(*d),
8005 Value::Property(PropertyValue::LocalDateTime { epoch_seconds, .. }) => {
8006 Ok(temporal::split_epoch_seconds(*epoch_seconds).0)
8007 }
8008 Value::Property(PropertyValue::DateTime {
8009 epoch_seconds,
8010 zone,
8011 ..
8012 }) => {
8013 let offset_seconds = temporal::resolve_offset(&tz_from_graph(zone), *epoch_seconds);
8014 Ok(temporal::split_epoch_seconds(epoch_seconds + offset_seconds as i64).0)
8015 }
8016 other => Err(QueryError::Type(format!(
8017 "'{key}' must be a Date, LocalDateTime, or DateTime, got {other:?}"
8018 ))),
8019 }
8020}
8021
8022type ClockBase = (i64, i64, i64, i64, Option<(temporal::TzId, i32)>);
8036
8037fn extract_time_base(key: &str, v: &Value) -> Result<ClockBase, QueryError> {
8038 let hms_nanos = |nanos_of_day: i64| {
8039 (
8040 temporal::local_time_component(nanos_of_day, "hour").unwrap(),
8041 temporal::local_time_component(nanos_of_day, "minute").unwrap(),
8042 temporal::local_time_component(nanos_of_day, "second").unwrap(),
8043 temporal::local_time_component(nanos_of_day, "nanosecond").unwrap(),
8044 )
8045 };
8046 match v {
8047 Value::Property(PropertyValue::LocalTime(n)) => {
8048 let (h, m, s, ns) = hms_nanos(*n);
8049 Ok((h, m, s, ns, None))
8050 }
8051 Value::Property(PropertyValue::Time {
8052 nanos_of_day,
8053 offset_seconds,
8054 }) => {
8055 let (h, m, s, ns) = hms_nanos(*nanos_of_day);
8056 Ok((
8057 h,
8058 m,
8059 s,
8060 ns,
8061 Some((temporal::TzId::Offset(*offset_seconds), *offset_seconds)),
8062 ))
8063 }
8064 Value::Property(PropertyValue::LocalDateTime {
8065 epoch_seconds,
8066 nanos,
8067 }) => {
8068 let (_, nanos_of_day) = temporal::split_epoch_seconds(*epoch_seconds);
8069 let (h, m, s, _) = hms_nanos(nanos_of_day);
8070 Ok((h, m, s, *nanos as i64, None))
8071 }
8072 Value::Property(PropertyValue::DateTime {
8073 epoch_seconds,
8074 nanos,
8075 zone,
8076 }) => {
8077 let tz = tz_from_graph(zone);
8078 let offset_seconds = temporal::resolve_offset(&tz, *epoch_seconds);
8079 let local = epoch_seconds + offset_seconds as i64;
8080 let (_, nanos_of_day) = temporal::split_epoch_seconds(local);
8081 let (h, m, s, _) = hms_nanos(nanos_of_day);
8082 Ok((h, m, s, *nanos as i64, Some((tz, offset_seconds))))
8083 }
8084 other => Err(QueryError::Type(format!(
8085 "'{key}' must be a LocalTime, Time, LocalDateTime, or DateTime, got {other:?}"
8086 ))),
8087 }
8088}
8089
8090const DATE_ALLOWED_KEYS: &[&str] = &[
8091 "year",
8092 "month",
8093 "day",
8094 "week",
8095 "dayOfWeek",
8096 "ordinalDay",
8097 "quarter",
8098 "dayOfQuarter",
8099 "date",
8100];
8101
8102fn date_from_map(m: &BTreeMap<String, Value>) -> Result<i32, QueryError> {
8103 let (year, month, day) = calendar_fields_from_map("date", m, DATE_ALLOWED_KEYS)?;
8104 temporal::epoch_day_from_ymd(year, month, day).ok_or_else(|| {
8105 QueryError::Type(format!(
8106 "{year:04}-{month:02}-{day:02} isn't a valid calendar date"
8107 ))
8108 })
8109}
8110
8111fn calendar_fields_from_map(
8125 caller: &str,
8126 m: &BTreeMap<String, Value>,
8127 allowed: &[&str],
8128) -> Result<(i32, u32, u32), QueryError> {
8129 if let Some(bad) = m.keys().find(|k| !allowed.contains(&k.as_str())) {
8130 return Err(QueryError::Type(format!(
8131 "{caller}({{...}}) key '{bad}' isn't a recognized field"
8132 )));
8133 }
8134 let int_field = |key: &str, value: &Value| {
8135 value_as_i64(value).ok_or_else(|| {
8136 QueryError::Type(format!("{caller}({{...}})'s '{key}' must be an integer"))
8137 })
8138 };
8139 let base_epoch_day = m
8140 .get("date")
8141 .map(|v| ("date", v))
8142 .or_else(|| m.get("datetime").map(|v| ("datetime", v)))
8143 .map(|(k, v)| extract_date_base_epoch_day(k, v))
8144 .transpose()?;
8145 let epoch_day_from_component =
8146 |prop: &str| base_epoch_day.map(|ed| temporal::date_component(ed, prop).unwrap());
8147
8148 if m.contains_key("week") || m.contains_key("dayOfWeek") {
8149 let week_year = match m.get("year") {
8150 Some(v) => i32::try_from(int_field("year", v)?).map_err(|_| {
8151 QueryError::Type(format!("{caller}({{...}})'s 'year' is out of range"))
8152 })?,
8153 None => i32::try_from(epoch_day_from_component("weekYear").ok_or_else(|| {
8154 QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
8155 })?)
8156 .unwrap(),
8157 };
8158 let week = match m.get("week") {
8159 Some(v) => u32::try_from(int_field("week", v)?).map_err(|_| {
8160 QueryError::Type(format!("{caller}({{...}})'s 'week' is out of range"))
8161 })?,
8162 None => u32::try_from(epoch_day_from_component("week").ok_or_else(|| {
8163 QueryError::Type(format!("{caller}({{...}}) requires a 'week' key"))
8164 })?)
8165 .unwrap(),
8166 };
8167 let day_of_week = match m.get("dayOfWeek") {
8168 Some(v) => int_field("dayOfWeek", v)?,
8169 None => epoch_day_from_component("dayOfWeek").unwrap_or(1),
8170 };
8171 let epoch_day = temporal::epoch_day_from_week_fields(week_year, week, day_of_week)
8172 .ok_or_else(|| {
8173 QueryError::Type(format!(
8174 "{caller}({{...}}) has an out-of-range week-date field"
8175 ))
8176 })?;
8177 return Ok((
8178 temporal::date_component(epoch_day, "year").unwrap() as i32,
8179 temporal::date_component(epoch_day, "month").unwrap() as u32,
8180 temporal::date_component(epoch_day, "day").unwrap() as u32,
8181 ));
8182 }
8183
8184 if m.contains_key("ordinalDay") {
8185 let year = match m.get("year") {
8186 Some(v) => i32::try_from(int_field("year", v)?).map_err(|_| {
8187 QueryError::Type(format!("{caller}({{...}})'s 'year' is out of range"))
8188 })?,
8189 None => i32::try_from(epoch_day_from_component("year").ok_or_else(|| {
8190 QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
8191 })?)
8192 .unwrap(),
8193 };
8194 let ordinal_raw = int_field("ordinalDay", m.get("ordinalDay").unwrap())?;
8195 let ordinal_day = u32::try_from(ordinal_raw).map_err(|_| {
8196 QueryError::Type(format!("{caller}({{...}})'s 'ordinalDay' is out of range"))
8197 })?;
8198 let epoch_day =
8199 temporal::epoch_day_from_ordinal_fields(year, ordinal_day).ok_or_else(|| {
8200 QueryError::Type(format!(
8201 "{caller}({{...}}) has an out-of-range ordinalDay field"
8202 ))
8203 })?;
8204 return Ok((
8205 year,
8206 temporal::date_component(epoch_day, "month").unwrap() as u32,
8207 temporal::date_component(epoch_day, "day").unwrap() as u32,
8208 ));
8209 }
8210
8211 if m.contains_key("quarter") || m.contains_key("dayOfQuarter") {
8212 let year = match m.get("year") {
8213 Some(v) => i32::try_from(int_field("year", v)?).map_err(|_| {
8214 QueryError::Type(format!("{caller}({{...}})'s 'year' is out of range"))
8215 })?,
8216 None => i32::try_from(epoch_day_from_component("year").ok_or_else(|| {
8217 QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
8218 })?)
8219 .unwrap(),
8220 };
8221 let quarter = match m.get("quarter") {
8222 Some(v) => u32::try_from(int_field("quarter", v)?).map_err(|_| {
8223 QueryError::Type(format!("{caller}({{...}})'s 'quarter' is out of range"))
8224 })?,
8225 None => u32::try_from(epoch_day_from_component("quarter").ok_or_else(|| {
8226 QueryError::Type(format!("{caller}({{...}}) requires a 'quarter' key"))
8227 })?)
8228 .unwrap(),
8229 };
8230 let day_of_quarter = match m.get("dayOfQuarter") {
8231 Some(v) => int_field("dayOfQuarter", v)?,
8232 None => epoch_day_from_component("dayOfQuarter").unwrap_or(1),
8233 };
8234 let epoch_day = temporal::epoch_day_from_quarter_fields(year, quarter, day_of_quarter)
8235 .ok_or_else(|| {
8236 QueryError::Type(format!(
8237 "{caller}({{...}}) has an out-of-range quarter-date field"
8238 ))
8239 })?;
8240 return Ok((
8241 year,
8242 temporal::date_component(epoch_day, "month").unwrap() as u32,
8243 temporal::date_component(epoch_day, "day").unwrap() as u32,
8244 ));
8245 }
8246
8247 let year_raw = match m.get("year") {
8248 Some(v) => int_field("year", v)?,
8249 None => epoch_day_from_component("year")
8250 .ok_or_else(|| QueryError::Type(format!("{caller}({{...}}) requires a 'year' key")))?,
8251 };
8252 let year = i32::try_from(year_raw).map_err(|_| {
8253 QueryError::Type(format!(
8254 "{caller}({{...}})'s 'year' is out of range: {year_raw}"
8255 ))
8256 })?;
8257 let month_raw = match m.get("month") {
8258 Some(v) => int_field("month", v)?,
8259 None => epoch_day_from_component("month").unwrap_or(1),
8260 };
8261 let month = u32::try_from(month_raw).map_err(|_| {
8262 QueryError::Type(format!(
8263 "{caller}({{...}})'s 'month' is out of range: {month_raw}"
8264 ))
8265 })?;
8266 let day_raw = match m.get("day") {
8267 Some(v) => int_field("day", v)?,
8268 None => epoch_day_from_component("day").unwrap_or(1),
8269 };
8270 let day = u32::try_from(day_raw).map_err(|_| {
8271 QueryError::Type(format!(
8272 "{caller}({{...}})'s 'day' is out of range: {day_raw}"
8273 ))
8274 })?;
8275 Ok((year, month, day))
8276}
8277
8278fn duration_builtin(args: &[Value]) -> Result<Value, QueryError> {
8284 if args.len() != 1 {
8285 return Err(QueryError::Semantic(format!(
8286 "duration() expects exactly one argument, got {}",
8287 args.len()
8288 )));
8289 }
8290 let arg = &args[0];
8291 if matches!(arg, Value::Null) {
8292 return Ok(Value::Null);
8293 }
8294 let (months, days, seconds, nanos) = if let Some(s) = as_arith_str(arg) {
8295 temporal::parse_duration(s).ok_or_else(|| {
8296 QueryError::Type(format!(
8297 "'{s}' isn't a duration string MarsDB can parse -- only ISO-8601 'PnYnMnWnDTnHnMnS' text is \
8298 supported, not the alternate combined date-time duration syntax"
8299 ))
8300 })?
8301 } else if let Value::Map(m) = arg {
8302 temporal::normalize_duration(duration_fields_from_map(m)?)
8303 } else {
8304 return Err(QueryError::Type(format!(
8305 "duration() doesn't support this argument: {arg:?}"
8306 )));
8307 };
8308 Ok(Value::Property(PropertyValue::Duration {
8309 months,
8310 days,
8311 seconds,
8312 nanos,
8313 }))
8314}
8315
8316fn duration_fields_from_map(
8317 m: &BTreeMap<String, Value>,
8318) -> Result<temporal::DurationFields, QueryError> {
8319 const ALLOWED: &[&str] = &[
8320 "years",
8321 "months",
8322 "weeks",
8323 "days",
8324 "hours",
8325 "minutes",
8326 "seconds",
8327 "milliseconds",
8328 "microseconds",
8329 "nanoseconds",
8330 ];
8331 if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
8332 return Err(QueryError::Type(format!(
8333 "duration({{...}}) key '{bad}' isn't a recognized duration unit"
8334 )));
8335 }
8336 let field = |key: &str| -> Result<f64, QueryError> {
8337 match m.get(key) {
8338 None => Ok(0.0),
8339 Some(v) => value_as_f64(v).ok_or_else(|| {
8340 QueryError::Type(format!("duration({{...}})'s '{key}' must be a number"))
8341 }),
8342 }
8343 };
8344 Ok(temporal::DurationFields {
8345 years: field("years")?,
8346 months: field("months")?,
8347 weeks: field("weeks")?,
8348 days: field("days")?,
8349 hours: field("hours")?,
8350 minutes: field("minutes")?,
8351 seconds: field("seconds")?,
8352 milliseconds: field("milliseconds")?,
8353 microseconds: field("microseconds")?,
8354 nanoseconds: field("nanoseconds")?,
8355 })
8356}
8357
8358fn sub_second_nanos_from_map(
8377 base_fraction_ns: i64,
8378 m: &BTreeMap<String, Value>,
8379) -> Result<i64, QueryError> {
8380 let base_ms = base_fraction_ns / 1_000_000;
8381 let base_us = (base_fraction_ns / 1_000) % 1000;
8382 let base_ns = base_fraction_ns % 1000;
8383 let ms = int_field(m, "millisecond", base_ms)?;
8384 let us = int_field(m, "microsecond", base_us)?;
8385 let ns = int_field(m, "nanosecond", base_ns)?;
8386 Ok(ms * 1_000_000 + us * 1_000 + ns)
8387}
8388
8389fn int_field(m: &BTreeMap<String, Value>, key: &str, default: i64) -> Result<i64, QueryError> {
8390 match m.get(key) {
8391 None => Ok(default),
8392 Some(v) => {
8393 value_as_i64(v).ok_or_else(|| QueryError::Type(format!("'{key}' must be an integer")))
8394 }
8395 }
8396}
8397
8398fn clock_fields_from_map(
8429 m: &BTreeMap<String, Value>,
8430 epoch_day: Option<i32>,
8431) -> Result<ClockBase, QueryError> {
8432 let (base_h, base_m, base_s, base_ns, base_zone) = if let Some(v) = m.get("time") {
8433 extract_time_base("time", v)?
8434 } else if let Some(v) = m.get("datetime") {
8435 extract_time_base("datetime", v)?
8436 } else {
8437 (0, 0, 0, 0, None)
8438 };
8439 let has_explicit_timezone = m.contains_key("timezone");
8440 let effective_zone = match m.get("timezone") {
8441 Some(v) => Some(timezone_value_to_tzid(v)?),
8442 None => base_zone.as_ref().map(|(tz, _)| tz.clone()),
8447 };
8448 let base_nanos_of_day =
8458 base_h * 3_600_000_000_000 + base_m * 60_000_000_000 + base_s * 1_000_000_000 + base_ns;
8459 let (base_h, base_m, base_s, base_ns, effective_offset) = if has_explicit_timezone {
8460 let from_offset = match base_zone.as_ref() {
8468 Some((temporal::TzId::Offset(o), _)) => Some(*o),
8469 Some((zone @ temporal::TzId::Named(_), resolved)) => Some(match epoch_day {
8470 Some(ed) => temporal::resolve_offset(
8471 zone,
8472 temporal::combine_epoch_day_and_nanos_of_day(ed, base_nanos_of_day),
8473 ),
8474 None => *resolved,
8475 }),
8476 None => None,
8477 };
8478 let to_offset = match (from_offset, effective_zone.as_ref(), epoch_day) {
8479 (Some(_), Some(temporal::TzId::Offset(to)), _) => Some(*to),
8480 (Some(from), Some(zone @ temporal::TzId::Named(_)), Some(ed)) => {
8481 let approx_epoch_seconds =
8482 temporal::combine_epoch_day_and_nanos_of_day(ed, base_nanos_of_day)
8483 - from as i64;
8484 Some(temporal::resolve_offset(zone, approx_epoch_seconds))
8485 }
8486 _ => None,
8487 };
8488 match (from_offset, to_offset) {
8489 (Some(from), Some(to)) if from != to => {
8490 let shifted = (base_nanos_of_day + (to - from) as i64 * 1_000_000_000)
8491 .rem_euclid(86_400_000_000_000);
8492 (
8493 shifted / 3_600_000_000_000,
8494 (shifted / 60_000_000_000) % 60,
8495 (shifted / 1_000_000_000) % 60,
8496 shifted % 1_000_000_000,
8497 to_offset.unwrap_or(0),
8498 )
8499 }
8500 _ => (base_h, base_m, base_s, base_ns, to_offset.unwrap_or(0)),
8501 }
8502 } else {
8503 (
8509 base_h,
8510 base_m,
8511 base_s,
8512 base_ns,
8513 base_zone.as_ref().map_or(0, |(_, o)| *o),
8514 )
8515 };
8516 Ok((
8517 int_field(m, "hour", base_h)?,
8518 int_field(m, "minute", base_m)?,
8519 int_field(m, "second", base_s)?,
8520 sub_second_nanos_from_map(base_ns, m)?,
8521 effective_zone.map(|z| (z, effective_offset)),
8522 ))
8523}
8524
8525fn local_time_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
8531 if args.len() > 1 {
8532 return Err(QueryError::Semantic(format!(
8533 "localtime() expects zero or one argument, got {}",
8534 args.len()
8535 )));
8536 }
8537 let Some(arg) = args.first() else {
8538 return Ok(Value::Property(PropertyValue::LocalTime(now.nanos_of_day)));
8539 };
8540 if matches!(arg, Value::Null) {
8541 return Ok(Value::Null);
8542 }
8543 if let Value::Property(PropertyValue::LocalTime(t)) = arg {
8544 return Ok(Value::Property(PropertyValue::LocalTime(*t)));
8545 }
8546 if matches!(
8550 arg,
8551 Value::Property(
8552 PropertyValue::Time { .. }
8553 | PropertyValue::LocalDateTime { .. }
8554 | PropertyValue::DateTime { .. }
8555 )
8556 ) {
8557 let (hour, minute, second, nanos, _) = extract_time_base("localtime() argument", arg)?;
8558 let t = temporal::local_time_nanos_from_fields(hour, minute, second, nanos).ok_or_else(
8559 || QueryError::Type("localtime() argument has an out-of-range field".into()),
8560 )?;
8561 return Ok(Value::Property(PropertyValue::LocalTime(t)));
8562 }
8563 if let Some(s) = as_arith_str(arg) {
8564 let t = temporal::parse_local_time(s).ok_or_else(|| {
8565 QueryError::Type(format!("'{s}' isn't a local time string MarsDB can parse"))
8566 })?;
8567 return Ok(Value::Property(PropertyValue::LocalTime(t)));
8568 }
8569 if let Value::Map(m) = arg {
8570 const ALLOWED: &[&str] = &[
8571 "hour",
8572 "minute",
8573 "second",
8574 "millisecond",
8575 "microsecond",
8576 "nanosecond",
8577 "time",
8578 ];
8579 if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
8580 return Err(QueryError::Type(format!(
8581 "localtime({{...}}) key '{bad}' isn't a recognized field"
8582 )));
8583 }
8584 let (hour, minute, second, nanos, _) = clock_fields_from_map(m, None)?;
8585 let t = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
8586 .ok_or_else(|| QueryError::Type("localtime({...}) has an out-of-range field".into()))?;
8587 return Ok(Value::Property(PropertyValue::LocalTime(t)));
8588 }
8589 Err(QueryError::Type(format!(
8590 "localtime() doesn't support this argument: {arg:?}"
8591 )))
8592}
8593
8594fn time_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
8601 if args.len() > 1 {
8602 return Err(QueryError::Semantic(format!(
8603 "time() expects zero or one argument, got {}",
8604 args.len()
8605 )));
8606 }
8607 let Some(arg) = args.first() else {
8608 return Ok(Value::Property(PropertyValue::Time {
8609 nanos_of_day: now.nanos_of_day,
8610 offset_seconds: 0,
8611 }));
8612 };
8613 if matches!(arg, Value::Null) {
8614 return Ok(Value::Null);
8615 }
8616 if let Value::Property(PropertyValue::Time {
8617 nanos_of_day,
8618 offset_seconds,
8619 }) = arg
8620 {
8621 return Ok(Value::Property(PropertyValue::Time {
8622 nanos_of_day: *nanos_of_day,
8623 offset_seconds: *offset_seconds,
8624 }));
8625 }
8626 if matches!(
8632 arg,
8633 Value::Property(
8634 PropertyValue::LocalTime(_)
8635 | PropertyValue::LocalDateTime { .. }
8636 | PropertyValue::DateTime { .. }
8637 )
8638 ) {
8639 let (hour, minute, second, nanos, zone) = extract_time_base("time() argument", arg)?;
8640 let nanos_of_day = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
8641 .ok_or_else(|| QueryError::Type("time() argument has an out-of-range field".into()))?;
8642 return Ok(Value::Property(PropertyValue::Time {
8643 nanos_of_day,
8644 offset_seconds: zone.map_or(0, |(_, o)| o),
8650 }));
8651 }
8652 if let Some(s) = as_arith_str(arg) {
8653 if s.contains('[') {
8654 return Err(QueryError::Type(
8655 "time('...'): named timezones (e.g. '[Europe/Stockholm]') aren't supported, only a fixed UTC \
8656 offset like '+01:00'"
8657 .into(),
8658 ));
8659 }
8660 let (nanos_of_day, offset_seconds) = temporal::parse_time(s).ok_or_else(|| {
8661 QueryError::Type(format!("'{s}' isn't a time string MarsDB can parse"))
8662 })?;
8663 return Ok(Value::Property(PropertyValue::Time {
8664 nanos_of_day,
8665 offset_seconds,
8666 }));
8667 }
8668 if let Value::Map(m) = arg {
8669 const ALLOWED: &[&str] = &[
8670 "hour",
8671 "minute",
8672 "second",
8673 "millisecond",
8674 "microsecond",
8675 "nanosecond",
8676 "timezone",
8677 "time",
8678 ];
8679 if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
8680 return Err(QueryError::Type(format!(
8681 "time({{...}}) key '{bad}' isn't a recognized field"
8682 )));
8683 }
8684 let (hour, minute, second, nanos, zone) = clock_fields_from_map(m, None)?;
8685 let offset_seconds = match zone {
8686 None => 0,
8687 Some((_, o)) if !m.contains_key("timezone") => o,
8696 Some((temporal::TzId::Offset(o), _)) => o,
8697 Some((temporal::TzId::Named(name), _)) => {
8698 return Err(QueryError::Type(format!(
8699 "'timezone': '{name}' looks like a named timezone (e.g. 'Europe/Stockholm') -- TIME has \
8700 no calendar date to resolve a named zone's DST-dependent offset against, only a fixed \
8701 UTC offset like '+01:00' is supported"
8702 )));
8703 }
8704 };
8705 let nanos_of_day = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
8706 .ok_or_else(|| QueryError::Type("time({...}) has an out-of-range field".into()))?;
8707 return Ok(Value::Property(PropertyValue::Time {
8708 nanos_of_day,
8709 offset_seconds,
8710 }));
8711 }
8712 Err(QueryError::Type(format!(
8713 "time() doesn't support this argument: {arg:?}"
8714 )))
8715}
8716
8717fn timezone_value_to_tzid(v: &Value) -> Result<temporal::TzId, QueryError> {
8725 let s = as_arith_str(v).ok_or_else(|| {
8726 QueryError::Type(
8727 "'timezone' must be a string offset or IANA zone name, e.g. '+01:00' or \
8728 'Europe/Stockholm'"
8729 .into(),
8730 )
8731 })?;
8732 if let Some(offset) = temporal::parse_offset_seconds(s) {
8733 return Ok(temporal::TzId::Offset(offset));
8734 }
8735 if temporal::parse_timezone_name(s).is_some() {
8736 return Ok(temporal::TzId::Named(s.to_string()));
8737 }
8738 Err(QueryError::Type(format!(
8739 "'timezone': '{s}' isn't a valid UTC offset or a recognized IANA zone name"
8740 )))
8741}
8742
8743fn local_date_time_builtin(
8747 args: &[Value],
8748 now: temporal::NowSnapshot,
8749) -> Result<Value, QueryError> {
8750 if args.len() > 1 {
8751 return Err(QueryError::Semantic(format!(
8752 "localdatetime() expects zero or one argument, got {}",
8753 args.len()
8754 )));
8755 }
8756 let Some(arg) = args.first() else {
8757 return Ok(Value::Property(PropertyValue::LocalDateTime {
8758 epoch_seconds: now.epoch_seconds,
8759 nanos: now.nanos,
8760 }));
8761 };
8762 if matches!(arg, Value::Null) {
8763 return Ok(Value::Null);
8764 }
8765 if let Value::Property(PropertyValue::LocalDateTime {
8766 epoch_seconds,
8767 nanos,
8768 }) = arg
8769 {
8770 return Ok(Value::Property(PropertyValue::LocalDateTime {
8771 epoch_seconds: *epoch_seconds,
8772 nanos: *nanos,
8773 }));
8774 }
8775 if matches!(arg, Value::Property(PropertyValue::DateTime { .. })) {
8779 let epoch_day = extract_date_base_epoch_day("localdatetime() argument", arg)?;
8780 let year = temporal::date_component(epoch_day, "year").unwrap() as i32;
8781 let month = temporal::date_component(epoch_day, "month").unwrap() as u32;
8782 let day = temporal::date_component(epoch_day, "day").unwrap() as u32;
8783 let (hour, minute, second, nanos, _) = extract_time_base("localdatetime() argument", arg)?;
8784 let (epoch_seconds, nanos) =
8785 temporal::local_date_time_from_fields(temporal::CalendarDateTime {
8786 year,
8787 month,
8788 day,
8789 hour,
8790 minute,
8791 second,
8792 nanos,
8793 })
8794 .ok_or_else(|| {
8795 QueryError::Type("localdatetime() argument has an out-of-range field".into())
8796 })?;
8797 return Ok(Value::Property(PropertyValue::LocalDateTime {
8798 epoch_seconds,
8799 nanos,
8800 }));
8801 }
8802 if let Some(s) = as_arith_str(arg) {
8803 let (epoch_seconds, nanos) = temporal::parse_local_date_time(s).ok_or_else(|| {
8804 QueryError::Type(format!(
8805 "'{s}' isn't a local date-time string MarsDB can parse"
8806 ))
8807 })?;
8808 return Ok(Value::Property(PropertyValue::LocalDateTime {
8809 epoch_seconds,
8810 nanos,
8811 }));
8812 }
8813 if let Value::Map(m) = arg {
8814 let (year, month, day) =
8815 calendar_fields_from_map("localdatetime", m, DATE_TIME_ALLOWED_KEYS)?;
8816 let (hour, minute, second, nanos, _) = clock_fields_from_map(m, None)?;
8817 let (epoch_seconds, nanos) =
8818 temporal::local_date_time_from_fields(temporal::CalendarDateTime {
8819 year,
8820 month,
8821 day,
8822 hour,
8823 minute,
8824 second,
8825 nanos,
8826 })
8827 .ok_or_else(|| {
8828 QueryError::Type("localdatetime({...}) has an out-of-range field".into())
8829 })?;
8830 return Ok(Value::Property(PropertyValue::LocalDateTime {
8831 epoch_seconds,
8832 nanos,
8833 }));
8834 }
8835 Err(QueryError::Type(format!(
8836 "localdatetime() doesn't support this argument: {arg:?}"
8837 )))
8838}
8839
8840const DATE_TIME_ALLOWED_KEYS: &[&str] = &[
8841 "year",
8842 "month",
8843 "day",
8844 "week",
8845 "dayOfWeek",
8846 "ordinalDay",
8847 "quarter",
8848 "dayOfQuarter",
8849 "hour",
8850 "minute",
8851 "second",
8852 "millisecond",
8853 "microsecond",
8854 "nanosecond",
8855 "timezone",
8856 "date",
8857 "time",
8858 "datetime",
8859];
8860
8861fn date_time_builtin(args: &[Value], now: temporal::NowSnapshot) -> Result<Value, QueryError> {
8868 if args.len() > 1 {
8869 return Err(QueryError::Semantic(format!(
8870 "datetime() expects zero or one argument, got {}",
8871 args.len()
8872 )));
8873 }
8874 let Some(arg) = args.first() else {
8875 return Ok(Value::Property(PropertyValue::DateTime {
8876 epoch_seconds: now.epoch_seconds,
8877 nanos: now.nanos,
8878 zone: GraphTzId::Offset(0),
8879 }));
8880 };
8881 if matches!(arg, Value::Null) {
8882 return Ok(Value::Null);
8883 }
8884 if let Value::Property(PropertyValue::DateTime {
8885 epoch_seconds,
8886 nanos,
8887 zone,
8888 }) = arg
8889 {
8890 return Ok(Value::Property(PropertyValue::DateTime {
8891 epoch_seconds: *epoch_seconds,
8892 nanos: *nanos,
8893 zone: zone.clone(),
8894 }));
8895 }
8896 if let Value::Property(PropertyValue::LocalDateTime {
8900 epoch_seconds,
8901 nanos,
8902 }) = arg
8903 {
8904 return Ok(Value::Property(PropertyValue::DateTime {
8905 epoch_seconds: *epoch_seconds,
8906 nanos: *nanos,
8907 zone: GraphTzId::Offset(0),
8908 }));
8909 }
8910 if let Some(s) = as_arith_str(arg) {
8911 let (epoch_seconds, nanos, zone) = temporal::parse_date_time(s).ok_or_else(|| {
8912 QueryError::Type(format!("'{s}' isn't a date-time string MarsDB can parse"))
8913 })?;
8914 return Ok(Value::Property(PropertyValue::DateTime {
8915 epoch_seconds,
8916 nanos,
8917 zone: tz_to_graph(zone),
8918 }));
8919 }
8920 if let Value::Map(m) = arg {
8921 let (year, month, day) = calendar_fields_from_map("datetime", m, DATE_TIME_ALLOWED_KEYS)?;
8922 let epoch_day = temporal::epoch_day_from_ymd(year, month, day);
8923 let (hour, minute, second, nanos, zone) = clock_fields_from_map(m, epoch_day)?;
8924 let zone = zone.map_or(temporal::TzId::Offset(0), |(z, _)| z);
8925 let (epoch_seconds, nanos) = temporal::date_time_from_fields(
8926 temporal::CalendarDateTime {
8927 year,
8928 month,
8929 day,
8930 hour,
8931 minute,
8932 second,
8933 nanos,
8934 },
8935 &zone,
8936 )
8937 .ok_or_else(|| QueryError::Type("datetime({...}) has an out-of-range field".into()))?;
8938 return Ok(Value::Property(PropertyValue::DateTime {
8939 epoch_seconds,
8940 nanos,
8941 zone: tz_to_graph(zone),
8942 }));
8943 }
8944 Err(QueryError::Type(format!(
8945 "datetime() doesn't support this argument: {arg:?}"
8946 )))
8947}
8948
8949fn between_operand(name: &str, v: &Value) -> Result<BetweenOperand, QueryError> {
8960 match v {
8961 Value::Property(PropertyValue::Date(d)) => Ok((Some(*d), None, None)),
8962 Value::Property(PropertyValue::LocalTime(n)) => Ok((None, Some(*n), None)),
8963 Value::Property(PropertyValue::Time {
8964 nanos_of_day,
8965 offset_seconds,
8966 }) => Ok((
8967 None,
8968 Some(*nanos_of_day),
8969 Some(temporal::TzId::Offset(*offset_seconds)),
8970 )),
8971 Value::Property(PropertyValue::LocalDateTime {
8972 epoch_seconds,
8973 nanos,
8974 }) => {
8975 let (d, n) = temporal::split_epoch_seconds(*epoch_seconds);
8976 Ok((Some(d), Some(n + *nanos as i64), None))
8977 }
8978 Value::Property(PropertyValue::DateTime {
8979 epoch_seconds,
8980 nanos,
8981 zone,
8982 }) => {
8983 let tz = tz_from_graph(zone);
8984 let offset_seconds = temporal::resolve_offset(&tz, *epoch_seconds);
8985 let local = epoch_seconds + offset_seconds as i64;
8986 let (d, n) = temporal::split_epoch_seconds(local);
8987 Ok((Some(d), Some(n + *nanos as i64), Some(tz)))
8988 }
8989 other => Err(QueryError::Type(format!(
8990 "{name}() needs a Date, LocalTime, Time, LocalDateTime, or DateTime, got {other:?}"
8991 ))),
8992 }
8993}
8994
8995type BetweenOperand = (Option<i32>, Option<i64>, Option<temporal::TzId>);
8997
8998type BetweenFn = fn(
9003 Option<i32>,
9004 Option<i64>,
9005 Option<&temporal::TzId>,
9006 Option<i32>,
9007 Option<i64>,
9008 Option<&temporal::TzId>,
9009) -> temporal::DurationParts;
9010
9011fn duration_between_builtin(name: &str, args: &[Value], f: BetweenFn) -> Result<Value, QueryError> {
9016 if args.len() != 2 {
9017 return Err(QueryError::Semantic(format!(
9018 "{name}() expects exactly two arguments, got {}",
9019 args.len()
9020 )));
9021 }
9022 if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
9023 return Ok(Value::Null);
9024 }
9025 let (a_date, a_time, a_zone) = between_operand(name, &args[0])?;
9026 let (b_date, b_time, b_zone) = between_operand(name, &args[1])?;
9027 Ok(duration_value(f(
9028 a_date,
9029 a_time,
9030 a_zone.as_ref(),
9031 b_date,
9032 b_time,
9033 b_zone.as_ref(),
9034 )))
9035}
9036
9037type TruncateArgs<'a> = (&'a str, &'a Value, Option<&'a BTreeMap<String, Value>>);
9042
9043fn parse_truncate_args<'a>(name: &str, args: &'a [Value]) -> Result<TruncateArgs<'a>, QueryError> {
9044 if args.len() < 2 || args.len() > 3 {
9045 return Err(QueryError::Semantic(format!(
9046 "{name}() expects 2 or 3 arguments, got {}",
9047 args.len()
9048 )));
9049 }
9050 let unit = as_arith_str(&args[0]).ok_or_else(|| {
9051 QueryError::Type(format!("{name}()'s first argument must be a unit string"))
9052 })?;
9053 let map = match args.get(2) {
9054 None | Some(Value::Null) => None,
9055 Some(Value::Map(m)) => Some(m),
9056 Some(other) => {
9057 return Err(QueryError::Type(format!(
9058 "{name}()'s third argument must be a map, got {other:?}"
9059 )))
9060 }
9061 };
9062 Ok((unit, &args[1], map))
9063}
9064
9065fn apply_date_overrides(
9074 base_epoch_day: i32,
9075 map: Option<&BTreeMap<String, Value>>,
9076) -> Result<i32, QueryError> {
9077 let base_y = temporal::date_component(base_epoch_day, "year").unwrap();
9078 let base_m = temporal::date_component(base_epoch_day, "month").unwrap();
9079 let base_d = temporal::date_component(base_epoch_day, "day").unwrap();
9080 let Some(m) = map else {
9081 return Ok(base_epoch_day);
9082 };
9083 let year_raw = int_field(m, "year", base_y)?;
9084 let year = i32::try_from(year_raw)
9085 .map_err(|_| QueryError::Type(format!("'year' is out of range: {year_raw}")))?;
9086 let month_raw = int_field(m, "month", base_m)?;
9087 let month = u32::try_from(month_raw)
9088 .map_err(|_| QueryError::Type(format!("'month' is out of range: {month_raw}")))?;
9089 let day_raw = int_field(m, "day", base_d)?;
9090 let day = u32::try_from(day_raw)
9091 .map_err(|_| QueryError::Type(format!("'day' is out of range: {day_raw}")))?;
9092 let result = temporal::epoch_day_from_ymd(year, month, day).ok_or_else(|| {
9093 QueryError::Type(format!(
9094 "{year:04}-{month:02}-{day:02} isn't a valid calendar date"
9095 ))
9096 })?;
9097 match m.get("dayOfWeek") {
9098 None => Ok(result),
9099 Some(v) => {
9100 let dow = value_as_i64(v)
9101 .ok_or_else(|| QueryError::Type("'dayOfWeek' must be an integer".into()))?;
9102 temporal::set_iso_weekday(result, dow).ok_or_else(|| {
9103 QueryError::Type(format!(
9104 "'dayOfWeek' must be 1..7 (Monday..Sunday), got {dow}"
9105 ))
9106 })
9107 }
9108 }
9109}
9110
9111fn apply_time_overrides(
9116 base_nanos_of_day: i64,
9117 map: Option<&BTreeMap<String, Value>>,
9118) -> Result<i64, QueryError> {
9119 let base_h = temporal::local_time_component(base_nanos_of_day, "hour").unwrap();
9120 let base_min = temporal::local_time_component(base_nanos_of_day, "minute").unwrap();
9121 let base_s = temporal::local_time_component(base_nanos_of_day, "second").unwrap();
9122 let base_ns = temporal::local_time_component(base_nanos_of_day, "nanosecond").unwrap();
9123 let Some(m) = map else {
9124 return Ok(base_nanos_of_day);
9125 };
9126 let nanos = sub_second_nanos_from_map(base_ns, m)?;
9127 let hour = int_field(m, "hour", base_h)?;
9128 let minute = int_field(m, "minute", base_min)?;
9129 let second = int_field(m, "second", base_s)?;
9130 temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
9131 .ok_or_else(|| QueryError::Type("truncate(...)'s map has an out-of-range field".into()))
9132}
9133
9134fn validate_truncate_map_keys(
9141 name: &str,
9142 map: Option<&BTreeMap<String, Value>>,
9143 allowed: &[&str],
9144) -> Result<(), QueryError> {
9145 let Some(m) = map else { return Ok(()) };
9146 if let Some(bad) = m.keys().find(|k| !allowed.contains(&k.as_str())) {
9147 return Err(QueryError::Type(format!(
9148 "{name}(...)'s map has an unrecognized field '{bad}'"
9149 )));
9150 }
9151 Ok(())
9152}
9153
9154fn date_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9155 let (unit, other, map) = parse_truncate_args("date.truncate", args)?;
9156 validate_truncate_map_keys("date.truncate", map, &["year", "month", "day", "dayOfWeek"])?;
9157 if matches!(other, Value::Null) {
9158 return Ok(Value::Null);
9159 }
9160 let (base_date, _, _) = between_operand("date.truncate", other)?;
9161 let base_date = base_date.ok_or_else(|| {
9162 QueryError::Type(
9163 "date.truncate() needs a value with a calendar date (Date, LocalDateTime, or DateTime)"
9164 .into(),
9165 )
9166 })?;
9167 let truncated = temporal::truncate_date_unit(base_date, unit).ok_or_else(|| {
9168 QueryError::Type(format!(
9169 "date.truncate(): '{unit}' isn't a recognized date unit"
9170 ))
9171 })?;
9172 Ok(Value::Property(PropertyValue::Date(apply_date_overrides(
9173 truncated, map,
9174 )?)))
9175}
9176
9177const TIME_TRUNCATE_MAP_KEYS: &[&str] = &[
9178 "hour",
9179 "minute",
9180 "second",
9181 "millisecond",
9182 "microsecond",
9183 "nanosecond",
9184];
9185
9186fn local_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9187 let (unit, other, map) = parse_truncate_args("localtime.truncate", args)?;
9188 validate_truncate_map_keys("localtime.truncate", map, TIME_TRUNCATE_MAP_KEYS)?;
9189 if matches!(other, Value::Null) {
9190 return Ok(Value::Null);
9191 }
9192 let (_, base_time, _) = between_operand("localtime.truncate", other)?;
9193 let base_time = base_time.ok_or_else(|| {
9194 QueryError::Type(
9195 "localtime.truncate() needs a value with a time-of-day (LocalTime, Time, \
9196 LocalDateTime, or DateTime)"
9197 .into(),
9198 )
9199 })?;
9200 let truncated = temporal::truncate_time_unit(base_time, unit).ok_or_else(|| {
9201 QueryError::Type(format!(
9202 "localtime.truncate(): '{unit}' isn't a recognized time unit"
9203 ))
9204 })?;
9205 Ok(Value::Property(PropertyValue::LocalTime(
9206 apply_time_overrides(truncated, map)?,
9207 )))
9208}
9209
9210fn time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9211 let (unit, other, map) = parse_truncate_args("time.truncate", args)?;
9212 validate_truncate_map_keys(
9213 "time.truncate",
9214 map,
9215 &[
9216 "hour",
9217 "minute",
9218 "second",
9219 "millisecond",
9220 "microsecond",
9221 "nanosecond",
9222 "timezone",
9223 ],
9224 )?;
9225 if matches!(other, Value::Null) {
9226 return Ok(Value::Null);
9227 }
9228 let (_, base_time, base_offset) = between_operand("time.truncate", other)?;
9229 let base_time = base_time.ok_or_else(|| {
9230 QueryError::Type(
9231 "time.truncate() needs a value with a time-of-day (LocalTime, Time, LocalDateTime, \
9232 or DateTime)"
9233 .into(),
9234 )
9235 })?;
9236 let truncated = temporal::truncate_time_unit(base_time, unit).ok_or_else(|| {
9237 QueryError::Type(format!(
9238 "time.truncate(): '{unit}' isn't a recognized time unit"
9239 ))
9240 })?;
9241 let nanos_of_day = apply_time_overrides(truncated, map)?;
9242 let offset_seconds = match map.and_then(|m| m.get("timezone")) {
9243 Some(v) => match timezone_value_to_tzid(v)? {
9244 temporal::TzId::Offset(o) => o,
9245 temporal::TzId::Named(name) => {
9246 return Err(QueryError::Type(format!(
9247 "'timezone': '{name}' looks like a named timezone (e.g. 'Europe/Stockholm') -- TIME has \
9248 no calendar date to resolve a named zone's DST-dependent offset against, only a fixed \
9249 UTC offset like '+01:00' is supported"
9250 )));
9251 }
9252 },
9253 None => match base_offset {
9254 Some(temporal::TzId::Offset(o)) => o,
9255 _ => 0,
9256 },
9257 };
9258 Ok(Value::Property(PropertyValue::Time {
9259 nanos_of_day,
9260 offset_seconds,
9261 }))
9262}
9263
9264fn truncate_date_time(base_date: i32, base_time: i64, unit: &str) -> Option<(i32, i64)> {
9272 if let Some(d) = temporal::truncate_date_unit(base_date, unit) {
9273 Some((d, 0))
9274 } else {
9275 temporal::truncate_time_unit(base_time, unit).map(|t| (base_date, t))
9276 }
9277}
9278
9279fn local_date_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9280 let (unit, other, map) = parse_truncate_args("localdatetime.truncate", args)?;
9281 validate_truncate_map_keys(
9282 "localdatetime.truncate",
9283 map,
9284 &[
9285 "year",
9286 "month",
9287 "day",
9288 "dayOfWeek",
9289 "hour",
9290 "minute",
9291 "second",
9292 "millisecond",
9293 "microsecond",
9294 "nanosecond",
9295 ],
9296 )?;
9297 if matches!(other, Value::Null) {
9298 return Ok(Value::Null);
9299 }
9300 let (base_date, base_time, _) = between_operand("localdatetime.truncate", other)?;
9301 let base_date = base_date.ok_or_else(|| {
9302 QueryError::Type(
9303 "localdatetime.truncate() needs a value with a calendar date (Date, LocalDateTime, \
9304 or DateTime)"
9305 .into(),
9306 )
9307 })?;
9308 let (trunc_date, trunc_time) = truncate_date_time(base_date, base_time.unwrap_or(0), unit)
9309 .ok_or_else(|| {
9310 QueryError::Type(format!(
9311 "localdatetime.truncate(): '{unit}' isn't a recognized unit"
9312 ))
9313 })?;
9314 let final_date = apply_date_overrides(trunc_date, map)?;
9315 let final_time = apply_time_overrides(trunc_time, map)?;
9316 let (epoch_seconds, nanos) = temporal::combine_date_and_time(final_date, final_time);
9317 Ok(Value::Property(PropertyValue::LocalDateTime {
9318 epoch_seconds,
9319 nanos,
9320 }))
9321}
9322
9323fn date_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
9324 let (unit, other, map) = parse_truncate_args("datetime.truncate", args)?;
9325 validate_truncate_map_keys(
9326 "datetime.truncate",
9327 map,
9328 &[
9329 "year",
9330 "month",
9331 "day",
9332 "dayOfWeek",
9333 "hour",
9334 "minute",
9335 "second",
9336 "millisecond",
9337 "microsecond",
9338 "nanosecond",
9339 "timezone",
9340 ],
9341 )?;
9342 if matches!(other, Value::Null) {
9343 return Ok(Value::Null);
9344 }
9345 let (base_date, base_time, base_offset) = between_operand("datetime.truncate", other)?;
9346 let base_date = base_date.ok_or_else(|| {
9347 QueryError::Type(
9348 "datetime.truncate() needs a value with a calendar date (Date, LocalDateTime, or \
9349 DateTime)"
9350 .into(),
9351 )
9352 })?;
9353 let (trunc_date, trunc_time) = truncate_date_time(base_date, base_time.unwrap_or(0), unit)
9354 .ok_or_else(|| {
9355 QueryError::Type(format!(
9356 "datetime.truncate(): '{unit}' isn't a recognized unit"
9357 ))
9358 })?;
9359 let final_date = apply_date_overrides(trunc_date, map)?;
9360 let final_time = apply_time_overrides(trunc_time, map)?;
9361 let zone = match map.and_then(|m| m.get("timezone")) {
9362 Some(v) => timezone_value_to_tzid(v)?,
9363 None => base_offset.unwrap_or(temporal::TzId::Offset(0)),
9364 };
9365 let calendar = temporal::CalendarDateTime {
9366 year: temporal::date_component(final_date, "year").unwrap() as i32,
9367 month: temporal::date_component(final_date, "month").unwrap() as u32,
9368 day: temporal::date_component(final_date, "day").unwrap() as u32,
9369 hour: temporal::local_time_component(final_time, "hour").unwrap(),
9370 minute: temporal::local_time_component(final_time, "minute").unwrap(),
9371 second: temporal::local_time_component(final_time, "second").unwrap(),
9372 nanos: temporal::local_time_component(final_time, "nanosecond").unwrap(),
9373 };
9374 let (epoch_seconds, nanos) =
9375 temporal::date_time_from_fields(calendar, &zone).ok_or_else(|| {
9376 QueryError::Type("datetime.truncate() produced an out-of-range value".into())
9377 })?;
9378 Ok(Value::Property(PropertyValue::DateTime {
9379 epoch_seconds,
9380 nanos,
9381 zone: tz_to_graph(zone),
9382 }))
9383}
9384
9385fn value_as_i64(v: &Value) -> Option<i64> {
9386 match v {
9387 Value::Property(PropertyValue::Int(i)) | Value::Literal(Literal::Int(i)) => Some(*i),
9388 _ => None,
9389 }
9390}
9391
9392fn value_as_f64(v: &Value) -> Option<f64> {
9393 match as_arith_num(v)? {
9394 ArithNum::Int(i) => Some(i as f64),
9395 ArithNum::Float(f) => Some(f),
9396 }
9397}
9398
9399fn is_temporal_property_value(pv: &PropertyValue) -> bool {
9414 matches!(
9415 pv,
9416 PropertyValue::Date(_)
9417 | PropertyValue::Duration { .. }
9418 | PropertyValue::LocalTime(_)
9419 | PropertyValue::Time { .. }
9420 | PropertyValue::LocalDateTime { .. }
9421 | PropertyValue::DateTime { .. }
9422 )
9423}
9424
9425fn property_of_value(v: &Value, prop: &str) -> Result<Value, QueryError> {
9435 match v {
9436 Value::Node(n) => Ok(n
9437 .props
9438 .get(prop)
9439 .cloned()
9440 .map(property_value_to_value)
9441 .unwrap_or(Value::Null)),
9442 Value::Edge(e) => Ok(e
9443 .props
9444 .get(prop)
9445 .cloned()
9446 .map(property_value_to_value)
9447 .unwrap_or(Value::Null)),
9448 Value::Map(m) => Ok(m.get(prop).cloned().unwrap_or(Value::Null)),
9449 Value::Null => Ok(Value::Null),
9450 Value::Property(PropertyValue::Null) => Ok(Value::Null),
9451 Value::Property(pv) => match temporal_component(pv, prop) {
9452 Some(component) => Ok(Value::Property(component)),
9453 None if is_temporal_property_value(pv) => Ok(Value::Null),
9454 None => Err(QueryError::Type(
9455 "property access requires a node, relationship, map, or temporal value".into(),
9456 )),
9457 },
9458 Value::List(_) | Value::Path(_) => Err(QueryError::Type(
9459 "property access requires a node, relationship, map, or temporal value, not a list \
9460 or path"
9461 .into(),
9462 )),
9463 Value::Literal(_) => Err(QueryError::Type(
9464 "property access requires a node, relationship, map, or temporal value".into(),
9465 )),
9466 }
9467}
9468
9469fn temporal_component(pv: &PropertyValue, prop: &str) -> Option<PropertyValue> {
9470 match pv {
9471 PropertyValue::Date(d) => temporal::date_component(*d, prop).map(PropertyValue::Int),
9472 PropertyValue::Duration {
9473 months,
9474 days,
9475 seconds,
9476 nanos,
9477 } => temporal::duration_component(*months, *days, *seconds, *nanos, prop)
9478 .map(PropertyValue::Int),
9479 PropertyValue::LocalTime(nanos_of_day) => {
9480 temporal::local_time_component(*nanos_of_day, prop).map(PropertyValue::Int)
9481 }
9482 PropertyValue::Time {
9483 nanos_of_day,
9484 offset_seconds,
9485 } => time_component(*nanos_of_day, *offset_seconds, prop),
9486 PropertyValue::LocalDateTime {
9487 epoch_seconds,
9488 nanos,
9489 } => date_time_component(*epoch_seconds, *nanos, None, prop),
9490 PropertyValue::DateTime {
9491 epoch_seconds,
9492 nanos,
9493 zone,
9494 } => date_time_component(*epoch_seconds, *nanos, Some(&tz_from_graph(zone)), prop),
9495 _ => None,
9496 }
9497}
9498
9499fn time_component(nanos_of_day: i64, offset_seconds: i32, prop: &str) -> Option<PropertyValue> {
9503 match prop {
9504 "timezone" | "offset" => Some(PropertyValue::String(temporal::format_offset(
9505 offset_seconds,
9506 ))),
9507 "offsetSeconds" => Some(PropertyValue::Int(offset_seconds as i64)),
9508 "offsetMinutes" => Some(PropertyValue::Int(offset_seconds as i64 / 60)),
9509 _ => temporal::local_time_component(nanos_of_day, prop).map(PropertyValue::Int),
9510 }
9511}
9512
9513fn date_time_component(
9528 epoch_seconds: i64,
9529 nanos: i32,
9530 zone: Option<&temporal::TzId>,
9531 prop: &str,
9532) -> Option<PropertyValue> {
9533 if let Some(zone) = zone {
9534 let offset_seconds = temporal::resolve_offset(zone, epoch_seconds);
9535 match prop {
9536 "timezone" => {
9543 let text = match zone {
9544 temporal::TzId::Named(name) => name.clone(),
9545 temporal::TzId::Offset(_) => temporal::format_offset(offset_seconds),
9546 };
9547 return Some(PropertyValue::String(text));
9548 }
9549 "offset" => {
9550 return Some(PropertyValue::String(temporal::format_offset(
9551 offset_seconds,
9552 )))
9553 }
9554 "offsetSeconds" => return Some(PropertyValue::Int(offset_seconds as i64)),
9555 "offsetMinutes" => return Some(PropertyValue::Int(offset_seconds as i64 / 60)),
9556 "epochSeconds" => return Some(PropertyValue::Int(epoch_seconds)),
9557 "epochMillis" => {
9558 return Some(PropertyValue::Int(
9559 temporal::epoch_seconds_and_millis(epoch_seconds, nanos).1,
9560 ))
9561 }
9562 _ => {}
9563 }
9564 }
9565 let offset_seconds = zone.map_or(0, |z| temporal::resolve_offset(z, epoch_seconds));
9566 let local_epoch_seconds = epoch_seconds + offset_seconds as i64;
9567 temporal::date_time_calendar_component(local_epoch_seconds, prop)
9568 .or_else(|| temporal::date_time_clock_component(local_epoch_seconds, nanos, prop))
9569 .map(PropertyValue::Int)
9570}
9571
9572fn apply_order_by(
9577 rows: Vec<Vec<Value>>,
9578 columns: &[String],
9579 order_by: &[(ReturnExpr, SortDir)],
9580 items: Option<&[ReturnItem]>,
9581 skip: Option<i64>,
9582 limit: Option<i64>,
9583) -> Result<Vec<Vec<Value>>, QueryError> {
9584 let order_by_col: Vec<Option<usize>> = order_by
9596 .iter()
9597 .map(|(expr, _)| {
9598 columns
9599 .iter()
9600 .position(|c| *c == default_column_name(expr, 0))
9601 .or_else(|| {
9602 items.and_then(|items| items.iter().position(|item| item.expr == *expr))
9603 })
9604 })
9605 .collect();
9606 let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(rows.len());
9607 for row in rows {
9608 let row_map: HashMap<String, Value> =
9609 columns.iter().cloned().zip(row.iter().cloned()).collect();
9610 let keys = order_by
9611 .iter()
9612 .zip(&order_by_col)
9613 .map(|((expr, _), col)| match col {
9614 Some(i) => Ok(row[*i].clone()),
9615 None => eval_projected_expr(expr, &row_map),
9616 })
9617 .collect::<Result<Vec<_>, _>>()?;
9618 keyed.push((keys, row));
9619 }
9620 Ok(top_k_by(keyed, order_by, skip, limit)
9621 .into_iter()
9622 .map(|(_, row)| row)
9623 .collect())
9624}
9625
9626fn eval_projected_expr(
9632 expr: &ReturnExpr,
9633 row: &HashMap<String, Value>,
9634) -> Result<Value, QueryError> {
9635 match expr {
9636 ReturnExpr::Var(name) => row
9637 .get(name)
9638 .cloned()
9639 .ok_or_else(|| QueryError::UnboundVariable(name.clone())),
9640 ReturnExpr::Prop(pa) => {
9641 let base = row
9642 .get(&pa.var)
9643 .ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
9644 match base {
9645 Value::Map(m) => Ok(m.get(&pa.prop).cloned().unwrap_or(Value::Null)),
9646 Value::Node(n) => Ok(match n.props.get(&pa.prop).cloned() {
9647 Some(PropertyValue::Null) | None => Value::Null,
9648 Some(v) => property_value_to_value(v),
9649 }),
9650 Value::Edge(e) => Ok(match e.props.get(&pa.prop).cloned() {
9651 Some(PropertyValue::Null) | None => Value::Null,
9652 Some(v) => property_value_to_value(v),
9653 }),
9654 Value::Property(pv) => Ok(match temporal_component(pv, &pa.prop) {
9659 Some(component) => Value::Property(component),
9660 None => Value::Null,
9661 }),
9662 _ => Ok(Value::Null),
9663 }
9664 }
9665 ReturnExpr::PropOf(base, prop) => {
9666 let v = eval_projected_expr(base, row)?;
9667 property_of_value(&v, prop)
9668 }
9669 ReturnExpr::Lit(lit) => Ok(match lit {
9670 Literal::Null => Value::Null,
9671 other => Value::Literal(other.clone()),
9672 }),
9673 ReturnExpr::Call { name, args, .. } => {
9674 if is_aggregate_name(name) {
9681 return Err(QueryError::Semantic(format!(
9682 "aggregate function '{name}' can only be used as a return item's top-level expression"
9683 )));
9684 }
9685 let arg_values = args
9686 .iter()
9687 .map(|a| eval_projected_expr(a, row))
9688 .collect::<Result<Vec<_>, _>>()?;
9689 call_builtin(name, &arg_values, temporal::capture_now())
9697 }
9698 ReturnExpr::CountStar => Err(QueryError::Semantic(
9699 "count(*) can only be used as a return item's top-level expression".into(),
9700 )),
9701 ReturnExpr::Case { test, whens, else_ } => {
9702 let test_value = match test {
9703 Some(t) => Some(eval_projected_expr(t, row)?),
9704 None => None,
9705 };
9706 for (when, then) in whens {
9707 let when_value = eval_projected_expr(when, row)?;
9708 let matched = match &test_value {
9709 Some(tv) => value_eq(tv, &when_value),
9710 None => matches!(when_value, Value::Literal(Literal::Bool(true))),
9711 };
9712 if matched {
9713 return eval_projected_expr(then, row);
9714 }
9715 }
9716 match else_ {
9717 Some(e) => eval_projected_expr(e, row),
9718 None => Ok(Value::Null),
9719 }
9720 }
9721 ReturnExpr::Arith(l, op, r) => {
9722 let lv = eval_projected_expr(l, row)?;
9723 let rv = eval_projected_expr(r, row)?;
9724 apply_arith(*op, &lv, &rv)
9725 }
9726 ReturnExpr::Neg(e) => {
9727 let v = eval_projected_expr(e, row)?;
9728 apply_neg(&v)
9729 }
9730 ReturnExpr::ListLit(items) => Ok(Value::List(
9731 items
9732 .iter()
9733 .map(|item| eval_projected_expr(item, row))
9734 .collect::<Result<Vec<_>, _>>()?,
9735 )),
9736 ReturnExpr::Index(base, index) => {
9737 let base_v = eval_projected_expr(base, row)?;
9738 let index_v = eval_projected_expr(index, row)?;
9739 apply_index(&base_v, &index_v)
9740 }
9741 ReturnExpr::Slice(base, start, end) => {
9742 let base_v = eval_projected_expr(base, row)?;
9743 let start_v = start
9744 .as_deref()
9745 .map(|s| eval_projected_expr(s, row))
9746 .transpose()?;
9747 let end_v = end
9748 .as_deref()
9749 .map(|e| eval_projected_expr(e, row))
9750 .transpose()?;
9751 apply_slice(&base_v, start_v.as_ref(), end_v.as_ref())
9752 }
9753 ReturnExpr::ListComp {
9754 var,
9755 source,
9756 where_clause,
9757 project,
9758 } => {
9759 let source_v = eval_projected_expr(source, row)?;
9760 let items = match source_v {
9761 Value::List(items) => items,
9762 Value::Null => return Ok(Value::Null),
9763 other => {
9764 return Err(QueryError::Type(format!(
9765 "list comprehension source must be a list, got {other:?}"
9766 )))
9767 }
9768 };
9769 let mut result = Vec::with_capacity(items.len());
9770 for item in items {
9771 let mut scoped_row = row.clone();
9772 scoped_row.insert(var.clone(), item.clone());
9773 let keep = match where_clause {
9774 Some(w) => value_to_bool3(&eval_projected_expr(w, &scoped_row)?)? == Some(true),
9775 None => true,
9776 };
9777 if !keep {
9778 continue;
9779 }
9780 result.push(match project {
9781 Some(p) => eval_projected_expr(p, &scoped_row)?,
9782 None => item,
9783 });
9784 }
9785 Ok(Value::List(result))
9786 }
9787 ReturnExpr::Quantifier {
9788 kind,
9789 var,
9790 source,
9791 where_clause,
9792 } => {
9793 let source_v = eval_projected_expr(source, row)?;
9794 let items = match source_v {
9795 Value::List(items) => items,
9796 Value::Null => return Ok(Value::Null),
9797 other => {
9798 return Err(QueryError::Type(format!(
9799 "quantifier source must be a list, got {other:?}"
9800 )))
9801 }
9802 };
9803 let mut preds = Vec::with_capacity(items.len());
9804 for item in &items {
9805 let mut scoped_row = row.clone();
9806 scoped_row.insert(var.clone(), item.clone());
9807 preds.push(match where_clause {
9808 Some(w) => value_to_bool3(&eval_projected_expr(w, &scoped_row)?)?,
9809 None => item_truthy(item),
9810 });
9811 }
9812 Ok(match eval_quantifier(*kind, &preds) {
9813 Some(b) => Value::Literal(Literal::Bool(b)),
9814 None => Value::Null,
9815 })
9816 }
9817 ReturnExpr::MapLit(entries) => {
9818 let mut map = BTreeMap::new();
9819 for (k, v) in entries {
9820 map.insert(k.clone(), eval_projected_expr(v, row)?);
9821 }
9822 Ok(Value::Map(map))
9823 }
9824 ReturnExpr::And(l, r) => Ok(bool3_to_value(and3(
9825 value_to_bool3(&eval_projected_expr(l, row)?)?,
9826 value_to_bool3(&eval_projected_expr(r, row)?)?,
9827 ))),
9828 ReturnExpr::Or(l, r) => Ok(bool3_to_value(or3(
9829 value_to_bool3(&eval_projected_expr(l, row)?)?,
9830 value_to_bool3(&eval_projected_expr(r, row)?)?,
9831 ))),
9832 ReturnExpr::Xor(l, r) => Ok(bool3_to_value(xor3(
9833 value_to_bool3(&eval_projected_expr(l, row)?)?,
9834 value_to_bool3(&eval_projected_expr(r, row)?)?,
9835 ))),
9836 ReturnExpr::Not(e) => Ok(bool3_to_value(
9837 value_to_bool3(&eval_projected_expr(e, row)?)?.map(|b| !b),
9838 )),
9839 ReturnExpr::Compare(l, op, r) => {
9840 let lv = eval_projected_expr(l, row)?;
9841 let rv = eval_projected_expr(r, row)?;
9842 Ok(bool3_to_value(compare_values(&lv, *op, &rv)))
9843 }
9844 ReturnExpr::IsNull(e) => Ok(Value::Literal(Literal::Bool(matches!(
9845 eval_projected_expr(e, row)?,
9846 Value::Null
9847 )))),
9848 ReturnExpr::In(needle, haystack) => {
9849 let nv = eval_projected_expr(needle, row)?;
9850 let hv = eval_projected_expr(haystack, row)?;
9851 Ok(bool3_to_value(list_membership_ternary(&nv, &hv)?))
9852 }
9853 ReturnExpr::HasLabel(var, labels) => {
9854 let binding = row
9855 .get(var)
9856 .ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
9857 match binding {
9858 Value::Node(n) => Ok(Value::Literal(Literal::Bool(
9859 labels.iter().all(|l| n.labels.contains(l)),
9860 ))),
9861 Value::Null => Ok(Value::Null),
9862 other => Err(QueryError::Type(format!(
9863 "'{var}' isn't a node — (n:Label) needs a node binding, got {other:?}"
9864 ))),
9865 }
9866 }
9867 ReturnExpr::PatternPredicate(_) => Err(QueryError::Semantic(
9868 "a pattern predicate (`(n)-->()` etc) can only be used inside WHERE".into(),
9869 )),
9870 ReturnExpr::PatternComprehension { .. } => Err(QueryError::Semantic(
9880 "a pattern comprehension can only be used in RETURN/WITH position, or as an ORDER BY \
9881 key that repeats one of their items verbatim"
9882 .into(),
9883 )),
9884 ReturnExpr::ExistsPattern { .. } | ReturnExpr::ExistsSubquery(_) => Err(
9885 QueryError::Semantic("an exists {} subquery can only be used inside WHERE".into()),
9886 ),
9887 }
9888}
9889
9890fn dedup_rows(rows: Vec<Vec<Value>>) -> Result<Vec<Vec<Value>>, QueryError> {
9898 let mut seen: HashSet<Vec<HashKey>> = HashSet::with_capacity(rows.len());
9899 let mut out = Vec::with_capacity(rows.len());
9900 for row in rows {
9901 let key = row
9902 .iter()
9903 .map(value_hash_key)
9904 .collect::<Result<Vec<_>, _>>()?;
9905 if seen.insert(key) {
9906 out.push(row);
9907 }
9908 }
9909 Ok(out)
9910}
9911
9912fn dedup_binding_rows(
9919 items: &[ReturnItem],
9920 rows: Vec<BindingRow>,
9921) -> Result<Vec<BindingRow>, QueryError> {
9922 let names: Vec<String> = items
9923 .iter()
9924 .enumerate()
9925 .map(with_item_output_name)
9926 .collect();
9927 let mut seen: HashSet<Vec<HashKey>> = HashSet::with_capacity(rows.len());
9928 let mut out = Vec::with_capacity(rows.len());
9929 for row in rows {
9930 let key = names
9931 .iter()
9932 .map(|name| {
9933 binding_hash_key(row.get(name).unwrap_or_else(|| {
9934 panic!("DISTINCT row missing its own projected column '{name}'")
9935 }))
9936 })
9937 .collect::<Result<Vec<_>, _>>()?;
9938 if seen.insert(key) {
9939 out.push(row);
9940 }
9941 }
9942 Ok(out)
9943}
9944
9945fn top_k_by<T>(
9962 mut keyed: Vec<(Vec<Value>, T)>,
9963 order_by: &[(ReturnExpr, SortDir)],
9964 skip: Option<i64>,
9965 limit: Option<i64>,
9966) -> Vec<(Vec<Value>, T)> {
9967 let cmp = |a: &(Vec<Value>, T), b: &(Vec<Value>, T)| -> std::cmp::Ordering {
9968 for (i, (_, dir)) in order_by.iter().enumerate() {
9969 let ord = compare_with_dir(&a.0[i], &b.0[i], *dir);
9970 if ord != std::cmp::Ordering::Equal {
9971 return ord;
9972 }
9973 }
9974 std::cmp::Ordering::Equal
9975 };
9976 let skip_n = skip.unwrap_or(0).max(0) as usize;
9977 match limit {
9978 Some(n) => {
9979 let k = skip_n + n.max(0) as usize;
9980 if k == 0 {
9981 keyed.clear();
9982 } else if k < keyed.len() {
9983 keyed.select_nth_unstable_by(k - 1, cmp);
9984 keyed.truncate(k);
9985 keyed.sort_by(cmp);
9986 } else {
9987 keyed.sort_by(cmp);
9988 }
9989 }
9990 None => keyed.sort_by(cmp),
9991 }
9992 if skip_n > 0 {
9993 keyed.drain(0..skip_n.min(keyed.len()));
9994 }
9995 keyed
9996}
9997
9998fn compare_with_dir(a: &Value, b: &Value, dir: SortDir) -> std::cmp::Ordering {
10007 let ord = compare_non_null(a, b);
10008 if dir == SortDir::Desc {
10009 ord.reverse()
10010 } else {
10011 ord
10012 }
10013}
10014
10015fn cmp_f64_nan_greatest(x: f64, y: f64) -> std::cmp::Ordering {
10028 use std::cmp::Ordering;
10029 match (x.is_nan(), y.is_nan()) {
10030 (true, true) => Ordering::Equal,
10031 (true, false) => Ordering::Greater,
10032 (false, true) => Ordering::Less,
10033 (false, false) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
10034 }
10035}
10036
10037fn compare_non_null(a: &Value, b: &Value) -> std::cmp::Ordering {
10038 use std::cmp::Ordering;
10039 if let (Value::List(_), Value::List(_)) = (a, b) {
10048 return list_cmp_asc(a, b);
10049 }
10050 let pa = value_to_comparable(a);
10051 let pb = value_to_comparable(b);
10052 match (pa, pb) {
10053 (Some(PropertyValue::Int(x)), Some(PropertyValue::Int(y))) => x.cmp(&y),
10054 (Some(PropertyValue::Int(x)), Some(PropertyValue::Float(y))) => {
10055 cmp_f64_nan_greatest(x as f64, y)
10056 }
10057 (Some(PropertyValue::Float(x)), Some(PropertyValue::Int(y))) => {
10058 cmp_f64_nan_greatest(x, y as f64)
10059 }
10060 (Some(PropertyValue::Float(x)), Some(PropertyValue::Float(y))) => {
10061 cmp_f64_nan_greatest(x, y)
10062 }
10063 (Some(PropertyValue::String(x)), Some(PropertyValue::String(y))) => x.cmp(&y),
10064 (Some(PropertyValue::Bool(x)), Some(PropertyValue::Bool(y))) => x.cmp(&y),
10065 (Some(PropertyValue::Date(x)), Some(PropertyValue::Date(y))) => x.cmp(&y),
10066 (Some(PropertyValue::LocalTime(x)), Some(PropertyValue::LocalTime(y))) => x.cmp(&y),
10067 (
10068 Some(PropertyValue::Time {
10069 nanos_of_day: x,
10070 offset_seconds: ox,
10071 }),
10072 Some(PropertyValue::Time {
10073 nanos_of_day: y,
10074 offset_seconds: oy,
10075 }),
10076 ) => (x - ox as i64 * 1_000_000_000).cmp(&(y - oy as i64 * 1_000_000_000)),
10077 (
10078 Some(PropertyValue::LocalDateTime {
10079 epoch_seconds: xs,
10080 nanos: xn,
10081 }),
10082 Some(PropertyValue::LocalDateTime {
10083 epoch_seconds: ys,
10084 nanos: yn,
10085 }),
10086 ) => (xs, xn).cmp(&(ys, yn)),
10087 (
10088 Some(PropertyValue::DateTime {
10089 epoch_seconds: xs,
10090 nanos: xn,
10091 ..
10092 }),
10093 Some(PropertyValue::DateTime {
10094 epoch_seconds: ys,
10095 nanos: yn,
10096 ..
10097 }),
10098 ) => (xs, xn).cmp(&(ys, yn)),
10099 _ => match (type_rank(a), type_rank(b)) {
10107 (Some(ra), Some(rb)) if ra != rb => ra.cmp(&rb),
10108 _ => Ordering::Equal,
10109 },
10110 }
10111}
10112
10113fn type_rank(v: &Value) -> Option<u8> {
10142 match v {
10143 Value::Map(_) => Some(0),
10144 Value::Node(_) => Some(1),
10145 Value::Edge(_) => Some(2),
10146 Value::List(_) => Some(3),
10147 Value::Path(_) => Some(4),
10148 Value::Literal(Literal::String(_)) | Value::Property(PropertyValue::String(_)) => Some(5),
10149 Value::Literal(Literal::Bool(_)) | Value::Property(PropertyValue::Bool(_)) => Some(6),
10150 Value::Literal(Literal::Int(_))
10151 | Value::Property(PropertyValue::Int(_))
10152 | Value::Literal(Literal::Float(_))
10153 | Value::Property(PropertyValue::Float(_)) => Some(7),
10154 Value::Property(PropertyValue::Date(_)) => Some(8),
10155 Value::Property(PropertyValue::LocalTime(_)) => Some(9),
10156 Value::Property(PropertyValue::Time { .. }) => Some(10),
10157 Value::Property(PropertyValue::LocalDateTime { .. }) => Some(11),
10158 Value::Property(PropertyValue::DateTime { .. }) => Some(12),
10159 Value::Null | Value::Literal(Literal::Null) | Value::Property(PropertyValue::Null) => {
10160 Some(13)
10161 }
10162 _ => None,
10163 }
10164}
10165
10166fn list_cmp_asc(a: &Value, b: &Value) -> std::cmp::Ordering {
10178 use std::cmp::Ordering;
10179 let a_null = matches!(a, Value::Null);
10180 let b_null = matches!(b, Value::Null);
10181 match (a_null, b_null) {
10182 (true, true) => return Ordering::Equal,
10183 (true, false) => return Ordering::Greater,
10184 (false, true) => return Ordering::Less,
10185 (false, false) => {}
10186 }
10187 if let (Value::List(xs), Value::List(ys)) = (a, b) {
10188 for (x, y) in xs.iter().zip(ys) {
10189 match list_cmp_asc(x, y) {
10190 Ordering::Equal => continue,
10191 other => return other,
10192 }
10193 }
10194 return xs.len().cmp(&ys.len());
10195 }
10196 compare_non_null(a, b)
10197}
10198
10199fn value_to_comparable(v: &Value) -> Option<PropertyValue> {
10200 match v {
10201 Value::Property(pv) => Some(pv.clone()),
10202 Value::Literal(lit) => Some(literal_to_value(lit)),
10203 _ => None,
10204 }
10205}
10206
10207pub(crate) fn comparable_ordering(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
10223 if let (Value::List(_), Value::List(_)) = (a, b) {
10224 return Some(list_cmp_asc(a, b));
10225 }
10226 let (pa, pb) = match (value_to_comparable(a), value_to_comparable(b)) {
10227 (Some(pa), Some(pb)) => (pa, pb),
10228 _ => {
10229 return match (type_rank(a), type_rank(b)) {
10230 (Some(ra), Some(rb)) if ra != rb => Some(ra.cmp(&rb)),
10234 _ => None,
10246 };
10247 }
10248 };
10249 Some(match (pa, pb) {
10250 (PropertyValue::Int(x), PropertyValue::Int(y)) => x.cmp(&y),
10251 (PropertyValue::Int(x), PropertyValue::Float(y)) => cmp_f64_nan_greatest(x as f64, y),
10252 (PropertyValue::Float(x), PropertyValue::Int(y)) => cmp_f64_nan_greatest(x, y as f64),
10253 (PropertyValue::Float(x), PropertyValue::Float(y)) => cmp_f64_nan_greatest(x, y),
10254 (PropertyValue::String(x), PropertyValue::String(y)) => x.cmp(&y),
10255 (PropertyValue::Bool(x), PropertyValue::Bool(y)) => x.cmp(&y),
10256 (PropertyValue::Date(x), PropertyValue::Date(y)) => x.cmp(&y),
10261 (PropertyValue::LocalTime(x), PropertyValue::LocalTime(y)) => x.cmp(&y),
10262 (
10263 PropertyValue::Time {
10264 nanos_of_day: x,
10265 offset_seconds: ox,
10266 },
10267 PropertyValue::Time {
10268 nanos_of_day: y,
10269 offset_seconds: oy,
10270 },
10271 ) => (x - ox as i64 * 1_000_000_000).cmp(&(y - oy as i64 * 1_000_000_000)),
10272 (
10273 PropertyValue::LocalDateTime {
10274 epoch_seconds: xs,
10275 nanos: xn,
10276 },
10277 PropertyValue::LocalDateTime {
10278 epoch_seconds: ys,
10279 nanos: yn,
10280 },
10281 ) => (xs, xn).cmp(&(ys, yn)),
10282 (
10283 PropertyValue::DateTime {
10284 epoch_seconds: xs,
10285 nanos: xn,
10286 ..
10287 },
10288 PropertyValue::DateTime {
10289 epoch_seconds: ys,
10290 nanos: yn,
10291 ..
10292 },
10293 ) => (xs, xn).cmp(&(ys, yn)),
10294 _ => return None,
10295 })
10296}
10297
10298fn compare_values(a: &Value, op: CompareOp, b: &Value) -> Option<bool> {
10307 if matches!(a, Value::Null) || matches!(b, Value::Null) {
10308 return None;
10309 }
10310 match op {
10311 CompareOp::Eq => value_equal_ternary(a, b),
10312 CompareOp::Ne => value_equal_ternary(a, b).map(|eq| !eq),
10313 CompareOp::Lt => ordered_compare(a, b, |o| o == std::cmp::Ordering::Less),
10314 CompareOp::Le => ordered_compare(a, b, |o| o != std::cmp::Ordering::Greater),
10315 CompareOp::Gt => ordered_compare(a, b, |o| o == std::cmp::Ordering::Greater),
10316 CompareOp::Ge => ordered_compare(a, b, |o| o != std::cmp::Ordering::Less),
10317 CompareOp::StartsWith | CompareOp::EndsWith | CompareOp::Contains => {
10318 let (Some(s), Some(p)) = (as_arith_str(a), as_arith_str(b)) else {
10319 return None;
10320 };
10321 Some(match op {
10322 CompareOp::StartsWith => s.starts_with(p),
10323 CompareOp::EndsWith => s.ends_with(p),
10324 CompareOp::Contains => s.contains(p),
10325 _ => unreachable!("only StartsWith/EndsWith/Contains reach this arm"),
10326 })
10327 }
10328 }
10329}
10330
10331fn ordered_compare(
10343 a: &Value,
10344 b: &Value,
10345 pred: impl Fn(std::cmp::Ordering) -> bool,
10346) -> Option<bool> {
10347 if let (Some(x), Some(y)) = (value_as_f64(a), value_as_f64(b)) {
10348 return Some(x.partial_cmp(&y).map(pred).unwrap_or(false));
10349 }
10350 value_partial_cmp(a, b).map(pred)
10351}
10352
10353fn value_partial_cmp(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
10369 use std::cmp::Ordering;
10370 if matches!(a, Value::Null) || matches!(b, Value::Null) {
10371 return None;
10372 }
10373 if let (Value::List(xs), Value::List(ys)) = (a, b) {
10374 for (x, y) in xs.iter().zip(ys) {
10375 match value_partial_cmp(x, y) {
10376 Some(Ordering::Equal) => continue,
10377 other => return other,
10378 }
10379 }
10380 return Some(xs.len().cmp(&ys.len()));
10381 }
10382 if value_to_comparable(a).is_none() || value_to_comparable(b).is_none() {
10396 return None;
10397 }
10398 comparable_ordering(a, b)
10399}
10400
10401fn value_equal_ternary(a: &Value, b: &Value) -> Option<bool> {
10419 match (a, b) {
10420 (Value::Null, _) | (_, Value::Null) => None,
10421 (Value::List(xs), Value::List(ys)) => {
10422 if xs.len() != ys.len() {
10423 return Some(false);
10424 }
10425 fold_ternary_eq(xs.iter().zip(ys).map(|(x, y)| value_equal_ternary(x, y)))
10426 }
10427 (Value::Map(x), Value::Map(y)) => {
10428 if !x.keys().eq(y.keys()) {
10429 return Some(false);
10430 }
10431 fold_ternary_eq(x.iter().map(|(k, xv)| value_equal_ternary(xv, &y[k])))
10432 }
10433 _ => Some(values_equal_numeric_aware(a, b)),
10434 }
10435}
10436
10437fn list_membership_ternary(needle: &Value, haystack: &Value) -> Result<Option<bool>, QueryError> {
10450 match haystack {
10451 Value::Null => Ok(None),
10452 Value::List(items) => {
10453 let mut saw_unknown = false;
10454 for item in items {
10455 match value_equal_ternary(needle, item) {
10456 Some(true) => return Ok(Some(true)),
10457 Some(false) => {}
10458 None => saw_unknown = true,
10459 }
10460 }
10461 Ok(if saw_unknown { None } else { Some(false) })
10462 }
10463 other => Err(QueryError::Type(format!(
10464 "IN requires a list on the right-hand side, got {other:?}"
10465 ))),
10466 }
10467}
10468
10469fn fold_ternary_eq(mut results: impl Iterator<Item = Option<bool>>) -> Option<bool> {
10475 let mut saw_unknown = false;
10476 for r in results.by_ref() {
10477 match r {
10478 Some(false) => return Some(false),
10479 Some(true) => {}
10480 None => saw_unknown = true,
10481 }
10482 }
10483 if saw_unknown {
10484 None
10485 } else {
10486 Some(true)
10487 }
10488}
10489
10490fn values_equal_numeric_aware(a: &Value, b: &Value) -> bool {
10499 match (as_arith_num(a), as_arith_num(b)) {
10500 (Some(ArithNum::Int(x)), Some(ArithNum::Int(y))) => x == y,
10501 (Some(ArithNum::Int(x)), Some(ArithNum::Float(y)))
10502 | (Some(ArithNum::Float(y)), Some(ArithNum::Int(x))) => x as f64 == y,
10503 (Some(ArithNum::Float(x)), Some(ArithNum::Float(y))) => x == y,
10504 _ => value_eq(a, b),
10505 }
10506}