1use std::any::Any;
8use std::collections::HashMap;
9use std::sync::{Arc, LazyLock};
10
11use datafusion::arrow::array::{
12 Array, FixedSizeListArray, LargeListArray, ListArray, new_empty_array,
13};
14use datafusion::arrow::datatypes::{DataType, Field, FieldRef};
15use datafusion::logical_expr::expr::Placeholder;
16use datafusion::logical_expr::{
17 ColumnarValue, Expr as DfExpr, ExprSchemable, Operator, ReturnFieldArgs, ScalarFunctionArgs,
18 ScalarUDF, ScalarUDFImpl, Signature, Volatility, cast, col, lit, not, when,
19};
20use datafusion::scalar::ScalarValue;
21
22use graphforge_core::PropId;
23use graphforge_ir::expr::{BinaryOpKind, IrExpr, IrLiteral, UnaryOpKind};
24use graphforge_ir::{ExprArena, ExprId, VarId};
25use graphforge_ontology::OntologyHandle;
26
27#[derive(Debug, Clone, Default)]
38pub struct VarMap(HashMap<u32, String>);
39
40impl VarMap {
41 #[must_use]
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn insert(&mut self, var: VarId, col_name: impl Into<String>) {
49 self.0.insert(var.0, col_name.into());
50 }
51
52 pub fn clear(&mut self) {
55 self.0.clear();
56 }
57
58 #[must_use]
60 pub fn get(&self, var: VarId) -> Option<&str> {
61 self.0.get(&var.0).map(String::as_str)
62 }
63
64 pub fn var_ids(&self) -> impl Iterator<Item = VarId> + '_ {
69 self.0.keys().map(|&k| VarId(k))
70 }
71}
72
73#[derive(Debug, Clone, thiserror::Error)]
75pub enum LoweringError {
76 #[error("unknown built-in function: {0}")]
79 UnknownFunction(String),
80
81 #[error("unsupported expression: {0}")]
83 UnsupportedExpr(String),
84
85 #[error("unbound variable: VarId({0})")]
88 UnboundVar(u32),
89
90 #[error("invalid argument type: {0}")]
95 InvalidType(String),
96}
97
98#[derive(Clone, Debug)]
102pub struct NodeShape {
103 pub prop_names: Vec<String>,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110enum EntityIdentityKind {
111 Node,
112 Edge,
113}
114
115pub struct ExprLowerer<'a> {
124 arena: &'a ExprArena,
125 var_map: &'a VarMap,
126 prop_names: HashMap<u32, String>,
130 node_shapes: HashMap<u32, NodeShape>,
133 type_id_to_entity_name: HashMap<u32, String>,
138 entity_name_to_type_id: HashMap<String, u32>,
143 props_authoritative: bool,
150 input_schema: Option<datafusion::common::DFSchemaRef>,
156 elem_struct_cols: Vec<String>,
165 read_target: Option<std::path::PathBuf>,
171 now: std::sync::OnceLock<chrono::NaiveDateTime>,
176}
177
178impl<'a> ExprLowerer<'a> {
179 #[must_use]
190 pub fn new(
191 arena: &'a ExprArena,
192 ontology: Option<&'a OntologyHandle>,
193 var_map: &'a VarMap,
194 ) -> Self {
195 Self {
196 arena,
197 var_map,
198 prop_names: build_prop_names(ontology),
199 node_shapes: HashMap::new(),
200 type_id_to_entity_name: HashMap::new(),
201 entity_name_to_type_id: HashMap::new(),
202 props_authoritative: false,
203 input_schema: None,
204 elem_struct_cols: Vec::new(),
205 read_target: None,
206 now: std::sync::OnceLock::new(),
207 }
208 }
209
210 #[must_use]
216 pub fn with_prop_names(
217 arena: &'a ExprArena,
218 var_map: &'a VarMap,
219 prop_names: HashMap<u32, String>,
220 ) -> Self {
221 Self {
222 arena,
223 var_map,
224 prop_names,
225 node_shapes: HashMap::new(),
226 type_id_to_entity_name: HashMap::new(),
227 entity_name_to_type_id: HashMap::new(),
228 props_authoritative: false,
229 input_schema: None,
230 elem_struct_cols: Vec::new(),
231 read_target: None,
232 now: std::sync::OnceLock::new(),
233 }
234 }
235
236 #[must_use]
243 pub fn with_prop_names_and_nodes(
244 arena: &'a ExprArena,
245 var_map: &'a VarMap,
246 prop_names: HashMap<u32, String>,
247 node_shapes: HashMap<u32, NodeShape>,
248 type_id_to_entity_name: HashMap<u32, String>,
249 props_authoritative: bool,
250 ) -> Self {
251 let entity_name_to_type_id = type_id_to_entity_name
252 .iter()
253 .map(|(id, name)| (name.clone(), *id))
254 .collect();
255 Self {
256 arena,
257 var_map,
258 prop_names,
259 node_shapes,
260 type_id_to_entity_name,
261 entity_name_to_type_id,
262 props_authoritative,
263 input_schema: None,
264 elem_struct_cols: Vec::new(),
265 read_target: None,
266 now: std::sync::OnceLock::new(),
267 }
268 }
269
270 #[must_use]
274 pub fn with_input_schema(mut self, schema: datafusion::common::DFSchemaRef) -> Self {
275 self.input_schema = Some(schema);
276 self
277 }
278
279 #[must_use]
284 pub fn with_elem_struct_col(mut self, col: String) -> Self {
285 self.elem_struct_cols.push(col);
286 self
287 }
288
289 #[must_use]
292 pub fn with_read_target(mut self, dir: std::path::PathBuf) -> Self {
293 self.read_target = Some(dir);
294 self
295 }
296
297 #[allow(
303 clippy::too_many_lines,
304 reason = "one cohesive dispatch match over every IrExpr variant plus the \
305 namespaced temporal builtins (date/time/datetime truncate); \
306 splitting the arms would scatter the lowering logic"
307 )]
308 pub fn lower(&self, id: ExprId) -> Result<DfExpr, LoweringError> {
309 match self.arena.get(id) {
310 IrExpr::Literal(lit_val) => Ok(lower_literal(lit_val)),
311
312 IrExpr::VarRef(var_id) => {
313 let col_name = self
314 .var_map
315 .get(*var_id)
316 .ok_or(LoweringError::UnboundVar(var_id.0))?;
317 if let Some(schema) = self.input_schema.as_ref()
318 && schema.field_with_unqualified_name(col_name).is_err()
319 {
320 let qual = datafusion::common::TableReference::bare(col_name);
321 if schema
322 .index_of_column_by_name(Some(&qual), "node_uuid")
323 .is_some()
324 {
325 return Ok(col(format!("{col_name}.node_uuid")));
326 }
327 if schema
328 .index_of_column_by_name(Some(&qual), "edge_uuid")
329 .is_some()
330 {
331 return Ok(col(format!("{col_name}.edge_uuid")));
332 }
333 }
334 if self
339 .input_schema
340 .as_ref()
341 .is_some_and(|schema| schema.index_of_column_by_name(None, col_name).is_some())
342 {
343 Ok(DfExpr::Column(datafusion::common::Column::new_unqualified(
344 col_name,
345 )))
346 } else {
347 Ok(col_literal(col_name))
348 }
349 }
350
351 IrExpr::PropertyAccess { base, prop } => {
352 if let Some(prop_name) = self.prop_names.get(&prop.0).cloned()
353 && let Some(out) = self.lower_static_value_access(*base, &prop_name)?
354 {
355 return Ok(out);
356 }
357 if self.props_authoritative
372 && let IrExpr::VarRef(v) = self.arena.get(*base)
373 && let Some(shape) = self.node_shapes.get(&v.0)
374 {
375 let prop_name = self
376 .prop_names
377 .get(&prop.0)
378 .cloned()
379 .unwrap_or_else(|| format!("prop_{}", prop.0));
380 let is_topology = graphforge_storage::TOPOLOGY_NODES_SCHEMA
381 .field_with_name(&prop_name)
382 .is_ok();
383 if !is_topology && !shape.prop_names.contains(&prop_name) {
384 return Ok(lit(ScalarValue::Null));
385 }
386 }
387 if let IrExpr::VarRef(v) = self.arena.get(*base)
394 && let Some(col_name) = self.var_map.get(*v)
395 && let Some(prop_name) = self.prop_names.get(&prop.0)
396 && crate::temporal::is_date_accessor(prop_name)
397 && let Some(schema) = self.input_schema.as_ref()
398 && let Ok(field) = schema.field_with_unqualified_name(col_name)
399 && is_date_struct(field.data_type())
400 {
401 return Ok(CYPHER_DATE_COMPONENT
402 .call(vec![col_literal(col_name), lit(prop_name.as_str())]));
403 }
404 if let IrExpr::VarRef(v) = self.arena.get(*base)
407 && let Some(col_name) = self.var_map.get(*v)
408 && let Some(prop_name) = self.prop_names.get(&prop.0)
409 && crate::temporal::is_duration_accessor(prop_name)
410 && let Some(schema) = self.input_schema.as_ref()
411 && let Ok(field) = schema.field_with_unqualified_name(col_name)
412 && is_duration_struct(field.data_type())
413 {
414 return Ok(CYPHER_DURATION_COMPONENT
415 .call(vec![col_literal(col_name), lit(prop_name.as_str())]));
416 }
417 if let IrExpr::VarRef(v) = self.arena.get(*base)
424 && let Some(col_name) = self.var_map.get(*v)
425 && let Some(prop_name) = self.prop_names.get(&prop.0)
426 && let Some(schema) = self.input_schema.as_ref()
427 && let Ok(field) = schema.field_with_unqualified_name(col_name)
428 && temporal_accessor_valid(field.data_type(), prop_name)
429 {
430 let args = vec![col_literal(col_name), lit(prop_name.as_str())];
431 return Ok(if crate::temporal::is_zone_str_accessor(prop_name) {
432 CYPHER_TEMPORAL_ZONE_STR.call(args)
433 } else {
434 CYPHER_TEMPORAL_COMPONENT.call(args)
435 });
436 }
437 if let IrExpr::VarRef(v) = self.arena.get(*base)
445 && let Some(col_name) = self.var_map.get(*v)
446 && let Some(schema) = self.input_schema.as_ref()
447 && let Ok(field) = schema.field_with_unqualified_name(col_name)
448 && is_plain_map_struct_type(field.data_type())
449 {
450 let prop_name = self
451 .prop_names
452 .get(&prop.0)
453 .cloned()
454 .unwrap_or_else(|| format!("prop_{}", prop.0));
455 return Ok(datafusion::functions::core::expr_fn::get_field(
456 col_literal(col_name),
457 prop_name,
458 ));
459 }
460 let base_expr = if let IrExpr::VarRef(v) = self.arena.get(*base) {
465 col_literal(self.var_map.get(*v).ok_or(LoweringError::UnboundVar(v.0))?)
466 } else {
467 self.lower(*base)?
468 };
469 if self.is_known_non_value_access_container(&base_expr) {
470 let prop_name = self
471 .prop_names
472 .get(&prop.0)
473 .cloned()
474 .unwrap_or_else(|| format!("prop_{}", prop.0));
475 return Err(LoweringError::InvalidType(format!(
476 "property access `{prop_name}` requires a map or graph element"
477 )));
478 }
479 let prop_col = self.resolve_prop_col(base_expr, *prop);
480 Ok(prop_col)
481 }
482
483 IrExpr::BinaryOp { op, left, right } => self.lower_binary(*op, *left, *right),
484
485 IrExpr::UnaryOp { op, expr } => self.lower_unary(*op, *expr),
486
487 IrExpr::FunctionCall { name, args } if name == "_node_struct" => {
488 self.lower_node_struct(args)
489 }
490
491 IrExpr::FunctionCall { name, args } if name == "_node_struct_list" => {
492 self.lower_node_struct_list(args)
493 }
494
495 IrExpr::FunctionCall { name, args } if name == "_rel_struct" => {
496 self.lower_rel_struct(args)
497 }
498
499 IrExpr::FunctionCall { name, args } if name == "_rel_struct_list" => {
500 self.lower_rel_struct_list(args)
501 }
502
503 IrExpr::FunctionCall { name, args } if name == "keys" => self.lower_keys(args),
504
505 IrExpr::FunctionCall { name, args } if name == "properties" => {
506 self.lower_properties(args)
507 }
508
509 IrExpr::FunctionCall { name, args } if name == "labels" => self.lower_labels(args),
510
511 IrExpr::FunctionCall { name, args }
512 if matches!(name.as_str(), "nodes" | "relationships") =>
513 {
514 let [arg] = args.as_slice() else {
515 return Err(LoweringError::InvalidType(format!(
516 "{name}() expects one path argument"
517 )));
518 };
519 Ok(datafusion::functions::core::expr_fn::get_field(
520 self.lower(*arg)?,
521 name,
522 ))
523 }
524
525 IrExpr::FunctionCall { name, args } if name == "_subscript" => {
526 self.lower_subscript(args)
527 }
528
529 IrExpr::FunctionCall { name, args }
530 if matches!(
531 name.as_str(),
532 "date" | "localtime" | "time" | "localdatetime" | "datetime" | "duration"
533 ) =>
534 {
535 self.lower_temporal(name, args)
536 }
537
538 IrExpr::FunctionCall { name, args }
539 if matches!(
540 name.as_str(),
541 "datetime.fromepoch" | "datetime.fromepochmillis"
542 ) =>
543 {
544 self.lower_from_epoch(name, args)
545 }
546
547 IrExpr::FunctionCall { name, args } if name == "date.truncate" => {
548 self.lower_date_truncate(args)
549 }
550 IrExpr::FunctionCall { name, args } if name == "localtime.truncate" => {
551 self.lower_localtime_truncate(args)
552 }
553 IrExpr::FunctionCall { name, args } if name == "localdatetime.truncate" => {
554 self.lower_localdatetime_truncate(args)
555 }
556 IrExpr::FunctionCall { name, args } if name == "time.truncate" => {
557 self.lower_time_truncate(args)
558 }
559 IrExpr::FunctionCall { name, args } if name == "datetime.truncate" => {
560 self.lower_datetime_truncate(args)
561 }
562 IrExpr::FunctionCall { name, args } if is_temporal_clock_fn(name) => {
568 if self.sole_arg_is_null(args) {
569 Ok(DfExpr::Literal(temporal_null_scalar(name), None))
570 } else {
571 Err(LoweringError::UnsupportedExpr(format!(
572 "{name}: temporal clock functions are not supported in a \
573 deterministic query context"
574 )))
575 }
576 }
577 IrExpr::FunctionCall { name, args }
581 if matches!(
582 name.to_ascii_lowercase().as_str(),
583 "duration.between"
584 | "duration.inmonths"
585 | "duration.indays"
586 | "duration.inseconds"
587 ) =>
588 {
589 self.lower_duration_between(&name.to_ascii_lowercase(), args)
590 }
591
592 IrExpr::FunctionCall { name, args } => {
593 let lowered = if is_path_builtin_name(name) {
594 args.iter()
595 .map(|&a| self.lower_path_builtin_arg(a))
596 .collect::<Result<Vec<_>, _>>()?
597 } else {
598 args.iter()
599 .map(|&a| self.lower(a))
600 .collect::<Result<Vec<_>, _>>()?
601 };
602 if name == "reverse"
607 && let [arg] = lowered.as_slice()
608 {
609 return Ok(if self.is_string_typed(arg) {
610 datafusion::functions::unicode::expr_fn::reverse(arg.clone())
611 } else if self.is_list_typed(arg) {
612 datafusion::functions_nested::expr_fn::array_reverse(arg.clone())
613 } else {
614 CYPHER_REVERSE.call(vec![arg.clone()])
618 });
619 }
620 resolve_builtin(name, lowered, || self.path_node_hydration())
621 .ok_or_else(|| LoweringError::UnknownFunction(name.clone()))
622 }
623
624 IrExpr::Parameter(name) => Ok(DfExpr::Placeholder(Placeholder {
625 id: format!("${name}"),
631 field: None,
632 })),
633
634 IrExpr::Case {
635 operand,
636 arms,
637 else_expr,
638 } => self.lower_case(operand.as_ref().copied(), arms, else_expr.as_ref().copied()),
639
640 IrExpr::ListLiteral(ids) => {
641 let elems: Vec<DfExpr> = ids
642 .iter()
643 .map(|&id| self.lower_value(id))
644 .collect::<Result<_, _>>()?;
645 Ok(lower_list_literal(elems, self.input_schema.as_deref()))
646 }
647
648 IrExpr::MapLiteral(entries) => self.lower_map_literal(entries),
649
650 IrExpr::Quantifier {
651 kind,
652 loop_var,
653 list,
654 predicate,
655 } => self.lower_quantifier(*kind, *loop_var, *list, *predicate),
656
657 IrExpr::ListComprehension {
658 loop_var,
659 list,
660 filter,
661 projection,
662 } => self.lower_list_comprehension(*loop_var, *list, *filter, *projection),
663 }
664 }
665
666 fn lower_value(&self, id: ExprId) -> Result<DfExpr, LoweringError> {
667 if let IrExpr::VarRef(var_id) = self.arena.get(id) {
668 let base = self
669 .var_map
670 .get(*var_id)
671 .ok_or(LoweringError::UnboundVar(var_id.0))?;
672 if self.node_shapes.contains_key(&var_id.0) || self.is_node_var(base) {
673 let prop_names = self
674 .node_shapes
675 .get(&var_id.0)
676 .map(|s| s.prop_names.clone())
677 .unwrap_or_default();
678 return Ok(node_value_struct(
679 base,
680 None,
681 &self.type_id_to_entity_name,
682 &prop_names,
683 ));
684 }
685 if self.is_edge_var(base) {
686 let props = self.edge_prop_names(base);
687 let value =
688 relationship_value_struct(base, col(format!("{base}.rel_type_name")), &props);
689 return Ok(null_unless(edge_present_qual(base), value));
690 }
691 }
692 self.lower(id)
693 }
694
695 fn lower_path_builtin_arg(&self, id: ExprId) -> Result<DfExpr, LoweringError> {
696 if let IrExpr::VarRef(v) = self.arena.get(id) {
697 return Ok(col_literal(
698 self.var_map.get(*v).ok_or(LoweringError::UnboundVar(v.0))?,
699 ));
700 }
701 self.lower(id)
702 }
703
704 fn lower_subscript(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
705 let [base_id, key_id] = args else {
706 return Err(LoweringError::UnsupportedExpr(
707 "_subscript expects two arguments".into(),
708 ));
709 };
710 let key_expr = self.lower(*key_id)?;
711 if let Some(key) = const_string_key(&key_expr) {
712 match key {
713 ConstStringKey::Null => return Ok(lit(ScalarValue::Null)),
714 ConstStringKey::Value(k) => {
715 if let Some(value) = self.lower_static_value_access(*base_id, &k)? {
716 return Ok(value);
717 }
718 }
719 }
720 }
721 if let Some(container) = self.lower_dynamic_access_container(*base_id)? {
722 return Ok(CYPHER_VALUE_ACCESS.call(vec![container, key_expr]));
723 }
724 let base = self.lower(*base_id)?;
725 if self.expr_data_type(&base).is_some_and(|dt| {
726 !matches!(
727 dt,
728 DataType::Null
729 | DataType::List(_)
730 | DataType::LargeList(_)
731 | DataType::FixedSizeList(_, _)
732 ) && !is_plain_map_struct_type(&dt)
733 && !is_het_struct_type(Some(&dt))
734 }) {
735 return Err(LoweringError::InvalidType(
736 "subscript requires a list, map, node, relationship, or null".into(),
737 ));
738 }
739 if self.is_list_typed(&base) {
740 match self.expr_data_type(&key_expr) {
741 Some(dt) if is_integer_data_type(&dt) => {
742 return Ok(datafusion::functions_nested::expr_fn::array_element(
743 base,
744 one_based_index(key_expr),
745 ));
746 }
747 Some(DataType::Null) | None => {}
748 Some(_) => {
749 return Err(LoweringError::InvalidType(
750 "list subscript index must be an integer or null".into(),
751 ));
752 }
753 }
754 }
755 Ok(CYPHER_VALUE_ACCESS.call(vec![base, key_expr]))
756 }
757
758 fn lower_static_value_access(
759 &self,
760 base: ExprId,
761 key: &str,
762 ) -> Result<Option<DfExpr>, LoweringError> {
763 if matches!(self.arena.get(base), IrExpr::Literal(IrLiteral::Null)) {
764 return Ok(Some(lit(ScalarValue::Null)));
765 }
766 if let Some(value) = self.lower_static_indexed_value_access(base, key)? {
767 return Ok(Some(value));
768 }
769 if let Some(value) = self.lower_map_literal_static_field(base, key)? {
770 return Ok(Some(value));
771 }
772 if let IrExpr::VarRef(var_id) = self.arena.get(base)
773 && let Some(base_name) = self.var_map.get(*var_id)
774 {
775 if let Some(value) = self.lower_entity_static_property(*var_id, base_name, key) {
776 return Ok(Some(value));
777 }
778 if let Some(value) = self.lower_struct_static_field(col_literal(base_name), key) {
779 return Ok(Some(value));
780 }
781 }
782
783 let base_expr = self.lower(base)?;
784 Ok(self.lower_struct_static_field(base_expr, key))
785 }
786
787 fn lower_map_literal_static_field(
788 &self,
789 base: ExprId,
790 key: &str,
791 ) -> Result<Option<DfExpr>, LoweringError> {
792 let IrExpr::MapLiteral(entries) = self.arena.get(base) else {
793 return Ok(None);
794 };
795 entries
796 .iter()
797 .find(|(k, _)| k == key)
798 .map_or(Ok(Some(lit(ScalarValue::Null))), |(_, id)| {
799 self.lower(*id).map(Some)
800 })
801 }
802
803 fn lower_entity_static_property(&self, var_id: VarId, base: &str, key: &str) -> Option<DfExpr> {
804 let qual = datafusion::common::TableReference::bare(base);
805 if let Some(schema) = self.input_schema.as_ref() {
806 if schema.index_of_column_by_name(Some(&qual), key).is_some() {
807 return Some(qualified_col(base, key));
808 }
809 if self.is_node_var(base) || self.is_edge_var(base) {
810 return Some(lit(ScalarValue::Null));
811 }
812 }
813 if let Some(shape) = self.node_shapes.get(&var_id.0) {
814 let is_topology = graphforge_storage::TOPOLOGY_NODES_SCHEMA
815 .field_with_name(key)
816 .is_ok();
817 if is_topology || shape.prop_names.iter().any(|p| p == key) {
818 return Some(qualified_col(base, key));
819 }
820 if self.props_authoritative {
821 return Some(lit(ScalarValue::Null));
822 }
823 }
824 None
825 }
826
827 fn lower_static_indexed_value_access(
828 &self,
829 base: ExprId,
830 key: &str,
831 ) -> Result<Option<DfExpr>, LoweringError> {
832 let IrExpr::FunctionCall { name, args } = self.arena.get(base) else {
833 return Ok(None);
834 };
835 if name != "_subscript" {
836 return Ok(None);
837 }
838 let [list_id, index_id] = args.as_slice() else {
839 return Ok(None);
840 };
841 let IrExpr::ListLiteral(items) = self.arena.get(*list_id) else {
842 return Ok(None);
843 };
844 let IrExpr::Literal(IrLiteral::Int(idx)) = self.arena.get(*index_id) else {
845 return Ok(None);
846 };
847 let len = i64::try_from(items.len()).map_err(|_| {
848 LoweringError::UnsupportedExpr("list literal length exceeds i64 range".into())
849 })?;
850 let pos = if *idx < 0 { len + idx } else { *idx };
851 if pos < 0 || pos >= len {
852 return Ok(Some(lit(ScalarValue::Null)));
853 }
854 let pos = usize::try_from(pos)
855 .map_err(|_| LoweringError::UnsupportedExpr("list index exceeds usize".into()))?;
856 self.lower_static_value_access(items[pos], key)
857 }
858
859 fn lower_struct_static_field(&self, base_expr: DfExpr, key: &str) -> Option<DfExpr> {
860 let dt = self.expr_data_type(&base_expr)?;
861 match dt {
862 DataType::Null => Some(lit(ScalarValue::Null)),
863 dt if is_het_struct_type(Some(&dt)) => Some(
864 ScalarUDF::new_from_impl(CypherStaticValueAccess::new(key.to_owned()))
865 .call(vec![base_expr]),
866 ),
867 DataType::Struct(fields)
868 if is_plain_map_struct_type(&DataType::Struct(fields.clone())) =>
869 {
870 if fields.iter().any(|f| f.name() == key) {
871 Some(datafusion::functions::core::expr_fn::get_field(
872 base_expr,
873 key.to_owned(),
874 ))
875 } else {
876 Some(lit(ScalarValue::Null))
877 }
878 }
879 DataType::Struct(fields) => {
880 let is_entity = fields
881 .iter()
882 .any(|field| matches!(field.name().as_str(), "node_uuid" | "edge_uuid"));
883 if is_entity && fields.iter().any(|field| field.name() == key) {
884 Some(datafusion::functions::core::expr_fn::get_field(
885 base_expr,
886 key.to_owned(),
887 ))
888 } else if is_entity {
889 Some(lit(ScalarValue::Null))
890 } else {
891 None
892 }
893 }
894 _ => None,
895 }
896 }
897
898 fn lower_dynamic_access_container(
899 &self,
900 base: ExprId,
901 ) -> Result<Option<DfExpr>, LoweringError> {
902 if matches!(self.arena.get(base), IrExpr::Literal(IrLiteral::Null)) {
903 return Ok(Some(lit(ScalarValue::Null)));
904 }
905 if matches!(self.arena.get(base), IrExpr::MapLiteral(_)) {
906 return self.lower(base).map(Some);
907 }
908 if let IrExpr::VarRef(var_id) = self.arena.get(base)
909 && let Some(base_name) = self.var_map.get(*var_id)
910 {
911 if let Some(expr) = self.entity_property_bag(*var_id, base_name) {
912 return Ok(Some(expr));
913 }
914 let base_expr = col_literal(base_name);
915 if self.expr_data_type(&base_expr).is_some_and(|dt| {
916 matches!(dt, DataType::Null)
917 || is_plain_map_struct_type(&dt)
918 || is_het_struct_type(Some(&dt))
919 }) {
920 return Ok(Some(base_expr));
921 }
922 }
923 let base_expr = self.lower(base)?;
924 Ok(self
925 .expr_data_type(&base_expr)
926 .is_some_and(|dt| {
927 matches!(dt, DataType::Null)
928 || is_plain_map_struct_type(&dt)
929 || is_het_struct_type(Some(&dt))
930 })
931 .then_some(base_expr))
932 }
933
934 fn entity_property_bag(&self, var_id: VarId, base: &str) -> Option<DfExpr> {
935 self.entity_property_bag_inner(var_id, base, false)
936 }
937
938 fn entity_property_bag_with_empty(&self, var_id: VarId, base: &str) -> Option<DfExpr> {
939 self.entity_property_bag_inner(var_id, base, true)
940 }
941
942 fn entity_property_bag_inner(
943 &self,
944 var_id: VarId,
945 base: &str,
946 empty_map_for_present_entity: bool,
947 ) -> Option<DfExpr> {
948 use datafusion::functions::core::expr_fn::named_struct;
949
950 let (prop_names, present) = if let Some(shape) = self.node_shapes.get(&var_id.0) {
951 let has_node_uuid = self.input_schema.as_ref().is_some_and(|schema| {
952 let qual = datafusion::common::TableReference::bare(base);
953 schema
954 .index_of_column_by_name(Some(&qual), "node_uuid")
955 .is_some()
956 });
957 let present = if has_node_uuid {
958 col(format!("{base}.node_uuid")).is_not_null()
959 } else {
960 lit(true)
961 };
962 (shape.prop_names.clone(), present)
963 } else if self.is_edge_var(base) {
964 (self.edge_prop_names(base), edge_present_qual(base))
965 } else {
966 return None;
967 };
968 if prop_names.is_empty() {
969 let value = if empty_map_for_present_entity {
970 ScalarUDF::new_from_impl(CypherEntityProperties::new(1)).call(vec![present.clone()])
971 } else {
972 lit(ScalarValue::Null)
973 };
974 return Some(null_unless(present, value));
975 }
976 if empty_map_for_present_entity {
977 let mut args = Vec::with_capacity(1 + prop_names.len() * 2);
978 args.push(present);
979 for prop in prop_names {
980 args.push(lit(prop.as_str()));
981 args.push(qualified_col(base, &prop));
982 }
983 return Some(
984 ScalarUDF::new_from_impl(CypherEntityProperties::new(args.len())).call(args),
985 );
986 }
987 let mut args = Vec::with_capacity(prop_names.len() * 2);
988 for prop in prop_names {
989 args.push(lit(prop.as_str()));
990 args.push(qualified_col(base, &prop));
991 }
992 Some(null_unless(present, named_struct(args)))
993 }
994
995 fn lower_quantifier(
1002 &self,
1003 kind: graphforge_ir::QuantifierKind,
1004 loop_var: VarId,
1005 list: ExprId,
1006 predicate: ExprId,
1007 ) -> Result<DfExpr, LoweringError> {
1008 let elem_name = match self.elem_struct_cols.len() {
1012 0 => "__gf_elem".to_owned(),
1013 d => format!("__gf_elem_{d}"),
1014 };
1015 let list_expr = self.lower(list)?;
1016 if let IrExpr::Literal(IrLiteral::Bool(predicate)) = self.arena.get(predicate) {
1017 let udf =
1018 ScalarUDF::new_from_impl(CypherInvariantQuantifier::new(kind, Some(*predicate)));
1019 return Ok(udf.call(vec![list_expr]));
1020 }
1021 if matches!(self.arena.get(predicate), IrExpr::Literal(IrLiteral::Null)) {
1022 let udf = ScalarUDF::new_from_impl(CypherInvariantQuantifier::new(kind, None));
1023 return Ok(udf.call(vec![list_expr]));
1024 }
1025 let mut elem_vars = VarMap::new();
1028 for v in self.var_map.var_ids() {
1029 if let Some(name) = self.var_map.get(v) {
1030 elem_vars.insert(v, name.to_owned());
1031 }
1032 }
1033 elem_vars.insert(loop_var, elem_name.as_str());
1034 let pred_lowerer = {
1035 let mut l =
1036 ExprLowerer::with_prop_names(self.arena, &elem_vars, self.prop_names.clone());
1037 if let Some(s) = self.input_schema.as_ref() {
1038 l = l.with_input_schema(s.clone());
1039 }
1040 if let Some(t) = self.read_target.as_ref() {
1041 l = l.with_read_target(t.clone());
1042 }
1043 for c in &self.elem_struct_cols {
1046 l = l.with_elem_struct_col(c.clone());
1047 }
1048 l.with_elem_struct_col(elem_name.clone())
1049 };
1050 let pred_expr = pred_lowerer.lower(predicate)?;
1051
1052 let mut outer: Vec<String> = pred_expr
1056 .column_refs()
1057 .into_iter()
1058 .map(|c| c.name.clone())
1059 .filter(|n| n != &elem_name)
1060 .collect();
1061 outer.sort();
1062 outer.dedup();
1063
1064 if outer.is_empty()
1074 && !is_empty_list_literal(&list_expr)
1075 && let Some(elem_type) = self.list_element_type(&list_expr)
1076 {
1077 use datafusion::arrow::datatypes::{Field, Schema};
1078 use datafusion::common::DFSchema;
1079 use datafusion::logical_expr::execution_props::ExecutionProps;
1080 use datafusion::physical_expr::create_physical_expr;
1081 let schema = Schema::new(vec![Field::new(&elem_name, elem_type, true)]);
1082 if let Ok(df_schema) = DFSchema::try_from(schema)
1083 && create_physical_expr(&pred_expr, &df_schema, &ExecutionProps::new()).is_err()
1084 {
1085 return Err(LoweringError::InvalidType(format!(
1086 "quantifier predicate cannot apply to the list's element type ({kind:?})"
1087 )));
1088 }
1089 }
1090
1091 let mut call_args = Vec::with_capacity(1 + outer.len());
1092 call_args.push(list_expr);
1093 for name in &outer {
1094 call_args.push(col_literal(name));
1095 }
1096 let udf =
1097 ScalarUDF::new_from_impl(CypherQuantifier::new(kind, pred_expr, elem_name, outer));
1098 Ok(udf.call(call_args))
1099 }
1100
1101 #[allow(
1107 clippy::too_many_lines,
1108 reason = "schema synthesis, correlation rebinding, and UDF construction stay aligned"
1109 )]
1110 fn lower_list_comprehension(
1111 &self,
1112 loop_var: VarId,
1113 list: ExprId,
1114 filter: Option<ExprId>,
1115 projection: Option<ExprId>,
1116 ) -> Result<DfExpr, LoweringError> {
1117 let elem_name = match self.elem_struct_cols.len() {
1121 0 => "__gf_elem".to_owned(),
1122 d => format!("__gf_elem_{d}"),
1123 };
1124 let list_expr = self.lower(list)?;
1125 let clause_schema = self.list_element_type(&list_expr).and_then(|element_type| {
1126 let mut fields = self.input_schema.as_ref().map_or_else(Vec::new, |schema| {
1127 schema
1128 .iter()
1129 .map(|(qualifier, field)| (qualifier.cloned(), Arc::clone(field)))
1130 .collect()
1131 });
1132 fields.push((
1133 None,
1134 Arc::new(datafusion::arrow::datatypes::Field::new(
1135 &elem_name,
1136 element_type,
1137 true,
1138 )),
1139 ));
1140 datafusion::common::DFSchema::new_with_metadata(fields, HashMap::new())
1141 .ok()
1142 .map(Arc::new)
1143 });
1144
1145 let mut elem_vars = VarMap::new();
1148 for v in self.var_map.var_ids() {
1149 if let Some(name) = self.var_map.get(v) {
1150 elem_vars.insert(v, name.to_owned());
1151 }
1152 }
1153 elem_vars.insert(loop_var, elem_name.as_str());
1154 let clause_lowerer = {
1155 let mut l =
1156 ExprLowerer::with_prop_names(self.arena, &elem_vars, self.prop_names.clone());
1157 if let Some(s) = clause_schema.as_ref().or(self.input_schema.as_ref()) {
1158 l = l.with_input_schema(s.clone());
1159 }
1160 if let Some(t) = self.read_target.as_ref() {
1161 l = l.with_read_target(t.clone());
1162 }
1163 for c in &self.elem_struct_cols {
1165 l = l.with_elem_struct_col(c.clone());
1166 }
1167 l.with_elem_struct_col(elem_name.clone())
1168 };
1169 let mut filter_expr = filter.map(|f| clause_lowerer.lower(f)).transpose()?;
1170 let mut projection_expr = projection.map(|p| clause_lowerer.lower(p)).transpose()?;
1171
1172 let mut outer_columns = Vec::new();
1174 for e in [filter_expr.as_ref(), projection_expr.as_ref()]
1175 .into_iter()
1176 .flatten()
1177 {
1178 for c in e.column_refs() {
1179 if c.name != elem_name && !outer_columns.contains(c) {
1180 outer_columns.push(c.clone());
1181 }
1182 }
1183 }
1184 outer_columns.sort_by_key(datafusion::common::Column::flat_name);
1185 let outer = (0..outer_columns.len())
1186 .map(|index| format!("__gf_outer_{index}"))
1187 .collect::<Vec<_>>();
1188 if !outer_columns.is_empty() {
1189 use datafusion::common::tree_node::{Transformed, TreeNode};
1190 let rewrite = |expr: DfExpr| {
1191 expr.transform_up(|expr| {
1192 let DfExpr::Column(column) = &expr else {
1193 return Ok(Transformed::no(expr));
1194 };
1195 let Some(index) = outer_columns.iter().position(|outer| outer == column) else {
1196 return Ok(Transformed::no(expr));
1197 };
1198 Ok(Transformed::yes(DfExpr::Column(
1199 datafusion::common::Column::from_name(outer[index].clone()),
1200 )))
1201 })
1202 .map(|transformed| transformed.data)
1203 };
1204 filter_expr = filter_expr
1205 .map(&rewrite)
1206 .transpose()
1207 .map_err(|error| LoweringError::UnsupportedExpr(error.to_string()))?;
1208 projection_expr = projection_expr
1209 .map(rewrite)
1210 .transpose()
1211 .map_err(|error| LoweringError::UnsupportedExpr(error.to_string()))?;
1212 }
1213
1214 if let Some(fexpr) = filter_expr.as_ref()
1221 && outer.is_empty()
1222 && let Some(elem_type) = self.list_element_type(&list_expr)
1223 {
1224 use datafusion::arrow::datatypes::{Field, Schema};
1225 use datafusion::common::DFSchema;
1226 use datafusion::logical_expr::execution_props::ExecutionProps;
1227 use datafusion::physical_expr::create_physical_expr;
1228 let schema = Schema::new(vec![Field::new(&elem_name, elem_type, true)]);
1229 if let Ok(df_schema) = DFSchema::try_from(schema)
1230 && create_physical_expr(fexpr, &df_schema, &ExecutionProps::new()).is_err()
1231 {
1232 return Err(LoweringError::InvalidType(
1233 "list comprehension filter cannot apply to the list's element type".into(),
1234 ));
1235 }
1236 }
1237
1238 let mut call_args = Vec::with_capacity(1 + outer.len());
1239 call_args.push(list_expr);
1240 for column in &outer_columns {
1241 call_args.push(DfExpr::Column(column.clone()));
1242 }
1243 let udf = ScalarUDF::new_from_impl(CypherListComp::new(
1244 filter_expr,
1245 projection_expr,
1246 elem_name,
1247 outer,
1248 ));
1249 Ok(udf.call(call_args))
1250 }
1251
1252 fn lower_map_literal(&self, entries: &[(String, ExprId)]) -> Result<DfExpr, LoweringError> {
1263 use datafusion::functions::core::expr_fn::named_struct;
1264 if entries.is_empty() {
1265 return Ok(empty_map_struct());
1267 }
1268 let mut args: Vec<DfExpr> = Vec::with_capacity(entries.len() * 2);
1269 for (key, value) in entries {
1270 args.push(lit(key.as_str()));
1271 args.push(self.lower_value(*value)?);
1272 }
1273 Ok(named_struct(args))
1274 }
1275
1276 fn sole_arg_is_null(&self, args: &[ExprId]) -> bool {
1289 matches!(args, [a] if matches!(self.arena.get(*a), IrExpr::Literal(graphforge_ir::IrLiteral::Null)))
1290 }
1291
1292 fn lower_clock_now(&self, name: &str) -> DfExpr {
1300 use chrono::Timelike;
1301 let now = *self.now.get_or_init(|| chrono::Utc::now().naive_utc());
1302 let days = crate::temporal::date_to_epoch_days(now.date()).unwrap_or(0);
1303 let nanos = i64::from(now.time().num_seconds_from_midnight()) * 1_000_000_000
1304 + i64::from(now.time().nanosecond());
1305 let scalar = match name {
1306 "date" => date_scalar(Some(days)),
1307 "localtime" => ScalarValue::Time64Nanosecond(Some(nanos)),
1308 "localdatetime" => localdatetime_scalar(Some((days, nanos))),
1309 "time" => time_scalar(Some((nanos, 0))),
1310 _ => datetime_scalar(Some((days, nanos, 0, None))),
1312 };
1313 lit(scalar)
1314 }
1315
1316 fn lower_temporal(&self, name: &str, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
1317 if self.sole_arg_is_null(args) {
1320 return Ok(DfExpr::Literal(temporal_null_scalar(name), None));
1321 }
1322 if args.is_empty() && name != "duration" {
1325 return Ok(self.lower_clock_now(name));
1326 }
1327 if let [arg] = args {
1328 if name == "date" {
1333 if let Some(days) = self.const_date(*arg) {
1334 return Ok(DfExpr::Literal(date_scalar(Some(days)), None));
1335 }
1336 if let Some(projected) = self.lower_date_runtime(*arg)? {
1340 return Ok(projected);
1341 }
1342 }
1343 if name == "localtime" {
1347 if let Some(nanos) = self.const_local_time(*arg) {
1348 return Ok(DfExpr::Literal(
1349 ScalarValue::Time64Nanosecond(Some(nanos)),
1350 None,
1351 ));
1352 }
1353 if let Some(projected) = self.lower_localtime_runtime(*arg)? {
1354 return Ok(projected);
1355 }
1356 }
1357 if name == "localdatetime" {
1363 if let Some((days, nanos)) = self.const_local_date_time(*arg) {
1364 return Ok(DfExpr::Literal(
1365 localdatetime_scalar(Some((days, nanos))),
1366 None,
1367 ));
1368 }
1369 if let Some(projected) = self.lower_localdatetime_runtime(*arg)? {
1370 return Ok(projected);
1371 }
1372 }
1373 if name == "time" {
1377 if let Some((nanos, offset)) = self.const_time(*arg) {
1378 return Ok(DfExpr::Literal(time_scalar(Some((nanos, offset))), None));
1379 }
1380 if let Some(projected) = self.lower_time_runtime(*arg)? {
1381 return Ok(projected);
1382 }
1383 }
1384 if name == "datetime" {
1390 if let Some(parts) = self.const_datetime(*arg) {
1391 return Ok(DfExpr::Literal(datetime_scalar(Some(parts)), None));
1392 }
1393 if let Some(projected) = self.lower_datetime_runtime(*arg)? {
1394 return Ok(projected);
1395 }
1396 }
1397 if name == "duration" {
1402 if let Some(dur) = self.const_duration(*arg) {
1403 return Ok(DfExpr::Literal(duration_scalar(Some(dur)), None));
1404 }
1405 let lowered = self.lower(*arg)?;
1406 if self.is_string_typed(&lowered) {
1407 return Ok(CYPHER_DURATION_PARSE.call(vec![lowered]));
1408 }
1409 }
1410 match self.arena.get(*arg) {
1411 IrExpr::Literal(IrLiteral::Str(s)) => {
1412 if let Some(rendered) = render_temporal(name, s) {
1413 return Ok(lit(rendered));
1414 }
1415 }
1416 IrExpr::MapLiteral(entries) => {
1417 if let Some(fields) = self.extract_temporal_fields(entries)
1418 && let Some(rendered) = crate::temporal::render_temporal_map(name, &fields)
1419 {
1420 return Ok(lit(rendered));
1421 }
1422 }
1423 _ => {}
1424 }
1425 }
1426 let lowered: Vec<DfExpr> = args
1427 .iter()
1428 .map(|&a| self.lower(a))
1429 .collect::<Result<_, _>>()?;
1430 resolve_builtin(name, lowered, || self.path_node_hydration())
1431 .ok_or_else(|| LoweringError::UnknownFunction(name.to_string()))
1432 }
1433
1434 fn const_date(&self, arg: ExprId) -> Option<i64> {
1437 match self.arena.get(arg) {
1438 IrExpr::Literal(IrLiteral::Str(s)) => crate::temporal::parse_date_string(s),
1439 IrExpr::MapLiteral(entries) => {
1440 let fields = self.extract_temporal_fields(entries)?;
1441 crate::temporal::date_from_map(&fields)
1442 }
1443 _ => None,
1444 }
1445 }
1446
1447 fn lower_date_runtime(&self, arg: ExprId) -> Result<Option<DfExpr>, LoweringError> {
1454 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1455 let (base, overrides) = match self.arena.get(arg) {
1456 IrExpr::MapLiteral(entries) if entries.iter().any(|(k, _)| k == "date") => {
1457 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1458 let base = self.lower(field("date").expect("checked `date` key exists"))?;
1459 let ov = |name: &str| match field(name) {
1460 Some(id) => self.lower(id),
1461 None => Ok(null_i64()),
1462 };
1463 let overrides = [
1464 ov("year")?,
1465 ov("month")?,
1466 ov("day")?,
1467 ov("week")?,
1468 ov("dayOfWeek")?,
1469 ov("ordinalDay")?,
1470 ov("quarter")?,
1471 ov("dayOfQuarter")?,
1472 ];
1473 (base, overrides)
1474 }
1475 IrExpr::MapLiteral(_) => return Ok(None),
1478 _ => (self.lower(arg)?, std::array::from_fn(|_| null_i64())),
1480 };
1481 let mut call_args = Vec::with_capacity(9);
1482 call_args.push(base);
1483 call_args.extend(overrides);
1484 Ok(Some(CYPHER_DATE_PROJECT.call(call_args)))
1485 }
1486
1487 fn const_local_time(&self, arg: ExprId) -> Option<i64> {
1490 match self.arena.get(arg) {
1491 IrExpr::Literal(IrLiteral::Str(s)) => crate::temporal::localtime_nanos_from_str(s),
1492 IrExpr::MapLiteral(entries) => {
1493 let fields = self.extract_temporal_fields(entries)?;
1494 crate::temporal::localtime_nanos_from_map(&fields)
1495 }
1496 _ => None,
1497 }
1498 }
1499
1500 fn lower_localtime_runtime(&self, arg: ExprId) -> Result<Option<DfExpr>, LoweringError> {
1507 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1508 let (base, overrides) = match self.arena.get(arg) {
1509 IrExpr::MapLiteral(entries) if entries.iter().any(|(k, _)| k == "time") => {
1510 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1511 let base = self.lower(field("time").expect("checked `time` key exists"))?;
1512 let ov = |name: &str| match field(name) {
1513 Some(id) => self.lower(id),
1514 None => Ok(null_i64()),
1515 };
1516 let overrides = [
1517 ov("hour")?,
1518 ov("minute")?,
1519 ov("second")?,
1520 ov("millisecond")?,
1521 ov("microsecond")?,
1522 ov("nanosecond")?,
1523 ];
1524 (base, overrides)
1525 }
1526 IrExpr::MapLiteral(_) => return Ok(None),
1527 _ => (self.lower(arg)?, std::array::from_fn(|_| null_i64())),
1528 };
1529 let mut call_args = Vec::with_capacity(7);
1530 call_args.push(base);
1531 call_args.extend(overrides);
1532 Ok(Some(CYPHER_LOCALTIME_PROJECT.call(call_args)))
1533 }
1534
1535 fn const_local_date_time(&self, arg: ExprId) -> Option<(i64, i64)> {
1539 match self.arena.get(arg) {
1540 IrExpr::Literal(IrLiteral::Str(s)) => crate::temporal::localdatetime_parts_from_str(s),
1541 IrExpr::MapLiteral(entries) => {
1542 let fields = self.extract_temporal_fields(entries)?;
1543 crate::temporal::localdatetime_parts_from_map(&fields)
1544 }
1545 _ => None,
1546 }
1547 }
1548
1549 fn lower_localdatetime_runtime(&self, arg: ExprId) -> Result<Option<DfExpr>, LoweringError> {
1556 let null = || DfExpr::Literal(ScalarValue::Null, None);
1557 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1558 let (date_src, time_src, overrides) =
1559 if let IrExpr::MapLiteral(entries) = self.arena.get(arg) {
1560 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1561 let lower_or = |id: Option<ExprId>, default: &dyn Fn() -> DfExpr| match id {
1562 Some(id) => self.lower(id),
1563 None => Ok(default()),
1564 };
1565 let date_anchor = field("datetime").or_else(|| field("date"));
1568 let time_anchor = field("datetime").or_else(|| field("time"));
1569 let date_src = lower_or(date_anchor, &null)?;
1570 let time_src = lower_or(time_anchor, &null)?;
1571 let ov = |name: &str| lower_or(field(name), &null_i64);
1572 let overrides = [
1573 ov("year")?,
1574 ov("month")?,
1575 ov("day")?,
1576 ov("week")?,
1577 ov("dayOfWeek")?,
1578 ov("ordinalDay")?,
1579 ov("quarter")?,
1580 ov("dayOfQuarter")?,
1581 ov("hour")?,
1582 ov("minute")?,
1583 ov("second")?,
1584 ov("millisecond")?,
1585 ov("microsecond")?,
1586 ov("nanosecond")?,
1587 ];
1588 (date_src, time_src, overrides)
1589 } else {
1590 let base = self.lower(arg)?;
1593 (base.clone(), base, std::array::from_fn(|_| null_i64()))
1594 };
1595 let mut call_args = Vec::with_capacity(16);
1596 call_args.push(date_src);
1597 call_args.push(time_src);
1598 call_args.extend(overrides);
1599 Ok(Some(CYPHER_LOCALDATETIME_PROJECT.call(call_args)))
1600 }
1601
1602 fn const_time(&self, arg: ExprId) -> Option<(i64, i32)> {
1606 match self.arena.get(arg) {
1607 IrExpr::Literal(IrLiteral::Str(s)) => crate::temporal::time_value_from_str(s),
1608 IrExpr::MapLiteral(entries) => {
1609 let fields = self.extract_temporal_fields(entries)?;
1610 crate::temporal::time_value_from_map(&fields)
1611 }
1612 _ => None,
1613 }
1614 }
1615
1616 fn lower_time_runtime(&self, arg: ExprId) -> Result<Option<DfExpr>, LoweringError> {
1623 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1624 let null_str = || DfExpr::Literal(ScalarValue::Utf8(None), None);
1625 let (base, overrides, timezone) = match self.arena.get(arg) {
1626 IrExpr::MapLiteral(entries) if entries.iter().any(|(k, _)| k == "time") => {
1627 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1628 let base = self.lower(field("time").expect("checked `time` key exists"))?;
1629 let ov = |name: &str| match field(name) {
1630 Some(id) => self.lower(id),
1631 None => Ok(null_i64()),
1632 };
1633 let overrides = [
1634 ov("hour")?,
1635 ov("minute")?,
1636 ov("second")?,
1637 ov("millisecond")?,
1638 ov("microsecond")?,
1639 ov("nanosecond")?,
1640 ];
1641 let timezone = match field("timezone") {
1642 Some(id) => self.lower(id)?,
1643 None => null_str(),
1644 };
1645 (base, overrides, timezone)
1646 }
1647 IrExpr::MapLiteral(_) | IrExpr::Literal(IrLiteral::Str(_)) => return Ok(None),
1653 _ => (
1654 self.lower(arg)?,
1655 std::array::from_fn(|_| null_i64()),
1656 null_str(),
1657 ),
1658 };
1659 let mut call_args = Vec::with_capacity(8);
1660 call_args.push(base);
1661 call_args.extend(overrides);
1662 call_args.push(timezone);
1663 Ok(Some(CYPHER_TIME_PROJECT.call(call_args)))
1664 }
1665
1666 fn const_datetime(&self, arg: ExprId) -> Option<(i64, i64, i32, Option<String>)> {
1670 match self.arena.get(arg) {
1671 IrExpr::Literal(IrLiteral::Str(s)) => crate::temporal::datetime_value_from_str(s),
1672 IrExpr::MapLiteral(entries) => {
1673 let fields = self.extract_temporal_fields(entries)?;
1674 crate::temporal::datetime_value_from_map(&fields)
1675 }
1676 _ => None,
1677 }
1678 }
1679
1680 fn const_duration(&self, arg: ExprId) -> Option<crate::temporal::DurationValue> {
1683 match self.arena.get(arg) {
1684 IrExpr::Literal(IrLiteral::Str(s)) => crate::temporal::duration_value_from_str(s),
1685 IrExpr::MapLiteral(entries) => {
1686 let fields = self.extract_temporal_fields(entries)?;
1687 crate::temporal::duration_value_from_map(&fields)
1688 }
1689 _ => None,
1690 }
1691 }
1692
1693 fn lower_datetime_runtime(&self, arg: ExprId) -> Result<Option<DfExpr>, LoweringError> {
1700 let null = || DfExpr::Literal(ScalarValue::Null, None);
1701 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1702 let null_str = || DfExpr::Literal(ScalarValue::Utf8(None), None);
1703 let (date_src, time_src, overrides, timezone) = match self.arena.get(arg) {
1704 IrExpr::MapLiteral(entries) => {
1705 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1706 let lower_or = |id: Option<ExprId>, default: &dyn Fn() -> DfExpr| match id {
1707 Some(id) => self.lower(id),
1708 None => Ok(default()),
1709 };
1710 let date_anchor = field("datetime").or_else(|| field("date"));
1711 let time_anchor = field("datetime").or_else(|| field("time"));
1712 let date_src = lower_or(date_anchor, &null)?;
1713 let time_src = lower_or(time_anchor, &null)?;
1714 let ov = |name: &str| lower_or(field(name), &null_i64);
1715 let overrides = [
1716 ov("year")?,
1717 ov("month")?,
1718 ov("day")?,
1719 ov("week")?,
1720 ov("dayOfWeek")?,
1721 ov("ordinalDay")?,
1722 ov("quarter")?,
1723 ov("dayOfQuarter")?,
1724 ov("hour")?,
1725 ov("minute")?,
1726 ov("second")?,
1727 ov("millisecond")?,
1728 ov("microsecond")?,
1729 ov("nanosecond")?,
1730 ];
1731 (
1732 date_src,
1733 time_src,
1734 overrides,
1735 lower_or(field("timezone"), &null_str)?,
1736 )
1737 }
1738 IrExpr::Literal(IrLiteral::Str(_)) => return Ok(None),
1741 _ => {
1742 let base = self.lower(arg)?;
1743 (
1744 base.clone(),
1745 base,
1746 std::array::from_fn(|_| null_i64()),
1747 null_str(),
1748 )
1749 }
1750 };
1751 let mut call_args = Vec::with_capacity(17);
1752 call_args.push(date_src);
1753 call_args.push(time_src);
1754 call_args.extend(overrides);
1755 call_args.push(timezone);
1756 Ok(Some(CYPHER_DATETIME_PROJECT.call(call_args)))
1757 }
1758
1759 fn lower_date_truncate(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
1763 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1764 let [unit_id, value_id, rest @ ..] = args else {
1765 return Err(LoweringError::UnknownFunction("date.truncate".to_string()));
1766 };
1767 let unit = self.lower(*unit_id)?;
1768 let value = self.lower(*value_id)?;
1769 let overrides: [DfExpr; 8] = match rest.first() {
1774 None => std::array::from_fn(|_| null_i64()),
1775 Some(map_id) => {
1776 let IrExpr::MapLiteral(entries) = self.arena.get(*map_id) else {
1777 return Err(LoweringError::UnsupportedExpr(
1778 "date.truncate override map must be a literal map".to_string(),
1779 ));
1780 };
1781 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1782 let ov = |name: &str| match field(name) {
1783 Some(id) => self.lower(id),
1784 None => Ok(null_i64()),
1785 };
1786 [
1787 ov("year")?,
1788 ov("month")?,
1789 ov("day")?,
1790 ov("week")?,
1791 ov("dayOfWeek")?,
1792 ov("ordinalDay")?,
1793 ov("quarter")?,
1794 ov("dayOfQuarter")?,
1795 ]
1796 }
1797 };
1798 let mut call_args = Vec::with_capacity(10);
1799 call_args.push(value);
1800 call_args.push(unit);
1801 call_args.extend(overrides);
1802 Ok(CYPHER_DATE_TRUNCATE.call(call_args))
1803 }
1804
1805 fn lower_localtime_truncate(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
1809 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1810 let [unit_id, value_id, rest @ ..] = args else {
1811 return Err(LoweringError::UnknownFunction(
1812 "localtime.truncate".to_string(),
1813 ));
1814 };
1815 let unit = self.lower(*unit_id)?;
1816 let value = self.lower(*value_id)?;
1817 let overrides: [DfExpr; 6] = match rest.first() {
1819 None => std::array::from_fn(|_| null_i64()),
1820 Some(map_id) => {
1821 let IrExpr::MapLiteral(entries) = self.arena.get(*map_id) else {
1822 return Err(LoweringError::UnsupportedExpr(
1823 "localtime.truncate override map must be a literal map".to_string(),
1824 ));
1825 };
1826 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1827 let ov = |name: &str| match field(name) {
1828 Some(id) => self.lower(id),
1829 None => Ok(null_i64()),
1830 };
1831 [
1832 ov("hour")?,
1833 ov("minute")?,
1834 ov("second")?,
1835 ov("millisecond")?,
1836 ov("microsecond")?,
1837 ov("nanosecond")?,
1838 ]
1839 }
1840 };
1841 let mut call_args = Vec::with_capacity(8);
1842 call_args.push(value);
1843 call_args.push(unit);
1844 call_args.extend(overrides);
1845 Ok(CYPHER_LOCALTIME_TRUNCATE.call(call_args))
1846 }
1847
1848 fn lower_localdatetime_truncate(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
1853 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1854 let [unit_id, value_id, rest @ ..] = args else {
1855 return Err(LoweringError::UnknownFunction(
1856 "localdatetime.truncate".to_string(),
1857 ));
1858 };
1859 let unit = self.lower(*unit_id)?;
1860 let value = self.lower(*value_id)?;
1861 let overrides: [DfExpr; 14] = match rest.first() {
1864 None => std::array::from_fn(|_| null_i64()),
1865 Some(map_id) => {
1866 let IrExpr::MapLiteral(entries) = self.arena.get(*map_id) else {
1867 return Err(LoweringError::UnsupportedExpr(
1868 "localdatetime.truncate override map must be a literal map".to_string(),
1869 ));
1870 };
1871 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1872 let ov = |name: &str| match field(name) {
1873 Some(id) => self.lower(id),
1874 None => Ok(null_i64()),
1875 };
1876 [
1877 ov("year")?,
1878 ov("month")?,
1879 ov("day")?,
1880 ov("week")?,
1881 ov("dayOfWeek")?,
1882 ov("ordinalDay")?,
1883 ov("quarter")?,
1884 ov("dayOfQuarter")?,
1885 ov("hour")?,
1886 ov("minute")?,
1887 ov("second")?,
1888 ov("millisecond")?,
1889 ov("microsecond")?,
1890 ov("nanosecond")?,
1891 ]
1892 }
1893 };
1894 let mut call_args = Vec::with_capacity(16);
1895 call_args.push(value);
1896 call_args.push(unit);
1897 call_args.extend(overrides);
1898 Ok(CYPHER_LOCALDATETIME_TRUNCATE.call(call_args))
1899 }
1900
1901 fn lower_time_truncate(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
1906 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1907 let null_str = || DfExpr::Literal(ScalarValue::Utf8(None), None);
1908 let [unit_id, value_id, rest @ ..] = args else {
1909 return Err(LoweringError::UnknownFunction("time.truncate".to_string()));
1910 };
1911 let unit = self.lower(*unit_id)?;
1912 let value = self.lower(*value_id)?;
1913 let (overrides, timezone): ([DfExpr; 6], DfExpr) = match rest.first() {
1914 None => (std::array::from_fn(|_| null_i64()), null_str()),
1915 Some(map_id) => {
1916 let IrExpr::MapLiteral(entries) = self.arena.get(*map_id) else {
1917 return Err(LoweringError::UnsupportedExpr(
1918 "time.truncate override map must be a literal map".to_string(),
1919 ));
1920 };
1921 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1922 let ov = |name: &str| match field(name) {
1923 Some(id) => self.lower(id),
1924 None => Ok(null_i64()),
1925 };
1926 let tz = match field("timezone") {
1927 Some(id) => self.lower(id)?,
1928 None => null_str(),
1929 };
1930 (
1931 [
1932 ov("hour")?,
1933 ov("minute")?,
1934 ov("second")?,
1935 ov("millisecond")?,
1936 ov("microsecond")?,
1937 ov("nanosecond")?,
1938 ],
1939 tz,
1940 )
1941 }
1942 };
1943 let mut call_args = Vec::with_capacity(9);
1944 call_args.push(value);
1945 call_args.push(unit);
1946 call_args.extend(overrides);
1947 call_args.push(timezone);
1948 Ok(CYPHER_TIME_TRUNCATE.call(call_args))
1949 }
1950
1951 fn lower_datetime_truncate(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
1956 let null_i64 = || DfExpr::Literal(ScalarValue::Int64(None), None);
1957 let null_str = || DfExpr::Literal(ScalarValue::Utf8(None), None);
1958 let [unit_id, value_id, rest @ ..] = args else {
1959 return Err(LoweringError::UnknownFunction(
1960 "datetime.truncate".to_string(),
1961 ));
1962 };
1963 let unit = self.lower(*unit_id)?;
1964 let value = self.lower(*value_id)?;
1965 let (overrides, timezone): ([DfExpr; 14], DfExpr) = match rest.first() {
1966 None => (std::array::from_fn(|_| null_i64()), null_str()),
1967 Some(map_id) => {
1968 let IrExpr::MapLiteral(entries) = self.arena.get(*map_id) else {
1969 return Err(LoweringError::UnsupportedExpr(
1970 "datetime.truncate override map must be a literal map".to_string(),
1971 ));
1972 };
1973 let field = |name: &str| entries.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
1974 let ov = |name: &str| match field(name) {
1975 Some(id) => self.lower(id),
1976 None => Ok(null_i64()),
1977 };
1978 let tz = match field("timezone") {
1979 Some(id) => self.lower(id)?,
1980 None => null_str(),
1981 };
1982 (
1983 [
1984 ov("year")?,
1985 ov("month")?,
1986 ov("day")?,
1987 ov("week")?,
1988 ov("dayOfWeek")?,
1989 ov("ordinalDay")?,
1990 ov("quarter")?,
1991 ov("dayOfQuarter")?,
1992 ov("hour")?,
1993 ov("minute")?,
1994 ov("second")?,
1995 ov("millisecond")?,
1996 ov("microsecond")?,
1997 ov("nanosecond")?,
1998 ],
1999 tz,
2000 )
2001 }
2002 };
2003 let mut call_args = Vec::with_capacity(17);
2004 call_args.push(value);
2005 call_args.push(unit);
2006 call_args.extend(overrides);
2007 call_args.push(timezone);
2008 Ok(CYPHER_DATETIME_TRUNCATE.call(call_args))
2009 }
2010
2011 fn lower_duration_between(&self, name: &str, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2015 let [a_id, b_id] = args else {
2016 return Err(LoweringError::UnknownFunction(name.to_string()));
2017 };
2018 let a = self.lower(*a_id)?;
2019 let b = self.lower(*b_id)?;
2020 Ok(CYPHER_DURATION_BETWEEN.call(vec![a, b, lit(name)]))
2021 }
2022
2023 fn extract_temporal_fields(
2024 &self,
2025 entries: &[(String, ExprId)],
2026 ) -> Option<std::collections::HashMap<String, crate::temporal::TemporalField>> {
2027 let mut fields = std::collections::HashMap::with_capacity(entries.len());
2028 for (key, value) in entries {
2029 fields.insert(key.clone(), self.extract_temporal_field(*value)?);
2030 }
2031 Some(fields)
2032 }
2033
2034 fn extract_temporal_field(&self, id: ExprId) -> Option<crate::temporal::TemporalField> {
2037 use crate::temporal::TemporalField;
2038 use graphforge_ir::expr::UnaryOpKind;
2039 match self.arena.get(id) {
2040 IrExpr::Literal(IrLiteral::Int(n)) => Some(TemporalField::Int(*n)),
2041 IrExpr::Literal(IrLiteral::Float(x)) => Some(TemporalField::Float(*x)),
2042 IrExpr::Literal(IrLiteral::Str(s)) => Some(TemporalField::Str(s.clone())),
2043 IrExpr::UnaryOp {
2046 op: UnaryOpKind::Neg,
2047 expr,
2048 } => match self.arena.get(*expr) {
2049 IrExpr::Literal(IrLiteral::Int(n)) => Some(TemporalField::Int(-n)),
2050 IrExpr::Literal(IrLiteral::Float(x)) => Some(TemporalField::Float(-x)),
2051 _ => None,
2052 },
2053 IrExpr::FunctionCall { name, args } if name == "date" => {
2054 if let [a] = args.as_slice()
2055 && let IrExpr::Literal(IrLiteral::Str(s)) = self.arena.get(*a)
2056 {
2057 crate::temporal::parse_date_string(s).map(TemporalField::Date)
2058 } else {
2059 None
2060 }
2061 }
2062 _ => None,
2063 }
2064 }
2065
2066 fn lower_from_epoch(&self, name: &str, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2070 self.try_from_epoch(name, args)
2071 .map(lit)
2072 .ok_or_else(|| LoweringError::UnknownFunction(name.to_string()))
2073 }
2074
2075 fn try_from_epoch(&self, name: &str, args: &[ExprId]) -> Option<String> {
2076 match (name, args) {
2077 ("datetime.fromepoch", &[a, b]) => {
2078 crate::temporal::render_from_epoch(self.int_literal(a)?, self.int_literal(b)?)
2079 }
2080 ("datetime.fromepochmillis", &[a]) => {
2081 crate::temporal::render_from_epoch_millis(self.int_literal(a)?)
2082 }
2083 _ => None,
2084 }
2085 }
2086
2087 fn int_literal(&self, id: ExprId) -> Option<i64> {
2089 match self.arena.get(id) {
2090 IrExpr::Literal(IrLiteral::Int(n)) => Some(*n),
2091 _ => None,
2092 }
2093 }
2094
2095 fn lower_node_struct(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2105 let Some(&base_id) = args.first() else {
2108 return Err(LoweringError::UnsupportedExpr(
2109 "_node_struct expects at least one argument".into(),
2110 ));
2111 };
2112 let IrExpr::VarRef(var_id) = self.arena.get(base_id) else {
2113 return Err(LoweringError::UnsupportedExpr(
2114 "_node_struct argument must be a node variable".into(),
2115 ));
2116 };
2117 let base = self
2118 .var_map
2119 .get(*var_id)
2120 .ok_or(LoweringError::UnboundVar(var_id.0))?;
2121 let label = args.get(1).and_then(|&id| match self.arena.get(id) {
2122 IrExpr::Literal(IrLiteral::Str(s)) => Some(s.as_str()),
2123 _ => None,
2124 });
2125 let prop_names = self
2126 .node_shapes
2127 .get(&var_id.0)
2128 .map(|s| s.prop_names.clone())
2129 .unwrap_or_default();
2130 let labels = self.input_schema.as_ref().and_then(|schema| {
2131 let qualifier = datafusion::common::TableReference::bare(base);
2132 schema
2133 .index_of_column_by_name(Some(&qualifier), "labels")
2134 .is_some()
2135 .then(|| qualified_col(base, "labels"))
2136 });
2137 Ok(labels.map_or_else(
2138 || node_value_struct(base, label, &self.type_id_to_entity_name, &prop_names),
2139 |labels| node_value_struct_with_labels(base, labels, &prop_names),
2140 ))
2141 }
2142
2143 fn lower_node_struct_list(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2144 use datafusion::functions_nested::expr_fn::make_array;
2145
2146 let [first, second, edge] = args else {
2147 return Err(LoweringError::UnsupportedExpr(
2148 "_node_struct_list expects two nodes and one relationship".into(),
2149 ));
2150 };
2151 let node_vars = [first, second].map(|id| match self.arena.get(*id) {
2152 IrExpr::VarRef(var) => Ok(*var),
2153 _ => Err(LoweringError::UnsupportedExpr(
2154 "_node_struct_list node arguments must be variables".into(),
2155 )),
2156 });
2157 let [first_var, second_var] = node_vars;
2158 let node_vars = [first_var?, second_var?];
2159 let prop_names = node_vars
2160 .iter()
2161 .filter_map(|var| self.node_shapes.get(&var.0))
2162 .flat_map(|shape| shape.prop_names.iter().cloned())
2163 .collect::<std::collections::BTreeSet<_>>()
2164 .into_iter()
2165 .collect::<Vec<_>>();
2166 let nodes = node_vars
2167 .iter()
2168 .map(|var| {
2169 let base = self
2170 .var_map
2171 .get(*var)
2172 .ok_or(LoweringError::UnboundVar(var.0))?;
2173 Ok(node_value_struct(
2174 base,
2175 None,
2176 &self.type_id_to_entity_name,
2177 &prop_names,
2178 ))
2179 })
2180 .collect::<Result<Vec<_>, LoweringError>>()?;
2181 let edge = self.lower_path_builtin_arg(*edge)?;
2182 let present = edge_present(&edge).ok_or_else(|| {
2183 LoweringError::UnsupportedExpr(
2184 "_node_struct_list relationship argument must be a bound edge".into(),
2185 )
2186 })?;
2187 Ok(null_unless(present, make_array(nodes)))
2188 }
2189
2190 fn lower_rel_struct(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2191 let (base, rel_type) = self.lower_rel_struct_args(args)?;
2192 let props = self.edge_prop_names(base);
2193 let value = relationship_value_struct(base, rel_type, &props);
2194 Ok(null_unless(edge_present_qual(base), value))
2195 }
2196
2197 fn lower_rel_struct_list(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2198 use datafusion::functions_nested::expr_fn::make_array;
2199
2200 let (base, rel_type) = self.lower_rel_struct_args(args)?;
2201 let props = self.edge_prop_names(base);
2202 let value = relationship_value_struct(base, rel_type, &props);
2203 Ok(null_unless(
2204 edge_present_qual(base),
2205 make_array(vec![value]),
2206 ))
2207 }
2208
2209 fn lower_rel_struct_args(&self, args: &[ExprId]) -> Result<(&str, DfExpr), LoweringError> {
2210 let Some(&base_id) = args.first() else {
2211 return Err(LoweringError::UnsupportedExpr(
2212 "_rel_struct expects an edge variable".into(),
2213 ));
2214 };
2215 let IrExpr::VarRef(var_id) = self.arena.get(base_id) else {
2216 return Err(LoweringError::UnsupportedExpr(
2217 "_rel_struct argument must be a relationship variable".into(),
2218 ));
2219 };
2220 let base = self
2221 .var_map
2222 .get(*var_id)
2223 .ok_or(LoweringError::UnboundVar(var_id.0))?;
2224 let rel_type = match args.get(1).map(|&id| (id, self.arena.get(id))) {
2225 Some((_, IrExpr::Literal(IrLiteral::Null))) | None => {
2226 col(format!("{base}.rel_type_name"))
2227 }
2228 Some((id, _)) => self.lower(id)?,
2229 };
2230 Ok((base, rel_type))
2231 }
2232
2233 fn edge_prop_names(&self, base: &str) -> Vec<String> {
2234 let Some(schema) = self.input_schema.as_ref() else {
2235 return Vec::new();
2236 };
2237 schema
2238 .iter()
2239 .filter_map(|(qualifier, field)| {
2240 let q = qualifier?;
2241 if q.to_string() != base
2242 || is_edge_value_topology_field(field.name())
2243 || matches!(field.data_type(), DataType::Null)
2244 {
2245 return None;
2246 }
2247 Some(field.name().clone())
2248 })
2249 .collect()
2250 }
2251
2252 fn lower_labels(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2255 let Some(&base_id) = args.first() else {
2256 return Err(LoweringError::UnsupportedExpr(
2257 "labels() expects one argument".into(),
2258 ));
2259 };
2260 match self.arena.get(base_id) {
2261 IrExpr::Literal(IrLiteral::Null) => Ok(null_utf8_list()),
2262 IrExpr::VarRef(var_id) if self.node_shapes.contains_key(&var_id.0) => {
2263 let base = self
2264 .var_map
2265 .get(*var_id)
2266 .ok_or(LoweringError::UnboundVar(var_id.0))?;
2267 Ok(null_unless(
2268 col(format!("{base}.node_uuid")).is_not_null(),
2269 node_labels_list(base, None, &self.type_id_to_entity_name),
2270 ))
2271 }
2272 IrExpr::VarRef(var_id) => {
2273 let base = self
2274 .var_map
2275 .get(*var_id)
2276 .ok_or(LoweringError::UnboundVar(var_id.0))?;
2277 if self.is_node_var(base) {
2278 Ok(null_unless(
2279 col(format!("{base}.node_uuid")).is_not_null(),
2280 node_labels_list(base, None, &self.type_id_to_entity_name),
2281 ))
2282 } else {
2283 let value = self.lower(base_id)?;
2284 Ok(CYPHER_LABELS.call(vec![value]))
2285 }
2286 }
2287 _ => {
2288 let value = self.lower(base_id)?;
2289 Ok(CYPHER_LABELS.call(vec![value]))
2290 }
2291 }
2292 }
2293
2294 fn lower_keys(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2297 use datafusion::functions_nested::expr_fn::{array_concat, make_array};
2298
2299 let Some(&base_id) = args.first() else {
2300 return Err(LoweringError::UnsupportedExpr(
2301 "keys() expects one argument".into(),
2302 ));
2303 };
2304 if let Some(map_keys) = self.lower_map_keys(base_id)? {
2305 return Ok(map_keys);
2306 }
2307 let var_id = match self.arena.get(base_id) {
2308 IrExpr::VarRef(var_id) => *var_id,
2309 _ => {
2310 return Err(LoweringError::InvalidType(
2311 "keys() requires a map, node, relationship, or null".into(),
2312 ));
2313 }
2314 };
2315 let base = self
2316 .var_map
2317 .get(var_id)
2318 .ok_or(LoweringError::UnboundVar(var_id.0))?;
2319 let (prop_names, present) = if let Some(shape) = self.node_shapes.get(&var_id.0) {
2320 let has_node_uuid = self.input_schema.as_ref().is_some_and(|schema| {
2321 let qual = datafusion::common::TableReference::bare(base);
2322 schema
2323 .index_of_column_by_name(Some(&qual), "node_uuid")
2324 .is_some()
2325 });
2326 let node_present = if has_node_uuid {
2327 col(format!("{base}.node_uuid")).is_not_null()
2328 } else {
2329 lit(true)
2330 };
2331 (shape.prop_names.clone(), node_present)
2332 } else if self.is_edge_var(base) {
2333 (self.edge_prop_names(base), edge_present_qual(base))
2334 } else {
2335 return Err(LoweringError::UnsupportedExpr(
2336 "keys() requires an entity with a known shape".into(),
2337 ));
2338 };
2339 let empty = empty_utf8_list();
2342 let parts: Vec<DfExpr> = prop_names
2343 .iter()
2344 .map(|p| {
2345 when(
2346 qualified_col(base, p).is_not_null(),
2347 make_array(vec![lit(p.as_str())]),
2348 )
2349 .otherwise(empty.clone())
2350 .expect("CASE build")
2351 })
2352 .collect();
2353 let value = parts
2354 .into_iter()
2355 .reduce(|acc, part| array_concat(vec![acc, part]))
2356 .unwrap_or(empty);
2357 Ok(null_unless(present, value))
2358 }
2359
2360 fn lower_map_keys(&self, base_id: ExprId) -> Result<Option<DfExpr>, LoweringError> {
2361 match self.arena.get(base_id) {
2362 IrExpr::Literal(IrLiteral::Null) => return Ok(Some(null_utf8_list())),
2363 IrExpr::MapLiteral(_) => {
2364 return Ok(Some(CYPHER_MAP_KEYS.call(vec![self.lower(base_id)?])));
2365 }
2366 IrExpr::ListLiteral(_) => {
2367 return Err(LoweringError::InvalidType(
2368 "keys() requires a map, node, relationship, or null".into(),
2369 ));
2370 }
2371 _ => {}
2372 }
2373 if let IrExpr::VarRef(var_id) = self.arena.get(base_id)
2374 && let Some(base) = self.var_map.get(*var_id)
2375 {
2376 if self.node_shapes.contains_key(&var_id.0) || self.is_edge_var(base) {
2377 return Ok(None);
2378 }
2379 if let Some(schema) = self.input_schema.as_ref()
2380 && let Ok(field) = schema.field_with_unqualified_name(base)
2381 {
2382 if matches!(field.data_type(), DataType::Null)
2383 || is_plain_map_struct_type(field.data_type())
2384 || is_het_struct_type(Some(field.data_type()))
2385 {
2386 return Ok(Some(CYPHER_MAP_KEYS.call(vec![col_literal(base)])));
2387 }
2388 return Ok(None);
2389 }
2390 }
2391 let value = self.lower(base_id)?;
2392 if let Some(dt) = self.expr_data_type(&value) {
2393 if matches!(dt, DataType::Null)
2394 || is_plain_map_struct_type(&dt)
2395 || is_het_struct_type(Some(&dt))
2396 {
2397 return Ok(Some(CYPHER_MAP_KEYS.call(vec![value])));
2398 }
2399 return Err(LoweringError::InvalidType(
2400 "keys() requires a map, node, relationship, or null".into(),
2401 ));
2402 }
2403 Ok(Some(CYPHER_MAP_KEYS.call(vec![value])))
2404 }
2405
2406 fn lower_properties(&self, args: &[ExprId]) -> Result<DfExpr, LoweringError> {
2407 let Some(&base_id) = args.first() else {
2408 return Err(LoweringError::UnsupportedExpr(
2409 "properties() expects one argument".into(),
2410 ));
2411 };
2412 match self.arena.get(base_id) {
2413 IrExpr::Literal(IrLiteral::Null) => return Ok(lit(ScalarValue::Null)),
2414 IrExpr::MapLiteral(_) => return self.lower(base_id),
2415 IrExpr::ListLiteral(_) => {
2416 return Err(LoweringError::InvalidType(
2417 "properties() requires a map, node, relationship, or null".into(),
2418 ));
2419 }
2420 _ => {}
2421 }
2422 if let IrExpr::VarRef(var_id) = self.arena.get(base_id)
2423 && let Some(base) = self.var_map.get(*var_id)
2424 {
2425 if let Some(value) = self.entity_property_bag_with_empty(*var_id, base) {
2426 return Ok(value);
2427 }
2428 if let Some(schema) = self.input_schema.as_ref()
2429 && let Ok(field) = schema.field_with_unqualified_name(base)
2430 {
2431 return match field.data_type() {
2432 DataType::Null => Ok(lit(ScalarValue::Null)),
2433 dt if is_plain_map_struct_type(dt) => Ok(col_literal(base)),
2434 _ => Err(LoweringError::InvalidType(
2435 "properties() requires a map, node, relationship, or null".into(),
2436 )),
2437 };
2438 }
2439 }
2440 let value = self.lower(base_id)?;
2441 if let Some(dt) = self.expr_data_type(&value) {
2442 return match dt {
2443 DataType::Null => Ok(lit(ScalarValue::Null)),
2444 dt if is_plain_map_struct_type(&dt) => Ok(value),
2445 _ => Err(LoweringError::InvalidType(
2446 "properties() requires a map, node, relationship, or null".into(),
2447 )),
2448 };
2449 }
2450 Ok(value)
2451 }
2452
2453 fn is_edge_var(&self, base: &str) -> bool {
2454 let Some(schema) = self.input_schema.as_ref() else {
2455 return false;
2456 };
2457 let qual = datafusion::common::TableReference::bare(base);
2458 schema
2459 .index_of_column_by_name(Some(&qual), "edge_uuid")
2460 .is_some()
2461 }
2462
2463 fn is_node_var(&self, base: &str) -> bool {
2464 let Some(schema) = self.input_schema.as_ref() else {
2465 return false;
2466 };
2467 let qual = datafusion::common::TableReference::bare(base);
2468 schema
2469 .index_of_column_by_name(Some(&qual), "node_uuid")
2470 .is_some()
2471 }
2472
2473 fn identity_uuid_of(&self, id: ExprId) -> Option<(EntityIdentityKind, DfExpr)> {
2478 if let IrExpr::VarRef(v) = self.arena.get(id)
2479 && self.node_shapes.contains_key(&v.0)
2480 {
2481 let base = self.var_map.get(*v)?;
2482 return Some((EntityIdentityKind::Node, col(format!("{base}.node_uuid"))));
2483 }
2484 if let IrExpr::VarRef(v) = self.arena.get(id) {
2485 let base = self.var_map.get(*v)?;
2486 let qual = datafusion::common::TableReference::bare(base);
2487 if let Some(schema) = self.input_schema.as_ref()
2488 && schema
2489 .index_of_column_by_name(Some(&qual), "edge_uuid")
2490 .is_some()
2491 {
2492 return Some((EntityIdentityKind::Edge, col(format!("{base}.edge_uuid"))));
2493 }
2494 }
2495 None
2496 }
2497
2498 fn is_list_typed(&self, e: &DfExpr) -> bool {
2502 if is_list_literal(e) {
2503 return true;
2504 }
2505 if let Some(schema) = self.input_schema.as_ref()
2506 && let Ok(dt) = e.get_type(schema)
2507 {
2508 return matches!(
2509 dt,
2510 DataType::List(_) | DataType::LargeList(_) | DataType::FixedSizeList(_, _)
2511 );
2512 }
2513 false
2514 }
2515
2516 fn is_string_typed(&self, e: &DfExpr) -> bool {
2520 if matches!(
2521 e,
2522 DfExpr::Literal(ScalarValue::Utf8(_) | ScalarValue::LargeUtf8(_), _)
2523 ) {
2524 return true;
2525 }
2526 if let Some(schema) = self.input_schema.as_ref()
2527 && let Ok(dt) = e.get_type(schema)
2528 {
2529 return matches!(
2530 dt,
2531 DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View
2532 );
2533 }
2534 false
2535 }
2536
2537 fn is_known_non_string(&self, e: &DfExpr) -> bool {
2538 if is_list_literal(e)
2539 || matches!(e, DfExpr::ScalarFunction(f) if f.func.name() == "named_struct")
2540 {
2541 return true;
2542 }
2543 self.expr_data_type(e).is_some_and(|dt| {
2544 !matches!(
2545 dt,
2546 DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View | DataType::Null
2547 )
2548 })
2549 }
2550
2551 fn expr_data_type(&self, e: &DfExpr) -> Option<DataType> {
2554 if let DfExpr::Literal(sv, _) = e {
2555 return Some(sv.data_type());
2556 }
2557 self.input_schema.as_ref().and_then(|s| e.get_type(s).ok())
2558 }
2559
2560 fn is_known_non_bool(&self, e: &DfExpr) -> bool {
2569 if is_list_literal(e)
2573 || matches!(e, DfExpr::ScalarFunction(f) if f.func.name() == "named_struct")
2574 {
2575 return true;
2576 }
2577 !matches!(
2578 self.expr_data_type(e),
2579 None | Some(DataType::Boolean | DataType::Null)
2580 )
2581 }
2582
2583 fn is_known_non_numeric(&self, e: &DfExpr) -> bool {
2588 match self.expr_data_type(e) {
2589 None | Some(DataType::Null) => false,
2590 Some(dt) => !dt.is_numeric(),
2591 }
2592 }
2593
2594 fn is_known_non_list(&self, e: &DfExpr) -> bool {
2595 if matches!(e, DfExpr::ScalarFunction(f) if f.func.name() == "named_struct") {
2596 return true;
2597 }
2598 match self.expr_data_type(e) {
2599 None
2600 | Some(
2601 DataType::Null
2602 | DataType::List(_)
2603 | DataType::LargeList(_)
2604 | DataType::FixedSizeList(_, _),
2605 ) => false,
2606 Some(dt) if is_het_struct_type(Some(&dt)) => false,
2607 Some(_) => true,
2608 }
2609 }
2610
2611 fn is_temporal_typed(&self, e: &DfExpr) -> bool {
2614 match self.expr_data_type(e) {
2615 Some(DataType::Time64(_)) => true,
2616 Some(dt) => {
2617 is_date_struct(&dt)
2618 || is_localdatetime_struct(&dt)
2619 || is_time_struct(&dt)
2620 || is_datetime_struct(&dt)
2621 }
2622 None => false,
2623 }
2624 }
2625
2626 fn is_known_non_value_access_container(&self, e: &DfExpr) -> bool {
2627 match self.expr_data_type(e) {
2628 None | Some(DataType::Null) => false,
2629 Some(dt) => {
2630 !is_plain_map_struct_type(&dt)
2631 && !is_het_struct_type(Some(&dt))
2632 && !matches!(dt, DataType::Struct(_))
2633 }
2634 }
2635 }
2636
2637 fn is_duration_typed(&self, e: &DfExpr) -> bool {
2639 self.expr_data_type(e)
2640 .is_some_and(|dt| is_duration_struct(&dt))
2641 }
2642
2643 fn list_element_type(&self, list: &DfExpr) -> Option<DataType> {
2646 let schema = self
2647 .input_schema
2648 .clone()
2649 .unwrap_or_else(|| std::sync::Arc::new(datafusion::common::DFSchema::empty()));
2650 match list.get_type(&schema).ok()? {
2651 DataType::List(f) | DataType::LargeList(f) | DataType::FixedSizeList(f, _) => {
2652 Some(f.data_type().clone())
2653 }
2654 _ => None,
2655 }
2656 }
2657
2658 #[allow(
2659 clippy::too_many_lines,
2660 reason = "one arm per Cypher binary operator, several with temporal/list/string dispatch"
2661 )]
2662 fn lower_binary(
2663 &self,
2664 op: BinaryOpKind,
2665 left: ExprId,
2666 right: ExprId,
2667 ) -> Result<DfExpr, LoweringError> {
2668 if op == BinaryOpKind::In
2674 && let Some(membership) = self.lower_known_label_membership(left, right)?
2675 {
2676 return Ok(membership);
2677 }
2678
2679 if matches!(op, BinaryOpKind::Eq | BinaryOpKind::Neq)
2683 && let (Some((lk, lc)), Some((rk, rc))) =
2684 (self.identity_uuid_of(left), self.identity_uuid_of(right))
2685 {
2686 if lk != rk {
2687 let value = matches!(op, BinaryOpKind::Neq);
2688 return Ok(when(
2689 lc.clone().is_null().or(rc.clone().is_null()),
2690 lit(ScalarValue::Boolean(None)),
2691 )
2692 .otherwise(lit(value))
2693 .expect("CASE build is infallible for mismatched entity comparison"));
2694 }
2695 return Ok(if matches!(op, BinaryOpKind::Eq) {
2696 lc.eq(rc)
2697 } else {
2698 lc.not_eq(rc)
2699 });
2700 }
2701 let l = self.lower(left)?;
2702 let r = self.lower(right)?;
2703 let expr = match op {
2704 BinaryOpKind::Eq => CYPHER_EQ.call(vec![l, r]),
2710 BinaryOpKind::Neq => datafusion::logical_expr::not(CYPHER_EQ.call(vec![l, r])),
2711 BinaryOpKind::Lt | BinaryOpKind::Lte | BinaryOpKind::Gt | BinaryOpKind::Gte => {
2716 let code = match op {
2717 BinaryOpKind::Lt => 0i8,
2718 BinaryOpKind::Lte => 1i8,
2719 BinaryOpKind::Gt => 2i8,
2720 _ => 3i8,
2721 };
2722 CYPHER_CMP_PRED.call(vec![l, r, lit(code)])
2723 }
2724 BinaryOpKind::And | BinaryOpKind::Or | BinaryOpKind::Xor => {
2730 let keyword = match op {
2731 BinaryOpKind::And => "AND",
2732 BinaryOpKind::Or => "OR",
2733 BinaryOpKind::Xor => "XOR",
2734 _ => unreachable!("matched boolean operator"),
2735 };
2736 if self.is_known_non_bool(&l) || self.is_known_non_bool(&r) {
2737 return Err(LoweringError::InvalidType(format!(
2738 "{keyword} requires boolean operands"
2739 )));
2740 }
2741 match op {
2742 BinaryOpKind::And => CYPHER_AND.call(vec![l, r]),
2743 BinaryOpKind::Or => CYPHER_OR.call(vec![l, r]),
2744 BinaryOpKind::Xor => CYPHER_XOR.call(vec![l, r]),
2745 _ => unreachable!("matched boolean operator"),
2746 }
2747 }
2748 BinaryOpKind::Add => {
2749 if self.is_temporal_typed(&l) && self.is_duration_typed(&r) {
2757 CYPHER_TEMPORAL_ARITH.call(vec![l, r, lit(1i64)])
2759 } else if self.is_duration_typed(&l) && self.is_temporal_typed(&r) {
2760 CYPHER_TEMPORAL_ARITH.call(vec![r, l, lit(1i64)])
2762 } else if self.is_duration_typed(&l) && self.is_duration_typed(&r) {
2763 CYPHER_DURATION_ADD.call(vec![l, r, lit(1i64)])
2765 } else if let (Some(le), Some(re)) =
2766 (self.list_element_type(&l), self.list_element_type(&r))
2767 {
2768 if le == re && !is_het_struct_type(Some(&le)) {
2769 datafusion::functions_nested::expr_fn::array_concat(vec![l, r])
2770 } else if graph_value_types_compatible(&le, &re) {
2771 let target = self.expr_data_type(&l).ok_or_else(|| {
2772 LoweringError::UnsupportedExpr(
2773 "cannot resolve path-list type for concatenation".into(),
2774 )
2775 })?;
2776 datafusion::functions_nested::expr_fn::array_concat(vec![
2777 l,
2778 cast(r, target),
2779 ])
2780 } else {
2781 CYPHER_LIST_PLUS.call(vec![l, r])
2782 }
2783 } else if let Some(le) = self.list_element_type(&l) {
2784 if !is_het_struct_type(Some(&le))
2785 && self
2786 .expr_data_type(&r)
2787 .is_some_and(|rt| rt == le || matches!(rt, DataType::Null))
2788 {
2789 datafusion::functions_nested::expr_fn::array_append(l, r)
2790 } else {
2791 CYPHER_LIST_PLUS.call(vec![l, r])
2792 }
2793 } else if let Some(re) = self.list_element_type(&r) {
2794 if !is_het_struct_type(Some(&re))
2795 && self
2796 .expr_data_type(&l)
2797 .is_some_and(|lt| lt == re || matches!(lt, DataType::Null))
2798 {
2799 datafusion::functions_nested::expr_fn::array_prepend(l, r)
2800 } else {
2801 CYPHER_LIST_PLUS.call(vec![l, r])
2802 }
2803 } else if self.is_list_typed(&l) || self.is_list_typed(&r) {
2804 CYPHER_LIST_PLUS.call(vec![l, r])
2805 } else if (self.is_string_typed(&l) && !self.is_known_non_string(&r))
2806 || (self.is_string_typed(&r) && !self.is_known_non_string(&l))
2807 {
2808 DfExpr::BinaryExpr(datafusion::logical_expr::BinaryExpr {
2809 left: Box::new(l),
2810 op: Operator::StringConcat,
2811 right: Box::new(r),
2812 })
2813 } else {
2814 DfExpr::BinaryExpr(datafusion::logical_expr::BinaryExpr {
2815 left: Box::new(l),
2816 op: Operator::Plus,
2817 right: Box::new(r),
2818 })
2819 }
2820 }
2821 BinaryOpKind::Sub => {
2822 if self.is_temporal_typed(&l) && self.is_duration_typed(&r) {
2823 CYPHER_TEMPORAL_ARITH.call(vec![l, r, lit(-1i64)])
2825 } else if self.is_duration_typed(&l) && self.is_duration_typed(&r) {
2826 CYPHER_DURATION_ADD.call(vec![l, r, lit(-1i64)])
2828 } else {
2829 DfExpr::BinaryExpr(datafusion::logical_expr::BinaryExpr {
2830 left: Box::new(l),
2831 op: Operator::Minus,
2832 right: Box::new(r),
2833 })
2834 }
2835 }
2836 BinaryOpKind::Mul => {
2837 if self.is_duration_typed(&l) {
2839 CYPHER_DURATION_SCALE.call(vec![l, r, lit(false)])
2840 } else if self.is_duration_typed(&r) {
2841 CYPHER_DURATION_SCALE.call(vec![r, l, lit(false)])
2842 } else {
2843 DfExpr::BinaryExpr(datafusion::logical_expr::BinaryExpr {
2844 left: Box::new(l),
2845 op: Operator::Multiply,
2846 right: Box::new(r),
2847 })
2848 }
2849 }
2850 BinaryOpKind::Div => {
2851 if self.is_duration_typed(&l) {
2853 CYPHER_DURATION_SCALE.call(vec![l, r, lit(true)])
2854 } else {
2855 let (l, r) = match (self.expr_data_type(&l), self.expr_data_type(&r)) {
2856 (Some(DataType::Float64), Some(rt)) if is_integer_data_type(&rt) => {
2857 (l, cast(r, DataType::Float64))
2858 }
2859 (Some(lt), Some(DataType::Float64)) if is_integer_data_type(<) => {
2860 (cast(l, DataType::Float64), r)
2861 }
2862 _ => (l, r),
2863 };
2864 DfExpr::BinaryExpr(datafusion::logical_expr::BinaryExpr {
2865 left: Box::new(l),
2866 op: Operator::Divide,
2867 right: Box::new(r),
2868 })
2869 }
2870 }
2871 BinaryOpKind::Mod => {
2872 if self.is_known_non_numeric(&l) || self.is_known_non_numeric(&r) {
2873 return Err(LoweringError::InvalidType(
2874 "% requires numeric operands".into(),
2875 ));
2876 }
2877 DfExpr::BinaryExpr(datafusion::logical_expr::BinaryExpr {
2878 left: Box::new(l),
2879 op: Operator::Modulo,
2880 right: Box::new(r),
2881 })
2882 }
2883 BinaryOpKind::Pow => {
2884 if self.is_known_non_numeric(&l) || self.is_known_non_numeric(&r) {
2885 return Err(LoweringError::InvalidType(
2886 "^ requires numeric operands".into(),
2887 ));
2888 }
2889 datafusion::functions::math::expr_fn::power(l, r)
2890 }
2891 BinaryOpKind::In => {
2896 if self.is_known_non_list(&r) {
2897 return Err(LoweringError::InvalidType(
2898 "IN requires a list or null right-hand operand".into(),
2899 ));
2900 }
2901 CYPHER_IN.call(vec![l, r])
2902 }
2903 BinaryOpKind::StartsWith => CYPHER_STARTS_WITH.call(vec![l, r]),
2904 BinaryOpKind::EndsWith => CYPHER_ENDS_WITH.call(vec![l, r]),
2905 BinaryOpKind::Contains => CYPHER_CONTAINS.call(vec![l, r]),
2906 BinaryOpKind::RegexMatch => {
2907 datafusion::functions::regex::expr_fn::regexp_like(l, r, None)
2909 }
2910 };
2911 Ok(expr)
2912 }
2913
2914 fn lower_known_label_membership(
2920 &self,
2921 left: ExprId,
2922 right: ExprId,
2923 ) -> Result<Option<DfExpr>, LoweringError> {
2924 use datafusion::functions_nested::expr_fn::array_has;
2925
2926 let IrExpr::Literal(IrLiteral::Str(label)) = self.arena.get(left) else {
2927 return Ok(None);
2928 };
2929 let IrExpr::FunctionCall { name, args } = self.arena.get(right) else {
2930 return Ok(None);
2931 };
2932 let [arg] = args.as_slice() else {
2933 return Ok(None);
2934 };
2935 if name != "labels" {
2936 return Ok(None);
2937 }
2938 let IrExpr::VarRef(var_id) = self.arena.get(*arg) else {
2939 return Ok(None);
2940 };
2941 let Some(type_id) = self.entity_name_to_type_id.get(label) else {
2942 return Ok(None);
2943 };
2944 let base = self
2945 .var_map
2946 .get(*var_id)
2947 .ok_or(LoweringError::UnboundVar(var_id.0))?;
2948 Ok(Some(array_has(
2949 col(format!("{base}.type_ids")),
2950 lit(*type_id),
2951 )))
2952 }
2953
2954 fn lower_unary(&self, op: UnaryOpKind, expr: ExprId) -> Result<DfExpr, LoweringError> {
2955 let e = self.lower(expr)?;
2956 let result = match op {
2957 UnaryOpKind::Not => {
2958 if self.is_known_non_bool(&e) {
2959 return Err(LoweringError::InvalidType(
2960 "NOT requires a boolean operand".into(),
2961 ));
2962 }
2963 not(e)
2964 }
2965 UnaryOpKind::Neg => {
2966 if self.is_known_non_numeric(&e) {
2967 return Err(LoweringError::InvalidType(
2968 "unary minus requires a numeric operand".into(),
2969 ));
2970 }
2971 DfExpr::Negative(Box::new(e))
2972 }
2973 UnaryOpKind::IsNull => e.is_null(),
2974 UnaryOpKind::IsNotNull => e.is_not_null(),
2975 };
2976 Ok(result)
2977 }
2978
2979 fn lower_case(
2980 &self,
2981 operand: Option<ExprId>,
2982 arms: &[graphforge_ir::expr::CaseArm],
2983 else_expr: Option<ExprId>,
2984 ) -> Result<DfExpr, LoweringError> {
2985 let when_thens: Result<Vec<_>, _> = arms
2986 .iter()
2987 .map(|arm| {
2988 let when = self.lower(arm.when)?;
2989 let then = self.lower(arm.then)?;
2990 Ok((Box::new(when), Box::new(then)))
2991 })
2992 .collect();
2993 let when_thens = when_thens?;
2994 let else_expr_df = else_expr.map(|id| self.lower(id)).transpose()?;
2995
2996 Ok(DfExpr::Case(datafusion::logical_expr::expr::Case {
2997 expr: operand.map(|id| self.lower(id)).transpose()?.map(Box::new),
2998 when_then_expr: when_thens,
2999 else_expr: else_expr_df.map(Box::new),
3000 }))
3001 }
3002
3003 fn resolve_prop_col(&self, base_expr: DfExpr, prop: PropId) -> DfExpr {
3009 let prop_name = self
3010 .prop_names
3011 .get(&prop.0)
3012 .cloned()
3013 .unwrap_or_else(|| format!("prop_{}", prop.0));
3014
3015 if let DfExpr::Column(col_ref) = &base_expr
3022 && !self
3023 .elem_struct_cols
3024 .iter()
3025 .any(|c| c == col_ref.name.as_str())
3026 {
3027 return qualified_col(&col_ref.name, &prop_name);
3028 }
3029
3030 datafusion::functions::core::expr_fn::get_field(base_expr, prop_name)
3033 }
3034
3035 fn path_node_hydration(&self) -> Option<PathNodeHydration> {
3042 use datafusion::arrow::datatypes::Field;
3043 let dir = self.read_target.as_ref()?;
3044 let stems = graphforge_storage::list_property_stems(dir);
3045 let mut fields = vec![
3046 Field::new("node_uuid", DataType::FixedSizeBinary(16), false),
3047 Field::new("labels", DataType::new_list(DataType::Utf8, true), true),
3048 ];
3049 let mut seen: std::collections::HashSet<String> =
3050 fields.iter().map(|f| f.name().clone()).collect();
3051 for stem in &stems {
3052 let table = graphforge_storage::PropertyTable::open_discovered(dir, stem);
3053 for f in table.schema_ref().fields() {
3054 if f.name() == "node_uuid" || !seen.insert(f.name().clone()) {
3055 continue;
3056 }
3057 fields.push(f.as_ref().clone().with_nullable(true));
3058 }
3059 }
3060 let mut labels_by_type: Vec<(u32, String)> = self
3061 .type_id_to_entity_name
3062 .iter()
3063 .map(|(id, name)| (*id, name.clone()))
3064 .collect();
3065 labels_by_type.sort();
3066 Some(PathNodeHydration {
3067 dir: dir.clone(),
3068 labels_by_type,
3069 prop_stems: stems,
3070 fields: fields.into(),
3071 })
3072 }
3073}
3074
3075fn col_literal(name: &str) -> DfExpr {
3087 if name.contains('.') {
3088 col(name)
3089 } else {
3090 DfExpr::Column(datafusion::common::Column::new_unqualified(name))
3091 }
3092}
3093
3094pub(crate) fn qualified_col(relation: &str, name: &str) -> DfExpr {
3095 DfExpr::Column(datafusion::common::Column::new(
3096 Some(datafusion::common::TableReference::bare(relation)),
3097 name,
3098 ))
3099}
3100
3101fn build_prop_names(_ontology: Option<&OntologyHandle>) -> HashMap<u32, String> {
3111 HashMap::new()
3112}
3113
3114fn is_empty_list_literal(e: &DfExpr) -> bool {
3137 use datafusion::arrow::array::Array;
3138 matches!(e, DfExpr::Literal(ScalarValue::List(arr), _) if arr.value(0).is_empty())
3139}
3140
3141fn try_const_scalar(e: &DfExpr) -> Option<ScalarValue> {
3147 match e {
3148 DfExpr::Literal(s, _) => Some(s.clone()),
3149 DfExpr::ScalarFunction(f) if f.func.name() == "named_struct" => {
3150 let mut entries: Vec<(String, ScalarValue)> = Vec::with_capacity(f.args.len() / 2);
3151 let pairs = f.args.chunks_exact(2);
3152 if !pairs.remainder().is_empty() {
3153 return None;
3154 }
3155 for pair in pairs {
3156 let DfExpr::Literal(ScalarValue::Utf8(Some(k)), _) = &pair[0] else {
3157 return None;
3158 };
3159 entries.push((k.clone(), try_const_scalar(&pair[1])?));
3160 }
3161 const_map_scalar(&entries)
3162 }
3163 _ => None,
3164 }
3165}
3166
3167enum ConstStringKey {
3168 Null,
3169 Value(String),
3170}
3171
3172fn const_string_key(e: &DfExpr) -> Option<ConstStringKey> {
3173 match e {
3174 DfExpr::Literal(ScalarValue::Utf8(v) | ScalarValue::LargeUtf8(v), _) => Some(
3175 v.clone()
3176 .map_or(ConstStringKey::Null, ConstStringKey::Value),
3177 ),
3178 DfExpr::BinaryExpr(b) if b.op == Operator::StringConcat => {
3179 let l = const_string_key(&b.left)?;
3180 let r = const_string_key(&b.right)?;
3181 Some(match (l, r) {
3182 (ConstStringKey::Value(l), ConstStringKey::Value(r)) => {
3183 ConstStringKey::Value(format!("{l}{r}"))
3184 }
3185 _ => ConstStringKey::Null,
3186 })
3187 }
3188 _ => None,
3189 }
3190}
3191
3192fn const_map_scalar(entries: &[(String, ScalarValue)]) -> Option<ScalarValue> {
3196 use datafusion::arrow::array::{ArrayRef, StructArray};
3197 use datafusion::arrow::datatypes::{Field, Fields};
3198 use std::sync::Arc;
3199 if entries.is_empty() {
3200 return Some(ScalarValue::Struct(Arc::new(
3201 StructArray::new_empty_fields(1, None),
3202 )));
3203 }
3204 let mut fields: Vec<Field> = Vec::with_capacity(entries.len());
3205 let mut arrays: Vec<ArrayRef> = Vec::with_capacity(entries.len());
3206 for (k, sv) in entries {
3207 let arr = sv.to_array().ok()?; fields.push(Field::new(k, arr.data_type().clone(), true));
3209 arrays.push(arr);
3210 }
3211 let s = StructArray::try_new(Fields::from(fields), arrays, None).ok()?;
3212 Some(ScalarValue::Struct(Arc::new(s)))
3213}
3214
3215fn is_plain_map_struct(arr: &datafusion::arrow::array::StructArray) -> bool {
3220 use datafusion::arrow::array::Array;
3221 is_plain_map_struct_type(arr.data_type())
3222}
3223
3224fn is_plain_map_struct_type(dt: &DataType) -> bool {
3228 let DataType::Struct(fields) = dt else {
3229 return false;
3230 };
3231 let is_entity = fields.iter().any(|f| {
3233 matches!(
3234 f.name().as_str(),
3235 "node_uuid" | "src_uuid" | "dst_uuid" | "nodes" | "relationships" | "labels"
3236 )
3237 });
3238 !is_entity
3239 && !is_het_struct_type(Some(dt))
3240 && !is_date_struct(dt)
3241 && !is_localdatetime_struct(dt)
3242 && !is_duration_struct(dt)
3243 && !is_time_struct(dt)
3244 && !is_datetime_struct(dt)
3245}
3246
3247fn het_map_entry_fields(depth: usize) -> datafusion::arrow::datatypes::Fields {
3251 use datafusion::arrow::datatypes::{DataType, Field};
3252 datafusion::arrow::datatypes::Fields::from(vec![
3253 Field::new("__het_mkey", DataType::Utf8, false),
3254 Field::new("__het_mval", DataType::Struct(het_fields(depth)), true),
3255 ])
3256}
3257
3258fn het_fields(depth: usize) -> datafusion::arrow::datatypes::Fields {
3268 use datafusion::arrow::datatypes::{DataType, Field};
3269 use std::sync::Arc;
3270 let mut v = vec![
3271 Field::new("__het_key", DataType::Float64, true),
3272 Field::new("__het_tag", DataType::Int8, false),
3273 Field::new("__het_int", DataType::Int64, true),
3274 Field::new("__het_float", DataType::Float64, true),
3275 Field::new("__het_str", DataType::Utf8, true),
3276 Field::new("__het_bool", DataType::Boolean, true),
3277 ];
3278 if depth >= 1 {
3279 let inner = Field::new("item", DataType::Struct(het_fields(depth - 1)), true);
3280 v.push(Field::new(
3281 "__het_list",
3282 DataType::List(Arc::new(inner)),
3283 true,
3284 ));
3285 let entry = Field::new(
3286 "item",
3287 DataType::Struct(het_map_entry_fields(depth - 1)),
3288 true,
3289 );
3290 v.push(Field::new(
3291 "__het_map",
3292 DataType::List(Arc::new(entry)),
3293 true,
3294 ));
3295 }
3296 datafusion::arrow::datatypes::Fields::from(v)
3297}
3298
3299fn het_depth(s: &ScalarValue) -> Option<usize> {
3303 use datafusion::arrow::array::Array;
3304 let s = unwrap_het(s.clone());
3305 match &s {
3306 ScalarValue::Int64(_)
3307 | ScalarValue::Float64(_)
3308 | ScalarValue::Utf8(_)
3309 | ScalarValue::LargeUtf8(_)
3310 | ScalarValue::Utf8View(_)
3311 | ScalarValue::Boolean(_)
3312 | ScalarValue::Null => Some(0),
3313 ScalarValue::List(arr) => {
3314 let inner = arr.value(0);
3315 let mut d = 0;
3316 for i in 0..inner.len() {
3317 let e = unwrap_het(ScalarValue::try_from_array(&inner, i).ok()?);
3321 d = d.max(het_depth(&e)?);
3322 }
3323 Some(1 + d)
3324 }
3325 ScalarValue::Struct(arr) if is_plain_map_struct(arr) => {
3328 let mut d = 0;
3329 for i in 0..arr.num_columns() {
3330 let v = unwrap_het(ScalarValue::try_from_array(arr.column(i), 0).ok()?);
3331 d = d.max(het_depth(&v)?);
3332 }
3333 Some(1 + d)
3334 }
3335 _ => None,
3336 }
3337}
3338
3339fn unwrap_het(s: ScalarValue) -> ScalarValue {
3342 if let ScalarValue::Dictionary(_, value) = s {
3343 return unwrap_het(*value);
3344 }
3345 decode_het(&s).unwrap_or(s)
3346}
3347
3348#[allow(
3351 clippy::too_many_lines,
3352 reason = "one cohesive per-field array builder; splitting it would obscure the field/offset bookkeeping"
3353)]
3354fn build_het_struct(
3355 scalars: &[ScalarValue],
3356 depth: usize,
3357) -> Option<datafusion::arrow::array::StructArray> {
3358 use datafusion::arrow::array::{
3359 ArrayRef, BooleanArray, Float64Array, Int8Array, Int64Array, ListArray, StringArray,
3360 StructArray,
3361 };
3362 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer};
3363 use datafusion::arrow::datatypes::{DataType, Field};
3364 use std::sync::Arc;
3365
3366 let n = scalars.len();
3367 let mut keys: Vec<Option<f64>> = Vec::with_capacity(n);
3368 let mut tags: Vec<i8> = Vec::with_capacity(n);
3369 let mut ints: Vec<Option<i64>> = Vec::with_capacity(n);
3370 let mut floats: Vec<Option<f64>> = Vec::with_capacity(n);
3371 let mut strs: Vec<Option<String>> = Vec::with_capacity(n);
3372 let mut bools: Vec<Option<bool>> = Vec::with_capacity(n);
3373 let mut valid: Vec<bool> = Vec::with_capacity(n);
3374 let mut child_elems: Vec<ScalarValue> = Vec::new();
3376 let mut child_offsets: Vec<i32> = vec![0];
3377 let mut child_valid: Vec<bool> = Vec::new();
3378 let mut map_keys: Vec<String> = Vec::new();
3381 let mut map_vals: Vec<ScalarValue> = Vec::new();
3382 let mut map_offsets: Vec<i32> = vec![0];
3383 let mut map_valid: Vec<bool> = Vec::new();
3384 for scalar in scalars {
3385 let scalar = unwrap_het(scalar.clone());
3386 let (mut key, mut tag, mut int_v, mut float_v, mut str_v, mut bool_v, mut ok) =
3388 (None, 0i8, None, None, None, None, true);
3389 let mut child: Option<Vec<ScalarValue>> = None;
3390 let mut map_child: Option<Vec<(String, ScalarValue)>> = None;
3391 match &scalar {
3392 ScalarValue::Int64(Some(x)) => {
3393 #[allow(
3394 clippy::cast_precision_loss,
3395 reason = "key feeds only min/max ORDERING; the exact integer is preserved in __het_int"
3396 )]
3397 let k = Some(*x as f64);
3398 key = k;
3399 tag = 0;
3400 int_v = Some(*x);
3401 }
3402 ScalarValue::Float64(Some(x)) => {
3403 key = Some(*x);
3404 tag = 1;
3405 float_v = Some(*x);
3406 }
3407 ScalarValue::Utf8(Some(x))
3408 | ScalarValue::LargeUtf8(Some(x))
3409 | ScalarValue::Utf8View(Some(x)) => {
3410 tag = 2;
3411 str_v = Some(x.clone());
3412 }
3413 ScalarValue::Boolean(Some(x)) => {
3414 tag = 3;
3415 bool_v = Some(*x);
3416 }
3417 ScalarValue::List(arr) => {
3418 if depth == 0 {
3419 return None; }
3421 tag = 4;
3422 let inner = arr.value(0);
3423 let mut elems = Vec::with_capacity(inner.len());
3424 for idx in 0..inner.len() {
3425 elems.push(unwrap_het(ScalarValue::try_from_array(&inner, idx).ok()?));
3426 }
3427 child = Some(elems);
3428 }
3429 ScalarValue::Struct(arr) if is_plain_map_struct(arr) => {
3432 if depth == 0 {
3433 return None; }
3435 tag = 5;
3436 let mut kv = Vec::with_capacity(arr.num_columns());
3437 for (i, f) in arr.fields().iter().enumerate() {
3438 let v = unwrap_het(ScalarValue::try_from_array(arr.column(i), 0).ok()?);
3439 kv.push((f.name().clone(), v));
3440 }
3441 map_child = Some(kv);
3442 }
3443 ScalarValue::Int64(None)
3445 | ScalarValue::Float64(None)
3446 | ScalarValue::Utf8(None)
3447 | ScalarValue::LargeUtf8(None)
3448 | ScalarValue::Utf8View(None)
3449 | ScalarValue::Boolean(None)
3450 | ScalarValue::Null => ok = false,
3451 _ => return None, }
3453 keys.push(key);
3454 tags.push(tag);
3455 ints.push(int_v);
3456 floats.push(float_v);
3457 strs.push(str_v);
3458 bools.push(bool_v);
3459 valid.push(ok);
3460 if depth >= 1 {
3461 if let Some(elems) = child {
3462 child_elems.extend(elems);
3463 child_valid.push(true);
3464 } else {
3465 child_valid.push(false);
3466 }
3467 child_offsets.push(i32::try_from(child_elems.len()).ok()?);
3468 if let Some(kv) = map_child {
3469 for (k, v) in kv {
3470 map_keys.push(k);
3471 map_vals.push(v);
3472 }
3473 map_valid.push(true);
3474 } else {
3475 map_valid.push(false);
3476 }
3477 map_offsets.push(i32::try_from(map_keys.len()).ok()?);
3478 }
3479 }
3480
3481 let mut arrays: Vec<ArrayRef> = vec![
3482 Arc::new(Float64Array::from(keys)),
3483 Arc::new(Int8Array::from(tags)),
3484 Arc::new(Int64Array::from(ints)),
3485 Arc::new(Float64Array::from(floats)),
3486 Arc::new(StringArray::from(strs)),
3487 Arc::new(BooleanArray::from(bools)),
3488 ];
3489 if depth >= 1 {
3490 let child_struct = build_het_struct(&child_elems, depth - 1)?;
3491 let inner_field = Arc::new(Field::new(
3492 "item",
3493 DataType::Struct(het_fields(depth - 1)),
3494 true,
3495 ));
3496 let het_list = ListArray::new(
3497 inner_field,
3498 OffsetBuffer::new(child_offsets.into()),
3499 Arc::new(child_struct),
3500 Some(NullBuffer::from(child_valid)),
3501 );
3502 arrays.push(Arc::new(het_list));
3503
3504 let entry_fields = het_map_entry_fields(depth - 1);
3507 let mkey_arr = Arc::new(StringArray::from(map_keys)) as ArrayRef;
3508 let mval_struct = build_het_struct(&map_vals, depth - 1)?;
3509 let entry_struct = StructArray::new(
3510 entry_fields.clone(),
3511 vec![mkey_arr, Arc::new(mval_struct)],
3512 None,
3513 );
3514 let entry_field = Arc::new(Field::new("item", DataType::Struct(entry_fields), true));
3515 let het_map = ListArray::new(
3516 entry_field,
3517 OffsetBuffer::new(map_offsets.into()),
3518 Arc::new(entry_struct),
3519 Some(NullBuffer::from(map_valid)),
3520 );
3521 arrays.push(Arc::new(het_map));
3522 }
3523 Some(StructArray::new(
3524 het_fields(depth),
3525 arrays,
3526 Some(NullBuffer::from(valid)),
3527 ))
3528}
3529
3530fn tagged_numeric_list(scalars: &[ScalarValue]) -> Option<DfExpr> {
3538 use datafusion::arrow::array::ListArray;
3539 use datafusion::arrow::buffer::OffsetBuffer;
3540 use datafusion::arrow::datatypes::{DataType, Field};
3541 use std::sync::Arc;
3542
3543 let mut depth = 0usize;
3546 for s in scalars {
3547 depth = depth.max(het_depth(s)?);
3548 }
3549 let n = scalars.len();
3550 let elem = build_het_struct(scalars, depth)?;
3551 let list_field = Arc::new(Field::new(
3552 "item",
3553 DataType::Struct(het_fields(depth)),
3554 true,
3555 ));
3556 let list = ListArray::new(
3557 list_field,
3558 OffsetBuffer::from_lengths([n]),
3559 Arc::new(elem),
3560 None,
3561 );
3562 Some(DfExpr::Literal(ScalarValue::List(Arc::new(list)), None))
3563}
3564
3565static CYPHER_DYNAMIC_HET_LIST: LazyLock<ScalarUDF> =
3566 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDynamicHetList::new()));
3567
3568#[derive(Debug, PartialEq, Eq, Hash)]
3569struct CypherDynamicHetList {
3570 signature: Signature,
3571}
3572
3573impl CypherDynamicHetList {
3574 fn new() -> Self {
3575 Self {
3576 signature: Signature::variadic_any(Volatility::Immutable),
3577 }
3578 }
3579}
3580
3581fn dynamic_het_type(arg_types: &[DataType]) -> DataType {
3582 use datafusion::arrow::datatypes::Fields;
3583 let mut fields = Vec::with_capacity(arg_types.len() + 1);
3584 fields.push(Field::new("__het_tag", DataType::Int8, false));
3585 fields.extend(
3586 arg_types
3587 .iter()
3588 .enumerate()
3589 .map(|(i, ty)| Field::new(format!("__het_value_{i}"), ty.clone(), true)),
3590 );
3591 DataType::new_list(DataType::Struct(Fields::from(fields)), true)
3592}
3593
3594impl ScalarUDFImpl for CypherDynamicHetList {
3595 fn as_any(&self) -> &dyn Any {
3596 self
3597 }
3598
3599 fn name(&self) -> &'static str {
3600 "cypher_dynamic_het_list"
3601 }
3602
3603 fn signature(&self) -> &Signature {
3604 &self.signature
3605 }
3606
3607 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
3608 Ok(dynamic_het_type(arg_types))
3609 }
3610
3611 fn return_field_from_args(&self, args: ReturnFieldArgs) -> datafusion::error::Result<FieldRef> {
3612 let arg_types = args
3613 .arg_fields
3614 .iter()
3615 .map(|field| field.data_type().clone())
3616 .collect::<Vec<_>>();
3617 Ok(Arc::new(Field::new(
3618 self.name(),
3619 dynamic_het_type(&arg_types),
3620 false,
3621 )))
3622 }
3623
3624 fn invoke_with_args(
3625 &self,
3626 args: ScalarFunctionArgs,
3627 ) -> datafusion::error::Result<ColumnarValue> {
3628 use datafusion::arrow::array::{ArrayRef, Int8Array, Int32Array, StructArray};
3629 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer};
3630 use datafusion::arrow::compute::take;
3631 use datafusion::error::DataFusionError;
3632
3633 let rows = args.number_rows;
3634 let width = args.args.len();
3635 let width_i8 = i8::try_from(width).map_err(|_| {
3636 DataFusionError::Plan("heterogeneous list literal exceeds 127 elements".into())
3637 })?;
3638 let values = args
3639 .args
3640 .iter()
3641 .map(|value| value.to_array(rows))
3642 .collect::<datafusion::error::Result<Vec<_>>>()?;
3643 let tags = Int8Array::from_iter_values((0..rows).flat_map(|_| 0..width_i8));
3644 let mut columns: Vec<ArrayRef> = vec![Arc::new(tags)];
3645 for (value_idx, value) in values.iter().enumerate() {
3646 let indices = (0..rows)
3647 .flat_map(|row| {
3648 (0..width).map(move |element_idx| {
3649 (element_idx == value_idx)
3650 .then(|| i32::try_from(row).ok())
3651 .flatten()
3652 })
3653 })
3654 .collect::<Int32Array>();
3655 columns.push(take(value.as_ref(), &indices, None)?);
3656 }
3657 let valid = (0..rows)
3658 .flat_map(|row| values.iter().map(move |value| !value.is_null(row)))
3659 .collect::<NullBuffer>();
3660 let DataType::List(item) = args.return_field.data_type() else {
3661 return Err(DataFusionError::Internal(
3662 "dynamic heterogeneous list has a non-list return type".into(),
3663 ));
3664 };
3665 let DataType::Struct(fields) = item.data_type() else {
3666 return Err(DataFusionError::Internal(
3667 "dynamic heterogeneous list has a non-struct element type".into(),
3668 ));
3669 };
3670 let elements = StructArray::new(fields.clone(), columns, Some(valid));
3671 let list = ListArray::new(
3672 item.clone(),
3673 OffsetBuffer::from_lengths(std::iter::repeat_n(width, rows)),
3674 Arc::new(elements),
3675 None,
3676 );
3677 Ok(ColumnarValue::Array(Arc::new(list)))
3678 }
3679}
3680
3681fn lower_list_literal(
3682 elems: Vec<DfExpr>,
3683 input_schema: Option<&datafusion::common::DFSchema>,
3684) -> DfExpr {
3685 if let Some(scalars) = elems
3688 .iter()
3689 .map(try_const_scalar)
3690 .collect::<Option<Vec<ScalarValue>>>()
3691 {
3692 let elem_type = scalars
3694 .iter()
3695 .map(ScalarValue::data_type)
3696 .find(|t| *t != DataType::Null)
3697 .unwrap_or(DataType::Int64);
3698 let typed: Vec<ScalarValue> = scalars
3702 .iter()
3703 .map(|s| {
3704 if matches!(s, ScalarValue::Null) {
3705 ScalarValue::try_from(&elem_type).unwrap_or(ScalarValue::Null)
3706 } else {
3707 s.clone()
3708 }
3709 })
3710 .collect();
3711 if typed.iter().all(|s| s.data_type() == elem_type) {
3716 let list = ScalarValue::new_list(&typed, &elem_type, true);
3717 return DfExpr::Literal(ScalarValue::List(list), None);
3718 }
3719 let all_maps = scalars
3727 .iter()
3728 .any(|s| matches!(s, ScalarValue::Struct(a) if is_plain_map_struct(a)))
3729 && scalars.iter().all(|s| {
3730 s.is_null() || matches!(s, ScalarValue::Struct(a) if is_plain_map_struct(a))
3731 });
3732 if all_maps {
3733 if let Some(padded) = all_map_union_list(&scalars) {
3734 return padded;
3735 }
3736 } else if let Some(tagged) = tagged_numeric_list(&scalars) {
3737 return tagged;
3738 }
3739 }
3740 if let Some(schema) = input_schema
3741 && let Some(types) = elems
3742 .iter()
3743 .map(|elem| elem.get_type(schema).ok())
3744 .collect::<Option<Vec<_>>>()
3745 && types.windows(2).any(|pair| pair[0] != pair[1])
3746 {
3747 return CYPHER_DYNAMIC_HET_LIST.call(elems);
3748 }
3749 datafusion::functions_nested::expr_fn::make_array(elems)
3750}
3751
3752static CYPHER_LIST_PLUS: LazyLock<ScalarUDF> =
3753 LazyLock::new(|| ScalarUDF::new_from_impl(CypherListPlus::new()));
3754
3755static CYPHER_RELATIONSHIP_DISJOINT: LazyLock<ScalarUDF> =
3756 LazyLock::new(|| ScalarUDF::new_from_impl(CypherRelationshipDisjoint::new()));
3757
3758pub(crate) fn relationship_disjoint(left: DfExpr, right: DfExpr) -> DfExpr {
3759 CYPHER_RELATIONSHIP_DISJOINT.call(vec![left, right])
3760}
3761
3762#[derive(Debug, PartialEq, Eq, Hash)]
3763struct CypherRelationshipDisjoint {
3764 signature: Signature,
3765}
3766
3767impl CypherRelationshipDisjoint {
3768 fn new() -> Self {
3769 Self {
3770 signature: Signature::any(2, Volatility::Immutable),
3771 }
3772 }
3773}
3774
3775impl ScalarUDFImpl for CypherRelationshipDisjoint {
3776 fn as_any(&self) -> &dyn Any {
3777 self
3778 }
3779
3780 fn name(&self) -> &'static str {
3781 "cypher_relationship_disjoint"
3782 }
3783
3784 fn signature(&self) -> &Signature {
3785 &self.signature
3786 }
3787
3788 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
3789 Ok(DataType::Boolean)
3790 }
3791
3792 fn invoke_with_args(
3793 &self,
3794 args: ScalarFunctionArgs,
3795 ) -> datafusion::error::Result<ColumnarValue> {
3796 use datafusion::arrow::array::BooleanArray;
3797
3798 let rows = args.number_rows;
3799 let left = args.args[0].to_array(rows)?;
3800 let right = args.args[1].to_array(rows)?;
3801 let values = (0..rows)
3802 .map(|row| {
3803 let left = ScalarValue::try_from_array(&left, row)?;
3804 let right = ScalarValue::try_from_array(&right, row)?;
3805 let mut left_ids = Vec::new();
3806 let mut right_ids = Vec::new();
3807 relationship_ids(&left, &mut left_ids);
3808 relationship_ids(&right, &mut right_ids);
3809 Ok(!left_ids.iter().any(|id| right_ids.contains(id)))
3810 })
3811 .collect::<datafusion::error::Result<BooleanArray>>()?;
3812 Ok(ColumnarValue::Array(std::sync::Arc::new(values)))
3813 }
3814}
3815
3816fn relationship_ids(value: &ScalarValue, ids: &mut Vec<Vec<u8>>) {
3817 match value {
3818 ScalarValue::FixedSizeBinary(_, Some(uuid)) => ids.push(uuid.clone()),
3819 ScalarValue::List(list) if !list.is_null(0) => {
3820 let values = list.value(0);
3821 for index in 0..values.len() {
3822 if let Ok(value) = ScalarValue::try_from_array(&values, index) {
3823 relationship_ids(&value, ids);
3824 }
3825 }
3826 }
3827 ScalarValue::Struct(value) if !value.is_null(0) => {
3828 if let Some(uuid) = value.column_by_name("edge_uuid")
3829 && let Ok(uuid) = ScalarValue::try_from_array(uuid, 0)
3830 {
3831 relationship_ids(&uuid, ids);
3832 }
3833 }
3834 _ => {}
3835 }
3836}
3837
3838fn graph_value_types_compatible(left: &DataType, right: &DataType) -> bool {
3842 let (DataType::Struct(left), DataType::Struct(right)) = (left, right) else {
3843 return false;
3844 };
3845 left.len() == right.len()
3846 && left.iter().zip(right.iter()).all(|(left, right)| {
3847 left.name() == right.name()
3848 && match (left.data_type(), right.data_type()) {
3849 (DataType::Struct(_), DataType::Struct(_)) => {
3850 graph_value_types_compatible(left.data_type(), right.data_type())
3851 }
3852 (DataType::List(left), DataType::List(right)) => {
3853 left.data_type() == right.data_type()
3854 }
3855 (left, right) => left == right,
3856 }
3857 })
3858}
3859
3860#[derive(Debug, PartialEq, Eq, Hash)]
3861struct CypherListPlus {
3862 signature: Signature,
3863}
3864
3865impl CypherListPlus {
3866 fn new() -> Self {
3867 Self {
3868 signature: Signature::any(2, Volatility::Immutable),
3869 }
3870 }
3871}
3872
3873impl ScalarUDFImpl for CypherListPlus {
3874 fn as_any(&self) -> &dyn Any {
3875 self
3876 }
3877
3878 fn name(&self) -> &'static str {
3879 "cypher_list_plus"
3880 }
3881
3882 fn signature(&self) -> &Signature {
3883 &self.signature
3884 }
3885
3886 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
3887 if list_plus_has_graph_value(arg_types) {
3888 Ok(list_plus_return_type(arg_types))
3889 } else {
3890 Ok(DataType::new_list(
3891 DataType::Struct(het_fields(list_plus_depth(arg_types))),
3892 true,
3893 ))
3894 }
3895 }
3896
3897 #[allow(
3898 clippy::too_many_lines,
3899 reason = "list/list and list/element shaping share one offset and validity pass"
3900 )]
3901 fn invoke_with_args(
3902 &self,
3903 args: ScalarFunctionArgs,
3904 ) -> datafusion::error::Result<ColumnarValue> {
3905 use datafusion::arrow::array::{Array, ArrayRef, ListArray};
3906 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
3907 use datafusion::arrow::datatypes::DataType;
3908 use datafusion::error::DataFusionError;
3909 use std::sync::Arc;
3910
3911 let rows = args.number_rows;
3912 let left = args.args[0].to_array(rows)?;
3913 let right = args.args[1].to_array(rows)?;
3914 let left_is_list = list_item_type(left.data_type()).is_some();
3915 let right_is_list = list_item_type(right.data_type()).is_some();
3916 if !left_is_list && !right_is_list {
3917 return Err(DataFusionError::Execution(
3918 "list + requires at least one list operand".into(),
3919 ));
3920 }
3921
3922 if left_is_list
3923 && !right_is_list
3924 && let Some(result) =
3925 invoke_tagged_list_element_plus(&left, &right, args.return_field.data_type())?
3926 {
3927 return Ok(ColumnarValue::Array(result));
3928 }
3929
3930 let mut flat: Vec<ScalarValue> = Vec::new();
3931 let mut offsets: Vec<i32> = Vec::with_capacity(rows + 1);
3932 let mut validity: Vec<bool> = Vec::with_capacity(rows);
3933 offsets.push(0);
3934
3935 for row in 0..rows {
3936 let row_values = match (left_is_list, right_is_list) {
3937 (true, true) => match (
3938 list_elements_at(&left, row)?,
3939 list_elements_at(&right, row)?,
3940 ) {
3941 (Some(mut l), Some(r)) => {
3942 l.extend(r);
3943 Some(l)
3944 }
3945 _ => None,
3946 },
3947 (true, false) => match list_elements_at(&left, row)? {
3948 Some(mut l) => {
3949 let r = decoded_scalar_at(&right, row)?;
3950 if let Some(r) = scalar_list_elements(&r)? {
3951 l.extend(r);
3952 } else {
3953 l.push(r);
3954 }
3955 Some(l)
3956 }
3957 None => None,
3958 },
3959 (false, true) => match list_elements_at(&right, row)? {
3960 Some(mut r) => {
3961 let l = decoded_scalar_at(&left, row)?;
3962 let mut l = scalar_list_elements(&l)?.unwrap_or_else(|| vec![l]);
3963 l.append(&mut r);
3964 Some(l)
3965 }
3966 None => None,
3967 },
3968 (false, false) => unreachable!("checked above"),
3969 };
3970
3971 match row_values {
3972 Some(values) => {
3973 flat.extend(values);
3974 validity.push(true);
3975 }
3976 None => validity.push(false),
3977 }
3978 offsets.push(i32::try_from(flat.len()).map_err(|_| {
3979 DataFusionError::Execution("cypher_list_plus: list too long".into())
3980 })?);
3981 }
3982
3983 let DataType::List(field) = args.return_field.data_type() else {
3984 return Err(DataFusionError::Internal(
3985 "cypher_list_plus return type is not a list".into(),
3986 ));
3987 };
3988 if is_het_struct_type(Some(field.data_type()))
3989 && !is_dynamic_variant_struct(field.data_type())
3990 {
3991 let depth = list_plus_depth(&[left.data_type().clone(), right.data_type().clone()]);
3992 let values = build_het_struct(&flat, depth).ok_or_else(|| {
3993 DataFusionError::Execution(
3994 "cypher_list_plus: cannot encode value in heterogeneous list".into(),
3995 )
3996 })?;
3997 let out = ListArray::new(
3998 field.clone(),
3999 OffsetBuffer::new(ScalarBuffer::from(offsets)),
4000 Arc::new(values) as ArrayRef,
4001 Some(NullBuffer::from(validity)),
4002 );
4003 return Ok(ColumnarValue::Array(Arc::new(out)));
4004 }
4005 if !is_dynamic_variant_struct(field.data_type()) {
4006 let flat = flat
4007 .into_iter()
4008 .map(|value| {
4009 if value.data_type() == *field.data_type() {
4010 Ok(value)
4011 } else {
4012 value.cast_to(field.data_type())
4013 }
4014 })
4015 .collect::<datafusion::error::Result<Vec<_>>>()?;
4016 let values = if flat.is_empty() {
4017 new_empty_array(field.data_type())
4018 } else {
4019 ScalarValue::iter_to_array(flat)?
4020 };
4021 let out = ListArray::new(
4022 field.clone(),
4023 OffsetBuffer::new(ScalarBuffer::from(offsets)),
4024 values,
4025 Some(NullBuffer::from(validity)),
4026 );
4027 return Ok(ColumnarValue::Array(Arc::new(out)));
4028 }
4029 let DataType::Struct(fields) = field.data_type() else {
4030 return Err(DataFusionError::Internal(
4031 "cypher_list_plus element type is not tagged".into(),
4032 ));
4033 };
4034 let variants = fields
4035 .iter()
4036 .filter(|field| field.name().starts_with("__het_value_"))
4037 .map(|field| field.data_type().clone())
4038 .collect::<Vec<_>>();
4039 let mut tags = Vec::with_capacity(flat.len());
4040 let mut valid = Vec::with_capacity(flat.len());
4041 let mut columns = Vec::with_capacity(variants.len() + 1);
4042 for value in &flat {
4043 let tag = variants
4044 .iter()
4045 .position(|variant| {
4046 value.data_type() == *variant
4047 || graph_value_types_compatible(&value.data_type(), variant)
4048 })
4049 .unwrap_or(0);
4050 tags.push(i8::try_from(tag).map_err(|_| {
4051 DataFusionError::Execution("cypher_list_plus has too many value variants".into())
4052 })?);
4053 valid.push(!value.is_null());
4054 }
4055 columns.push(Arc::new(datafusion::arrow::array::Int8Array::from(tags.clone())) as ArrayRef);
4056 for (variant_index, variant) in variants.iter().enumerate() {
4057 let null = ScalarValue::try_new_null(variant)?;
4058 let values = flat.iter().zip(&tags).map(|(value, tag)| {
4059 if usize::try_from(*tag).ok() == Some(variant_index) {
4060 value.clone()
4061 } else {
4062 null.clone()
4063 }
4064 });
4065 columns.push(ScalarValue::iter_to_array(values)?);
4066 }
4067 let values = datafusion::arrow::array::StructArray::new(
4068 fields.clone(),
4069 columns,
4070 Some(NullBuffer::from(valid)),
4071 );
4072 let out = ListArray::new(
4073 field.clone(),
4074 OffsetBuffer::new(ScalarBuffer::from(offsets)),
4075 Arc::new(values) as ArrayRef,
4076 Some(NullBuffer::from(validity)),
4077 );
4078 Ok(ColumnarValue::Array(Arc::new(out)))
4079 }
4080}
4081
4082#[allow(
4087 clippy::too_many_lines,
4088 reason = "one range-assembly pass keeps offsets, validity, and three Arrow sources synchronized"
4089)]
4090fn invoke_tagged_list_element_plus(
4091 left: &datafusion::arrow::array::ArrayRef,
4092 right: &datafusion::arrow::array::ArrayRef,
4093 return_type: &DataType,
4094) -> datafusion::error::Result<Option<datafusion::arrow::array::ArrayRef>> {
4095 use arrow_data::transform::MutableArrayData;
4096 use datafusion::arrow::array::{Array, Int8Array, ListArray, StructArray, make_array};
4097 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
4098 use datafusion::arrow::datatypes::DataType;
4099 use datafusion::arrow::error::ArrowError;
4100 use datafusion::error::DataFusionError;
4101 use std::sync::Arc;
4102
4103 let Some(left) = left.as_any().downcast_ref::<ListArray>() else {
4104 return Ok(None);
4105 };
4106 let Some(right) = right.as_any().downcast_ref::<StructArray>() else {
4107 return Ok(None);
4108 };
4109 let DataType::List(return_field) = return_type else {
4110 return Ok(None);
4111 };
4112 if left.value_type() != right.data_type().clone()
4113 || return_field.data_type() != right.data_type()
4114 || !is_het_struct_type(Some(right.data_type()))
4115 {
4116 return Ok(None);
4117 }
4118
4119 let Some(tags) = right
4120 .column_by_name("__het_tag")
4121 .and_then(|column| column.as_any().downcast_ref::<Int8Array>())
4122 else {
4123 return Ok(None);
4124 };
4125 let Some(nested) = right
4126 .column_by_name("__het_list")
4127 .and_then(|column| column.as_any().downcast_ref::<ListArray>())
4128 else {
4129 return Ok(None);
4130 };
4131
4132 let nested_values = nested.values();
4133 let nested_offsets = nested.value_offsets();
4134 let mut promoted_ranges = vec![None; right.len()];
4135 for row in 0..right.len() {
4136 if right.is_null(row) || tags.value(row) != 4 || nested.is_null(row) {
4137 continue;
4138 }
4139 let start = usize::try_from(nested_offsets[row]).map_err(|_| {
4140 DataFusionError::ArrowError(
4141 Box::new(ArrowError::ComputeError(
4142 "negative heterogeneous-list offset".into(),
4143 )),
4144 None,
4145 )
4146 })?;
4147 let end = usize::try_from(nested_offsets[row + 1]).map_err(|_| {
4148 DataFusionError::ArrowError(
4149 Box::new(ArrowError::ComputeError(
4150 "negative heterogeneous-list offset".into(),
4151 )),
4152 None,
4153 )
4154 })?;
4155 promoted_ranges[row] = Some((start, end));
4156 }
4157 let promoted = if nested_values.is_empty() {
4158 Arc::new(right.slice(0, 0)) as datafusion::arrow::array::ArrayRef
4159 } else {
4160 promote_het_array(nested_values, right.data_type())?
4161 };
4162
4163 let left_data = left.values().to_data();
4164 let right_data = right.to_data();
4165 let promoted_data = promoted.to_data();
4166 let capacity = left.values().len() + right.len() + promoted.len();
4167 let mut values = MutableArrayData::new(
4168 vec![&left_data, &right_data, &promoted_data],
4169 true,
4170 capacity,
4171 );
4172 let left_offsets = left.value_offsets();
4173 let mut offsets = Vec::with_capacity(left.len() + 1);
4174 let mut validity = Vec::with_capacity(left.len());
4175 let mut output_len = 0usize;
4176 offsets.push(0i32);
4177 for row in 0..left.len() {
4178 if left.is_null(row) {
4179 validity.push(false);
4180 offsets.push(i32::try_from(output_len).map_err(|_| {
4181 DataFusionError::Execution("cypher_list_plus: list too long".into())
4182 })?);
4183 continue;
4184 }
4185 validity.push(true);
4186 let start = usize::try_from(left_offsets[row]).map_err(|_| {
4187 DataFusionError::Execution("cypher_list_plus: negative list offset".into())
4188 })?;
4189 let end = usize::try_from(left_offsets[row + 1]).map_err(|_| {
4190 DataFusionError::Execution("cypher_list_plus: negative list offset".into())
4191 })?;
4192 values.extend(0, start, end);
4193 output_len += end - start;
4194 if let Some((start, end)) = promoted_ranges[row] {
4195 values.extend(2, start, end);
4196 output_len += end - start;
4197 } else {
4198 values.extend(1, row, row + 1);
4199 output_len += 1;
4200 }
4201 offsets.push(
4202 i32::try_from(output_len).map_err(|_| {
4203 DataFusionError::Execution("cypher_list_plus: list too long".into())
4204 })?,
4205 );
4206 }
4207
4208 let values = make_array(values.freeze());
4209 Ok(Some(Arc::new(ListArray::new(
4210 return_field.clone(),
4211 OffsetBuffer::new(ScalarBuffer::from(offsets)),
4212 values,
4213 Some(NullBuffer::from(validity)),
4214 ))))
4215}
4216
4217fn promote_het_array(
4221 source: &datafusion::arrow::array::ArrayRef,
4222 target: &DataType,
4223) -> datafusion::error::Result<datafusion::arrow::array::ArrayRef> {
4224 use datafusion::arrow::array::{Array, ListArray, StructArray, new_null_array};
4225 use datafusion::arrow::compute::cast;
4226 use datafusion::arrow::datatypes::DataType;
4227 use datafusion::error::DataFusionError;
4228 use std::sync::Arc;
4229
4230 if source.data_type() == target {
4231 return Ok(source.clone());
4232 }
4233 match (source.data_type(), target) {
4234 (DataType::Struct(_), DataType::Struct(target_fields)) => {
4235 let source = source
4236 .as_any()
4237 .downcast_ref::<StructArray>()
4238 .ok_or_else(|| {
4239 DataFusionError::Internal(
4240 "heterogeneous value has a non-struct physical array".into(),
4241 )
4242 })?;
4243 let columns = target_fields
4244 .iter()
4245 .map(|field| {
4246 source.column_by_name(field.name()).map_or_else(
4247 || Ok(new_null_array(field.data_type(), source.len())),
4248 |column| promote_het_array(column, field.data_type()),
4249 )
4250 })
4251 .collect::<datafusion::error::Result<Vec<_>>>()?;
4252 Ok(Arc::new(StructArray::new(
4253 target_fields.clone(),
4254 columns,
4255 source.nulls().cloned(),
4256 )))
4257 }
4258 (DataType::List(_), DataType::List(target_field)) => {
4259 let source = source.as_any().downcast_ref::<ListArray>().ok_or_else(|| {
4260 DataFusionError::Internal("heterogeneous list has a non-list physical array".into())
4261 })?;
4262 let values = promote_het_array(source.values(), target_field.data_type())?;
4263 Ok(Arc::new(ListArray::new(
4264 target_field.clone(),
4265 source.offsets().clone(),
4266 values,
4267 source.nulls().cloned(),
4268 )))
4269 }
4270 _ => Ok(cast(source, target)?),
4271 }
4272}
4273
4274fn list_plus_return_type(arg_types: &[DataType]) -> DataType {
4275 use datafusion::arrow::datatypes::Fields;
4276
4277 let value_types = arg_types
4278 .iter()
4279 .flat_map(|arg_type| {
4280 let value_type = list_item_type(arg_type).unwrap_or(arg_type);
4281 dynamic_variant_types(value_type)
4282 })
4283 .filter(|value_type| !matches!(value_type, DataType::Null))
4284 .collect::<Vec<_>>();
4285 if let Some(first) = value_types.first()
4286 && is_graph_value_struct(first)
4287 && value_types
4288 .iter()
4289 .all(|value_type| graph_value_types_compatible(first, value_type))
4290 {
4291 return DataType::new_list((*first).clone(), true);
4292 }
4293
4294 let mut variants = Vec::new();
4295 for arg_type in arg_types {
4296 let value_type = list_item_type(arg_type).unwrap_or(arg_type);
4297 if let DataType::Struct(fields) = value_type
4298 && fields
4299 .iter()
4300 .any(|field| field.name().starts_with("__het_value_"))
4301 {
4302 for field in fields
4303 .iter()
4304 .filter(|field| field.name().starts_with("__het_value_"))
4305 {
4306 if !variants.contains(field.data_type()) {
4307 variants.push(field.data_type().clone());
4308 }
4309 }
4310 } else if let DataType::Struct(fields) = value_type
4311 && fields.iter().any(|field| field.name() == "__het_tag")
4312 {
4313 for (name, data_type) in [
4314 ("__het_int", DataType::Int64),
4315 ("__het_float", DataType::Float64),
4316 ("__het_str", DataType::Utf8),
4317 ("__het_bool", DataType::Boolean),
4318 ] {
4319 if fields.iter().any(|field| field.name() == name) && !variants.contains(&data_type)
4320 {
4321 variants.push(data_type);
4322 }
4323 }
4324 } else if !matches!(value_type, DataType::Null) && !variants.contains(value_type) {
4325 variants.push(value_type.clone());
4326 }
4327 }
4328 if variants.is_empty() {
4329 variants.push(DataType::Null);
4330 }
4331 let mut fields = vec![Field::new("__het_tag", DataType::Int8, false)];
4332 fields.extend(
4333 variants
4334 .into_iter()
4335 .enumerate()
4336 .map(|(index, data_type)| Field::new(format!("__het_value_{index}"), data_type, true)),
4337 );
4338 DataType::new_list(DataType::Struct(Fields::from(fields)), true)
4339}
4340
4341fn list_plus_has_graph_value(arg_types: &[DataType]) -> bool {
4342 arg_types.iter().any(|arg_type| {
4343 let value_type = list_item_type(arg_type).unwrap_or(arg_type);
4344 dynamic_variant_types(value_type)
4345 .into_iter()
4346 .any(is_graph_value_struct)
4347 })
4348}
4349
4350fn is_graph_value_struct(data_type: &DataType) -> bool {
4351 matches!(data_type, DataType::Struct(fields) if fields.iter().any(|field| {
4352 matches!(
4353 field.name().as_str(),
4354 "node_uuid" | "edge_uuid" | "nodes" | "relationships"
4355 )
4356 }))
4357}
4358
4359fn is_dynamic_variant_struct(data_type: &DataType) -> bool {
4360 matches!(data_type, DataType::Struct(fields) if fields
4361 .iter()
4362 .any(|field| field.name().starts_with("__het_value_")))
4363}
4364
4365fn dynamic_variant_types(data_type: &DataType) -> Vec<&DataType> {
4366 if let DataType::Struct(fields) = data_type {
4367 let variants = fields
4368 .iter()
4369 .filter(|field| field.name().starts_with("__het_value_"))
4370 .map(|field| field.data_type())
4371 .collect::<Vec<_>>();
4372 if !variants.is_empty() {
4373 return variants;
4374 }
4375 }
4376 vec![data_type]
4377}
4378
4379fn list_item_type(dt: &DataType) -> Option<&DataType> {
4380 match dt {
4381 DataType::List(f) | DataType::LargeList(f) | DataType::FixedSizeList(f, _) => {
4382 Some(f.data_type())
4383 }
4384 _ => None,
4385 }
4386}
4387
4388fn list_plus_depth(arg_types: &[DataType]) -> usize {
4389 arg_types
4390 .iter()
4391 .filter_map(|data_type| {
4392 list_item_type(data_type)
4393 .or(Some(data_type))
4394 .and_then(het_depth_for_data_type)
4395 })
4396 .max()
4397 .unwrap_or(0)
4398}
4399
4400fn het_depth_for_data_type(data_type: &DataType) -> Option<usize> {
4401 if is_het_struct_type(Some(data_type)) {
4402 return het_struct_type_depth(data_type);
4403 }
4404 match data_type {
4405 DataType::Null
4406 | DataType::Boolean
4407 | DataType::Int8
4408 | DataType::Int16
4409 | DataType::Int32
4410 | DataType::Int64
4411 | DataType::UInt8
4412 | DataType::UInt16
4413 | DataType::UInt32
4414 | DataType::UInt64
4415 | DataType::Float16
4416 | DataType::Float32
4417 | DataType::Float64
4418 | DataType::Utf8
4419 | DataType::LargeUtf8 => Some(0),
4420 DataType::List(field) | DataType::LargeList(field) | DataType::FixedSizeList(field, _) => {
4421 Some(1 + het_depth_for_data_type(field.data_type())?)
4422 }
4423 DataType::Struct(fields) if is_plain_map_struct_type(data_type) => fields
4424 .iter()
4425 .filter_map(|field| het_depth_for_data_type(field.data_type()))
4426 .max()
4427 .map_or(Some(1), |depth| Some(1 + depth)),
4428 _ => None,
4429 }
4430}
4431
4432fn het_struct_type_depth(data_type: &DataType) -> Option<usize> {
4433 let DataType::Struct(fields) = data_type else {
4434 return None;
4435 };
4436 let Some(list_field) = fields.iter().find(|field| field.name() == "__het_list") else {
4437 return Some(0);
4438 };
4439 match list_field.data_type() {
4440 DataType::List(inner) => het_struct_type_depth(inner.data_type()).map(|depth| depth + 1),
4441 _ => Some(0),
4442 }
4443}
4444
4445fn list_elements_at(
4446 array: &datafusion::arrow::array::ArrayRef,
4447 row: usize,
4448) -> datafusion::error::Result<Option<Vec<ScalarValue>>> {
4449 use datafusion::arrow::array::{Array, FixedSizeListArray, LargeListArray, ListArray};
4450
4451 let values = if let Some(list) = array.as_any().downcast_ref::<ListArray>() {
4452 if list.is_null(row) {
4453 return Ok(None);
4454 }
4455 list.value(row)
4456 } else if let Some(list) = array.as_any().downcast_ref::<LargeListArray>() {
4457 if list.is_null(row) {
4458 return Ok(None);
4459 }
4460 list.value(row)
4461 } else if let Some(list) = array.as_any().downcast_ref::<FixedSizeListArray>() {
4462 if list.is_null(row) {
4463 return Ok(None);
4464 }
4465 list.value(row)
4466 } else {
4467 return Ok(None);
4468 };
4469
4470 (0..values.len())
4471 .map(|i| ScalarValue::try_from_array(&values, i).map(unwrap_het))
4472 .collect::<datafusion::error::Result<Vec<_>>>()
4473 .map(Some)
4474}
4475
4476fn scalar_list_elements(
4477 value: &ScalarValue,
4478) -> datafusion::error::Result<Option<Vec<ScalarValue>>> {
4479 use datafusion::arrow::array::Array;
4480
4481 match value {
4482 ScalarValue::List(list) => {
4483 if list.is_null(0) {
4484 return Ok(None);
4485 }
4486 let values = list.value(0);
4487 (0..values.len())
4488 .map(|i| ScalarValue::try_from_array(&values, i).map(unwrap_het))
4489 .collect::<datafusion::error::Result<Vec<_>>>()
4490 .map(Some)
4491 }
4492 ScalarValue::LargeList(list) => {
4493 if list.is_null(0) {
4494 return Ok(None);
4495 }
4496 let values = list.value(0);
4497 (0..values.len())
4498 .map(|i| ScalarValue::try_from_array(&values, i).map(unwrap_het))
4499 .collect::<datafusion::error::Result<Vec<_>>>()
4500 .map(Some)
4501 }
4502 _ => Ok(None),
4503 }
4504}
4505
4506fn all_map_union_list(scalars: &[ScalarValue]) -> Option<DfExpr> {
4513 use datafusion::arrow::array::{Array, ArrayRef, StructArray, new_null_array};
4514 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer};
4515 use datafusion::arrow::compute::{cast, concat};
4516 use datafusion::arrow::datatypes::{Field, Fields};
4517 use std::collections::HashMap;
4518 use std::sync::Arc;
4519
4520 let mut order: Vec<String> = Vec::new();
4523 let mut types: HashMap<String, DataType> = HashMap::new();
4524 for s in scalars {
4525 let arr = match s {
4526 ScalarValue::Struct(a) => a,
4527 ScalarValue::Null => continue,
4528 _ => return None,
4529 };
4530 for f in arr.fields() {
4531 let t = f.data_type().clone();
4532 match types.get(f.name()) {
4533 None => {
4534 order.push(f.name().clone());
4535 types.insert(f.name().clone(), t);
4536 }
4537 Some(prev) if *prev == DataType::Null => {
4538 types.insert(f.name().clone(), t);
4539 }
4540 Some(prev) if t != DataType::Null && t != *prev => return None,
4541 _ => {}
4542 }
4543 }
4544 }
4545 let union_fields: Fields = order
4546 .iter()
4547 .map(|n| Field::new(n, types.get(n).cloned().unwrap_or(DataType::Null), true))
4548 .collect::<Vec<_>>()
4549 .into();
4550
4551 let mut columns: Vec<ArrayRef> = Vec::with_capacity(order.len());
4554 for name in &order {
4555 let ut = types.get(name).cloned().unwrap_or(DataType::Null);
4556 let mut pieces: Vec<ArrayRef> = Vec::with_capacity(scalars.len());
4557 for s in scalars {
4558 let piece = match s {
4559 ScalarValue::Struct(a) => a
4560 .column_by_name(name)
4561 .and_then(|c| cast(c, &ut).ok())
4562 .unwrap_or_else(|| new_null_array(&ut, 1)),
4563 _ => new_null_array(&ut, 1),
4564 };
4565 pieces.push(piece);
4566 }
4567 let refs: Vec<&dyn Array> = pieces.iter().map(AsRef::as_ref).collect();
4568 columns.push(concat(&refs).ok()?);
4569 }
4570 let valid: NullBuffer = scalars.iter().map(|s| !s.is_null()).collect();
4572 let elem = StructArray::try_new(union_fields, columns, Some(valid)).ok()?;
4573 let n = scalars.len();
4574 let list_field = Arc::new(Field::new("item", elem.data_type().clone(), true));
4575 let list = datafusion::arrow::array::ListArray::new(
4576 list_field,
4577 OffsetBuffer::from_lengths([n]),
4578 Arc::new(elem),
4579 None,
4580 );
4581 Some(DfExpr::Literal(ScalarValue::List(Arc::new(list)), None))
4582}
4583
4584fn lower_literal(lit_val: &IrLiteral) -> DfExpr {
4586 lit(ir_literal_to_scalar(lit_val))
4587}
4588
4589#[must_use]
4595pub fn ir_literal_to_scalar(lit_val: &IrLiteral) -> ScalarValue {
4596 match lit_val {
4597 IrLiteral::Null => ScalarValue::Null,
4598 IrLiteral::Bool(b) => ScalarValue::Boolean(Some(*b)),
4599 IrLiteral::Int(n) => ScalarValue::Int64(Some(*n)),
4600 IrLiteral::Float(f) => ScalarValue::Float64(Some(*f)),
4601 IrLiteral::Str(s) => ScalarValue::Utf8(Some(s.clone())),
4602 IrLiteral::Uuid(uuid) => ScalarValue::FixedSizeBinary(16, Some(uuid.to_vec())),
4603 IrLiteral::Duration {
4604 months,
4605 days,
4606 seconds,
4607 nanos,
4608 } => duration_scalar(Some(crate::temporal::DurationValue {
4609 months: *months,
4610 days: *days,
4611 seconds: *seconds,
4612 nanos: *nanos,
4613 })),
4614 IrLiteral::DateTime(us) => ScalarValue::TimestampMicrosecond(Some(*us), Some("UTC".into())),
4615 IrLiteral::Date(days) => date_scalar(Some(*days)),
4616 IrLiteral::LocalDateTime { days, nanos } => localdatetime_scalar(Some((*days, *nanos))),
4617 IrLiteral::Time(nanos) => ScalarValue::Time64Nanosecond(Some(*nanos)),
4618 IrLiteral::ZonedTime { nanos, offset } => time_scalar(Some((*nanos, *offset))),
4619 IrLiteral::ZonedDateTime {
4620 days,
4621 nanos,
4622 offset,
4623 zone,
4624 } => datetime_scalar(Some((*days, *nanos, *offset, zone.clone()))),
4625 IrLiteral::List(items) => {
4629 let scalars: Vec<ScalarValue> = items.iter().map(ir_literal_to_scalar).collect();
4630 let elem_type = scalars
4631 .iter()
4632 .find(|s| !s.is_null())
4633 .map_or(DataType::Null, ScalarValue::data_type);
4634 let typed: Vec<ScalarValue> = scalars
4635 .iter()
4636 .map(|s| {
4637 if matches!(s, ScalarValue::Null) {
4638 ScalarValue::try_from(&elem_type).unwrap_or(ScalarValue::Null)
4639 } else {
4640 s.clone()
4641 }
4642 })
4643 .collect();
4644 ScalarValue::List(ScalarValue::new_list(&typed, &elem_type, true))
4645 }
4646 IrLiteral::Map(entries) => {
4647 let scalars: Vec<(String, ScalarValue)> = entries
4648 .iter()
4649 .map(|(key, value)| (key.clone(), ir_literal_to_scalar(value)))
4650 .collect();
4651 const_map_scalar(&scalars).expect("IR map literal should lower to an Arrow struct")
4652 }
4653 }
4654}
4655
4656pub fn scalar_to_ir_literal(value: &ScalarValue) -> Result<IrLiteral, LoweringError> {
4673 if value.is_null() {
4675 return Ok(IrLiteral::Null);
4676 }
4677 let lit = match value {
4678 ScalarValue::Boolean(Some(b)) => IrLiteral::Bool(*b),
4679 ScalarValue::Int8(Some(n)) => IrLiteral::Int(i64::from(*n)),
4680 ScalarValue::Int16(Some(n)) => IrLiteral::Int(i64::from(*n)),
4681 ScalarValue::Int32(Some(n)) => IrLiteral::Int(i64::from(*n)),
4682 ScalarValue::Int64(Some(n)) => IrLiteral::Int(*n),
4683 ScalarValue::UInt8(Some(n)) => IrLiteral::Int(i64::from(*n)),
4684 ScalarValue::UInt16(Some(n)) => IrLiteral::Int(i64::from(*n)),
4685 ScalarValue::UInt32(Some(n)) => IrLiteral::Int(i64::from(*n)),
4686 ScalarValue::UInt64(Some(n)) => i64::try_from(*n).map(IrLiteral::Int).map_err(|_| {
4687 LoweringError::UnsupportedExpr(format!("SET value {n} exceeds the i64 range"))
4688 })?,
4689 ScalarValue::Float32(Some(f)) => IrLiteral::Float(f64::from(*f)),
4690 ScalarValue::Float64(Some(f)) => IrLiteral::Float(*f),
4691 ScalarValue::Utf8(Some(s))
4692 | ScalarValue::LargeUtf8(Some(s))
4693 | ScalarValue::Utf8View(Some(s)) => IrLiteral::Str(s.clone()),
4694 ScalarValue::FixedSizeBinary(16, Some(_)) => {
4695 return Err(LoweringError::InvalidType(
4696 "UUID values cannot be stored as graph properties".into(),
4697 ));
4698 }
4699 ScalarValue::DurationSecond(Some(s)) => duration_value_to_ir(dur_secs_nanos(*s, 0)),
4704 ScalarValue::DurationMillisecond(Some(ms)) => duration_value_to_ir(dur_secs_nanos(
4705 ms.div_euclid(1_000),
4706 ms.rem_euclid(1_000) * 1_000_000,
4707 )),
4708 ScalarValue::DurationMicrosecond(Some(us)) => duration_value_to_ir(dur_secs_nanos(
4709 us.div_euclid(1_000_000),
4710 us.rem_euclid(1_000_000) * 1_000,
4711 )),
4712 ScalarValue::DurationNanosecond(Some(ns)) => {
4713 duration_value_to_ir(crate::temporal::DurationValue::from_total_nanos(0, 0, *ns))
4714 }
4715 ScalarValue::TimestampMicrosecond(Some(us), _) => IrLiteral::DateTime(*us),
4716 ScalarValue::TimestampSecond(Some(s), _) => IrLiteral::DateTime(s * 1_000_000),
4717 ScalarValue::TimestampMillisecond(Some(ms), _) => IrLiteral::DateTime(ms * 1_000),
4718 ScalarValue::TimestampNanosecond(Some(ns), _) => IrLiteral::DateTime(ns / 1_000),
4719 ScalarValue::Struct(arr) if is_date_struct(&DataType::Struct(arr.fields().clone())) => {
4723 match date_struct_value(arr, 0) {
4724 Some(days) => IrLiteral::Date(days),
4725 None => IrLiteral::Null,
4726 }
4727 }
4728 ScalarValue::Struct(arr) if is_duration_struct(&DataType::Struct(arr.fields().clone())) => {
4730 match duration_struct_parts(arr, 0) {
4731 Some(d) => duration_value_to_ir(d),
4732 None => IrLiteral::Null,
4733 }
4734 }
4735 ScalarValue::Struct(arr)
4737 if is_localdatetime_struct(&DataType::Struct(arr.fields().clone())) =>
4738 {
4739 match localdatetime_struct_parts(arr, 0) {
4740 Some((days, nanos)) => IrLiteral::LocalDateTime { days, nanos },
4741 None => IrLiteral::Null,
4742 }
4743 }
4744 ScalarValue::Time64Nanosecond(Some(n)) => IrLiteral::Time(*n),
4746 ScalarValue::Struct(arr) if is_time_struct(&DataType::Struct(arr.fields().clone())) => {
4748 match time_struct_parts(arr, 0) {
4749 Some((nanos, offset)) => IrLiteral::ZonedTime { nanos, offset },
4750 None => IrLiteral::Null,
4751 }
4752 }
4753 ScalarValue::Struct(arr) if is_datetime_struct(&DataType::Struct(arr.fields().clone())) => {
4755 match datetime_struct_parts(arr, 0) {
4756 Some((days, nanos, offset, zone)) => IrLiteral::ZonedDateTime {
4757 days,
4758 nanos,
4759 offset,
4760 zone,
4761 },
4762 None => IrLiteral::Null,
4763 }
4764 }
4765 ScalarValue::List(arr) => list_scalar_to_ir_literal(&arr.value(0))?,
4770 ScalarValue::LargeList(arr) => list_scalar_to_ir_literal(&arr.value(0))?,
4771 other => {
4772 return Err(LoweringError::InvalidType(format!(
4775 "invalid property type for SET value: {:?}",
4776 other.data_type()
4777 )));
4778 }
4779 };
4780 Ok(lit)
4781}
4782
4783fn list_scalar_to_ir_literal(
4786 elems: &datafusion::arrow::array::ArrayRef,
4787) -> Result<IrLiteral, LoweringError> {
4788 let mut items = Vec::with_capacity(elems.len());
4789 for j in 0..elems.len() {
4790 let ev = ScalarValue::try_from_array(elems, j).map_err(|e| {
4791 LoweringError::UnsupportedExpr(format!("list element is not a scalar value: {e}"))
4792 })?;
4793 items.push(scalar_to_ir_literal(&ev)?);
4794 }
4795 Ok(IrLiteral::List(items))
4796}
4797
4798fn render_temporal(name: &str, s: &str) -> Option<String> {
4803 use crate::temporal;
4804 match name {
4805 "date" => temporal::render_date(s),
4806 "localtime" => temporal::render_local_time(s),
4807 "time" => temporal::render_time(s),
4808 "localdatetime" => temporal::render_local_date_time(s),
4809 "datetime" => temporal::render_date_time(s),
4810 "duration" => temporal::render_duration(s),
4811 _ => None,
4812 }
4813}
4814
4815#[allow(
4819 clippy::too_many_lines,
4820 reason = "a flat one-arm-per-builtin dispatch table; clearest kept inline"
4821)]
4822fn resolve_builtin(
4823 name: &str,
4824 args: Vec<DfExpr>,
4825 path_hydration: impl FnOnce() -> Option<PathNodeHydration>,
4826) -> Option<DfExpr> {
4827 use datafusion::functions::math::expr_fn as mfn;
4828 use datafusion::functions::string::expr_fn as sfn;
4829
4830 let name = name.to_ascii_lowercase();
4831 let mut a = args;
4832 match name.as_str() {
4833 "toupper" | "upper" => Some(sfn::upper(a.remove(0))),
4835 "tolower" | "lower" => Some(sfn::lower(a.remove(0))),
4836 "trim" => Some(sfn::btrim(a)),
4837 "ltrim" => Some(sfn::ltrim(a)),
4838 "rtrim" => Some(sfn::rtrim(a)),
4839 "string.concat" | "concat" => Some(sfn::concat(a)),
4840 "replace" => Some(sfn::replace(a.remove(0), a.remove(0), a.remove(0))),
4841 "substring" if a.len() == 2 || a.len() == 3 => Some(cypher_substring(a)),
4842 "char_length" | "character_length" => Some(
4844 datafusion::functions::unicode::expr_fn::char_length(a.remove(0)),
4845 ),
4846 "tostring" => Some(CYPHER_TO_STRING.call(vec![a.remove(0)])),
4852 "tointeger" => Some(CYPHER_TO_INTEGER.call(vec![a.remove(0)])),
4853 "tofloat" => Some(CYPHER_TO_FLOAT.call(vec![a.remove(0)])),
4854 "toboolean" => Some(CYPHER_TO_BOOLEAN.call(vec![a.remove(0)])),
4855
4856 "date" if a.len() == 1 => {
4862 use datafusion::functions::datetime::expr_fn::to_date;
4863 Some(to_date(vec![a.remove(0)]))
4864 }
4865
4866 "abs" => Some(mfn::abs(a.remove(0))),
4868 "ceil" => Some(mfn::ceil(a.remove(0))),
4869 "floor" => Some(mfn::floor(a.remove(0))),
4870 "round" => Some(mfn::round(a)),
4871 "sqrt" => Some(mfn::sqrt(a.remove(0))),
4872 "log" => Some(mfn::log(a.remove(0), a.remove(0))),
4873 "exp" => Some(mfn::exp(a.remove(0))),
4874 "power" => Some(mfn::power(a.remove(0), a.remove(0))),
4875 "rand" if a.is_empty() => Some(mfn::random()),
4879 "sign" if a.len() == 1 => Some(cast(mfn::signum(a.remove(0)), DataType::Int64)),
4882 "coalesce" if !a.is_empty() => Some(datafusion::functions::core::expr_fn::coalesce(a)),
4884 "tail" if a.len() == 1 => Some(datafusion::functions_nested::expr_fn::array_pop_front(
4886 a.remove(0),
4887 )),
4888 "split" if a.len() == 2 => Some(datafusion::functions_nested::expr_fn::string_to_array(
4890 a.remove(0),
4891 a.remove(0),
4892 DfExpr::Literal(ScalarValue::Utf8(None), None),
4893 )),
4894
4895 "length" => Some(datafusion::functions_nested::expr_fn::array_length(
4899 a.remove(0),
4900 )),
4901
4902 "size" => Some(CYPHER_SIZE.call(vec![a.remove(0)])),
4907
4908 "_subscript" => {
4914 let list = a.remove(0);
4915 let idx = a.remove(0);
4916 Some(datafusion::functions_nested::expr_fn::array_element(
4917 list,
4918 one_based_index(idx),
4919 ))
4920 }
4921
4922 "head" => Some(datafusion::functions_nested::expr_fn::array_element(
4924 a.remove(0),
4925 lit(1_i64),
4926 )),
4927 "last" => Some(datafusion::functions_nested::expr_fn::array_element(
4928 a.remove(0),
4929 lit(-1_i64),
4930 )),
4931
4932 "_slice" => {
4943 let list = a.remove(0);
4944 let start = a.remove(0);
4945 let end = a.remove(0);
4946 Some(cypher_slice(list, Some(start), Some(end)))
4947 }
4948 "_slice_from_start" => {
4949 let list = a.remove(0);
4950 let end = a.remove(0);
4951 Some(cypher_slice(list, None, Some(end)))
4952 }
4953 "_slice_to_end" => {
4954 let list = a.remove(0);
4955 let start = a.remove(0);
4956 Some(cypher_slice(list, Some(start), None))
4957 }
4958
4959 "range" if a.len() == 2 || a.len() == 3 => {
4965 let from = a.remove(0);
4966 let end = a.remove(0);
4967 let by = if a.is_empty() {
4968 lit(1_i64)
4969 } else {
4970 a.remove(0)
4971 };
4972 Some(CYPHER_RANGE.call(vec![from, end, by]))
4973 }
4974
4975 "type" => Some(CYPHER_REL_TYPE.call(vec![a.remove(0)])),
4978
4979 other => resolve_path_builtin(other, a, path_hydration),
4981 }
4982}
4983
4984pub(crate) fn list_index_range(list: DfExpr) -> DfExpr {
4988 let len = cast(
4989 datafusion::functions_nested::expr_fn::array_length(list),
4990 DataType::Int64,
4991 );
4992 CYPHER_RANGE.call(vec![lit(0_i64), len - lit(1_i64), lit(1_i64)])
4993}
4994
4995fn cypher_substring(mut args: Vec<DfExpr>) -> DfExpr {
4996 let original = args.remove(0);
4997 let start = args.remove(0) + lit(1_i64);
4998 let substring = if args.is_empty() {
4999 datafusion::functions::unicode::expr_fn::substr(original, start)
5000 } else {
5001 datafusion::functions::unicode::expr_fn::substring(original, start, args.remove(0))
5002 };
5003 cast(substring, DataType::Utf8)
5004}
5005
5006fn is_path_builtin_name(name: &str) -> bool {
5007 matches!(name, "_path_nodes" | "_path_fixed_length" | "_path_struct")
5008}
5009
5010fn resolve_path_builtin(
5022 name: &str,
5023 args: Vec<DfExpr>,
5024 hydration: impl FnOnce() -> Option<PathNodeHydration>,
5025) -> Option<DfExpr> {
5026 use datafusion::functions::core::expr_fn::named_struct;
5027
5028 let mut a = args;
5029 match name {
5030 "_path_nodes" => {
5036 let seed = node_uuid_col(a.remove(0));
5037 let rels = a.remove(0);
5038 Some(match hydration() {
5039 Some(h) => ScalarUDF::new_from_impl(CypherPathNodes::with_hydration(h))
5040 .call(vec![seed, rels]),
5041 None => CYPHER_PATH_NODES.call(vec![seed, rels]),
5042 })
5043 }
5044
5045 "_path_fixed_length" => {
5049 let present = edge_present(&a.remove(0))?;
5050 Some(
5051 when(present, lit(1_u64))
5052 .otherwise(lit(ScalarValue::UInt64(None)))
5053 .expect("CASE build is infallible for a single WHEN + ELSE"),
5054 )
5055 }
5056
5057 "_path_struct" => {
5062 let nodes = a.remove(0);
5063 let rels = a.remove(0);
5064 let struct_expr = named_struct(vec![
5065 lit("nodes"),
5066 nodes.clone(),
5067 lit("relationships"),
5068 rels,
5069 ]);
5070 Some(null_unless(nodes.is_not_null(), struct_expr))
5071 }
5072
5073 _ => None,
5074 }
5075}
5076
5077fn edge_present(edge: &DfExpr) -> Option<DfExpr> {
5081 let DfExpr::Column(c) = edge else {
5082 return None;
5083 };
5084 Some(col(format!("{}.edge_uuid", c.name)).is_not_null())
5085}
5086
5087fn null_unless(present: DfExpr, value: DfExpr) -> DfExpr {
5091 when(present, value)
5092 .otherwise(lit(ScalarValue::Null))
5093 .expect("CASE build is infallible for a single WHEN + ELSE")
5094}
5095
5096fn node_uuid_col(base: DfExpr) -> DfExpr {
5103 match &base {
5104 DfExpr::Column(c) => col(format!("{}.node_uuid", c.name)),
5105 _ => datafusion::functions::core::expr_fn::get_field(base, "node_uuid"),
5106 }
5107}
5108
5109fn edge_present_qual(base: &str) -> DfExpr {
5110 col(format!("{base}.edge_uuid")).is_not_null()
5111}
5112
5113fn is_edge_value_topology_field(name: &str) -> bool {
5114 matches!(
5115 name,
5116 "edge_uuid"
5117 | "src_uuid"
5118 | "dst_uuid"
5119 | "edge_id"
5120 | "src_id"
5121 | "dst_id"
5122 | "created_at"
5123 | "rel_type_name"
5124 )
5125}
5126
5127fn empty_utf8_list() -> DfExpr {
5128 DfExpr::Literal(
5129 ScalarValue::List(ScalarValue::new_list(&[], &DataType::Utf8, true)),
5130 None,
5131 )
5132}
5133
5134fn empty_map_struct() -> DfExpr {
5135 DfExpr::Literal(
5136 ScalarValue::Struct(std::sync::Arc::new(
5137 datafusion::arrow::array::StructArray::new_empty_fields(1, None),
5138 )),
5139 None,
5140 )
5141}
5142
5143fn null_utf8_list() -> DfExpr {
5144 null_unless(lit(false), empty_utf8_list())
5145}
5146
5147fn node_labels_list(base: &str, label: Option<&str>, type_id_map: &HashMap<u32, String>) -> DfExpr {
5148 use datafusion::functions_nested::expr_fn::{array_concat, array_has, make_array};
5149
5150 if type_id_map.is_empty() {
5151 return label.map_or_else(empty_utf8_list, |name| make_array(vec![lit(name)]));
5152 }
5153
5154 let mut entries: Vec<(u32, &str)> = type_id_map
5155 .iter()
5156 .map(|(id, name)| (*id, name.as_str()))
5157 .collect();
5158 entries.sort_by_key(|(id, _)| *id);
5159 let labels = col(format!("{base}.type_ids"));
5160 let parts = entries
5161 .into_iter()
5162 .map(|(id, name)| {
5163 when(
5164 array_has(labels.clone(), lit(id)),
5165 make_array(vec![lit(name)]),
5166 )
5167 .otherwise(empty_utf8_list())
5168 .expect("CASE build is infallible for a single WHEN + ELSE")
5169 })
5170 .collect();
5171 array_concat(parts)
5172}
5173
5174fn node_value_struct(
5180 base: &str,
5181 label: Option<&str>,
5182 type_id_map: &HashMap<u32, String>,
5183 prop_names: &[String],
5184) -> DfExpr {
5185 let labels = node_labels_list(base, label, type_id_map);
5186 node_value_struct_with_labels(base, labels, prop_names)
5187}
5188
5189fn node_value_struct_with_labels(base: &str, labels: DfExpr, prop_names: &[String]) -> DfExpr {
5190 use datafusion::functions::core::expr_fn::named_struct;
5191
5192 let mut fields = vec![
5193 lit("node_uuid"),
5194 col(format!("{base}.node_uuid")),
5195 lit("labels"),
5196 labels,
5197 ];
5198 for name in prop_names {
5199 fields.push(lit(name.as_str()));
5200 fields.push(qualified_col(base, name));
5201 }
5202 let value = named_struct(fields);
5203 null_unless(qualified_col(base, "node_uuid").is_not_null(), value)
5204}
5205
5206fn relationship_value_struct(base: &str, rel_type: DfExpr, prop_names: &[String]) -> DfExpr {
5212 use datafusion::functions::core::expr_fn::named_struct;
5213
5214 let mut fields = vec![
5215 lit("edge_uuid"),
5216 col(format!("{base}.edge_uuid")),
5217 lit("src_uuid"),
5218 col(format!("{base}.src_uuid")),
5219 lit("dst_uuid"),
5220 col(format!("{base}.dst_uuid")),
5221 lit("rel_type"),
5222 cast(rel_type, DataType::Utf8),
5223 ];
5224 for name in prop_names {
5225 fields.push(lit(name.as_str()));
5226 fields.push(qualified_col(base, name));
5227 }
5228 named_struct(fields)
5229}
5230
5231fn one_based_index(idx: DfExpr) -> DfExpr {
5236 when(idx.clone().gt_eq(lit(0_i64)), idx.clone() + lit(1_i64))
5237 .otherwise(idx)
5238 .expect("CASE build is infallible for a single WHEN + ELSE")
5239}
5240
5241fn is_integer_data_type(dt: &DataType) -> bool {
5242 matches!(
5243 dt,
5244 DataType::Int8
5245 | DataType::Int16
5246 | DataType::Int32
5247 | DataType::Int64
5248 | DataType::UInt8
5249 | DataType::UInt16
5250 | DataType::UInt32
5251 | DataType::UInt64
5252 )
5253}
5254
5255fn as_null_literal(e: &DfExpr) -> bool {
5257 matches!(e, DfExpr::Literal(ScalarValue::Null, _))
5258}
5259
5260fn cypher_slice(list: DfExpr, start: Option<DfExpr>, end: Option<DfExpr>) -> DfExpr {
5261 let mut present = lit(true);
5262 let begin_expr = match start {
5263 Some(start) if as_null_literal(&start) => {
5264 present = lit(false);
5265 lit(1_i64)
5266 }
5267 Some(start) => {
5268 present = present.and(start.clone().is_not_null());
5269 when(start.clone().gt_eq(lit(0_i64)), start.clone() + lit(1_i64))
5270 .otherwise(start)
5271 .expect("CASE build")
5272 }
5273 None => lit(1_i64),
5274 };
5275 let end_expr = match end {
5276 Some(end) if as_null_literal(&end) => {
5277 present = lit(false);
5278 cast(
5279 datafusion::functions_nested::expr_fn::array_length(list.clone()),
5280 DataType::Int64,
5281 )
5282 }
5283 Some(end) => {
5284 present = present.and(end.clone().is_not_null());
5285 when(end.clone().gt_eq(lit(0_i64)), end.clone())
5286 .otherwise(end - lit(1_i64))
5287 .expect("CASE build")
5288 }
5289 None => cast(
5290 datafusion::functions_nested::expr_fn::array_length(list.clone()),
5291 DataType::Int64,
5292 ),
5293 };
5294 let slice =
5295 datafusion::functions_nested::expr_fn::array_slice(list, begin_expr, end_expr, None);
5296 null_unless(present, slice)
5297}
5298
5299static CYPHER_TO_INTEGER: LazyLock<ScalarUDF> = LazyLock::new(|| {
5304 ScalarUDF::new_from_impl(CypherConversion::new(CypherConversionKind::Integer))
5305});
5306static CYPHER_TO_FLOAT: LazyLock<ScalarUDF> =
5307 LazyLock::new(|| ScalarUDF::new_from_impl(CypherConversion::new(CypherConversionKind::Float)));
5308static CYPHER_TO_BOOLEAN: LazyLock<ScalarUDF> = LazyLock::new(|| {
5309 ScalarUDF::new_from_impl(CypherConversion::new(CypherConversionKind::Boolean))
5310});
5311
5312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5313enum CypherConversionKind {
5314 Integer,
5315 Float,
5316 Boolean,
5317}
5318
5319#[derive(Debug, PartialEq, Eq, Hash)]
5320struct CypherConversion {
5321 kind: CypherConversionKind,
5322 signature: Signature,
5323}
5324
5325impl CypherConversion {
5326 fn new(kind: CypherConversionKind) -> Self {
5327 Self {
5328 kind,
5329 signature: Signature::any(1, Volatility::Immutable),
5330 }
5331 }
5332}
5333
5334impl ScalarUDFImpl for CypherConversion {
5335 fn as_any(&self) -> &dyn Any {
5336 self
5337 }
5338
5339 fn name(&self) -> &'static str {
5340 match self.kind {
5341 CypherConversionKind::Integer => "cypher_to_integer",
5342 CypherConversionKind::Float => "cypher_to_float",
5343 CypherConversionKind::Boolean => "cypher_to_boolean",
5344 }
5345 }
5346
5347 fn signature(&self) -> &Signature {
5348 &self.signature
5349 }
5350
5351 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
5352 Ok(match self.kind {
5353 CypherConversionKind::Integer => DataType::Int64,
5354 CypherConversionKind::Float => DataType::Float64,
5355 CypherConversionKind::Boolean => DataType::Boolean,
5356 })
5357 }
5358
5359 fn invoke_with_args(
5360 &self,
5361 args: ScalarFunctionArgs,
5362 ) -> datafusion::error::Result<ColumnarValue> {
5363 use datafusion::arrow::array::{BooleanArray, Float64Array, Int64Array};
5364
5365 let array = args.args[0].to_array(args.number_rows)?;
5366 match self.kind {
5367 CypherConversionKind::Integer => {
5368 let out: datafusion::error::Result<Int64Array> = (0..array.len())
5369 .map(|i| {
5370 let value = decoded_scalar_at(&array, i)?;
5371 to_cypher_integer(&value)
5372 })
5373 .collect();
5374 Ok(ColumnarValue::Array(std::sync::Arc::new(out?)))
5375 }
5376 CypherConversionKind::Float => {
5377 let out: datafusion::error::Result<Float64Array> = (0..array.len())
5378 .map(|i| {
5379 let value = decoded_scalar_at(&array, i)?;
5380 to_cypher_float(&value)
5381 })
5382 .collect();
5383 Ok(ColumnarValue::Array(std::sync::Arc::new(out?)))
5384 }
5385 CypherConversionKind::Boolean => {
5386 let out: datafusion::error::Result<BooleanArray> = (0..array.len())
5387 .map(|i| {
5388 let value = decoded_scalar_at(&array, i)?;
5389 to_cypher_boolean(&value)
5390 })
5391 .collect();
5392 Ok(ColumnarValue::Array(std::sync::Arc::new(out?)))
5393 }
5394 }
5395 }
5396}
5397
5398fn decoded_scalar_at(
5399 array: &datafusion::arrow::array::ArrayRef,
5400 row: usize,
5401) -> datafusion::error::Result<ScalarValue> {
5402 let value = ScalarValue::try_from_array(array, row)?;
5403 Ok(unwrap_het(value))
5404}
5405
5406fn conversion_type_error(fn_name: &str, value: &ScalarValue) -> datafusion::error::DataFusionError {
5407 datafusion::error::DataFusionError::Execution(format!(
5408 "{fn_name}() cannot convert value of type {:?}",
5409 value.data_type()
5410 ))
5411}
5412
5413fn to_cypher_integer(value: &ScalarValue) -> datafusion::error::Result<Option<i64>> {
5414 if value.is_null() {
5415 return Ok(None);
5416 }
5417 if let Some(i) = scalar_as_i128(value) {
5418 return i64::try_from(i)
5419 .map(Some)
5420 .map_err(|_| conversion_type_error("toInteger", value));
5421 }
5422 match value {
5423 ScalarValue::Float32(Some(f)) => Ok(trunc_float_to_i64(f64::from(*f))),
5424 ScalarValue::Float64(Some(f)) => Ok(trunc_float_to_i64(*f)),
5425 ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => Ok(s
5426 .parse::<f64>()
5427 .ok()
5428 .filter(|f| f.is_finite())
5429 .and_then(trunc_float_to_i64)),
5430 _ => Err(conversion_type_error("toInteger", value)),
5431 }
5432}
5433
5434#[allow(
5435 clippy::cast_possible_truncation,
5436 clippy::cast_precision_loss,
5437 reason = "openCypher toInteger truncates finite floating values toward zero"
5438)]
5439fn trunc_float_to_i64(f: f64) -> Option<i64> {
5440 if !f.is_finite() {
5441 return None;
5442 }
5443 let truncated = f.trunc();
5444 if truncated < i64::MIN as f64 || truncated > i64::MAX as f64 {
5445 return None;
5446 }
5447 Some(truncated as i64)
5448}
5449
5450fn to_cypher_float(value: &ScalarValue) -> datafusion::error::Result<Option<f64>> {
5451 if value.is_null() {
5452 return Ok(None);
5453 }
5454 if let Some(f) = scalar_as_f64(value) {
5455 return Ok(Some(f));
5456 }
5457 match value {
5458 ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => {
5459 Ok(s.parse::<f64>().ok().filter(|f| f.is_finite()))
5460 }
5461 _ => Err(conversion_type_error("toFloat", value)),
5462 }
5463}
5464
5465fn to_cypher_boolean(value: &ScalarValue) -> datafusion::error::Result<Option<bool>> {
5466 if value.is_null() {
5467 return Ok(None);
5468 }
5469 match value {
5470 ScalarValue::Boolean(Some(b)) => Ok(Some(*b)),
5471 ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => match s.as_str() {
5472 "true" => Ok(Some(true)),
5473 "false" => Ok(Some(false)),
5474 _ => Ok(None),
5475 },
5476 _ => Err(conversion_type_error("toBoolean", value)),
5477 }
5478}
5479
5480fn cypher_float_string(f: f64) -> String {
5481 if f == 0.0 {
5482 "0.0".to_owned()
5483 } else if f.is_nan() {
5484 "NaN".to_owned()
5485 } else if f.is_infinite() {
5486 if f.is_sign_positive() {
5487 "Infinity".to_owned()
5488 } else {
5489 "-Infinity".to_owned()
5490 }
5491 } else {
5492 let mut s = f.to_string();
5493 if !s.contains('.') && !s.contains('e') && !s.contains('E') {
5494 s.push_str(".0");
5495 }
5496 s
5497 }
5498}
5499
5500fn to_cypher_string(value: &ScalarValue) -> datafusion::error::Result<Option<String>> {
5501 if value.is_null() {
5502 return Ok(None);
5503 }
5504 match value {
5505 ScalarValue::Int8(Some(n)) => Ok(Some(n.to_string())),
5506 ScalarValue::Int16(Some(n)) => Ok(Some(n.to_string())),
5507 ScalarValue::Int32(Some(n)) => Ok(Some(n.to_string())),
5508 ScalarValue::Int64(Some(n)) => Ok(Some(n.to_string())),
5509 ScalarValue::UInt8(Some(n)) => Ok(Some(n.to_string())),
5510 ScalarValue::UInt16(Some(n)) => Ok(Some(n.to_string())),
5511 ScalarValue::UInt32(Some(n)) => Ok(Some(n.to_string())),
5512 ScalarValue::UInt64(Some(n)) => Ok(Some(n.to_string())),
5513 ScalarValue::Float32(Some(f)) => Ok(Some(cypher_float_string(f64::from(*f)))),
5514 ScalarValue::Float64(Some(f)) => Ok(Some(cypher_float_string(*f))),
5515 ScalarValue::Boolean(Some(b)) => Ok(Some(b.to_string())),
5516 ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => Ok(Some(s.clone())),
5517 _ => Err(conversion_type_error("toString", value)),
5518 }
5519}
5520
5521static CYPHER_SIZE: LazyLock<ScalarUDF> =
5537 LazyLock::new(|| ScalarUDF::new_from_impl(CypherSize::new()));
5538
5539pub(crate) static CYPHER_ROW_MARKER: LazyLock<ScalarUDF> =
5543 LazyLock::new(|| ScalarUDF::new_from_impl(CypherRowMarker::new()));
5544
5545#[derive(Debug, PartialEq, Eq, Hash)]
5546struct CypherRowMarker {
5547 signature: Signature,
5548}
5549
5550impl CypherRowMarker {
5551 fn new() -> Self {
5552 Self {
5553 signature: Signature::any(1, Volatility::Immutable),
5554 }
5555 }
5556}
5557
5558impl ScalarUDFImpl for CypherRowMarker {
5559 fn as_any(&self) -> &dyn Any {
5560 self
5561 }
5562
5563 fn name(&self) -> &'static str {
5564 "cypher_row_marker"
5565 }
5566
5567 fn signature(&self) -> &Signature {
5568 &self.signature
5569 }
5570
5571 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
5572 Ok(DataType::Boolean)
5573 }
5574
5575 fn invoke_with_args(
5576 &self,
5577 args: ScalarFunctionArgs,
5578 ) -> datafusion::error::Result<ColumnarValue> {
5579 let values = datafusion::arrow::array::BooleanArray::from(vec![true; args.number_rows]);
5580 Ok(ColumnarValue::Array(Arc::new(values)))
5581 }
5582}
5583
5584#[derive(Debug, PartialEq, Eq, Hash)]
5585struct CypherSize {
5586 signature: Signature,
5587}
5588
5589impl CypherSize {
5590 fn new() -> Self {
5591 Self {
5593 signature: Signature::any(1, Volatility::Immutable),
5594 }
5595 }
5596}
5597
5598impl ScalarUDFImpl for CypherSize {
5599 fn as_any(&self) -> &dyn Any {
5600 self
5601 }
5602
5603 fn name(&self) -> &'static str {
5604 "cypher_size"
5605 }
5606
5607 fn signature(&self) -> &Signature {
5608 &self.signature
5609 }
5610
5611 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
5612 Ok(DataType::Int64)
5613 }
5614
5615 fn invoke_with_args(
5616 &self,
5617 args: ScalarFunctionArgs,
5618 ) -> datafusion::error::Result<ColumnarValue> {
5619 use datafusion::arrow::array::{Array, Int64Array};
5620 use datafusion::arrow::compute::kernels::length::length;
5621 use datafusion::common::cast::{as_large_list_array, as_list_array};
5622
5623 let array = args.args[0].to_array(args.number_rows)?;
5624 let out: Int64Array = match array.data_type() {
5625 DataType::List(_) => {
5627 let list = as_list_array(&array)?;
5628 (0..list.len())
5629 .map(|i| {
5630 (!list.is_null(i))
5631 .then(|| i64::try_from(list.value(i).len()).unwrap_or(i64::MAX))
5632 })
5633 .collect()
5634 }
5635 DataType::LargeList(_) => {
5636 let list = as_large_list_array(&array)?;
5637 (0..list.len())
5638 .map(|i| {
5639 (!list.is_null(i))
5640 .then(|| i64::try_from(list.value(i).len()).unwrap_or(i64::MAX))
5641 })
5642 .collect()
5643 }
5644 DataType::Utf8 | DataType::LargeUtf8 => {
5647 let lengths = length(&array)?;
5648 let casted = datafusion::arrow::compute::cast(&lengths, &DataType::Int64)?;
5649 casted
5650 .as_any()
5651 .downcast_ref::<Int64Array>()
5652 .expect("cast to Int64 yields Int64Array")
5653 .clone()
5654 }
5655 t if is_het_struct_type(Some(t)) => (0..array.len())
5661 .map(|i| {
5662 let sv = ScalarValue::try_from_array(&array, i).ok()?;
5663 match decode_het(&sv)? {
5664 ScalarValue::List(l) => (!l.is_null(0))
5665 .then(|| i64::try_from(l.value(0).len()).unwrap_or(i64::MAX)),
5666 ScalarValue::LargeList(l) => (!l.is_null(0))
5667 .then(|| i64::try_from(l.value(0).len()).unwrap_or(i64::MAX)),
5668 ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => {
5669 i64::try_from(s.len()).ok()
5670 }
5671 _ => None,
5672 }
5673 })
5674 .collect(),
5675 _ => (0..array.len()).map(|_| None).collect(),
5678 };
5679 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
5680 }
5681}
5682
5683static CYPHER_LABELS: LazyLock<ScalarUDF> =
5688 LazyLock::new(|| ScalarUDF::new_from_impl(CypherGraphMetadata::new(GraphMetadataKind::Labels)));
5689static CYPHER_REL_TYPE: LazyLock<ScalarUDF> = LazyLock::new(|| {
5690 ScalarUDF::new_from_impl(CypherGraphMetadata::new(
5691 GraphMetadataKind::RelationshipType,
5692 ))
5693});
5694
5695#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5696enum GraphMetadataKind {
5697 Labels,
5698 RelationshipType,
5699}
5700
5701#[derive(Debug, PartialEq, Eq, Hash)]
5702struct CypherGraphMetadata {
5703 kind: GraphMetadataKind,
5704 signature: Signature,
5705}
5706
5707impl CypherGraphMetadata {
5708 fn new(kind: GraphMetadataKind) -> Self {
5709 Self {
5710 kind,
5711 signature: Signature::any(1, Volatility::Immutable),
5712 }
5713 }
5714}
5715
5716impl ScalarUDFImpl for CypherGraphMetadata {
5717 fn as_any(&self) -> &dyn Any {
5718 self
5719 }
5720
5721 fn name(&self) -> &'static str {
5722 match self.kind {
5723 GraphMetadataKind::Labels => "cypher_labels",
5724 GraphMetadataKind::RelationshipType => "cypher_relationship_type",
5725 }
5726 }
5727
5728 fn signature(&self) -> &Signature {
5729 &self.signature
5730 }
5731
5732 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
5733 Ok(match self.kind {
5734 GraphMetadataKind::Labels => DataType::new_list(DataType::Utf8, true),
5735 GraphMetadataKind::RelationshipType => DataType::Utf8,
5736 })
5737 }
5738
5739 fn invoke_with_args(
5740 &self,
5741 args: ScalarFunctionArgs,
5742 ) -> datafusion::error::Result<ColumnarValue> {
5743 use datafusion::arrow::array::new_empty_array;
5744 use datafusion::error::DataFusionError;
5745
5746 let rows = args.number_rows;
5747 let values = args.args[0].to_array(rows)?;
5748 let field = match self.kind {
5749 GraphMetadataKind::Labels => "labels",
5750 GraphMetadataKind::RelationshipType => "rel_type",
5751 };
5752 let identity_field = match self.kind {
5753 GraphMetadataKind::Labels => "node_uuid",
5754 GraphMetadataKind::RelationshipType => "edge_uuid",
5755 };
5756 let mut output = Vec::with_capacity(rows);
5757 for row in 0..rows {
5758 let value = ScalarValue::try_from_array(&values, row)?;
5759 let value = decode_het(&value).unwrap_or(value);
5760 if value.is_null() {
5761 output.push(ScalarValue::try_new_null(&self.return_type(&[])?)?);
5762 continue;
5763 }
5764 let ScalarValue::Struct(entity) = value else {
5765 return Err(DataFusionError::Execution(format!(
5766 "InvalidArgumentValue: {}() requires a graph element",
5767 self.name()
5768 )));
5769 };
5770 if entity.column_by_name(identity_field).is_none() {
5771 return Err(DataFusionError::Execution(format!(
5772 "InvalidArgumentValue: {}() received the wrong graph element kind",
5773 self.name()
5774 )));
5775 }
5776 let column = entity.column_by_name(field).ok_or_else(|| {
5777 DataFusionError::Execution(format!(
5778 "InvalidArgumentValue: {}() received the wrong graph element kind",
5779 self.name()
5780 ))
5781 })?;
5782 output.push(ScalarValue::try_from_array(column, 0)?);
5783 }
5784 let data_type = self.return_type(&[])?;
5785 let array = if output.is_empty() {
5786 new_empty_array(&data_type)
5787 } else {
5788 ScalarValue::iter_to_array(output)?
5789 };
5790 Ok(ColumnarValue::Array(array))
5791 }
5792}
5793
5794const ENTITY_PROPERTY_MAP_HET_DEPTH: usize = 3;
5799
5800#[derive(Debug, PartialEq, Eq, Hash)]
5801struct CypherEntityProperties {
5802 signature: Signature,
5803}
5804
5805impl CypherEntityProperties {
5806 fn new(arity: usize) -> Self {
5807 Self {
5808 signature: Signature::any(arity, Volatility::Immutable),
5809 }
5810 }
5811}
5812
5813impl ScalarUDFImpl for CypherEntityProperties {
5814 fn as_any(&self) -> &dyn Any {
5815 self
5816 }
5817
5818 fn name(&self) -> &'static str {
5819 "cypher_entity_properties"
5820 }
5821
5822 fn signature(&self) -> &Signature {
5823 &self.signature
5824 }
5825
5826 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
5827 Ok(DataType::Struct(het_fields(ENTITY_PROPERTY_MAP_HET_DEPTH)))
5828 }
5829
5830 fn invoke_with_args(
5831 &self,
5832 args: ScalarFunctionArgs,
5833 ) -> datafusion::error::Result<ColumnarValue> {
5834 use datafusion::arrow::array::ArrayRef;
5835 use datafusion::error::DataFusionError;
5836 use std::sync::Arc;
5837
5838 let rows = args.number_rows;
5839 if args.args.is_empty() || !(args.args.len() - 1).is_multiple_of(2) {
5840 return Err(DataFusionError::Plan(
5841 "properties() entity map expects present plus key/value pairs".into(),
5842 ));
5843 }
5844 let cols: Vec<ArrayRef> = args
5845 .args
5846 .iter()
5847 .map(|arg| arg.to_array(rows))
5848 .collect::<datafusion::error::Result<_>>()?;
5849 let mut maps = Vec::with_capacity(rows);
5850 for row in 0..rows {
5851 let present = match ScalarValue::try_from_array(&cols[0], row)? {
5852 ScalarValue::Boolean(Some(true)) => true,
5853 ScalarValue::Boolean(Some(false) | None) | ScalarValue::Null => false,
5854 other => {
5855 return Err(DataFusionError::Execution(format!(
5856 "properties() entity presence must be boolean, got {other:?}"
5857 )));
5858 }
5859 };
5860 if !present {
5861 maps.push(ScalarValue::Null);
5862 continue;
5863 }
5864 let mut entries = Vec::with_capacity((cols.len() - 1) / 2);
5865 for pair in cols[1..].chunks_exact(2) {
5866 let key = ScalarValue::try_from_array(&pair[0], row)?;
5867 let Some(key) = scalar_access_key(&key)? else {
5868 continue;
5869 };
5870 let value = ScalarValue::try_from_array(&pair[1], row)?;
5871 if value.is_null() {
5872 continue;
5873 }
5874 entries.push((key, unwrap_het(value)));
5875 }
5876 let map = const_map_scalar(&entries).ok_or_else(|| {
5877 DataFusionError::Execution(
5878 "properties() could not encode entity property map".into(),
5879 )
5880 })?;
5881 maps.push(map);
5882 }
5883 let out = build_het_struct(&maps, ENTITY_PROPERTY_MAP_HET_DEPTH).ok_or_else(|| {
5884 DataFusionError::Execution("properties() could not encode entity property map".into())
5885 })?;
5886 Ok(ColumnarValue::Array(Arc::new(out)))
5887 }
5888}
5889
5890static CYPHER_MAP_KEYS: LazyLock<ScalarUDF> =
5891 LazyLock::new(|| ScalarUDF::new_from_impl(CypherMapKeys::new()));
5892
5893#[derive(Debug, PartialEq, Eq, Hash)]
5894struct CypherMapKeys {
5895 signature: Signature,
5896}
5897
5898impl CypherMapKeys {
5899 fn new() -> Self {
5900 Self {
5901 signature: Signature::any(1, Volatility::Immutable),
5902 }
5903 }
5904}
5905
5906impl ScalarUDFImpl for CypherMapKeys {
5907 fn as_any(&self) -> &dyn Any {
5908 self
5909 }
5910
5911 fn name(&self) -> &'static str {
5912 "cypher_map_keys"
5913 }
5914
5915 fn signature(&self) -> &Signature {
5916 &self.signature
5917 }
5918
5919 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
5920 Ok(DataType::new_list(DataType::Utf8, true))
5921 }
5922
5923 fn invoke_with_args(
5924 &self,
5925 args: ScalarFunctionArgs,
5926 ) -> datafusion::error::Result<ColumnarValue> {
5927 use datafusion::arrow::array::{Array, ArrayRef, ListArray, StringArray, StructArray};
5928 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
5929 use datafusion::error::DataFusionError;
5930 use std::sync::Arc;
5931
5932 let rows = args.number_rows;
5933 let values = args.args[0].to_array(rows)?;
5934 if matches!(values.data_type(), DataType::Null) {
5935 let nulls = NullBuffer::from(vec![false; rows]);
5936 let list = ListArray::new(
5937 Arc::new(Field::new("item", DataType::Utf8, true)),
5938 OffsetBuffer::new(ScalarBuffer::from(vec![0_i32; rows + 1])),
5939 Arc::new(StringArray::from(Vec::<Option<String>>::new())) as ArrayRef,
5940 Some(nulls),
5941 );
5942 return Ok(ColumnarValue::Array(Arc::new(list)));
5943 }
5944 let DataType::Struct(fields) = values.data_type() else {
5945 return Err(DataFusionError::Execution(format!(
5946 "keys() requires a map, node, relationship, or null, got {:?}",
5947 values.data_type()
5948 )));
5949 };
5950 let map = values
5951 .as_any()
5952 .downcast_ref::<StructArray>()
5953 .ok_or_else(|| DataFusionError::Execution("keys() expected a struct map".into()))?;
5954 if is_het_struct_type(Some(values.data_type())) {
5955 return tagged_map_keys(map, rows);
5956 }
5957 if !is_plain_map_struct_type(values.data_type()) {
5958 return Err(DataFusionError::Execution(format!(
5959 "keys() requires a map, node, relationship, or null, got {:?}",
5960 values.data_type()
5961 )));
5962 }
5963 let names: Vec<String> = fields.iter().map(|f| f.name().clone()).collect();
5964 let mut offsets = Vec::with_capacity(rows + 1);
5965 let mut values = Vec::new();
5966 let mut valid = Vec::with_capacity(rows);
5967 offsets.push(0_i32);
5968 for row in 0..rows {
5969 if map.is_null(row) {
5970 valid.push(false);
5971 } else {
5972 valid.push(true);
5973 values.extend(names.iter().cloned().map(Some));
5974 }
5975 offsets.push(i32::try_from(values.len()).map_err(|_| {
5976 DataFusionError::Execution("keys() result exceeded i32 list offsets".into())
5977 })?);
5978 }
5979 let list = ListArray::new(
5980 Arc::new(Field::new("item", DataType::Utf8, true)),
5981 OffsetBuffer::new(ScalarBuffer::from(offsets)),
5982 Arc::new(StringArray::from(values)) as ArrayRef,
5983 Some(NullBuffer::from(valid)),
5984 );
5985 Ok(ColumnarValue::Array(Arc::new(list)))
5986 }
5987}
5988
5989fn tagged_map_keys(
5990 map: &datafusion::arrow::array::StructArray,
5991 rows: usize,
5992) -> datafusion::error::Result<ColumnarValue> {
5993 use datafusion::arrow::array::{
5994 Array, ArrayRef, Int8Array, ListArray, StringArray, StructArray,
5995 };
5996 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
5997 use datafusion::error::DataFusionError;
5998 use std::sync::Arc;
5999
6000 let tags = map
6001 .column_by_name("__het_tag")
6002 .and_then(|c| c.as_any().downcast_ref::<Int8Array>())
6003 .ok_or_else(|| DataFusionError::Plan("tagged map is missing __het_tag".into()))?;
6004 let entries = map
6005 .column_by_name("__het_map")
6006 .and_then(|c| c.as_any().downcast_ref::<ListArray>())
6007 .ok_or_else(|| DataFusionError::Plan("tagged map is missing __het_map".into()))?;
6008 let mut offsets = Vec::with_capacity(rows + 1);
6009 let mut values = Vec::new();
6010 let mut valid = Vec::with_capacity(rows);
6011 offsets.push(0_i32);
6012 for row in 0..rows {
6013 if map.is_null(row) {
6014 valid.push(false);
6015 } else {
6016 if tags.value(row) != 5 {
6017 return Err(DataFusionError::Execution(
6018 "keys() requires a map, node, relationship, or null".into(),
6019 ));
6020 }
6021 valid.push(true);
6022 if !entries.is_null(row) {
6023 let entry_values = entries.value(row);
6024 let entry_struct = entry_values
6025 .as_any()
6026 .downcast_ref::<StructArray>()
6027 .ok_or_else(|| {
6028 DataFusionError::Plan("tagged map entries must be structs".into())
6029 })?;
6030 let map_keys = entry_struct
6031 .column_by_name("__het_mkey")
6032 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
6033 .ok_or_else(|| {
6034 DataFusionError::Plan("tagged map entries must carry __het_mkey".into())
6035 })?;
6036 for idx in 0..entry_struct.len() {
6037 if !map_keys.is_null(idx) {
6038 values.push(Some(map_keys.value(idx).to_owned()));
6039 }
6040 }
6041 }
6042 }
6043 offsets.push(i32::try_from(values.len()).map_err(|_| {
6044 DataFusionError::Execution("keys() result exceeded i32 list offsets".into())
6045 })?);
6046 }
6047 let list = ListArray::new(
6048 Arc::new(Field::new("item", DataType::Utf8, true)),
6049 OffsetBuffer::new(ScalarBuffer::from(offsets)),
6050 Arc::new(StringArray::from(values)) as ArrayRef,
6051 Some(NullBuffer::from(valid)),
6052 );
6053 Ok(ColumnarValue::Array(Arc::new(list)))
6054}
6055
6056static CYPHER_VALUE_ACCESS: LazyLock<ScalarUDF> =
6061 LazyLock::new(|| ScalarUDF::new_from_impl(CypherValueAccess::new()));
6062
6063#[derive(Debug, PartialEq, Eq, Hash)]
6064struct CypherStaticValueAccess {
6065 key: String,
6066 signature: Signature,
6067}
6068
6069impl CypherStaticValueAccess {
6070 fn new(key: String) -> Self {
6071 Self {
6072 key,
6073 signature: Signature::any(1, Volatility::Immutable),
6074 }
6075 }
6076}
6077
6078impl ScalarUDFImpl for CypherStaticValueAccess {
6079 fn as_any(&self) -> &dyn Any {
6080 self
6081 }
6082
6083 fn name(&self) -> &'static str {
6084 "cypher_static_value_access"
6085 }
6086
6087 fn signature(&self) -> &Signature {
6088 &self.signature
6089 }
6090
6091 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6092 static_value_access_return_type(arg_types.first(), &self.key)
6093 }
6094
6095 fn return_field_from_args(&self, args: ReturnFieldArgs) -> datafusion::error::Result<FieldRef> {
6096 Ok(Arc::new(Field::new(
6097 self.name(),
6098 static_value_access_return_type(
6099 args.arg_fields.first().map(|field| field.data_type()),
6100 &self.key,
6101 )?,
6102 true,
6103 )))
6104 }
6105
6106 fn invoke_with_args(
6107 &self,
6108 args: ScalarFunctionArgs,
6109 ) -> datafusion::error::Result<ColumnarValue> {
6110 use datafusion::error::DataFusionError;
6111
6112 let rows = args.number_rows;
6113 let values = args.args[0].to_array(rows)?;
6114 let return_type = args.return_field.data_type();
6115 let null_value = ScalarValue::try_new_null(return_type)?;
6116 let output = (0..rows)
6117 .map(|row| {
6118 let value = ScalarValue::try_from_array(&values, row)?;
6119 let value = decode_het(&value).unwrap_or(value);
6120 if value.is_null() {
6121 return Ok(null_value.clone());
6122 }
6123 let ScalarValue::Struct(value) = value else {
6124 return Err(DataFusionError::Execution(
6125 "InvalidArgumentValue: property access requires a map or graph element"
6126 .into(),
6127 ));
6128 };
6129 let Some(column) = value.column_by_name(&self.key) else {
6130 return Ok(null_value.clone());
6131 };
6132 let result = ScalarValue::try_from_array(column, 0)?;
6133 if result.data_type() == *return_type {
6134 Ok(result)
6135 } else if result.is_null() {
6136 Ok(null_value.clone())
6137 } else {
6138 Err(DataFusionError::Execution(format!(
6139 "property `{}` has incompatible runtime type {:?}; expected {:?}",
6140 self.key,
6141 result.data_type(),
6142 return_type
6143 )))
6144 }
6145 })
6146 .collect::<datafusion::error::Result<Vec<_>>>()?;
6147 Ok(ColumnarValue::Array(ScalarValue::iter_to_array(output)?))
6148 }
6149}
6150
6151#[derive(Debug, PartialEq, Eq, Hash)]
6152struct CypherValueAccess {
6153 signature: Signature,
6154}
6155
6156impl CypherValueAccess {
6157 fn new() -> Self {
6158 Self {
6159 signature: Signature::any(2, Volatility::Immutable),
6160 }
6161 }
6162}
6163
6164impl ScalarUDFImpl for CypherValueAccess {
6165 fn as_any(&self) -> &dyn Any {
6166 self
6167 }
6168
6169 fn name(&self) -> &'static str {
6170 "cypher_value_access"
6171 }
6172
6173 fn signature(&self) -> &Signature {
6174 &self.signature
6175 }
6176
6177 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6178 value_access_return_type(arg_types.first())
6179 }
6180
6181 fn return_field_from_args(&self, args: ReturnFieldArgs) -> datafusion::error::Result<FieldRef> {
6182 Ok(std::sync::Arc::new(Field::new(
6183 self.name(),
6184 value_access_return_type(args.arg_fields.first().map(|f| f.data_type()))?,
6185 true,
6186 )))
6187 }
6188
6189 fn invoke_with_args(
6190 &self,
6191 args: ScalarFunctionArgs,
6192 ) -> datafusion::error::Result<ColumnarValue> {
6193 use datafusion::arrow::array::StructArray;
6194 use datafusion::error::DataFusionError;
6195
6196 let rows = args.number_rows;
6197 let values = args.args[0].to_array(rows)?;
6198 let keys = args.args[1].to_array(rows)?;
6199 let return_type = args.return_field.data_type().clone();
6200 let null_value = ScalarValue::try_from(&return_type).unwrap_or(ScalarValue::Null);
6201 if matches!(values.data_type(), DataType::Null) {
6202 return Ok(ColumnarValue::Array(ScalarValue::iter_to_array(
6203 (0..rows).map(|_| null_value.clone()),
6204 )?));
6205 }
6206 if let Some(list) = ListView::from_array(&values) {
6207 let out = (0..rows)
6208 .map(|i| list_access_value(&list, &keys, i, &null_value))
6209 .collect::<datafusion::error::Result<Vec<_>>>()?;
6210 return Ok(ColumnarValue::Array(ScalarValue::iter_to_array(out)?));
6211 }
6212 let map = values
6213 .as_any()
6214 .downcast_ref::<StructArray>()
6215 .ok_or_else(|| {
6216 DataFusionError::Execution(format!(
6217 "dynamic subscript requires a list or map/entity struct, got {:?}",
6218 values.data_type()
6219 ))
6220 })?;
6221 if is_het_struct_type(Some(values.data_type())) {
6222 let out = (0..rows)
6223 .map(|i| het_map_access_value(map, &keys, i, &null_value))
6224 .collect::<datafusion::error::Result<Vec<_>>>()?;
6225 return Ok(ColumnarValue::Array(ScalarValue::iter_to_array(out)?));
6226 }
6227 let out = (0..rows)
6228 .map(|i| {
6229 if map.is_null(i) {
6230 return Ok(null_value.clone());
6231 }
6232 let key = ScalarValue::try_from_array(&keys, i)?;
6233 let Some(key) = scalar_access_key(&key)? else {
6234 return Ok(null_value.clone());
6235 };
6236 let Some(col) = map.column_by_name(&key) else {
6237 return Ok(null_value.clone());
6238 };
6239 ScalarValue::try_from_array(col, i)
6240 })
6241 .collect::<datafusion::error::Result<Vec<_>>>()?;
6242 Ok(ColumnarValue::Array(ScalarValue::iter_to_array(out)?))
6243 }
6244}
6245
6246fn value_access_return_type(dt: Option<&DataType>) -> datafusion::error::Result<DataType> {
6247 let Some(dt) = dt else {
6248 return Ok(DataType::Null);
6249 };
6250 match dt {
6251 DataType::Null => Ok(DataType::Null),
6252 DataType::List(field) | DataType::LargeList(field) | DataType::FixedSizeList(field, _) => {
6253 Ok(field.data_type().clone())
6254 }
6255 dt if is_het_struct_type(Some(dt)) => het_value_access_return_type(dt),
6256 DataType::Struct(fields) => common_struct_field_type(fields),
6257 _ => Ok(DataType::Null),
6261 }
6262}
6263
6264fn static_value_access_return_type(
6265 dt: Option<&DataType>,
6266 key: &str,
6267) -> datafusion::error::Result<DataType> {
6268 let Some(DataType::Struct(fields)) = dt else {
6269 return Ok(DataType::Null);
6270 };
6271 if !is_het_struct_type(dt) {
6272 return fields
6273 .iter()
6274 .find(|field| field.name() == key)
6275 .map_or(Ok(DataType::Null), |field| Ok(field.data_type().clone()));
6276 }
6277 if fields.iter().any(|field| field.name() == "__het_map") {
6278 return het_value_access_return_type(&DataType::Struct(fields.clone()));
6279 }
6280 let mut data_type = None;
6281 for variant in fields
6282 .iter()
6283 .filter(|field| field.name().starts_with("__het_value_"))
6284 {
6285 let DataType::Struct(value_fields) = variant.data_type() else {
6286 continue;
6287 };
6288 let Some(property) = value_fields.iter().find(|field| field.name() == key) else {
6289 continue;
6290 };
6291 if matches!(property.data_type(), DataType::Null) {
6292 continue;
6293 }
6294 match &data_type {
6295 None => data_type = Some(property.data_type().clone()),
6296 Some(existing) if existing == property.data_type() => {}
6297 Some(existing) => {
6298 return Err(datafusion::error::DataFusionError::Plan(format!(
6299 "property `{key}` has incompatible graph-value types {existing:?} and {:?}",
6300 property.data_type()
6301 )));
6302 }
6303 }
6304 }
6305 Ok(data_type.unwrap_or(DataType::Null))
6306}
6307
6308fn list_access_value(
6309 list: &ListView<'_>,
6310 keys: &datafusion::arrow::array::ArrayRef,
6311 row: usize,
6312 null_value: &ScalarValue,
6313) -> datafusion::error::Result<ScalarValue> {
6314 if list.is_null(row) {
6315 return Ok(null_value.clone());
6316 }
6317 let key = ScalarValue::try_from_array(keys, row)?;
6318 let Some(idx) = scalar_list_index(&key)? else {
6319 return Ok(null_value.clone());
6320 };
6321 let elems = list.value(row);
6322 let len = i64::try_from(elems.len()).map_err(|_| {
6323 datafusion::error::DataFusionError::Execution(
6324 "dynamic list access length exceeds i64 range".into(),
6325 )
6326 })?;
6327 let pos = if idx < 0 { len + idx } else { idx };
6328 if pos < 0 || pos >= len {
6329 return Ok(null_value.clone());
6330 }
6331 let pos = usize::try_from(pos).map_err(|_| {
6332 datafusion::error::DataFusionError::Execution(
6333 "dynamic list access index exceeds usize range".into(),
6334 )
6335 })?;
6336 ScalarValue::try_from_array(&elems, pos)
6337}
6338
6339fn scalar_list_index(s: &ScalarValue) -> datafusion::error::Result<Option<i64>> {
6340 if s.is_null() {
6341 return Ok(None);
6342 }
6343 macro_rules! signed_index {
6344 ($value:expr) => {
6345 $value.map(i64::from)
6346 };
6347 }
6348 let idx = match s {
6349 ScalarValue::Int8(v) => signed_index!(*v),
6350 ScalarValue::Int16(v) => signed_index!(*v),
6351 ScalarValue::Int32(v) => signed_index!(*v),
6352 ScalarValue::Int64(v) => *v,
6353 ScalarValue::UInt8(v) => v.map(i64::from),
6354 ScalarValue::UInt16(v) => v.map(i64::from),
6355 ScalarValue::UInt32(v) => v.map(i64::from),
6356 ScalarValue::UInt64(v) => v.map(i64::try_from).transpose().map_err(|_| {
6357 datafusion::error::DataFusionError::Execution(
6358 "dynamic list access index exceeds i64 range".into(),
6359 )
6360 })?,
6361 other => {
6362 return Err(datafusion::error::DataFusionError::Execution(format!(
6363 "dynamic list access index must be an integer, got {other:?}"
6364 )));
6365 }
6366 };
6367 Ok(idx)
6368}
6369
6370fn het_value_access_return_type(dt: &DataType) -> datafusion::error::Result<DataType> {
6371 use datafusion::error::DataFusionError;
6372
6373 let DataType::Struct(fields) = dt else {
6374 unreachable!("caller checked het struct type")
6375 };
6376 let Some(map_field) = fields.iter().find(|f| f.name() == "__het_map") else {
6377 return Err(DataFusionError::Plan(
6378 "dynamic value access requires a tagged map element".into(),
6379 ));
6380 };
6381 let DataType::List(entry_field) = map_field.data_type() else {
6382 return Err(DataFusionError::Plan(
6383 "tagged map field must be a list".into(),
6384 ));
6385 };
6386 let DataType::Struct(entry_fields) = entry_field.data_type() else {
6387 return Err(DataFusionError::Plan(
6388 "tagged map entries must be structs".into(),
6389 ));
6390 };
6391 entry_fields
6392 .iter()
6393 .find(|f| f.name() == "__het_mval")
6394 .map(|f| f.data_type().clone())
6395 .ok_or_else(|| DataFusionError::Plan("tagged map entries must carry __het_mval".into()))
6396}
6397
6398fn het_map_access_value(
6399 map: &datafusion::arrow::array::StructArray,
6400 keys: &datafusion::arrow::array::ArrayRef,
6401 row: usize,
6402 null_value: &ScalarValue,
6403) -> datafusion::error::Result<ScalarValue> {
6404 use datafusion::arrow::array::{Array, Int8Array, ListArray, StringArray, StructArray};
6405 use datafusion::error::DataFusionError;
6406
6407 if map.is_null(row) {
6408 return Ok(null_value.clone());
6409 }
6410 let key = ScalarValue::try_from_array(keys, row)?;
6411 let Some(key) = scalar_access_key(&key)? else {
6412 return Ok(null_value.clone());
6413 };
6414 let tag = map
6415 .column_by_name("__het_tag")
6416 .and_then(|c| c.as_any().downcast_ref::<Int8Array>())
6417 .ok_or_else(|| DataFusionError::Plan("tagged value is missing __het_tag".into()))?;
6418 if tag.value(row) != 5 {
6419 return Err(DataFusionError::Execution(
6420 "invalid argument type: dynamic value access requires a map".into(),
6421 ));
6422 }
6423 let entries = map
6424 .column_by_name("__het_map")
6425 .and_then(|c| c.as_any().downcast_ref::<ListArray>())
6426 .ok_or_else(|| DataFusionError::Plan("tagged map value is missing __het_map".into()))?;
6427 if entries.is_null(row) {
6428 return Ok(null_value.clone());
6429 }
6430 let entry_values = entries.value(row);
6431 let entry_struct = entry_values
6432 .as_any()
6433 .downcast_ref::<StructArray>()
6434 .ok_or_else(|| DataFusionError::Plan("tagged map entries must be structs".into()))?;
6435 let map_keys = entry_struct
6436 .column_by_name("__het_mkey")
6437 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
6438 .ok_or_else(|| DataFusionError::Plan("tagged map entries must carry __het_mkey".into()))?;
6439 let map_values = entry_struct
6440 .column_by_name("__het_mval")
6441 .ok_or_else(|| DataFusionError::Plan("tagged map entries must carry __het_mval".into()))?;
6442 for idx in 0..entry_struct.len() {
6443 if !map_keys.is_null(idx) && map_keys.value(idx) == key {
6444 return ScalarValue::try_from_array(map_values, idx);
6445 }
6446 }
6447 Ok(null_value.clone())
6448}
6449
6450fn common_struct_field_type(
6451 fields: &datafusion::arrow::datatypes::Fields,
6452) -> datafusion::error::Result<DataType> {
6453 let mut dtype: Option<DataType> = None;
6454 for field in fields {
6455 let field_type = field.data_type();
6456 if matches!(field_type, DataType::Null) {
6457 continue;
6458 }
6459 match &dtype {
6460 None => dtype = Some(field_type.clone()),
6461 Some(prev) if prev == field_type => {}
6462 Some(prev) => {
6463 return Err(datafusion::error::DataFusionError::Plan(format!(
6464 "dynamic value access over mixed field types is not supported: {prev:?} and {field_type:?}"
6465 )));
6466 }
6467 }
6468 }
6469 Ok(dtype.unwrap_or(DataType::Null))
6470}
6471
6472fn scalar_access_key(s: &ScalarValue) -> datafusion::error::Result<Option<String>> {
6473 if s.is_null() {
6474 return Ok(None);
6475 }
6476 match s {
6477 ScalarValue::Utf8(v) | ScalarValue::LargeUtf8(v) | ScalarValue::Utf8View(v) => {
6478 Ok(v.clone())
6479 }
6480 other => Err(datafusion::error::DataFusionError::Execution(format!(
6481 "dynamic map/property access key must be a string, got {other:?}"
6482 ))),
6483 }
6484}
6485
6486static CYPHER_REVERSE: LazyLock<ScalarUDF> =
6496 LazyLock::new(|| ScalarUDF::new_from_impl(CypherReverse::new()));
6497
6498#[derive(Debug, PartialEq, Eq, Hash)]
6499struct CypherReverse {
6500 signature: Signature,
6501}
6502
6503impl CypherReverse {
6504 fn new() -> Self {
6505 Self {
6506 signature: Signature::any(1, Volatility::Immutable),
6507 }
6508 }
6509}
6510
6511impl ScalarUDFImpl for CypherReverse {
6512 fn as_any(&self) -> &dyn Any {
6513 self
6514 }
6515 fn name(&self) -> &'static str {
6516 "cypher_reverse"
6517 }
6518 fn signature(&self) -> &Signature {
6519 &self.signature
6520 }
6521 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6522 Ok(arg_types.first().cloned().unwrap_or(DataType::Null))
6524 }
6525
6526 fn invoke_with_args(
6527 &self,
6528 args: ScalarFunctionArgs,
6529 ) -> datafusion::error::Result<ColumnarValue> {
6530 use datafusion::arrow::array::{Array, ListArray, StringArray, UInt32Array};
6531 use datafusion::common::cast::as_list_array;
6532 use datafusion::error::DataFusionError;
6533 use std::sync::Arc;
6534
6535 let array = args.args[0].to_array(args.number_rows)?;
6536 match array.data_type() {
6537 DataType::Utf8 | DataType::LargeUtf8 => {
6538 let s = datafusion::arrow::compute::cast(&array, &DataType::Utf8)?;
6539 let s = s.as_any().downcast_ref::<StringArray>().ok_or_else(|| {
6540 DataFusionError::Internal("cypher_reverse: not a string array".into())
6541 })?;
6542 let out: StringArray = (0..s.len())
6543 .map(|i| (!s.is_null(i)).then(|| s.value(i).chars().rev().collect::<String>()))
6544 .collect();
6545 Ok(ColumnarValue::Array(Arc::new(out)))
6546 }
6547 DataType::List(_) => {
6548 let list = as_list_array(&array)?;
6549 let values = list.values();
6550 let offsets = list.offsets();
6551 let mut idx: Vec<u32> = Vec::with_capacity(values.len());
6554 for w in offsets.windows(2) {
6555 let (start, end) = (w[0], w[1]);
6556 for j in (start..end).rev() {
6557 idx.push(u32::try_from(j).unwrap_or(0));
6558 }
6559 }
6560 let taken =
6561 datafusion::arrow::compute::take(values, &UInt32Array::from(idx), None)?;
6562 let field = match list.data_type() {
6563 DataType::List(f) => Arc::clone(f),
6564 _ => unreachable!("matched List above"),
6565 };
6566 let reversed = ListArray::new(field, offsets.clone(), taken, list.nulls().cloned());
6567 Ok(ColumnarValue::Array(Arc::new(reversed)))
6568 }
6569 other => Err(DataFusionError::Plan(format!(
6570 "reverse() expects a string or list, got {other:?}"
6571 ))),
6572 }
6573 }
6574}
6575
6576static CYPHER_AND: LazyLock<ScalarUDF> =
6581 LazyLock::new(|| ScalarUDF::new_from_impl(CypherBoolOp::new(CypherBoolOpKind::And)));
6582static CYPHER_OR: LazyLock<ScalarUDF> =
6583 LazyLock::new(|| ScalarUDF::new_from_impl(CypherBoolOp::new(CypherBoolOpKind::Or)));
6584static CYPHER_XOR: LazyLock<ScalarUDF> =
6585 LazyLock::new(|| ScalarUDF::new_from_impl(CypherBoolOp::new(CypherBoolOpKind::Xor)));
6586
6587#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6588enum CypherBoolOpKind {
6589 And,
6590 Or,
6591 Xor,
6592}
6593
6594#[derive(Debug, PartialEq, Eq, Hash)]
6595struct CypherBoolOp {
6596 signature: Signature,
6597 kind: CypherBoolOpKind,
6598}
6599
6600impl CypherBoolOp {
6601 fn new(kind: CypherBoolOpKind) -> Self {
6602 Self {
6603 signature: Signature::any(2, Volatility::Immutable),
6604 kind,
6605 }
6606 }
6607}
6608
6609impl ScalarUDFImpl for CypherBoolOp {
6610 fn as_any(&self) -> &dyn Any {
6611 self
6612 }
6613 fn name(&self) -> &'static str {
6614 match self.kind {
6615 CypherBoolOpKind::And => "cypher_and",
6616 CypherBoolOpKind::Or => "cypher_or",
6617 CypherBoolOpKind::Xor => "cypher_xor",
6618 }
6619 }
6620 fn signature(&self) -> &Signature {
6621 &self.signature
6622 }
6623 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6624 Ok(DataType::Boolean)
6625 }
6626 fn invoke_with_args(
6627 &self,
6628 args: ScalarFunctionArgs,
6629 ) -> datafusion::error::Result<ColumnarValue> {
6630 use datafusion::arrow::array::BooleanArray;
6631 let rows = args.number_rows;
6632 let lhs = args.args[0].to_array(rows)?;
6633 let rhs = args.args[1].to_array(rows)?;
6634 let out: BooleanArray = (0..rows)
6635 .map(|i| {
6636 let l = ScalarValue::try_from_array(&lhs, i)?;
6637 let r = ScalarValue::try_from_array(&rhs, i)?;
6638 let l = scalar_as_bool(&l)?;
6639 let r = scalar_as_bool(&r)?;
6640 Ok::<Option<bool>, datafusion::error::DataFusionError>(match self.kind {
6641 CypherBoolOpKind::And => match (l, r) {
6642 (Some(false), _) | (_, Some(false)) => Some(false),
6643 (Some(true), Some(true)) => Some(true),
6644 _ => None,
6645 },
6646 CypherBoolOpKind::Or => match (l, r) {
6647 (Some(true), _) | (_, Some(true)) => Some(true),
6648 (Some(false), Some(false)) => Some(false),
6649 _ => None,
6650 },
6651 CypherBoolOpKind::Xor => match (l, r) {
6652 (Some(left), Some(right)) => Some(left ^ right),
6653 _ => None,
6654 },
6655 })
6656 })
6657 .collect::<datafusion::error::Result<Vec<_>>>()?
6658 .into_iter()
6659 .collect();
6660 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
6661 }
6662}
6663
6664fn scalar_as_bool(s: &ScalarValue) -> datafusion::error::Result<Option<bool>> {
6665 let s = unwrap_het(s.clone());
6666 if s.is_null() {
6667 return Ok(None);
6668 }
6669 match s {
6670 ScalarValue::Boolean(v) => Ok(v),
6671 other => Err(datafusion::error::DataFusionError::Plan(format!(
6672 "expected boolean operand, got {other:?}"
6673 ))),
6674 }
6675}
6676
6677static CYPHER_CMP_PRED: LazyLock<ScalarUDF> =
6678 LazyLock::new(|| ScalarUDF::new_from_impl(CypherCmpPred::new()));
6679
6680#[derive(Debug, PartialEq, Eq, Hash)]
6681struct CypherCmpPred {
6682 signature: Signature,
6683}
6684
6685impl CypherCmpPred {
6686 fn new() -> Self {
6687 Self {
6688 signature: Signature::any(3, Volatility::Immutable),
6689 }
6690 }
6691}
6692
6693impl ScalarUDFImpl for CypherCmpPred {
6694 fn as_any(&self) -> &dyn Any {
6695 self
6696 }
6697 fn name(&self) -> &'static str {
6698 "cypher_cmp_pred"
6699 }
6700 fn signature(&self) -> &Signature {
6701 &self.signature
6702 }
6703 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6704 Ok(DataType::Boolean)
6705 }
6706 fn invoke_with_args(
6707 &self,
6708 args: ScalarFunctionArgs,
6709 ) -> datafusion::error::Result<ColumnarValue> {
6710 use datafusion::arrow::array::BooleanArray;
6711 let rows = args.number_rows;
6712 let lhs = args.args[0].to_array(rows)?;
6713 let rhs = args.args[1].to_array(rows)?;
6714 let op = args.args[2].to_array(rows)?;
6715 let out: BooleanArray = (0..rows)
6716 .map(|i| {
6717 let l = ScalarValue::try_from_array(&lhs, i)?;
6718 let r = ScalarValue::try_from_array(&rhs, i)?;
6719 let op = ScalarValue::try_from_array(&op, i)?;
6720 let op = scalar_as_i8(&op)?;
6721 Ok::<Option<bool>, datafusion::error::DataFusionError>(cypher_compare_pred(
6722 &l, &r, op,
6723 ))
6724 })
6725 .collect::<datafusion::error::Result<Vec<_>>>()?
6726 .into_iter()
6727 .collect();
6728 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
6729 }
6730}
6731
6732fn scalar_as_i8(s: &ScalarValue) -> datafusion::error::Result<i8> {
6733 match s {
6734 ScalarValue::Int8(Some(v)) => Ok(*v),
6735 ScalarValue::Int64(Some(v)) => i8::try_from(*v).map_err(|_| {
6736 datafusion::error::DataFusionError::Plan(format!(
6737 "comparison opcode {v} is outside i8 range"
6738 ))
6739 }),
6740 other => Err(datafusion::error::DataFusionError::Plan(format!(
6741 "comparison opcode must be an integer, got {other:?}"
6742 ))),
6743 }
6744}
6745
6746fn cypher_compare_pred(l: &ScalarValue, r: &ScalarValue, op: i8) -> Option<bool> {
6747 let l = unwrap_het(l.clone());
6748 let r = unwrap_het(r.clone());
6749 if l.is_null() || r.is_null() {
6750 return None;
6751 }
6752 if is_numeric_scalar(&l) && is_numeric_scalar(&r) {
6753 let lf = scalar_as_f64(&l)?;
6754 let rf = scalar_as_f64(&r)?;
6755 if lf.is_nan() || rf.is_nan() {
6756 return Some(false);
6757 }
6758 }
6759 let cmp = cypher_compare(&l, &r)?;
6760 Some(match op {
6761 0 => cmp < 0,
6762 1 => cmp <= 0,
6763 2 => cmp > 0,
6764 3 => cmp >= 0,
6765 _ => false,
6766 })
6767}
6768
6769fn is_numeric_scalar(s: &ScalarValue) -> bool {
6770 scalar_as_f64(s).is_some()
6771}
6772
6773static CYPHER_STARTS_WITH: LazyLock<ScalarUDF> =
6774 LazyLock::new(|| ScalarUDF::new_from_impl(CypherStringPredicate::new(StringPredicate::Starts)));
6775static CYPHER_ENDS_WITH: LazyLock<ScalarUDF> =
6776 LazyLock::new(|| ScalarUDF::new_from_impl(CypherStringPredicate::new(StringPredicate::Ends)));
6777static CYPHER_CONTAINS: LazyLock<ScalarUDF> = LazyLock::new(|| {
6778 ScalarUDF::new_from_impl(CypherStringPredicate::new(StringPredicate::Contains))
6779});
6780
6781#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6782enum StringPredicate {
6783 Starts,
6784 Ends,
6785 Contains,
6786}
6787
6788#[derive(Debug, PartialEq, Eq, Hash)]
6789struct CypherStringPredicate {
6790 signature: Signature,
6791 kind: StringPredicate,
6792}
6793
6794impl CypherStringPredicate {
6795 fn new(kind: StringPredicate) -> Self {
6796 Self {
6797 signature: Signature::any(2, Volatility::Immutable),
6798 kind,
6799 }
6800 }
6801}
6802
6803impl ScalarUDFImpl for CypherStringPredicate {
6804 fn as_any(&self) -> &dyn Any {
6805 self
6806 }
6807 fn name(&self) -> &'static str {
6808 match self.kind {
6809 StringPredicate::Starts => "cypher_starts_with",
6810 StringPredicate::Ends => "cypher_ends_with",
6811 StringPredicate::Contains => "cypher_contains",
6812 }
6813 }
6814 fn signature(&self) -> &Signature {
6815 &self.signature
6816 }
6817 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6818 Ok(DataType::Boolean)
6819 }
6820 fn invoke_with_args(
6821 &self,
6822 args: ScalarFunctionArgs,
6823 ) -> datafusion::error::Result<ColumnarValue> {
6824 use datafusion::arrow::array::BooleanArray;
6825 let rows = args.number_rows;
6826 let lhs = args.args[0].to_array(rows)?;
6827 let rhs = args.args[1].to_array(rows)?;
6828 let out: BooleanArray = (0..rows)
6829 .map(|i| {
6830 let l = ScalarValue::try_from_array(&lhs, i).ok()?;
6831 let r = ScalarValue::try_from_array(&rhs, i).ok()?;
6832 let l = scalar_as_string(&l)?;
6833 let r = scalar_as_string(&r)?;
6834 Some(match self.kind {
6835 StringPredicate::Starts => l.starts_with(&r),
6836 StringPredicate::Ends => l.ends_with(&r),
6837 StringPredicate::Contains => l.contains(&r),
6838 })
6839 })
6840 .collect();
6841 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
6842 }
6843}
6844
6845fn scalar_as_string(s: &ScalarValue) -> Option<String> {
6846 let s = unwrap_het(s.clone());
6847 if s.is_null() {
6848 return None;
6849 }
6850 match s {
6851 ScalarValue::Utf8(v) | ScalarValue::LargeUtf8(v) => v,
6852 _ => None,
6853 }
6854}
6855
6856static CYPHER_RANGE: LazyLock<ScalarUDF> =
6857 LazyLock::new(|| ScalarUDF::new_from_impl(CypherRange::new()));
6858pub(crate) static CYPHER_ORDER_KEY: LazyLock<ScalarUDF> =
6859 LazyLock::new(|| ScalarUDF::new_from_impl(CypherOrderKey::new()));
6860
6861#[derive(Debug, PartialEq, Eq, Hash)]
6862struct CypherRange {
6863 signature: Signature,
6864}
6865
6866impl CypherRange {
6867 fn new() -> Self {
6868 Self {
6869 signature: Signature::any(3, Volatility::Volatile),
6873 }
6874 }
6875}
6876
6877impl ScalarUDFImpl for CypherRange {
6878 fn as_any(&self) -> &dyn Any {
6879 self
6880 }
6881 fn name(&self) -> &'static str {
6882 "cypher_range"
6883 }
6884 fn signature(&self) -> &Signature {
6885 &self.signature
6886 }
6887 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6888 Ok(DataType::new_list(DataType::Int64, true))
6889 }
6890 fn invoke_with_args(
6891 &self,
6892 args: ScalarFunctionArgs,
6893 ) -> datafusion::error::Result<ColumnarValue> {
6894 use datafusion::arrow::array::{Int64Builder, ListBuilder};
6895 let rows = args.number_rows;
6896 let starts = args.args[0].to_array(rows)?;
6897 let ends = args.args[1].to_array(rows)?;
6898 let steps = args.args[2].to_array(rows)?;
6899 let mut out = ListBuilder::new(Int64Builder::new());
6900 for i in 0..rows {
6901 let start = ScalarValue::try_from_array(&starts, i)?;
6902 let end = ScalarValue::try_from_array(&ends, i)?;
6903 let step = ScalarValue::try_from_array(&steps, i)?;
6904 if start.is_null() || end.is_null() || step.is_null() {
6905 out.append_null();
6906 continue;
6907 }
6908 let start = scalar_as_i64_arg(&start, "range start")?;
6909 let end = scalar_as_i64_arg(&end, "range end")?;
6910 let step = scalar_as_i64_arg(&step, "range step")?;
6911 if step == 0 {
6912 return Err(datafusion::error::DataFusionError::Plan(
6913 "range step must not be zero".into(),
6914 ));
6915 }
6916 if (step > 0 && start > end) || (step < 0 && start < end) {
6917 out.append(true);
6918 continue;
6919 }
6920 let mut cur = start;
6921 loop {
6922 out.values().append_value(cur);
6923 if cur == end {
6924 break;
6925 }
6926 let Some(next) = cur.checked_add(step) else {
6927 return Err(datafusion::error::DataFusionError::Plan(
6928 "range overflowed i64".into(),
6929 ));
6930 };
6931 if (step > 0 && next > end) || (step < 0 && next < end) {
6932 break;
6933 }
6934 cur = next;
6935 }
6936 out.append(true);
6937 }
6938 Ok(ColumnarValue::Array(std::sync::Arc::new(out.finish())))
6939 }
6940}
6941
6942fn scalar_as_i64_arg(s: &ScalarValue, name: &str) -> datafusion::error::Result<i64> {
6943 match s {
6944 ScalarValue::Int8(Some(v)) => Ok(i64::from(*v)),
6945 ScalarValue::Int16(Some(v)) => Ok(i64::from(*v)),
6946 ScalarValue::Int32(Some(v)) => Ok(i64::from(*v)),
6947 ScalarValue::Int64(Some(v)) => Ok(*v),
6948 ScalarValue::UInt8(Some(v)) => Ok(i64::from(*v)),
6949 ScalarValue::UInt16(Some(v)) => Ok(i64::from(*v)),
6950 ScalarValue::UInt32(Some(v)) => Ok(i64::from(*v)),
6951 ScalarValue::UInt64(Some(v)) => i64::try_from(*v).map_err(|_| {
6952 datafusion::error::DataFusionError::Plan(format!("{name} exceeds i64::MAX"))
6953 }),
6954 other => Err(datafusion::error::DataFusionError::Plan(format!(
6955 "{name} must be an integer, got {other:?}"
6956 ))),
6957 }
6958}
6959
6960#[derive(Debug, PartialEq, Eq, Hash)]
6961struct CypherOrderKey {
6962 signature: Signature,
6963}
6964
6965impl CypherOrderKey {
6966 fn new() -> Self {
6967 Self {
6968 signature: Signature::any(1, Volatility::Immutable),
6969 }
6970 }
6971}
6972
6973impl ScalarUDFImpl for CypherOrderKey {
6974 fn as_any(&self) -> &dyn Any {
6975 self
6976 }
6977 fn name(&self) -> &'static str {
6978 "cypher_order_key"
6979 }
6980 fn signature(&self) -> &Signature {
6981 &self.signature
6982 }
6983 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
6984 Ok(DataType::Utf8)
6985 }
6986 fn invoke_with_args(
6987 &self,
6988 args: ScalarFunctionArgs,
6989 ) -> datafusion::error::Result<ColumnarValue> {
6990 use datafusion::arrow::array::StringArray;
6991 let rows = args.number_rows;
6992 let values = args.args[0].to_array(rows)?;
6993 let out: StringArray = (0..rows)
6994 .map(|i| {
6995 let v = ScalarValue::try_from_array(&values, i).ok()?;
6996 Some(cypher_order_key(&v))
6997 })
6998 .collect();
6999 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
7000 }
7001}
7002
7003pub(crate) fn needs_cypher_order_key_type(t: &DataType) -> bool {
7004 matches!(
7005 t,
7006 DataType::List(_) | DataType::LargeList(_) | DataType::FixedSizeList(_, _)
7007 ) || (matches!(t, DataType::Struct(_))
7008 && !is_date_struct(t)
7009 && !is_localdatetime_struct(t)
7010 && !is_duration_struct(t))
7011}
7012
7013fn cypher_order_key(v: &ScalarValue) -> String {
7014 let v = unwrap_het(v.clone());
7015 if v.is_null() {
7016 return "99:null".to_string();
7017 }
7018 match &v {
7019 ScalarValue::Struct(s) if is_time_struct(&v.data_type()) => {
7020 let Some((nanos, offset)) = time_struct_parts(s, 0) else {
7021 return "99:null".to_string();
7022 };
7023 let instant = i128::from(nanos) - i128::from(offset) * 1_000_000_000;
7024 format!("55:time:{}", ordered_i128_key(instant))
7025 }
7026 ScalarValue::Struct(s) if is_datetime_struct(&v.data_type()) => {
7027 let Some((days, nanos, offset, _)) = datetime_struct_parts(s, 0) else {
7028 return "99:null".to_string();
7029 };
7030 let instant = i128::from(days) * 86_400_000_000_000 + i128::from(nanos)
7031 - i128::from(offset) * 1_000_000_000;
7032 format!("55:datetime:{}", ordered_i128_key(instant))
7033 }
7034 ScalarValue::Struct(s) if is_path_struct(s) => "50:path".to_string(),
7035 ScalarValue::Struct(s) if is_rel_struct(s) => "30:rel".to_string(),
7036 ScalarValue::Struct(s) if is_node_struct(s) => "20:node".to_string(),
7037 ScalarValue::Struct(_) => "10:map".to_string(),
7038 ScalarValue::List(a) => format!("40:list:{}", cypher_list_order_key(&a.value(0))),
7039 ScalarValue::LargeList(a) => format!("40:list:{}", cypher_list_order_key(&a.value(0))),
7040 ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => format!("60:str:{s}"),
7041 ScalarValue::Boolean(Some(b)) => format!("70:bool:{}", u8::from(*b)),
7042 ScalarValue::Int64(Some(n)) => {
7043 #[allow(
7044 clippy::cast_precision_loss,
7045 reason = "Cypher numeric order shares a number bucket across ints and floats"
7046 )]
7047 let n = *n as f64;
7048 format!("80:num:{}", ordered_f64_key(n))
7049 }
7050 ScalarValue::Float64(Some(f)) if f.is_nan() => "90:nan".to_string(),
7051 ScalarValue::Float64(Some(f)) => format!("80:num:{}", ordered_f64_key(*f)),
7052 _ => "98:other".to_string(),
7053 }
7054}
7055
7056fn cypher_list_order_key(values: &datafusion::arrow::array::ArrayRef) -> String {
7057 let mut out = String::new();
7058 for i in 0..values.len() {
7059 let v = ScalarValue::try_from_array(values, i).unwrap_or(ScalarValue::Null);
7060 out.push_str(&cypher_order_key(&v));
7061 out.push('|');
7062 }
7063 out.push_str("00:end");
7064 out
7065}
7066
7067fn ordered_f64_key(f: f64) -> String {
7068 let bits = f.to_bits();
7069 let key = if (bits >> 63) == 0 {
7070 bits | (1 << 63)
7071 } else {
7072 !bits
7073 };
7074 format!("{key:016x}")
7075}
7076
7077fn ordered_i128_key(value: i128) -> String {
7078 let key = value.cast_unsigned() ^ (1_u128 << 127);
7079 format!("{key:032x}")
7080}
7081
7082fn is_node_struct(s: &datafusion::arrow::array::StructArray) -> bool {
7083 s.column_by_name("node_uuid").is_some() || s.column_by_name("labels").is_some()
7084}
7085
7086fn is_rel_struct(s: &datafusion::arrow::array::StructArray) -> bool {
7087 s.column_by_name("edge_uuid").is_some()
7088 || s.column_by_name("src_uuid").is_some()
7089 || s.column_by_name("dst_uuid").is_some()
7090}
7091
7092fn is_path_struct(s: &datafusion::arrow::array::StructArray) -> bool {
7093 s.column_by_name("nodes").is_some() || s.column_by_name("relationships").is_some()
7094}
7095
7096static CYPHER_EQ: LazyLock<ScalarUDF> = LazyLock::new(|| ScalarUDF::new_from_impl(CypherEq::new()));
7106
7107#[derive(Debug, PartialEq, Eq, Hash)]
7108struct CypherEq {
7109 signature: Signature,
7110}
7111
7112impl CypherEq {
7113 fn new() -> Self {
7114 Self {
7115 signature: Signature::any(2, Volatility::Immutable),
7116 }
7117 }
7118}
7119
7120impl ScalarUDFImpl for CypherEq {
7121 fn as_any(&self) -> &dyn Any {
7122 self
7123 }
7124
7125 fn name(&self) -> &'static str {
7126 "cypher_eq"
7127 }
7128
7129 fn signature(&self) -> &Signature {
7130 &self.signature
7131 }
7132
7133 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
7134 Ok(DataType::Boolean)
7135 }
7136
7137 fn simplify(
7147 &self,
7148 args: Vec<DfExpr>,
7149 info: &datafusion::logical_expr::simplify::SimplifyContext,
7150 ) -> datafusion::error::Result<datafusion::logical_expr::simplify::ExprSimplifyResult> {
7151 use datafusion::logical_expr::simplify::ExprSimplifyResult;
7152 let [l, r] = args.as_slice() else {
7153 return Ok(ExprSimplifyResult::Original(args));
7154 };
7155 let nested = |t: &DataType| {
7156 matches!(
7157 t,
7158 DataType::List(_) | DataType::LargeList(_) | DataType::Struct(_)
7159 )
7160 };
7161 let floaty =
7162 |t: &DataType| matches!(t, DataType::Float16 | DataType::Float32 | DataType::Float64);
7163 let placeholder = |e: &DfExpr| matches!(e, DfExpr::Placeholder(_));
7167 let keep_udf = if placeholder(l) || placeholder(r) {
7168 false
7169 } else if let (Ok(lt), Ok(rt)) = (info.get_data_type(l), info.get_data_type(r)) {
7170 nested(<) || nested(&rt) || floaty(<) || floaty(&rt) || lt != rt
7171 } else {
7172 false };
7174 if keep_udf {
7175 return Ok(ExprSimplifyResult::Original(args));
7176 }
7177 let [l, r]: [DfExpr; 2] = args.try_into().expect("checked length 2 above");
7178 Ok(ExprSimplifyResult::Simplified(l.eq(r)))
7179 }
7180
7181 fn invoke_with_args(
7182 &self,
7183 args: ScalarFunctionArgs,
7184 ) -> datafusion::error::Result<ColumnarValue> {
7185 use datafusion::arrow::array::BooleanArray;
7186 use datafusion::arrow::compute::kernels::cmp::eq;
7187
7188 let rows = args.number_rows;
7189 let lhs = args.args[0].to_array(rows)?;
7190 let rhs = args.args[1].to_array(rows)?;
7191 let (lt, rt) = (lhs.data_type(), rhs.data_type());
7192
7193 let nested = |t: &DataType| {
7194 matches!(
7195 t,
7196 DataType::List(_) | DataType::LargeList(_) | DataType::Struct(_)
7197 )
7198 };
7199
7200 let floaty =
7201 |t: &DataType| matches!(t, DataType::Float16 | DataType::Float32 | DataType::Float64);
7202
7203 if lt == rt && !nested(lt) && !floaty(lt) {
7207 let res = eq(&lhs, &rhs)?;
7208 return Ok(ColumnarValue::Array(std::sync::Arc::new(res)));
7209 }
7210
7211 let out: BooleanArray = (0..rows)
7215 .map(|i| {
7216 let l = ScalarValue::try_from_array(&lhs, i).ok()?;
7217 let r = ScalarValue::try_from_array(&rhs, i).ok()?;
7218 cypher_value_eq(&l, &r)
7219 })
7220 .collect();
7221 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
7222 }
7223}
7224
7225static CYPHER_IN: LazyLock<ScalarUDF> = LazyLock::new(|| ScalarUDF::new_from_impl(CypherIn::new()));
7231
7232#[derive(Debug, PartialEq, Eq, Hash)]
7233struct CypherIn {
7234 signature: Signature,
7235}
7236
7237impl CypherIn {
7238 fn new() -> Self {
7239 Self {
7240 signature: Signature::any(2, Volatility::Immutable),
7241 }
7242 }
7243}
7244
7245enum ListView<'a> {
7246 Fixed(&'a FixedSizeListArray),
7247 List(&'a ListArray),
7248 Large(&'a LargeListArray),
7249}
7250
7251impl ListView<'_> {
7252 fn from_array(array: &datafusion::arrow::array::ArrayRef) -> Option<ListView<'_>> {
7253 if let Some(list) = array.as_any().downcast_ref::<FixedSizeListArray>() {
7254 Some(ListView::Fixed(list))
7255 } else if let Some(list) = array.as_any().downcast_ref::<ListArray>() {
7256 Some(ListView::List(list))
7257 } else {
7258 array
7259 .as_any()
7260 .downcast_ref::<LargeListArray>()
7261 .map(ListView::Large)
7262 }
7263 }
7264
7265 fn is_null(&self, row: usize) -> bool {
7266 match self {
7267 Self::Fixed(a) => a.is_null(row),
7268 Self::List(a) => a.is_null(row),
7269 Self::Large(a) => a.is_null(row),
7270 }
7271 }
7272
7273 fn value(&self, row: usize) -> datafusion::arrow::array::ArrayRef {
7274 match self {
7275 Self::Fixed(a) => a.value(row),
7276 Self::List(a) => a.value(row),
7277 Self::Large(a) => a.value(row),
7278 }
7279 }
7280}
7281
7282fn cypher_in_elems(lhs: &ScalarValue, elems: &datafusion::arrow::array::ArrayRef) -> Option<bool> {
7283 let mut saw_null = false;
7284 for j in 0..elems.len() {
7285 let ev = ScalarValue::try_from_array(elems, j).ok()?;
7286 match cypher_value_eq(lhs, &ev) {
7287 Some(true) => return Some(true),
7288 None => saw_null = true,
7289 Some(false) => {}
7290 }
7291 }
7292 if saw_null { None } else { Some(false) }
7293}
7294
7295fn cypher_in_tagged_list(
7296 lhs: &ScalarValue,
7297 rhs: &datafusion::arrow::array::ArrayRef,
7298 row: usize,
7299) -> Option<bool> {
7300 let rv = ScalarValue::try_from_array(rhs, row).ok()?;
7301 match unwrap_het(rv) {
7302 ScalarValue::List(list) => {
7303 if list.is_null(0) {
7304 None
7305 } else {
7306 cypher_in_elems(lhs, &list.value(0))
7307 }
7308 }
7309 ScalarValue::LargeList(list) => {
7310 if list.is_null(0) {
7311 None
7312 } else {
7313 cypher_in_elems(lhs, &list.value(0))
7314 }
7315 }
7316 _ => None,
7317 }
7318}
7319
7320impl ScalarUDFImpl for CypherIn {
7321 fn as_any(&self) -> &dyn Any {
7322 self
7323 }
7324 fn name(&self) -> &'static str {
7325 "cypher_in"
7326 }
7327 fn signature(&self) -> &Signature {
7328 &self.signature
7329 }
7330 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
7331 Ok(DataType::Boolean)
7332 }
7333
7334 fn invoke_with_args(
7335 &self,
7336 args: ScalarFunctionArgs,
7337 ) -> datafusion::error::Result<ColumnarValue> {
7338 use datafusion::arrow::array::BooleanArray;
7339 let rows = args.number_rows;
7340 let lhs = args.args[0].to_array(rows)?;
7341 let rhs = args.args[1].to_array(rows)?;
7342 if is_het_struct_type(Some(rhs.data_type())) {
7343 let out: BooleanArray = (0..rows)
7344 .map(|i| {
7345 let lv = ScalarValue::try_from_array(&lhs, i).ok()?;
7346 cypher_in_tagged_list(&lv, &rhs, i)
7347 })
7348 .collect();
7349 return Ok(ColumnarValue::Array(std::sync::Arc::new(out)));
7350 }
7351 let list = if let Some(list) = rhs.as_any().downcast_ref::<FixedSizeListArray>() {
7352 ListView::Fixed(list)
7353 } else if let Some(list) = rhs.as_any().downcast_ref::<ListArray>() {
7354 ListView::List(list)
7355 } else if let Some(list) = rhs.as_any().downcast_ref::<LargeListArray>() {
7356 ListView::Large(list)
7357 } else {
7358 return Ok(ColumnarValue::Array(std::sync::Arc::new(
7359 BooleanArray::new_null(rows),
7360 )));
7361 };
7362 let out: BooleanArray = (0..rows)
7363 .map(|i| {
7364 if list.is_null(i) {
7365 return None; }
7367 let lv = ScalarValue::try_from_array(&lhs, i).ok()?;
7368 let elems = list.value(i);
7369 cypher_in_elems(&lv, &elems)
7370 })
7371 .collect();
7372 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
7373 }
7374}
7375
7376fn cypher_compare(a: &ScalarValue, b: &ScalarValue) -> Option<i8> {
7384 let to_i8 = |o: std::cmp::Ordering| o as i8;
7385 let a = unwrap_het(a.clone());
7386 let b = unwrap_het(b.clone());
7387 if a.is_null() || b.is_null() {
7388 return None;
7389 }
7390 match (&a, &b) {
7391 _ if is_numeric_scalar(&a) && is_numeric_scalar(&b) => {
7392 if let (Some(x), Some(y)) = (scalar_as_i128(&a), scalar_as_i128(&b)) {
7393 Some(to_i8(x.cmp(&y)))
7394 } else {
7395 scalar_as_f64(&a)?
7396 .partial_cmp(&scalar_as_f64(&b)?)
7397 .map(to_i8)
7398 }
7399 }
7400 (ScalarValue::Utf8(Some(x)), ScalarValue::Utf8(Some(y))) => Some(to_i8(x.cmp(y))),
7401 (ScalarValue::Boolean(Some(x)), ScalarValue::Boolean(Some(y))) => Some(to_i8(x.cmp(y))),
7402 (ScalarValue::Time64Nanosecond(Some(x)), ScalarValue::Time64Nanosecond(Some(y))) => {
7403 Some(to_i8(x.cmp(y)))
7404 }
7405 (ScalarValue::List(x), ScalarValue::List(y)) => {
7406 cypher_seq_compare(&x.value(0), &y.value(0))
7407 }
7408 (ScalarValue::Struct(x), ScalarValue::Struct(y))
7409 if is_date_struct(&a.data_type()) && is_date_struct(&b.data_type()) =>
7410 {
7411 Some(to_i8(
7412 date_struct_value(x, 0)?.cmp(&date_struct_value(y, 0)?),
7413 ))
7414 }
7415 (ScalarValue::Struct(x), ScalarValue::Struct(y))
7416 if is_localdatetime_struct(&a.data_type())
7417 && is_localdatetime_struct(&b.data_type()) =>
7418 {
7419 Some(to_i8(
7420 localdatetime_struct_parts(x, 0)?.cmp(&localdatetime_struct_parts(y, 0)?),
7421 ))
7422 }
7423 (ScalarValue::Struct(x), ScalarValue::Struct(y))
7426 if is_time_struct(&a.data_type()) && is_time_struct(&b.data_type()) =>
7427 {
7428 let (xn, xo) = time_struct_parts(x, 0)?;
7429 let (yn, yo) = time_struct_parts(y, 0)?;
7430 let xi = i128::from(xn) - i128::from(xo) * 1_000_000_000;
7431 let yi = i128::from(yn) - i128::from(yo) * 1_000_000_000;
7432 Some(to_i8(xi.cmp(&yi)))
7433 }
7434 (ScalarValue::Struct(x), ScalarValue::Struct(y))
7435 if is_datetime_struct(&a.data_type()) && is_datetime_struct(&b.data_type()) =>
7436 {
7437 let (xd, xn, xo, _) = datetime_struct_parts(x, 0)?;
7438 let (yd, yn, yo, _) = datetime_struct_parts(y, 0)?;
7439 let xi = i128::from(xd) * 86_400_000_000_000 + i128::from(xn)
7440 - i128::from(xo) * 1_000_000_000;
7441 let yi = i128::from(yd) * 86_400_000_000_000 + i128::from(yn)
7442 - i128::from(yo) * 1_000_000_000;
7443 Some(to_i8(xi.cmp(&yi)))
7444 }
7445 _ => None, }
7447}
7448
7449fn cypher_seq_compare(
7451 a: &datafusion::arrow::array::ArrayRef,
7452 b: &datafusion::arrow::array::ArrayRef,
7453) -> Option<i8> {
7454 let common = a.len().min(b.len());
7455 for i in 0..common {
7456 let av = ScalarValue::try_from_array(a, i).ok()?;
7457 let bv = ScalarValue::try_from_array(b, i).ok()?;
7458 match cypher_compare(&av, &bv)? {
7459 0 => {}
7460 c => return Some(c),
7461 }
7462 }
7463 Some(a.len().cmp(&b.len()) as i8) }
7465
7466fn is_list_literal(e: &DfExpr) -> bool {
7468 matches!(e, DfExpr::Literal(ScalarValue::List(_), _))
7469}
7470
7471pub(crate) fn is_het_struct_type(t: Option<&DataType>) -> bool {
7474 matches!(t, Some(DataType::Struct(fields)) if fields.iter().any(|f| f.name() == "__het_tag"))
7475}
7476
7477fn cypher_order(a: &ScalarValue, b: &ScalarValue) -> std::cmp::Ordering {
7484 use std::cmp::Ordering;
7485 let a = unwrap_het(a.clone());
7486 let b = unwrap_het(b.clone());
7487 let rank = |v: &ScalarValue| -> u8 {
7488 match v {
7489 ScalarValue::List(_) | ScalarValue::LargeList(_) => 1,
7490 ScalarValue::Utf8(_) | ScalarValue::LargeUtf8(_) => 2,
7491 ScalarValue::Boolean(_) => 3,
7492 _ if is_numeric_scalar(v) => 4,
7493 ScalarValue::Struct(_) => 5,
7494 _ => 0,
7495 }
7496 };
7497 let (ra, rb) = (rank(&a), rank(&b));
7498 if ra != rb {
7499 return ra.cmp(&rb);
7500 }
7501 match (&a, &b) {
7502 (ScalarValue::List(x), ScalarValue::List(y)) => cypher_seq_order(&x.value(0), &y.value(0)),
7503 (ScalarValue::Utf8(Some(x)), ScalarValue::Utf8(Some(y))) => x.cmp(y),
7504 (ScalarValue::Boolean(Some(x)), ScalarValue::Boolean(Some(y))) => x.cmp(y),
7505 (ScalarValue::Struct(x), ScalarValue::Struct(y)) => cypher_map_order(x, y),
7506 _ => match (scalar_as_f64(&a), scalar_as_f64(&b)) {
7507 (Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
7508 _ => Ordering::Equal,
7509 },
7510 }
7511}
7512
7513fn cypher_map_order(
7517 a: &datafusion::arrow::array::StructArray,
7518 b: &datafusion::arrow::array::StructArray,
7519) -> std::cmp::Ordering {
7520 let sorted = |s: &datafusion::arrow::array::StructArray| -> Vec<(String, ScalarValue)> {
7521 let mut e: Vec<(String, ScalarValue)> = s
7522 .fields()
7523 .iter()
7524 .enumerate()
7525 .map(|(i, f)| {
7526 (
7527 f.name().clone(),
7528 ScalarValue::try_from_array(s.column(i), 0).unwrap_or(ScalarValue::Null),
7529 )
7530 })
7531 .collect();
7532 e.sort_by(|x, y| x.0.cmp(&y.0));
7533 e
7534 };
7535 let (ea, eb) = (sorted(a), sorted(b));
7536 for ((ka, va), (kb, vb)) in ea.iter().zip(eb.iter()) {
7537 match ka.cmp(kb) {
7538 std::cmp::Ordering::Equal => {}
7539 c => return c,
7540 }
7541 match cypher_order(va, vb) {
7542 std::cmp::Ordering::Equal => {}
7543 c => return c,
7544 }
7545 }
7546 ea.len().cmp(&eb.len())
7547}
7548
7549fn cypher_seq_order(
7552 a: &datafusion::arrow::array::ArrayRef,
7553 b: &datafusion::arrow::array::ArrayRef,
7554) -> std::cmp::Ordering {
7555 let common = a.len().min(b.len());
7556 for i in 0..common {
7557 let av = ScalarValue::try_from_array(a, i).unwrap_or(ScalarValue::Null);
7558 let bv = ScalarValue::try_from_array(b, i).unwrap_or(ScalarValue::Null);
7559 match cypher_order(&av, &bv) {
7560 std::cmp::Ordering::Equal => {}
7561 c => return c,
7562 }
7563 }
7564 a.len().cmp(&b.len())
7565}
7566
7567pub(crate) static CYPHER_MAX: LazyLock<datafusion::logical_expr::AggregateUDF> =
7571 LazyLock::new(|| {
7572 datafusion::logical_expr::AggregateUDF::new_from_impl(CypherExtreme::new(true))
7573 });
7574pub(crate) static CYPHER_MIN: LazyLock<datafusion::logical_expr::AggregateUDF> =
7575 LazyLock::new(|| {
7576 datafusion::logical_expr::AggregateUDF::new_from_impl(CypherExtreme::new(false))
7577 });
7578pub(crate) static CYPHER_COLLECT: LazyLock<datafusion::logical_expr::AggregateUDF> =
7579 LazyLock::new(|| {
7580 datafusion::logical_expr::AggregateUDF::new_from_impl(CypherCollect::new(false))
7581 });
7582pub(crate) static CYPHER_COLLECT_DISTINCT: LazyLock<datafusion::logical_expr::AggregateUDF> =
7583 LazyLock::new(|| {
7584 datafusion::logical_expr::AggregateUDF::new_from_impl(CypherCollect::new(true))
7585 });
7586pub(crate) static CYPHER_PERCENTILE_DISC: LazyLock<datafusion::logical_expr::AggregateUDF> =
7587 LazyLock::new(|| {
7588 datafusion::logical_expr::AggregateUDF::new_from_impl(CypherPercentile::new(false))
7589 });
7590pub(crate) static CYPHER_PERCENTILE_CONT: LazyLock<datafusion::logical_expr::AggregateUDF> =
7591 LazyLock::new(|| {
7592 datafusion::logical_expr::AggregateUDF::new_from_impl(CypherPercentile::new(true))
7593 });
7594
7595#[derive(Debug)]
7596struct CypherExtreme {
7597 signature: Signature,
7598 is_max: bool,
7599}
7600impl CypherExtreme {
7601 fn new(is_max: bool) -> Self {
7602 Self {
7603 signature: Signature::any(1, Volatility::Immutable),
7604 is_max,
7605 }
7606 }
7607}
7608impl PartialEq for CypherExtreme {
7609 fn eq(&self, o: &Self) -> bool {
7610 self.is_max == o.is_max
7611 }
7612}
7613impl Eq for CypherExtreme {}
7614impl std::hash::Hash for CypherExtreme {
7615 fn hash<H: std::hash::Hasher>(&self, st: &mut H) {
7616 self.is_max.hash(st);
7617 }
7618}
7619impl datafusion::logical_expr::AggregateUDFImpl for CypherExtreme {
7620 fn as_any(&self) -> &dyn Any {
7621 self
7622 }
7623 fn name(&self) -> &str {
7624 if self.is_max {
7625 "cypher_max"
7626 } else {
7627 "cypher_min"
7628 }
7629 }
7630 fn signature(&self) -> &Signature {
7631 &self.signature
7632 }
7633 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
7634 Ok(arg_types[0].clone())
7635 }
7636 fn accumulator(
7637 &self,
7638 args: datafusion::logical_expr::function::AccumulatorArgs,
7639 ) -> datafusion::error::Result<Box<dyn datafusion::logical_expr::Accumulator>> {
7640 Ok(Box::new(ExtremeAcc {
7641 is_max: self.is_max,
7642 dtype: args.return_field.data_type().clone(),
7643 best: None,
7644 }))
7645 }
7646 fn state_fields(
7647 &self,
7648 args: datafusion::logical_expr::function::StateFieldsArgs,
7649 ) -> datafusion::error::Result<Vec<datafusion::arrow::datatypes::FieldRef>> {
7650 use datafusion::arrow::datatypes::Field;
7651 Ok(vec![std::sync::Arc::new(Field::new(
7652 "best",
7653 args.return_field.data_type().clone(),
7654 true,
7655 ))])
7656 }
7657}
7658
7659#[derive(Debug)]
7660struct ExtremeAcc {
7661 is_max: bool,
7662 dtype: DataType,
7663 best: Option<ScalarValue>,
7664}
7665impl datafusion::logical_expr::Accumulator for ExtremeAcc {
7666 fn update_batch(
7667 &mut self,
7668 values: &[datafusion::arrow::array::ArrayRef],
7669 ) -> datafusion::error::Result<()> {
7670 use datafusion::arrow::array::Array;
7671 let arr = &values[0];
7672 for i in 0..arr.len() {
7673 if arr.is_null(i) {
7674 continue;
7675 }
7676 let v = ScalarValue::try_from_array(arr, i)?;
7677 if v.is_null() {
7678 continue;
7679 }
7680 let take = match &self.best {
7681 None => true,
7682 Some(b) => {
7683 let ord = cypher_order(&v, b);
7684 (self.is_max && ord == std::cmp::Ordering::Greater)
7685 || (!self.is_max && ord == std::cmp::Ordering::Less)
7686 }
7687 };
7688 if take {
7689 self.best = Some(v);
7690 }
7691 }
7692 Ok(())
7693 }
7694 fn evaluate(&mut self) -> datafusion::error::Result<ScalarValue> {
7695 match &self.best {
7696 Some(b) => Ok(b.clone()),
7697 None => ScalarValue::try_from(&self.dtype),
7698 }
7699 }
7700 fn size(&self) -> usize {
7701 std::mem::size_of_val(self) + self.best.as_ref().map_or(0, ScalarValue::size)
7702 }
7703 fn state(&mut self) -> datafusion::error::Result<Vec<ScalarValue>> {
7704 Ok(vec![self.evaluate()?])
7705 }
7706 fn merge_batch(
7707 &mut self,
7708 states: &[datafusion::arrow::array::ArrayRef],
7709 ) -> datafusion::error::Result<()> {
7710 self.update_batch(states)
7711 }
7712}
7713
7714#[derive(Debug)]
7715struct CypherCollect {
7716 signature: Signature,
7717 distinct: bool,
7718}
7719
7720impl CypherCollect {
7721 fn new(distinct: bool) -> Self {
7722 Self {
7723 signature: Signature::any(1, Volatility::Immutable),
7724 distinct,
7725 }
7726 }
7727}
7728
7729impl PartialEq for CypherCollect {
7730 fn eq(&self, o: &Self) -> bool {
7731 self.distinct == o.distinct
7732 }
7733}
7734
7735impl Eq for CypherCollect {}
7736
7737impl std::hash::Hash for CypherCollect {
7738 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
7739 self.distinct.hash(state);
7740 }
7741}
7742
7743impl datafusion::logical_expr::AggregateUDFImpl for CypherCollect {
7744 fn as_any(&self) -> &dyn Any {
7745 self
7746 }
7747 fn name(&self) -> &str {
7748 if self.distinct {
7749 "cypher_collect_distinct"
7750 } else {
7751 "cypher_collect"
7752 }
7753 }
7754 fn signature(&self) -> &Signature {
7755 &self.signature
7756 }
7757 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
7758 Ok(DataType::new_list(arg_types[0].clone(), true))
7759 }
7760 fn accumulator(
7761 &self,
7762 args: datafusion::logical_expr::function::AccumulatorArgs,
7763 ) -> datafusion::error::Result<Box<dyn datafusion::logical_expr::Accumulator>> {
7764 let DataType::List(field) = args.return_field.data_type() else {
7765 return Err(datafusion::error::DataFusionError::Plan(
7766 "cypher_collect return type must be a list".into(),
7767 ));
7768 };
7769 Ok(Box::new(CollectAcc {
7770 distinct: self.distinct,
7771 elem_type: field.data_type().clone(),
7772 values: Vec::new(),
7773 }))
7774 }
7775 fn state_fields(
7776 &self,
7777 args: datafusion::logical_expr::function::StateFieldsArgs,
7778 ) -> datafusion::error::Result<Vec<datafusion::arrow::datatypes::FieldRef>> {
7779 use datafusion::arrow::datatypes::Field;
7780 Ok(vec![std::sync::Arc::new(Field::new(
7781 "values",
7782 args.return_field.data_type().clone(),
7783 true,
7784 ))])
7785 }
7786}
7787
7788#[derive(Debug)]
7789struct CollectAcc {
7790 distinct: bool,
7791 elem_type: DataType,
7792 values: Vec<ScalarValue>,
7793}
7794
7795impl CollectAcc {
7796 fn push_value(&mut self, v: ScalarValue) {
7797 if v.is_null() {
7798 return;
7799 }
7800 if self.distinct
7801 && self
7802 .values
7803 .iter()
7804 .any(|seen| cypher_value_eq(seen, &v) == Some(true))
7805 {
7806 return;
7807 }
7808 self.values.push(v);
7809 }
7810
7811 fn as_list(&self) -> ScalarValue {
7812 ScalarValue::List(ScalarValue::new_list(&self.values, &self.elem_type, true))
7813 }
7814}
7815
7816impl datafusion::logical_expr::Accumulator for CollectAcc {
7817 fn update_batch(
7818 &mut self,
7819 values: &[datafusion::arrow::array::ArrayRef],
7820 ) -> datafusion::error::Result<()> {
7821 use datafusion::arrow::array::Array;
7822 let arr = &values[0];
7823 for i in 0..arr.len() {
7824 if arr.is_null(i) {
7825 continue;
7826 }
7827 self.push_value(ScalarValue::try_from_array(arr, i)?);
7828 }
7829 Ok(())
7830 }
7831 fn evaluate(&mut self) -> datafusion::error::Result<ScalarValue> {
7832 Ok(self.as_list())
7833 }
7834 fn size(&self) -> usize {
7835 std::mem::size_of_val(self) + self.values.iter().map(ScalarValue::size).sum::<usize>()
7836 }
7837 fn state(&mut self) -> datafusion::error::Result<Vec<ScalarValue>> {
7838 Ok(vec![self.as_list()])
7839 }
7840 fn merge_batch(
7841 &mut self,
7842 states: &[datafusion::arrow::array::ArrayRef],
7843 ) -> datafusion::error::Result<()> {
7844 use datafusion::arrow::array::{Array, ListArray};
7845 let arr = &states[0];
7846 let Some(list) = arr.as_any().downcast_ref::<ListArray>() else {
7847 return Err(datafusion::error::DataFusionError::Plan(
7848 "cypher_collect state must be a list".into(),
7849 ));
7850 };
7851 for row in 0..list.len() {
7852 if list.is_null(row) {
7853 continue;
7854 }
7855 let values = list.value(row);
7856 for i in 0..values.len() {
7857 if values.is_null(i) {
7858 continue;
7859 }
7860 self.push_value(ScalarValue::try_from_array(&values, i)?);
7861 }
7862 }
7863 Ok(())
7864 }
7865}
7866
7867#[derive(Debug)]
7868struct CypherPercentile {
7869 signature: Signature,
7870 continuous: bool,
7871}
7872
7873impl CypherPercentile {
7874 fn new(continuous: bool) -> Self {
7875 Self {
7876 signature: Signature::any(2, Volatility::Immutable),
7877 continuous,
7878 }
7879 }
7880}
7881
7882impl PartialEq for CypherPercentile {
7883 fn eq(&self, o: &Self) -> bool {
7884 self.continuous == o.continuous
7885 }
7886}
7887
7888impl Eq for CypherPercentile {}
7889
7890impl std::hash::Hash for CypherPercentile {
7891 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
7892 self.continuous.hash(state);
7893 }
7894}
7895
7896impl datafusion::logical_expr::AggregateUDFImpl for CypherPercentile {
7897 fn as_any(&self) -> &dyn Any {
7898 self
7899 }
7900
7901 fn name(&self) -> &str {
7902 if self.continuous {
7903 "cypher_percentile_cont"
7904 } else {
7905 "cypher_percentile_disc"
7906 }
7907 }
7908
7909 fn signature(&self) -> &Signature {
7910 &self.signature
7911 }
7912
7913 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
7914 let Some(value_type) = arg_types.first() else {
7915 return Err(datafusion::error::DataFusionError::Plan(
7916 "percentile aggregate requires value and percentile arguments".into(),
7917 ));
7918 };
7919 if !is_percentile_numeric_type(value_type) {
7920 return Err(datafusion::error::DataFusionError::Plan(format!(
7921 "percentile value expression must be numeric, got {value_type}"
7922 )));
7923 }
7924 if self.continuous {
7925 Ok(DataType::Float64)
7926 } else {
7927 Ok(value_type.clone())
7928 }
7929 }
7930
7931 fn accumulator(
7932 &self,
7933 args: datafusion::logical_expr::function::AccumulatorArgs,
7934 ) -> datafusion::error::Result<Box<dyn datafusion::logical_expr::Accumulator>> {
7935 let value_type = args.expr_fields.first().map_or_else(
7936 || args.return_field.data_type().clone(),
7937 |f| f.data_type().clone(),
7938 );
7939 Ok(Box::new(PercentileAcc {
7940 continuous: self.continuous,
7941 value_type,
7942 result_type: args.return_field.data_type().clone(),
7943 values: Vec::new(),
7944 percentile: None,
7945 }))
7946 }
7947
7948 fn state_fields(
7949 &self,
7950 args: datafusion::logical_expr::function::StateFieldsArgs,
7951 ) -> datafusion::error::Result<Vec<datafusion::arrow::datatypes::FieldRef>> {
7952 use datafusion::arrow::datatypes::Field;
7953 let value_type = args.input_fields.first().map_or_else(
7954 || args.return_field.data_type().clone(),
7955 |f| f.data_type().clone(),
7956 );
7957 Ok(vec![
7958 std::sync::Arc::new(Field::new(
7959 "values",
7960 DataType::new_list(value_type, true),
7961 true,
7962 )),
7963 std::sync::Arc::new(Field::new("percentile", DataType::Float64, true)),
7964 ])
7965 }
7966}
7967
7968#[derive(Debug)]
7969struct PercentileAcc {
7970 continuous: bool,
7971 value_type: DataType,
7972 result_type: DataType,
7973 values: Vec<ScalarValue>,
7974 percentile: Option<f64>,
7975}
7976
7977impl PercentileAcc {
7978 fn push_value(&mut self, v: ScalarValue) -> datafusion::error::Result<()> {
7979 if v.is_null() {
7980 return Ok(());
7981 }
7982 if scalar_as_f64(&v).is_none() {
7983 return Err(datafusion::error::DataFusionError::Execution(format!(
7984 "percentile value expression must be numeric, got {}",
7985 v.data_type()
7986 )));
7987 }
7988 self.values.push(v);
7989 Ok(())
7990 }
7991
7992 fn observe_percentile(&mut self, p: Option<f64>) -> datafusion::error::Result<()> {
7993 let Some(p) = p else {
7994 return Ok(());
7995 };
7996 if !p.is_finite() || !(0.0..=1.0).contains(&p) {
7997 return Err(datafusion::error::DataFusionError::Execution(format!(
7998 "percentile argument must be a finite number between 0.0 and 1.0 inclusive, got {p}"
7999 )));
8000 }
8001 match self.percentile {
8002 Some(existing) if (existing - p).abs() > f64::EPSILON => {
8003 Err(datafusion::error::DataFusionError::Execution(
8004 "percentile argument must be constant within an aggregate group".into(),
8005 ))
8006 }
8007 Some(_) => Ok(()),
8008 None => {
8009 self.percentile = Some(p);
8010 Ok(())
8011 }
8012 }
8013 }
8014
8015 fn null_result(&self) -> datafusion::error::Result<ScalarValue> {
8016 ScalarValue::try_from(&self.result_type)
8017 }
8018
8019 fn percentile_scalar(
8020 values: &[datafusion::arrow::array::ArrayRef],
8021 row: usize,
8022 ) -> datafusion::error::Result<Option<f64>> {
8023 use datafusion::arrow::array::Array;
8024 let arr = &values[1];
8025 if arr.is_null(row) {
8026 return Ok(None);
8027 }
8028 let scalar = ScalarValue::try_from_array(arr, row)?;
8029 if scalar.is_null() {
8030 Ok(None)
8031 } else {
8032 scalar_as_f64(&scalar).map(Some).ok_or_else(|| {
8033 datafusion::error::DataFusionError::Execution(format!(
8034 "percentile argument must be numeric, got {}",
8035 scalar.data_type()
8036 ))
8037 })
8038 }
8039 }
8040}
8041
8042impl datafusion::logical_expr::Accumulator for PercentileAcc {
8043 fn update_batch(
8044 &mut self,
8045 values: &[datafusion::arrow::array::ArrayRef],
8046 ) -> datafusion::error::Result<()> {
8047 use datafusion::arrow::array::Array;
8048 let value_arr = &values[0];
8049 for row in 0..value_arr.len() {
8050 self.observe_percentile(Self::percentile_scalar(values, row)?)?;
8051 if value_arr.is_null(row) {
8052 continue;
8053 }
8054 self.push_value(ScalarValue::try_from_array(value_arr, row)?)?;
8055 }
8056 Ok(())
8057 }
8058
8059 fn evaluate(&mut self) -> datafusion::error::Result<ScalarValue> {
8060 let Some(percentile) = self.percentile else {
8061 return self.null_result();
8062 };
8063 if self.values.is_empty() {
8064 return self.null_result();
8065 }
8066 let mut values: Vec<(f64, ScalarValue)> = self
8067 .values
8068 .iter()
8069 .filter_map(|v| scalar_as_f64(v).map(|f| (f, v.clone())))
8070 .collect();
8071 if values.is_empty() {
8072 return self.null_result();
8073 }
8074 values.sort_by(|(l, _), (r, _)| l.total_cmp(r));
8075
8076 if self.continuous {
8077 let len = values.len();
8078 if len == 1 {
8079 return Ok(ScalarValue::Float64(Some(values[0].0)));
8080 }
8081 let (lower_index, upper_index, fraction) = percentile_cont_indices(percentile, len);
8082 let result = if lower_index == upper_index {
8083 values[lower_index].0
8084 } else {
8085 let lower = values[lower_index].0;
8086 let upper = values[upper_index].0;
8087 lower + (upper - lower) * fraction
8088 };
8089 Ok(ScalarValue::Float64(Some(result)))
8090 } else {
8091 let index = percentile_disc_index(percentile, values.len());
8092 Ok(values[index].1.clone())
8093 }
8094 }
8095
8096 fn size(&self) -> usize {
8097 std::mem::size_of_val(self) + self.values.iter().map(ScalarValue::size).sum::<usize>()
8098 }
8099
8100 fn state(&mut self) -> datafusion::error::Result<Vec<ScalarValue>> {
8101 Ok(vec![
8102 ScalarValue::List(ScalarValue::new_list(&self.values, &self.value_type, true)),
8103 ScalarValue::Float64(self.percentile),
8104 ])
8105 }
8106
8107 fn merge_batch(
8108 &mut self,
8109 states: &[datafusion::arrow::array::ArrayRef],
8110 ) -> datafusion::error::Result<()> {
8111 use datafusion::arrow::array::{Array, Float64Array, ListArray};
8112 let values = &states[0];
8113 let Some(lists) = values.as_any().downcast_ref::<ListArray>() else {
8114 return Err(datafusion::error::DataFusionError::Plan(
8115 "percentile state values must be a list".into(),
8116 ));
8117 };
8118 let Some(percentiles) = states[1].as_any().downcast_ref::<Float64Array>() else {
8119 return Err(datafusion::error::DataFusionError::Plan(
8120 "percentile state percentile must be Float64".into(),
8121 ));
8122 };
8123 for row in 0..lists.len() {
8124 self.observe_percentile(if percentiles.is_null(row) {
8125 None
8126 } else {
8127 Some(percentiles.value(row))
8128 })?;
8129 if lists.is_null(row) {
8130 continue;
8131 }
8132 let values = lists.value(row);
8133 for i in 0..values.len() {
8134 if values.is_null(i) {
8135 continue;
8136 }
8137 self.push_value(ScalarValue::try_from_array(&values, i)?)?;
8138 }
8139 }
8140 Ok(())
8141 }
8142}
8143
8144#[allow(
8145 clippy::cast_possible_truncation,
8146 clippy::cast_precision_loss,
8147 clippy::cast_sign_loss,
8148 reason = "percentile ranks are defined by converting bounded [0, 1] floats into sorted indexes"
8149)]
8150fn percentile_cont_indices(percentile: f64, len: usize) -> (usize, usize, f64) {
8151 let index = percentile * ((len - 1) as f64);
8152 let lower = index.floor() as usize;
8153 let upper = index.ceil() as usize;
8154 (lower, upper, index.fract())
8155}
8156
8157#[allow(
8158 clippy::cast_possible_truncation,
8159 clippy::cast_precision_loss,
8160 clippy::cast_sign_loss,
8161 reason = "percentile ranks are defined by converting bounded [0, 1] floats into sorted indexes"
8162)]
8163fn percentile_disc_index(percentile: f64, len: usize) -> usize {
8164 if percentile <= f64::EPSILON {
8165 0
8166 } else {
8167 ((percentile * (len as f64)).ceil() as usize)
8168 .saturating_sub(1)
8169 .min(len - 1)
8170 }
8171}
8172
8173fn is_percentile_numeric_type(dt: &DataType) -> bool {
8174 matches!(
8175 dt,
8176 DataType::Int8
8177 | DataType::Null
8178 | DataType::Int16
8179 | DataType::Int32
8180 | DataType::Int64
8181 | DataType::UInt8
8182 | DataType::UInt16
8183 | DataType::UInt32
8184 | DataType::UInt64
8185 | DataType::Float32
8186 | DataType::Float64
8187 )
8188}
8189
8190fn cypher_value_eq(l: &ScalarValue, r: &ScalarValue) -> Option<bool> {
8196 if l.is_null() || r.is_null() {
8197 return None;
8198 }
8199 if let Some(dl) = decode_het(l) {
8203 return cypher_value_eq(&dl, r);
8204 }
8205 if let Some(dr) = decode_het(r) {
8206 return cypher_value_eq(l, &dr);
8207 }
8208 match (l, r) {
8209 (ScalarValue::List(a), ScalarValue::List(b)) => cypher_seq_eq(&a.value(0), &b.value(0)),
8210 (ScalarValue::LargeList(a), ScalarValue::LargeList(b)) => {
8211 cypher_seq_eq(&a.value(0), &b.value(0))
8212 }
8213 (ScalarValue::Struct(a), ScalarValue::Struct(b)) => {
8218 if is_entity_struct(a) || is_entity_struct(b) {
8219 Some(l == r)
8220 } else {
8221 cypher_struct_eq(a, b)
8222 }
8223 }
8224 _ if is_numeric_scalar(l) && is_numeric_scalar(r) => {
8225 if let (Some(li), Some(ri)) = (scalar_as_i128(l), scalar_as_i128(r)) {
8226 return Some(li == ri);
8227 }
8228 let (Some(lf), Some(rf)) = (scalar_as_f64(l), scalar_as_f64(r)) else {
8229 return None;
8230 };
8231 Some(!lf.is_nan() && !rf.is_nan() && lf == rf)
8232 }
8233 _ if std::mem::discriminant(l) == std::mem::discriminant(r) => Some(l == r),
8235 _ => Some(false),
8236 }
8237}
8238
8239fn decode_het(s: &ScalarValue) -> Option<ScalarValue> {
8246 use datafusion::arrow::array::{
8247 Array, ArrayRef, BooleanArray, Float64Array, Int8Array, Int64Array, ListArray, StringArray,
8248 StructArray,
8249 };
8250 use datafusion::arrow::datatypes::{Field, Fields};
8251 use std::sync::Arc;
8252 let ScalarValue::Struct(arr) = s else {
8253 return None;
8254 };
8255 arr.column_by_name("__het_tag")?; if arr.is_null(0) {
8257 return Some(ScalarValue::Null);
8258 }
8259 let tag = arr
8260 .column_by_name("__het_tag")?
8261 .as_any()
8262 .downcast_ref::<Int8Array>()?
8263 .value(0);
8264 let col = |name: &str| arr.column_by_name(name);
8265 if let Some(value) = col(&format!("__het_value_{tag}")) {
8266 return ScalarValue::try_from_array(value, 0).ok();
8267 }
8268 let v = match tag {
8269 0 => ScalarValue::Int64(Some(
8270 col("__het_int")?
8271 .as_any()
8272 .downcast_ref::<Int64Array>()?
8273 .value(0),
8274 )),
8275 1 => ScalarValue::Float64(Some(
8276 col("__het_float")?
8277 .as_any()
8278 .downcast_ref::<Float64Array>()?
8279 .value(0),
8280 )),
8281 2 => ScalarValue::Utf8(Some(
8282 col("__het_str")?
8283 .as_any()
8284 .downcast_ref::<StringArray>()?
8285 .value(0)
8286 .to_string(),
8287 )),
8288 3 => ScalarValue::Boolean(Some(
8289 col("__het_bool")?
8290 .as_any()
8291 .downcast_ref::<BooleanArray>()?
8292 .value(0),
8293 )),
8294 4 => ScalarValue::try_from_array(col("__het_list")?, 0).ok()?,
8295 5 => {
8296 let entries = col("__het_map")?
8297 .as_any()
8298 .downcast_ref::<ListArray>()?
8299 .value(0);
8300 let es = entries.as_any().downcast_ref::<StructArray>()?;
8301 if es.is_empty() {
8302 return Some(ScalarValue::Struct(Arc::new(
8303 StructArray::new_empty_fields(1, None),
8304 )));
8305 }
8306 let mkeys = es
8307 .column_by_name("__het_mkey")?
8308 .as_any()
8309 .downcast_ref::<StringArray>()?;
8310 let mvals = es.column_by_name("__het_mval")?;
8311 let mut fields: Vec<Field> = Vec::with_capacity(es.len());
8312 let mut cols: Vec<ArrayRef> = Vec::with_capacity(es.len());
8313 for i in 0..es.len() {
8314 let varr = mvals.slice(i, 1);
8317 fields.push(Field::new(mkeys.value(i), varr.data_type().clone(), true));
8318 cols.push(varr);
8319 }
8320 ScalarValue::Struct(Arc::new(
8321 StructArray::try_new(Fields::from(fields), cols, None).ok()?,
8322 ))
8323 }
8324 _ => return None,
8325 };
8326 Some(v)
8327}
8328
8329#[must_use]
8331pub fn decode_het_scalar(value: &ScalarValue) -> Option<ScalarValue> {
8332 decode_het(value)
8333}
8334
8335fn is_entity_struct(s: &datafusion::arrow::array::StructArray) -> bool {
8339 s.fields().iter().any(|f| {
8340 matches!(
8341 f.name().as_str(),
8342 "node_uuid" | "src_uuid" | "dst_uuid" | "nodes" | "relationships" | "labels"
8343 )
8344 })
8345}
8346
8347static CYPHER_DATE_COMPONENT: LazyLock<ScalarUDF> =
8356 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDateComponent::new()));
8357
8358#[derive(Debug, PartialEq, Eq, Hash)]
8359struct CypherDateComponent {
8360 signature: Signature,
8361}
8362
8363impl CypherDateComponent {
8364 fn new() -> Self {
8365 Self {
8366 signature: Signature::any(2, Volatility::Immutable),
8367 }
8368 }
8369}
8370
8371impl ScalarUDFImpl for CypherDateComponent {
8372 fn as_any(&self) -> &dyn Any {
8373 self
8374 }
8375
8376 fn name(&self) -> &'static str {
8377 "cypher_date_component"
8378 }
8379
8380 fn signature(&self) -> &Signature {
8381 &self.signature
8382 }
8383
8384 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
8385 Ok(DataType::Int64)
8386 }
8387
8388 fn invoke_with_args(
8389 &self,
8390 args: ScalarFunctionArgs,
8391 ) -> datafusion::error::Result<ColumnarValue> {
8392 use crate::temporal::date_component;
8393 use datafusion::arrow::array::{Array, Int64Array, StringArray, StructArray};
8394
8395 let rows = args.number_rows;
8396 let dates = args.args[0].to_array(rows)?;
8397 let names = args.args[1].to_array(rows)?;
8398 let d = dates.as_any().downcast_ref::<StructArray>();
8399 let n = names.as_any().downcast_ref::<StringArray>();
8400 let out: Int64Array = (0..rows)
8401 .map(|i| {
8402 let (d, n) = (d?, n?);
8403 if n.is_null(i) {
8404 return None;
8405 }
8406 date_component(date_struct_value(d, i)?, n.value(i))
8407 })
8408 .collect();
8409 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
8410 }
8411}
8412
8413static CYPHER_DURATION_COMPONENT: LazyLock<ScalarUDF> =
8420 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDurationComponent::new()));
8421
8422#[derive(Debug, PartialEq, Eq, Hash)]
8423struct CypherDurationComponent {
8424 signature: Signature,
8425}
8426
8427impl CypherDurationComponent {
8428 fn new() -> Self {
8429 Self {
8430 signature: Signature::any(2, Volatility::Immutable),
8431 }
8432 }
8433}
8434
8435impl ScalarUDFImpl for CypherDurationComponent {
8436 fn as_any(&self) -> &dyn Any {
8437 self
8438 }
8439
8440 fn name(&self) -> &'static str {
8441 "cypher_duration_component"
8442 }
8443
8444 fn signature(&self) -> &Signature {
8445 &self.signature
8446 }
8447
8448 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
8449 Ok(DataType::Int64)
8450 }
8451
8452 fn invoke_with_args(
8453 &self,
8454 args: ScalarFunctionArgs,
8455 ) -> datafusion::error::Result<ColumnarValue> {
8456 use crate::temporal::duration_component;
8457 use datafusion::arrow::array::{Array, Int64Array, StringArray, StructArray};
8458
8459 let rows = args.number_rows;
8460 let durs = args.args[0].to_array(rows)?;
8461 let names = args.args[1].to_array(rows)?;
8462 let d = durs.as_any().downcast_ref::<StructArray>();
8463 let n = names.as_any().downcast_ref::<StringArray>();
8464 let out: Int64Array = (0..rows)
8465 .map(|i| {
8466 let (d, n) = (d?, n?);
8467 if d.is_null(i) || n.is_null(i) {
8468 return None;
8469 }
8470 duration_component(&duration_struct_parts(d, i)?, n.value(i))
8471 })
8472 .collect();
8473 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
8474 }
8475}
8476
8477fn temporal_accessor_valid(dt: &DataType, name: &str) -> bool {
8487 use crate::temporal::{
8488 is_date_accessor, is_epoch_accessor, is_time_accessor, is_zone_int_accessor,
8489 is_zone_str_accessor,
8490 };
8491 match dt {
8492 DataType::Time64(_) => is_time_accessor(name),
8493 DataType::Struct(_) if is_time_struct(dt) => {
8494 is_time_accessor(name) || is_zone_int_accessor(name) || is_zone_str_accessor(name)
8495 }
8496 DataType::Struct(_) if is_localdatetime_struct(dt) => {
8497 is_date_accessor(name) || is_time_accessor(name)
8498 }
8499 DataType::Struct(_) if is_datetime_struct(dt) => {
8500 is_date_accessor(name)
8501 || is_time_accessor(name)
8502 || is_zone_int_accessor(name)
8503 || is_zone_str_accessor(name)
8504 || is_epoch_accessor(name)
8505 }
8506 _ => false,
8507 }
8508}
8509
8510static CYPHER_TEMPORAL_COMPONENT: LazyLock<ScalarUDF> =
8515 LazyLock::new(|| ScalarUDF::new_from_impl(CypherTemporalComponent::new()));
8516
8517#[derive(Debug, PartialEq, Eq, Hash)]
8518struct CypherTemporalComponent {
8519 signature: Signature,
8520}
8521
8522impl CypherTemporalComponent {
8523 fn new() -> Self {
8524 Self {
8525 signature: Signature::any(2, Volatility::Immutable),
8526 }
8527 }
8528}
8529
8530impl ScalarUDFImpl for CypherTemporalComponent {
8531 fn as_any(&self) -> &dyn Any {
8532 self
8533 }
8534
8535 fn name(&self) -> &'static str {
8536 "cypher_temporal_component"
8537 }
8538
8539 fn signature(&self) -> &Signature {
8540 &self.signature
8541 }
8542
8543 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
8544 Ok(DataType::Int64)
8545 }
8546
8547 fn invoke_with_args(
8548 &self,
8549 args: ScalarFunctionArgs,
8550 ) -> datafusion::error::Result<ColumnarValue> {
8551 use crate::temporal::{
8552 date_component, epoch_component, is_date_accessor, is_time_accessor,
8553 is_zone_int_accessor, time_component, zone_int_component,
8554 };
8555 use datafusion::arrow::array::{
8556 Array, Int64Array, StringArray, StructArray, Time64NanosecondArray,
8557 };
8558 use datafusion::arrow::datatypes::TimeUnit;
8559
8560 let rows = args.number_rows;
8561 let vals = args.args[0].to_array(rows)?;
8562 let names = args.args[1].to_array(rows)?;
8563 let n = names.as_any().downcast_ref::<StringArray>();
8564 let out: Int64Array = (0..rows)
8565 .map(|i| {
8566 let n = n?;
8567 if vals.is_null(i) || n.is_null(i) {
8568 return None;
8569 }
8570 let name = n.value(i);
8571 match vals.data_type() {
8572 DataType::Time64(TimeUnit::Nanosecond) => {
8573 let v = vals.as_any().downcast_ref::<Time64NanosecondArray>()?;
8574 time_component(v.value(i), name)
8575 }
8576 DataType::Struct(_) => {
8577 let s = vals.as_any().downcast_ref::<StructArray>()?;
8578 if is_time_struct(vals.data_type()) {
8579 let (nanos, offset) = time_struct_parts(s, i)?;
8580 if is_zone_int_accessor(name) {
8581 zone_int_component(offset, name)
8582 } else {
8583 time_component(nanos, name)
8584 }
8585 } else if is_localdatetime_struct(vals.data_type()) {
8586 let (days, nanos) = localdatetime_struct_parts(s, i)?;
8587 if is_date_accessor(name) {
8588 date_component(days, name)
8589 } else {
8590 time_component(nanos, name)
8591 }
8592 } else if is_datetime_struct(vals.data_type()) {
8593 let (days, nanos, offset, _) = datetime_struct_parts(s, i)?;
8594 if is_date_accessor(name) {
8595 date_component(days, name)
8596 } else if is_time_accessor(name) {
8597 time_component(nanos, name)
8598 } else if is_zone_int_accessor(name) {
8599 zone_int_component(offset, name)
8600 } else {
8601 epoch_component(days, nanos, offset, name)
8602 }
8603 } else {
8604 None
8605 }
8606 }
8607 _ => None,
8608 }
8609 })
8610 .collect();
8611 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
8612 }
8613}
8614
8615static CYPHER_TEMPORAL_ZONE_STR: LazyLock<ScalarUDF> =
8618 LazyLock::new(|| ScalarUDF::new_from_impl(CypherTemporalZoneStr::new()));
8619
8620#[derive(Debug, PartialEq, Eq, Hash)]
8621struct CypherTemporalZoneStr {
8622 signature: Signature,
8623}
8624
8625impl CypherTemporalZoneStr {
8626 fn new() -> Self {
8627 Self {
8628 signature: Signature::any(2, Volatility::Immutable),
8629 }
8630 }
8631}
8632
8633impl ScalarUDFImpl for CypherTemporalZoneStr {
8634 fn as_any(&self) -> &dyn Any {
8635 self
8636 }
8637
8638 fn name(&self) -> &'static str {
8639 "cypher_temporal_zone_str"
8640 }
8641
8642 fn signature(&self) -> &Signature {
8643 &self.signature
8644 }
8645
8646 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
8647 Ok(DataType::Utf8)
8648 }
8649
8650 fn invoke_with_args(
8651 &self,
8652 args: ScalarFunctionArgs,
8653 ) -> datafusion::error::Result<ColumnarValue> {
8654 use crate::temporal::zone_str_component;
8655 use datafusion::arrow::array::{Array, StringArray, StructArray};
8656
8657 let rows = args.number_rows;
8658 let vals = args.args[0].to_array(rows)?;
8659 let names = args.args[1].to_array(rows)?;
8660 let n = names.as_any().downcast_ref::<StringArray>();
8661 let out: StringArray = (0..rows)
8662 .map(|i| {
8663 let n = n?;
8664 if vals.is_null(i) || n.is_null(i) {
8665 return None;
8666 }
8667 let name = n.value(i);
8668 let s = vals.as_any().downcast_ref::<StructArray>()?;
8669 if is_time_struct(vals.data_type()) {
8670 let (_, offset) = time_struct_parts(s, i)?;
8671 zone_str_component(offset, None, name)
8672 } else if is_datetime_struct(vals.data_type()) {
8673 let (_, _, offset, zone) = datetime_struct_parts(s, i)?;
8674 zone_str_component(offset, zone.as_deref(), name)
8675 } else {
8676 None
8677 }
8678 })
8679 .collect();
8680 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
8681 }
8682}
8683
8684static CYPHER_DURATION_BETWEEN: LazyLock<ScalarUDF> =
8693 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDurationBetween::new()));
8694
8695#[derive(Debug, PartialEq, Eq, Hash)]
8696struct CypherDurationBetween {
8697 signature: Signature,
8698}
8699
8700impl CypherDurationBetween {
8701 fn new() -> Self {
8702 Self {
8703 signature: Signature::any(3, Volatility::Immutable),
8704 }
8705 }
8706}
8707
8708fn between_operand(
8711 arr: &datafusion::arrow::array::ArrayRef,
8712 i: usize,
8713) -> Option<crate::temporal::BetweenOperand> {
8714 use datafusion::arrow::array::{Array, StructArray, Time64NanosecondArray};
8715 use datafusion::arrow::datatypes::TimeUnit;
8716 if arr.is_null(i) {
8717 return None;
8718 }
8719 match arr.data_type() {
8720 DataType::Time64(TimeUnit::Nanosecond) => Some((
8721 None,
8722 arr.as_any()
8723 .downcast_ref::<Time64NanosecondArray>()?
8724 .value(i),
8725 None,
8726 None,
8727 )),
8728 DataType::Struct(_) => {
8729 let s = arr.as_any().downcast_ref::<StructArray>()?;
8730 if is_date_struct(arr.data_type()) {
8731 Some((Some(date_struct_value(s, i)?), 0, None, None))
8732 } else if is_datetime_struct(arr.data_type()) {
8733 let (days, nanos, offset, zone) = datetime_struct_parts(s, i)?;
8736 Some((Some(days), nanos, Some(offset), zone))
8737 } else if is_time_struct(arr.data_type()) {
8738 let (nanos, offset) = time_struct_parts(s, i)?;
8739 Some((None, nanos, Some(offset), None))
8740 } else {
8741 let (days, nanos) = localdatetime_struct_parts(s, i)?;
8742 Some((Some(days), nanos, None, None))
8743 }
8744 }
8745 _ => None,
8746 }
8747}
8748
8749impl ScalarUDFImpl for CypherDurationBetween {
8750 fn as_any(&self) -> &dyn Any {
8751 self
8752 }
8753
8754 fn name(&self) -> &'static str {
8755 "cypher_duration_between"
8756 }
8757
8758 fn signature(&self) -> &Signature {
8759 &self.signature
8760 }
8761
8762 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
8763 Ok(DataType::Struct(
8764 graphforge_storage::schemas::duration_struct_fields(),
8765 ))
8766 }
8767
8768 fn invoke_with_args(
8769 &self,
8770 args: ScalarFunctionArgs,
8771 ) -> datafusion::error::Result<ColumnarValue> {
8772 use crate::temporal::{BetweenMode, duration_between};
8773 use datafusion::arrow::array::{Array, StringArray};
8774 use datafusion::arrow::compute::cast;
8775 use datafusion::error::DataFusionError;
8776
8777 let rows = args.number_rows;
8778 let cols: Vec<_> = args
8779 .args
8780 .iter()
8781 .map(|a| a.to_array(rows))
8782 .collect::<datafusion::error::Result<_>>()?;
8783 let mode_arr = cast(&cols[2], &DataType::Utf8).map_err(DataFusionError::from)?;
8784 let modes = mode_arr.as_any().downcast_ref::<StringArray>();
8785
8786 let parts: Vec<Option<crate::temporal::DurationValue>> = (0..rows)
8787 .map(|i| {
8788 let m = modes?;
8789 if m.is_null(i) {
8790 return None;
8791 }
8792 let mode = match m.value(i) {
8793 "duration.between" => BetweenMode::Between,
8794 "duration.inmonths" => BetweenMode::Months,
8795 "duration.indays" => BetweenMode::Days,
8796 "duration.inseconds" => BetweenMode::Seconds,
8797 _ => return None,
8798 };
8799 let a = between_operand(&cols[0], i)?;
8800 let b = between_operand(&cols[1], i)?;
8801 duration_between(&a, &b, mode)
8802 })
8803 .collect();
8804 Ok(ColumnarValue::Array(std::sync::Arc::new(
8805 build_duration_struct(&parts),
8806 )))
8807 }
8808}
8809
8810static CYPHER_TEMPORAL_ARITH: LazyLock<ScalarUDF> =
8821 LazyLock::new(|| ScalarUDF::new_from_impl(CypherTemporalArith::new()));
8822
8823#[derive(Debug, PartialEq, Eq, Hash)]
8824struct CypherTemporalArith {
8825 signature: Signature,
8826}
8827
8828impl CypherTemporalArith {
8829 fn new() -> Self {
8830 Self {
8831 signature: Signature::any(3, Volatility::Immutable),
8832 }
8833 }
8834}
8835
8836impl ScalarUDFImpl for CypherTemporalArith {
8837 fn as_any(&self) -> &dyn Any {
8838 self
8839 }
8840
8841 fn name(&self) -> &'static str {
8842 "cypher_temporal_arith"
8843 }
8844
8845 fn signature(&self) -> &Signature {
8846 &self.signature
8847 }
8848
8849 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
8850 Ok(arg_types.first().cloned().unwrap_or(DataType::Null))
8852 }
8853
8854 #[allow(
8855 clippy::too_many_lines,
8856 reason = "one cohesive per-temporal-type dispatch (date/localtime/time/\
8857 localdatetime/datetime) applying a signed duration"
8858 )]
8859 fn invoke_with_args(
8860 &self,
8861 args: ScalarFunctionArgs,
8862 ) -> datafusion::error::Result<ColumnarValue> {
8863 use crate::temporal::{
8864 date_plus_duration, datetime_plus_duration, localtime_plus_duration,
8865 };
8866 use datafusion::arrow::array::{
8867 Array, ArrayRef, Int64Array, StructArray, Time64NanosecondArray,
8868 };
8869 use datafusion::arrow::datatypes::TimeUnit;
8870
8871 let rows = args.number_rows;
8872 let cols: Vec<_> = args
8873 .args
8874 .iter()
8875 .map(|a| a.to_array(rows))
8876 .collect::<datafusion::error::Result<_>>()?;
8877 let temporal = &cols[0];
8878 let dur = cols[1].as_any().downcast_ref::<StructArray>();
8879 let signs = cols[2].as_any().downcast_ref::<Int64Array>();
8880
8881 let signed = |i: usize| -> Option<crate::temporal::DurationValue> {
8883 let (d, sg) = (dur?, signs?);
8884 if d.is_null(i) || sg.is_null(i) {
8885 return None;
8886 }
8887 let dv = duration_struct_parts(d, i)?;
8888 Some(if sg.value(i) < 0 {
8889 crate::temporal::DurationValue {
8890 months: -dv.months,
8891 days: -dv.days,
8892 seconds: -dv.seconds,
8893 nanos: -dv.nanos,
8894 }
8895 } else {
8896 dv
8897 })
8898 };
8899 let sub_day_nanos = |d: &crate::temporal::DurationValue| {
8904 d.seconds.rem_euclid(86_400) * 1_000_000_000 + d.nanos
8905 };
8906
8907 let out: ArrayRef = match temporal.data_type() {
8908 DataType::Struct(_) if is_date_struct(temporal.data_type()) => {
8909 let t = temporal.as_any().downcast_ref::<StructArray>();
8910 let days: Vec<Option<i64>> = (0..rows)
8911 .map(|i| {
8912 let t = t?;
8913 let d = date_struct_value(t, i)?;
8914 let dv = signed(i)?;
8915 Some(date_plus_duration(d, &dv))
8916 })
8917 .collect();
8918 std::sync::Arc::new(build_date_struct(&days))
8919 }
8920 DataType::Time64(TimeUnit::Nanosecond) => {
8921 let t = temporal.as_any().downcast_ref::<Time64NanosecondArray>();
8922 let a: Time64NanosecondArray = (0..rows)
8923 .map(|i| {
8924 let t = t?;
8925 if t.is_null(i) {
8926 return None;
8927 }
8928 let dv = signed(i)?;
8929 Some(localtime_plus_duration(t.value(i), sub_day_nanos(&dv)))
8930 })
8931 .collect();
8932 std::sync::Arc::new(a)
8933 }
8934 DataType::Struct(_) if is_time_struct(temporal.data_type()) => {
8935 let s = temporal.as_any().downcast_ref::<StructArray>();
8936 let parts: Vec<Option<(i64, i32)>> = (0..rows)
8937 .map(|i| {
8938 let s = s?;
8939 let (nanos, offset) = time_struct_parts(s, i)?;
8940 let dv = signed(i)?;
8941 Some((localtime_plus_duration(nanos, sub_day_nanos(&dv)), offset))
8942 })
8943 .collect();
8944 std::sync::Arc::new(build_time_struct(&parts))
8945 }
8946 DataType::Struct(_) if is_datetime_struct(temporal.data_type()) => {
8947 let s = temporal.as_any().downcast_ref::<StructArray>();
8948 let parts: Vec<DateTimeRow> = (0..rows)
8949 .map(|i| {
8950 let s = s?;
8951 let (days, nanos, offset, zone) = datetime_struct_parts(s, i)?;
8952 let dv = signed(i)?;
8953 let (date, no) = datetime_plus_duration(days, nanos, &dv);
8954 Some((date, no, offset, zone))
8955 })
8956 .collect();
8957 std::sync::Arc::new(build_datetime_struct(&parts))
8958 }
8959 DataType::Struct(_) if is_localdatetime_struct(temporal.data_type()) => {
8961 let s = temporal.as_any().downcast_ref::<StructArray>();
8962 let parts: Vec<Option<(i64, i64)>> = (0..rows)
8963 .map(|i| {
8964 let s = s?;
8965 let (days, nanos) = localdatetime_struct_parts(s, i)?;
8966 let dv = signed(i)?;
8967 let (date, no) = datetime_plus_duration(days, nanos, &dv);
8968 Some((date, no))
8969 })
8970 .collect();
8971 std::sync::Arc::new(build_localdatetime_struct(&parts))
8972 }
8973 other => {
8974 return Err(datafusion::error::DataFusionError::Internal(format!(
8975 "cypher_temporal_arith: left operand is not a temporal value ({other:?})"
8976 )));
8977 }
8978 };
8979 Ok(ColumnarValue::Array(out))
8980 }
8981}
8982
8983static CYPHER_DURATION_PARSE: LazyLock<ScalarUDF> =
8992 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDurationParse::new()));
8993
8994#[derive(Debug, PartialEq, Eq, Hash)]
8995struct CypherDurationParse {
8996 signature: Signature,
8997}
8998
8999impl CypherDurationParse {
9000 fn new() -> Self {
9001 Self {
9002 signature: Signature::any(1, Volatility::Immutable),
9003 }
9004 }
9005}
9006
9007impl ScalarUDFImpl for CypherDurationParse {
9008 fn as_any(&self) -> &dyn Any {
9009 self
9010 }
9011 fn name(&self) -> &'static str {
9012 "cypher_duration_parse"
9013 }
9014 fn signature(&self) -> &Signature {
9015 &self.signature
9016 }
9017 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9018 Ok(DataType::Struct(
9019 graphforge_storage::schemas::duration_struct_fields(),
9020 ))
9021 }
9022
9023 fn invoke_with_args(
9024 &self,
9025 args: ScalarFunctionArgs,
9026 ) -> datafusion::error::Result<ColumnarValue> {
9027 use datafusion::arrow::array::{Array, StringArray};
9028 use datafusion::arrow::compute::cast;
9029
9030 let rows = args.number_rows;
9031 let arr = cast(&args.args[0].to_array(rows)?, &DataType::Utf8)?;
9032 let s = arr.as_any().downcast_ref::<StringArray>();
9033 let parts: Vec<Option<crate::temporal::DurationValue>> = (0..rows)
9034 .map(|i| {
9035 let s = s?;
9036 if s.is_null(i) {
9037 return Option::None;
9038 }
9039 crate::temporal::duration_value_from_str(s.value(i))
9040 })
9041 .collect();
9042 Ok(ColumnarValue::Array(std::sync::Arc::new(
9043 build_duration_struct(&parts),
9044 )))
9045 }
9046}
9047
9048static CYPHER_DURATION_ADD: LazyLock<ScalarUDF> =
9051 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDurationAdd::new()));
9052
9053#[derive(Debug, PartialEq, Eq, Hash)]
9054struct CypherDurationAdd {
9055 signature: Signature,
9056}
9057
9058impl CypherDurationAdd {
9059 fn new() -> Self {
9060 Self {
9061 signature: Signature::any(3, Volatility::Immutable),
9062 }
9063 }
9064}
9065
9066impl ScalarUDFImpl for CypherDurationAdd {
9067 fn as_any(&self) -> &dyn Any {
9068 self
9069 }
9070
9071 fn name(&self) -> &'static str {
9072 "cypher_duration_add"
9073 }
9074
9075 fn signature(&self) -> &Signature {
9076 &self.signature
9077 }
9078
9079 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9080 Ok(DataType::Struct(
9081 graphforge_storage::schemas::duration_struct_fields(),
9082 ))
9083 }
9084
9085 fn invoke_with_args(
9086 &self,
9087 args: ScalarFunctionArgs,
9088 ) -> datafusion::error::Result<ColumnarValue> {
9089 use datafusion::arrow::array::{Array, Int64Array, StructArray};
9090
9091 let rows = args.number_rows;
9092 let cols: Vec<_> = args
9093 .args
9094 .iter()
9095 .map(|a| a.to_array(rows))
9096 .collect::<datafusion::error::Result<_>>()?;
9097 let a = cols[0].as_any().downcast_ref::<StructArray>();
9098 let b = cols[1].as_any().downcast_ref::<StructArray>();
9099 let signs = cols[2].as_any().downcast_ref::<Int64Array>();
9100
9101 let parts: Vec<Option<crate::temporal::DurationValue>> = (0..rows)
9102 .map(|i| {
9103 let (a, b, sg) = (a?, b?, signs?);
9104 let av = duration_struct_parts(a, i)?;
9105 let bv = duration_struct_parts(b, i)?;
9106 let s: i64 = if sg.is_null(i) || sg.value(i) >= 0 {
9107 1
9108 } else {
9109 -1
9110 };
9111 let nanos_sum = av.nanos + s * bv.nanos;
9117 let seconds = av.seconds + s * bv.seconds + nanos_sum.div_euclid(1_000_000_000);
9118 Some(crate::temporal::DurationValue {
9119 months: av.months + s * bv.months,
9120 days: av.days + s * bv.days,
9121 seconds,
9122 nanos: nanos_sum.rem_euclid(1_000_000_000),
9123 })
9124 })
9125 .collect();
9126 Ok(ColumnarValue::Array(std::sync::Arc::new(
9127 build_duration_struct(&parts),
9128 )))
9129 }
9130}
9131
9132static CYPHER_DURATION_SCALE: LazyLock<ScalarUDF> =
9136 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDurationScale::new()));
9137
9138#[derive(Debug, PartialEq, Eq, Hash)]
9139struct CypherDurationScale {
9140 signature: Signature,
9141}
9142
9143impl CypherDurationScale {
9144 fn new() -> Self {
9145 Self {
9146 signature: Signature::any(3, Volatility::Immutable),
9147 }
9148 }
9149}
9150
9151impl ScalarUDFImpl for CypherDurationScale {
9152 fn as_any(&self) -> &dyn Any {
9153 self
9154 }
9155 fn name(&self) -> &'static str {
9156 "cypher_duration_scale"
9157 }
9158 fn signature(&self) -> &Signature {
9159 &self.signature
9160 }
9161 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9162 Ok(DataType::Struct(
9163 graphforge_storage::schemas::duration_struct_fields(),
9164 ))
9165 }
9166
9167 fn invoke_with_args(
9168 &self,
9169 args: ScalarFunctionArgs,
9170 ) -> datafusion::error::Result<ColumnarValue> {
9171 use datafusion::arrow::array::{Array, BooleanArray, Float64Array, StructArray};
9172 use datafusion::arrow::compute::cast;
9173
9174 let rows = args.number_rows;
9175 let cols: Vec<_> = args
9176 .args
9177 .iter()
9178 .map(|a| a.to_array(rows))
9179 .collect::<datafusion::error::Result<_>>()?;
9180 let dur = cols[0].as_any().downcast_ref::<StructArray>();
9181 let num = cast(&cols[1], &DataType::Float64)?;
9183 let num = num.as_any().downcast_ref::<Float64Array>();
9184 let is_div = cols[2].as_any().downcast_ref::<BooleanArray>();
9185
9186 let parts: Vec<Option<crate::temporal::DurationValue>> = (0..rows)
9187 .map(|i| {
9188 let (dur, num, is_div) = (dur?, num?, is_div?);
9189 if num.is_null(i) {
9190 return Option::None; }
9192 let dv = duration_struct_parts(dur, i)?;
9193 let divide = !is_div.is_null(i) && is_div.value(i);
9194 Some(crate::temporal::scale_duration(&dv, num.value(i), divide))
9195 })
9196 .collect();
9197 Ok(ColumnarValue::Array(std::sync::Arc::new(
9198 build_duration_struct(&parts),
9199 )))
9200 }
9201}
9202
9203#[allow(
9211 clippy::match_same_arms,
9212 reason = "per-quantifier arms read clearest grouped by kind, even where two \
9213 fallback bodies coincide (all/none both default true)"
9214)]
9215fn reduce_quantifier(
9216 kind: graphforge_ir::QuantifierKind,
9217 bools: &datafusion::arrow::array::BooleanArray,
9218 n: usize,
9219) -> Option<bool> {
9220 use datafusion::arrow::array::Array;
9221 use graphforge_ir::QuantifierKind as Q;
9222 let (mut any_true, mut any_false, mut any_null, mut count_true) = (false, false, false, 0u32);
9223 for j in 0..n {
9224 if bools.is_null(j) {
9225 any_null = true;
9226 } else if bools.value(j) {
9227 any_true = true;
9228 count_true += 1;
9229 } else {
9230 any_false = true;
9231 }
9232 }
9233 match kind {
9236 Q::All if any_false => Some(false),
9237 Q::All => (!any_null).then_some(true),
9238 Q::Any if any_true => Some(true),
9239 Q::Any => (!any_null).then_some(false),
9240 Q::None if any_true => Some(false),
9241 Q::None => (!any_null).then_some(true),
9242 Q::Single if count_true > 1 => Some(false),
9243 Q::Single => (!any_null).then_some(count_true == 1),
9244 }
9245}
9246
9247#[derive(Debug, PartialEq, Eq, Hash)]
9252struct CypherQuantifier {
9253 kind: graphforge_ir::QuantifierKind,
9254 predicate: DfExpr,
9255 elem_name: String,
9256 outer_names: Vec<String>,
9257 signature: Signature,
9258}
9259
9260impl CypherQuantifier {
9261 fn new(
9262 kind: graphforge_ir::QuantifierKind,
9263 predicate: DfExpr,
9264 elem_name: String,
9265 outer_names: Vec<String>,
9266 ) -> Self {
9267 let arity = 1 + outer_names.len();
9268 Self {
9269 kind,
9270 predicate,
9271 elem_name,
9272 outer_names,
9273 signature: Signature::any(arity, Volatility::Volatile),
9276 }
9277 }
9278}
9279
9280#[derive(Debug, PartialEq, Eq, Hash)]
9284struct CypherInvariantQuantifier {
9285 kind: graphforge_ir::QuantifierKind,
9286 predicate: Option<bool>,
9287 signature: Signature,
9288}
9289
9290#[cfg(test)]
9291static INVARIANT_QUANTIFIER_ROWS: std::sync::atomic::AtomicUsize =
9292 std::sync::atomic::AtomicUsize::new(0);
9293
9294impl CypherInvariantQuantifier {
9295 fn new(kind: graphforge_ir::QuantifierKind, predicate: Option<bool>) -> Self {
9296 Self {
9297 kind,
9298 predicate,
9299 signature: Signature::any(1, Volatility::Immutable),
9300 }
9301 }
9302}
9303
9304fn reduce_invariant_quantifier(
9305 kind: graphforge_ir::QuantifierKind,
9306 predicate: Option<bool>,
9307 len: usize,
9308) -> Option<bool> {
9309 use graphforge_ir::QuantifierKind as Q;
9310 if len == 0 {
9311 return Some(matches!(kind, Q::All | Q::None));
9312 }
9313 match (kind, predicate) {
9314 (_, None) => None,
9315 (Q::All | Q::Any, Some(value)) => Some(value),
9316 (Q::None, Some(value)) => Some(!value),
9317 (Q::Single, Some(true)) => Some(len == 1),
9318 (Q::Single, Some(false)) => Some(false),
9319 }
9320}
9321
9322impl ScalarUDFImpl for CypherInvariantQuantifier {
9323 fn as_any(&self) -> &dyn Any {
9324 self
9325 }
9326 fn name(&self) -> &'static str {
9327 "cypher_invariant_quantifier"
9328 }
9329 fn signature(&self) -> &Signature {
9330 &self.signature
9331 }
9332 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9333 Ok(DataType::Boolean)
9334 }
9335 fn invoke_with_args(
9336 &self,
9337 args: ScalarFunctionArgs,
9338 ) -> datafusion::error::Result<ColumnarValue> {
9339 use datafusion::arrow::array::{Array, BooleanArray, ListArray};
9340 use datafusion::error::DataFusionError;
9341
9342 let rows = args.number_rows;
9343 #[cfg(test)]
9344 INVARIANT_QUANTIFIER_ROWS.fetch_add(rows, std::sync::atomic::Ordering::SeqCst);
9345 let list = args.args[0].to_array(rows)?;
9346 let list = list.as_any().downcast_ref::<ListArray>().ok_or_else(|| {
9347 DataFusionError::Internal("cypher_invariant_quantifier: argument is not a list".into())
9348 })?;
9349 let values = (0..rows).map(|row| {
9350 if list.is_null(row) {
9351 None
9352 } else {
9353 reduce_invariant_quantifier(self.kind, self.predicate, list.value(row).len())
9354 }
9355 });
9356 Ok(ColumnarValue::Array(Arc::new(
9357 values.collect::<BooleanArray>(),
9358 )))
9359 }
9360}
9361
9362impl ScalarUDFImpl for CypherQuantifier {
9363 fn as_any(&self) -> &dyn Any {
9364 self
9365 }
9366 fn name(&self) -> &'static str {
9367 "cypher_quantifier"
9368 }
9369 fn signature(&self) -> &Signature {
9370 &self.signature
9371 }
9372 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9373 Ok(DataType::Boolean)
9374 }
9375
9376 fn invoke_with_args(
9377 &self,
9378 args: ScalarFunctionArgs,
9379 ) -> datafusion::error::Result<ColumnarValue> {
9380 use datafusion::arrow::array::{Array, ArrayRef, BooleanArray, ListArray, RecordBatch};
9381 use datafusion::arrow::datatypes::{Field, Schema};
9382 use datafusion::common::DFSchema;
9383 use datafusion::error::DataFusionError;
9384 use datafusion::logical_expr::execution_props::ExecutionProps;
9385 use datafusion::physical_expr::create_physical_expr;
9386 use std::sync::Arc;
9387
9388 let rows = args.number_rows;
9389 let cols: Vec<ArrayRef> = args
9390 .args
9391 .iter()
9392 .map(|a| a.to_array(rows))
9393 .collect::<datafusion::error::Result<_>>()?;
9394 let list = cols[0]
9395 .as_any()
9396 .downcast_ref::<ListArray>()
9397 .ok_or_else(|| {
9398 DataFusionError::Internal("cypher_quantifier: first argument is not a list".into())
9399 })?;
9400 let elem_type = match list.data_type() {
9401 DataType::List(f) | DataType::LargeList(f) => f.data_type().clone(),
9402 _ => DataType::Null,
9403 };
9404
9405 let mut fields = vec![Field::new(&self.elem_name, elem_type, true)];
9407 for (i, name) in self.outer_names.iter().enumerate() {
9408 fields.push(Field::new(name, cols[i + 1].data_type().clone(), true));
9409 }
9410 let schema = Arc::new(Schema::new(fields));
9411 let df_schema = DFSchema::try_from(schema.as_ref().clone())?;
9412 let phys = create_physical_expr(&self.predicate, &df_schema, &ExecutionProps::new());
9417
9418 let mut out = BooleanArray::builder(rows);
9419 for row in 0..rows {
9420 if list.is_null(row) {
9421 out.append_null(); continue;
9423 }
9424 let elems = list.value(row);
9425 let n = elems.len();
9426 if n == 0 {
9427 out.append_option(reduce_quantifier(self.kind, &BooleanArray::new_null(0), 0));
9431 continue;
9432 }
9433 let phys = phys
9434 .as_ref()
9435 .map_err(|e| DataFusionError::Execution(e.to_string()))?;
9436 let verdict = (|| {
9437 let mut batch_cols: Vec<ArrayRef> = Vec::with_capacity(1 + self.outer_names.len());
9438 batch_cols.push(elems);
9439 for i in 0..self.outer_names.len() {
9440 let sv = ScalarValue::try_from_array(&cols[i + 1], row).ok()?;
9441 batch_cols.push(sv.to_array_of_size(n).ok()?);
9442 }
9443 let batch = RecordBatch::try_new(Arc::clone(&schema), batch_cols).ok()?;
9444 let evaluated = phys.evaluate(&batch).ok()?.into_array(n).ok()?;
9445 if evaluated.data_type() == &DataType::Null {
9448 return reduce_quantifier(self.kind, &BooleanArray::new_null(n), n);
9449 }
9450 let bools = evaluated.as_any().downcast_ref::<BooleanArray>()?;
9451 reduce_quantifier(self.kind, bools, n)
9452 })();
9453 out.append_option(verdict);
9454 }
9455 Ok(ColumnarValue::Array(std::sync::Arc::new(out.finish())))
9456 }
9457}
9458
9459#[derive(Debug, PartialEq, Eq, Hash)]
9467struct CypherListComp {
9468 filter: Option<DfExpr>,
9469 projection: Option<DfExpr>,
9470 elem_name: String,
9471 outer_names: Vec<String>,
9472 signature: Signature,
9473}
9474
9475impl CypherListComp {
9476 fn new(
9477 filter: Option<DfExpr>,
9478 projection: Option<DfExpr>,
9479 elem_name: String,
9480 outer_names: Vec<String>,
9481 ) -> Self {
9482 let arity = 1 + outer_names.len();
9483 Self {
9484 filter,
9485 projection,
9486 elem_name,
9487 outer_names,
9488 signature: Signature::any(arity, Volatility::Volatile),
9491 }
9492 }
9493
9494 fn item_type(
9499 &self,
9500 elem_type: &DataType,
9501 outer_types: &[DataType],
9502 ) -> datafusion::error::Result<DataType> {
9503 use datafusion::arrow::datatypes::{Field, Schema};
9504 use datafusion::common::DFSchema;
9505 use datafusion::logical_expr::execution_props::ExecutionProps;
9506 use datafusion::physical_expr::create_physical_expr;
9507 let Some(proj) = &self.projection else {
9508 return Ok(elem_type.clone());
9509 };
9510 let mut fields = vec![Field::new(&self.elem_name, elem_type.clone(), true)];
9511 for (name, dt) in self.outer_names.iter().zip(outer_types) {
9512 fields.push(Field::new(name, dt.clone(), true));
9513 }
9514 let schema = Schema::new(fields);
9515 let df_schema = DFSchema::try_from(schema.clone())?;
9516 let phys = create_physical_expr(proj, &df_schema, &ExecutionProps::new())?;
9517 phys.data_type(&schema)
9518 }
9519
9520 #[allow(
9521 clippy::too_many_lines,
9522 reason = "one flatten/filter/project/reassemble pass keeps volatile evaluation and row offsets aligned"
9523 )]
9524 fn invoke_uncorrelated(
9525 list: &datafusion::arrow::array::ListArray,
9526 schema: datafusion::arrow::datatypes::SchemaRef,
9527 filter_phys: Option<&Arc<dyn datafusion::physical_expr::PhysicalExpr>>,
9528 proj_phys: Option<&Arc<dyn datafusion::physical_expr::PhysicalExpr>>,
9529 item_type: &DataType,
9530 ) -> datafusion::error::Result<ColumnarValue> {
9531 use datafusion::arrow::array::{
9532 Array, ArrayRef, BooleanArray, ListArray, UInt32Array, new_empty_array,
9533 };
9534 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
9535 use datafusion::arrow::compute::{cast, filter_record_batch, take};
9536 use datafusion::arrow::datatypes::Field;
9537 use datafusion::arrow::record_batch::RecordBatch;
9538 use datafusion::error::DataFusionError;
9539
9540 let rows = list.len();
9541 let mut validity = Vec::with_capacity(rows);
9542 let mut lengths = Vec::with_capacity(rows);
9543 let mut indices = Vec::new();
9544 let offsets = list.value_offsets();
9545 for row in 0..rows {
9546 let valid = list.is_valid(row);
9547 validity.push(valid);
9548 let length = if valid {
9549 usize::try_from(offsets[row + 1] - offsets[row]).map_err(|_| {
9550 DataFusionError::Internal("negative list-comprehension length".into())
9551 })?
9552 } else {
9553 0
9554 };
9555 lengths.push(length);
9556 if valid {
9557 for index in offsets[row]..offsets[row + 1] {
9558 indices.push(u32::try_from(index).map_err(|_| {
9559 DataFusionError::Internal(
9560 "list-comprehension element index exceeds u32::MAX".into(),
9561 )
9562 })?);
9563 }
9564 }
9565 }
9566
9567 let flat: ArrayRef = if indices.len() == list.values().len()
9568 && indices.first().is_none_or(|first| *first == 0)
9569 {
9570 Arc::clone(list.values())
9571 } else {
9572 take(list.values(), &UInt32Array::from(indices), None)?
9573 };
9574 let total = flat.len();
9575 let batch = RecordBatch::try_new(schema, vec![flat])?;
9576 let mask = if let Some(filter) = filter_phys
9577 && total > 0
9578 {
9579 let evaluated = filter.evaluate(&batch)?.into_array(total)?;
9580 let evaluated = evaluated
9581 .as_any()
9582 .downcast_ref::<BooleanArray>()
9583 .ok_or_else(|| {
9584 DataFusionError::Internal(
9585 "cypher_list_comprehension: filter did not evaluate to boolean".into(),
9586 )
9587 })?;
9588 Some(
9589 (0..total)
9590 .map(|index| evaluated.is_valid(index) && evaluated.value(index))
9591 .collect::<BooleanArray>(),
9592 )
9593 } else {
9594 None
9595 };
9596 let kept = if let Some(mask) = &mask {
9597 filter_record_batch(&batch, mask)?
9598 } else {
9599 batch
9600 };
9601 let kept_rows = kept.num_rows();
9602 let projected = if let Some(projection) = proj_phys {
9603 if kept_rows == 0 {
9604 new_empty_array(item_type)
9605 } else {
9606 projection.evaluate(&kept)?.into_array(kept_rows)?
9607 }
9608 } else {
9609 Arc::clone(kept.column(0))
9610 };
9611 let projected = if projected.data_type() == item_type {
9612 projected
9613 } else {
9614 cast(&projected, item_type)?
9615 };
9616
9617 let mut output_offsets = Vec::with_capacity(rows + 1);
9618 output_offsets.push(0i32);
9619 let mut input_offset = 0usize;
9620 let mut output_offset = 0i32;
9621 for length in lengths {
9622 let kept = mask.as_ref().map_or(length, |mask| {
9623 (input_offset..input_offset + length)
9624 .filter(|index| mask.value(*index))
9625 .count()
9626 });
9627 input_offset += length;
9628 let kept = i32::try_from(kept).map_err(|_| {
9629 DataFusionError::Internal(
9630 "cypher_list_comprehension: list length exceeds i32::MAX".into(),
9631 )
9632 })?;
9633 output_offset = output_offset.checked_add(kept).ok_or_else(|| {
9634 DataFusionError::Internal(
9635 "cypher_list_comprehension: total list length exceeds i32::MAX".into(),
9636 )
9637 })?;
9638 output_offsets.push(output_offset);
9639 }
9640 let list = ListArray::try_new(
9641 Arc::new(Field::new("item", item_type.clone(), true)),
9642 OffsetBuffer::new(ScalarBuffer::from(output_offsets)),
9643 projected,
9644 Some(NullBuffer::from(validity)),
9645 )?;
9646 Ok(ColumnarValue::Array(Arc::new(list)))
9647 }
9648}
9649
9650impl ScalarUDFImpl for CypherListComp {
9651 fn as_any(&self) -> &dyn Any {
9652 self
9653 }
9654 fn name(&self) -> &'static str {
9655 "cypher_list_comprehension"
9656 }
9657 fn signature(&self) -> &Signature {
9658 &self.signature
9659 }
9660 fn return_type(&self, arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9661 use datafusion::arrow::datatypes::Field;
9662 let elem_type = match arg_types.first() {
9667 Some(DataType::List(f)) => f.data_type().clone(),
9668 _ => DataType::Null,
9669 };
9670 let item = self.item_type(&elem_type, arg_types.get(1..).unwrap_or(&[]))?;
9671 Ok(DataType::List(std::sync::Arc::new(Field::new(
9672 "item", item, true,
9673 ))))
9674 }
9675
9676 #[allow(
9677 clippy::too_many_lines,
9678 reason = "the per-row filter/project/reassemble loop reads clearest inline"
9679 )]
9680 fn invoke_with_args(
9681 &self,
9682 args: ScalarFunctionArgs,
9683 ) -> datafusion::error::Result<ColumnarValue> {
9684 use datafusion::arrow::array::{
9685 Array, ArrayRef, BooleanArray, ListArray, RecordBatch, new_empty_array,
9686 };
9687 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
9688 use datafusion::arrow::compute::{cast, concat, filter_record_batch};
9689 use datafusion::arrow::datatypes::{Field, Schema};
9690 use datafusion::common::DFSchema;
9691 use datafusion::error::DataFusionError;
9692 use datafusion::logical_expr::execution_props::ExecutionProps;
9693 use datafusion::physical_expr::create_physical_expr;
9694 use std::sync::Arc;
9695
9696 let rows = args.number_rows;
9697 let cols: Vec<ArrayRef> = args
9698 .args
9699 .iter()
9700 .map(|a| a.to_array(rows))
9701 .collect::<datafusion::error::Result<_>>()?;
9702 let list = cols[0]
9703 .as_any()
9704 .downcast_ref::<ListArray>()
9705 .ok_or_else(|| {
9706 DataFusionError::Internal(
9707 "cypher_list_comprehension: first argument is not a list".into(),
9708 )
9709 })?;
9710 let elem_type = match list.data_type() {
9711 DataType::List(f) => f.data_type().clone(),
9712 _ => DataType::Null,
9713 };
9714 let outer_types: Vec<DataType> = (0..self.outer_names.len())
9715 .map(|i| cols[i + 1].data_type().clone())
9716 .collect();
9717 let item_type = self.item_type(&elem_type, &outer_types)?;
9718
9719 let mut fields = vec![Field::new(&self.elem_name, elem_type, true)];
9721 for (i, name) in self.outer_names.iter().enumerate() {
9722 fields.push(Field::new(name, cols[i + 1].data_type().clone(), true));
9723 }
9724 let schema = Arc::new(Schema::new(fields));
9725 let df_schema = DFSchema::try_from(schema.as_ref().clone())?;
9726 let props = ExecutionProps::new();
9727 let filter_phys = self
9728 .filter
9729 .as_ref()
9730 .map(|f| create_physical_expr(f, &df_schema, &props))
9731 .transpose()?;
9732 let proj_phys = self
9733 .projection
9734 .as_ref()
9735 .map(|p| create_physical_expr(p, &df_schema, &props))
9736 .transpose()?;
9737
9738 if self.outer_names.is_empty() {
9739 return Self::invoke_uncorrelated(
9740 list,
9741 schema,
9742 filter_phys.as_ref(),
9743 proj_phys.as_ref(),
9744 &item_type,
9745 );
9746 }
9747
9748 let mut pieces: Vec<ArrayRef> = Vec::new();
9749 let mut offsets: Vec<i32> = Vec::with_capacity(rows + 1);
9750 offsets.push(0);
9751 let mut validity: Vec<bool> = Vec::with_capacity(rows);
9752 let mut cur: i32 = 0;
9753
9754 for row in 0..rows {
9755 if list.is_null(row) {
9756 validity.push(false);
9757 offsets.push(cur);
9758 continue;
9759 }
9760 validity.push(true);
9761 let elems = list.value(row);
9762 let n = elems.len();
9763 let mut batch_cols: Vec<ArrayRef> = Vec::with_capacity(1 + self.outer_names.len());
9764 batch_cols.push(elems);
9765 for i in 0..self.outer_names.len() {
9766 let sv = ScalarValue::try_from_array(&cols[i + 1], row)?;
9767 batch_cols.push(sv.to_array_of_size(n)?);
9768 }
9769 let batch = RecordBatch::try_new(Arc::clone(&schema), batch_cols)?;
9770
9771 let kept = if let Some(fp) = &filter_phys {
9773 let mask = fp.evaluate(&batch)?.into_array(n)?;
9774 let mask = mask
9775 .as_any()
9776 .downcast_ref::<BooleanArray>()
9777 .ok_or_else(|| {
9778 DataFusionError::Internal(
9779 "cypher_list_comprehension: filter did not evaluate to boolean".into(),
9780 )
9781 })?;
9782 let clean: BooleanArray =
9783 (0..n).map(|j| mask.is_valid(j) && mask.value(j)).collect();
9784 filter_record_batch(&batch, &clean)?
9785 } else {
9786 batch
9787 };
9788
9789 let projected: ArrayRef = if let Some(pp) = &proj_phys {
9791 let m = kept.num_rows();
9792 if m == 0 {
9793 new_empty_array(&item_type)
9794 } else {
9795 pp.evaluate(&kept)?.into_array(m)?
9796 }
9797 } else {
9798 Arc::clone(kept.column(0))
9799 };
9800 let projected = if projected.data_type() == &item_type {
9801 projected
9802 } else {
9803 cast(&projected, &item_type)?
9804 };
9805
9806 let len = i32::try_from(projected.len()).map_err(|_| {
9807 DataFusionError::Internal("cypher_list_comprehension: list too long".into())
9808 })?;
9809 cur = cur.checked_add(len).ok_or_else(|| {
9810 DataFusionError::Internal(
9811 "cypher_list_comprehension: total list length exceeds i32::MAX".into(),
9812 )
9813 })?;
9814 offsets.push(cur);
9815 pieces.push(projected);
9816 }
9817
9818 let child: ArrayRef = if pieces.is_empty() {
9819 new_empty_array(&item_type)
9820 } else {
9821 let refs: Vec<&dyn Array> = pieces.iter().map(AsRef::as_ref).collect();
9822 concat(&refs)?
9823 };
9824 let field = Arc::new(Field::new("item", item_type, true));
9825 let list_arr = ListArray::try_new(
9826 field,
9827 OffsetBuffer::new(ScalarBuffer::from(offsets)),
9828 child,
9829 Some(NullBuffer::from(validity)),
9830 )?;
9831 Ok(ColumnarValue::Array(Arc::new(list_arr)))
9832 }
9833}
9834
9835static CYPHER_DATE_PROJECT: LazyLock<ScalarUDF> =
9845 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDateProject::new()));
9846
9847#[derive(Debug, PartialEq, Eq, Hash)]
9848struct CypherDateProject {
9849 signature: Signature,
9850}
9851
9852impl CypherDateProject {
9853 fn new() -> Self {
9854 Self {
9855 signature: Signature::any(9, Volatility::Immutable),
9856 }
9857 }
9858}
9859
9860impl ScalarUDFImpl for CypherDateProject {
9861 fn as_any(&self) -> &dyn Any {
9862 self
9863 }
9864
9865 fn name(&self) -> &'static str {
9866 "cypher_date_project"
9867 }
9868
9869 fn signature(&self) -> &Signature {
9870 &self.signature
9871 }
9872
9873 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9874 Ok(DataType::Struct(
9875 graphforge_storage::schemas::date_struct_fields(),
9876 ))
9877 }
9878
9879 fn invoke_with_args(
9880 &self,
9881 args: ScalarFunctionArgs,
9882 ) -> datafusion::error::Result<ColumnarValue> {
9883 use crate::temporal::{DateOverrides, parse_date_or_datetime_prefix};
9884 use datafusion::arrow::array::{Array, Int64Array, StringArray, StructArray};
9885 use datafusion::arrow::compute::cast;
9886
9887 let rows = args.number_rows;
9888 let cols: Vec<_> = args
9889 .args
9890 .iter()
9891 .map(|a| a.to_array(rows))
9892 .collect::<datafusion::error::Result<_>>()?;
9893 let base = if is_date_struct(cols[0].data_type())
9896 || is_localdatetime_struct(cols[0].data_type())
9897 || is_time_struct(cols[0].data_type())
9898 || is_datetime_struct(cols[0].data_type())
9899 {
9900 std::sync::Arc::clone(&cols[0])
9901 } else {
9902 cast(&cols[0], &DataType::Utf8).map_err(datafusion::error::DataFusionError::from)?
9903 };
9904 let ov: Vec<_> = cols[1..9]
9906 .iter()
9907 .map(|c| cast(c, &DataType::Int64))
9908 .collect::<std::result::Result<Vec<_>, _>>()
9909 .map_err(datafusion::error::DataFusionError::from)?;
9910
9911 let base_date = |i: usize| -> Option<i64> {
9912 if base.is_null(i) {
9913 return None;
9914 }
9915 match base.data_type() {
9916 DataType::Struct(_) => {
9917 let s = base.as_any().downcast_ref::<StructArray>()?;
9918 if is_date_struct(base.data_type()) {
9919 date_struct_value(s, i)
9920 } else {
9921 Some(localdatetime_struct_parts(s, i)?.0)
9923 }
9924 }
9925 DataType::Utf8 => parse_date_or_datetime_prefix(
9926 base.as_any().downcast_ref::<StringArray>()?.value(i),
9927 ),
9928 _ => None,
9929 }
9930 };
9931 let field = |idx: usize, i: usize| -> Option<i64> {
9932 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
9933 (!a.is_null(i)).then(|| a.value(i))
9934 };
9935
9936 let out: Vec<Option<i64>> = (0..rows)
9937 .map(|i| {
9938 let overrides = DateOverrides {
9939 year: field(0, i),
9940 month: field(1, i),
9941 day: field(2, i),
9942 week: field(3, i),
9943 day_of_week: field(4, i),
9944 ordinal_day: field(5, i),
9945 quarter: field(6, i),
9946 day_of_quarter: field(7, i),
9947 };
9948 crate::temporal::project_date(base_date(i)?, &overrides)
9949 })
9950 .collect();
9951 Ok(ColumnarValue::Array(std::sync::Arc::new(
9952 build_date_struct(&out),
9953 )))
9954 }
9955}
9956
9957static CYPHER_LOCALTIME_PROJECT: LazyLock<ScalarUDF> =
9968 LazyLock::new(|| ScalarUDF::new_from_impl(CypherLocalTimeProject::new()));
9969
9970#[derive(Debug, PartialEq, Eq, Hash)]
9971struct CypherLocalTimeProject {
9972 signature: Signature,
9973}
9974
9975impl CypherLocalTimeProject {
9976 fn new() -> Self {
9977 Self {
9978 signature: Signature::any(7, Volatility::Immutable),
9979 }
9980 }
9981}
9982
9983impl ScalarUDFImpl for CypherLocalTimeProject {
9984 fn as_any(&self) -> &dyn Any {
9985 self
9986 }
9987
9988 fn name(&self) -> &'static str {
9989 "cypher_localtime_project"
9990 }
9991
9992 fn signature(&self) -> &Signature {
9993 &self.signature
9994 }
9995
9996 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
9997 Ok(DataType::Time64(
9998 datafusion::arrow::datatypes::TimeUnit::Nanosecond,
9999 ))
10000 }
10001
10002 fn invoke_with_args(
10003 &self,
10004 args: ScalarFunctionArgs,
10005 ) -> datafusion::error::Result<ColumnarValue> {
10006 use crate::temporal::{LocalTimeOverrides, project_localtime, time_of_day_nanos_any};
10007 use datafusion::arrow::array::{
10008 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
10009 };
10010 use datafusion::arrow::compute::cast;
10011 use datafusion::arrow::datatypes::TimeUnit;
10012 use datafusion::error::DataFusionError;
10013
10014 let rows = args.number_rows;
10015 let cols: Vec<_> = args
10016 .args
10017 .iter()
10018 .map(|a| a.to_array(rows))
10019 .collect::<datafusion::error::Result<_>>()?;
10020 let base: ArrayRef =
10023 if matches!(cols[0].data_type(), DataType::Time64(TimeUnit::Nanosecond))
10024 || is_localdatetime_struct(cols[0].data_type())
10025 || is_time_struct(cols[0].data_type())
10026 || is_datetime_struct(cols[0].data_type())
10027 {
10028 std::sync::Arc::clone(&cols[0])
10029 } else {
10030 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
10031 };
10032 let ov: Vec<_> = cols[1..7]
10033 .iter()
10034 .map(|c| cast(c, &DataType::Int64))
10035 .collect::<std::result::Result<Vec<_>, _>>()
10036 .map_err(DataFusionError::from)?;
10037
10038 let base_nanos = |i: usize| -> Option<i64> {
10039 if base.is_null(i) {
10040 return None;
10041 }
10042 match base.data_type() {
10043 DataType::Time64(TimeUnit::Nanosecond) => Some(
10044 base.as_any()
10045 .downcast_ref::<Time64NanosecondArray>()?
10046 .value(i),
10047 ),
10048 DataType::Struct(_) => {
10050 let s = base.as_any().downcast_ref::<StructArray>()?;
10051 if is_time_struct(base.data_type()) {
10052 Some(time_struct_parts(s, i)?.0)
10053 } else {
10054 Some(localdatetime_struct_parts(s, i)?.1)
10055 }
10056 }
10057 DataType::Utf8 => {
10058 time_of_day_nanos_any(base.as_any().downcast_ref::<StringArray>()?.value(i))
10059 }
10060 _ => None,
10061 }
10062 };
10063 let field = |idx: usize, i: usize| -> Option<i64> {
10064 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
10065 (!a.is_null(i)).then(|| a.value(i))
10066 };
10067
10068 let out: Time64NanosecondArray = (0..rows)
10069 .map(|i| {
10070 let overrides = LocalTimeOverrides {
10071 hour: field(0, i),
10072 minute: field(1, i),
10073 second: field(2, i),
10074 millisecond: field(3, i),
10075 microsecond: field(4, i),
10076 nanosecond: field(5, i),
10077 };
10078 project_localtime(base_nanos(i)?, &overrides)
10079 })
10080 .collect();
10081 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
10082 }
10083}
10084
10085static CYPHER_LOCALTIME_TRUNCATE: LazyLock<ScalarUDF> =
10095 LazyLock::new(|| ScalarUDF::new_from_impl(CypherLocalTimeTruncate::new()));
10096
10097#[derive(Debug, PartialEq, Eq, Hash)]
10098struct CypherLocalTimeTruncate {
10099 signature: Signature,
10100}
10101
10102impl CypherLocalTimeTruncate {
10103 fn new() -> Self {
10104 Self {
10105 signature: Signature::any(8, Volatility::Immutable),
10106 }
10107 }
10108}
10109
10110impl ScalarUDFImpl for CypherLocalTimeTruncate {
10111 fn as_any(&self) -> &dyn Any {
10112 self
10113 }
10114
10115 fn name(&self) -> &'static str {
10116 "cypher_localtime_truncate"
10117 }
10118
10119 fn signature(&self) -> &Signature {
10120 &self.signature
10121 }
10122
10123 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
10124 Ok(DataType::Time64(
10125 datafusion::arrow::datatypes::TimeUnit::Nanosecond,
10126 ))
10127 }
10128
10129 fn invoke_with_args(
10130 &self,
10131 args: ScalarFunctionArgs,
10132 ) -> datafusion::error::Result<ColumnarValue> {
10133 use crate::temporal::{
10134 LocalTimeOverrides, project_localtime, time_of_day_nanos_any, truncate_time_nanos,
10135 };
10136 use datafusion::arrow::array::{
10137 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
10138 };
10139 use datafusion::arrow::compute::cast;
10140 use datafusion::arrow::datatypes::TimeUnit;
10141 use datafusion::error::DataFusionError;
10142
10143 let rows = args.number_rows;
10144 let cols: Vec<_> = args
10145 .args
10146 .iter()
10147 .map(|a| a.to_array(rows))
10148 .collect::<datafusion::error::Result<_>>()?;
10149 let base: ArrayRef =
10150 if matches!(cols[0].data_type(), DataType::Time64(TimeUnit::Nanosecond))
10151 || is_localdatetime_struct(cols[0].data_type())
10152 || is_time_struct(cols[0].data_type())
10153 || is_datetime_struct(cols[0].data_type())
10154 {
10155 std::sync::Arc::clone(&cols[0])
10156 } else {
10157 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
10158 };
10159 let units_arr = cast(&cols[1], &DataType::Utf8).map_err(DataFusionError::from)?;
10160 let units = units_arr.as_any().downcast_ref::<StringArray>();
10161 let ov: Vec<_> = cols[2..8]
10162 .iter()
10163 .map(|c| cast(c, &DataType::Int64))
10164 .collect::<std::result::Result<Vec<_>, _>>()
10165 .map_err(DataFusionError::from)?;
10166
10167 let base_nanos = |i: usize| -> Option<i64> {
10168 if base.is_null(i) {
10169 return None;
10170 }
10171 match base.data_type() {
10172 DataType::Time64(TimeUnit::Nanosecond) => Some(
10173 base.as_any()
10174 .downcast_ref::<Time64NanosecondArray>()?
10175 .value(i),
10176 ),
10177 DataType::Struct(_) => {
10178 let s = base.as_any().downcast_ref::<StructArray>()?;
10179 if is_time_struct(base.data_type()) {
10180 Some(time_struct_parts(s, i)?.0)
10181 } else {
10182 Some(localdatetime_struct_parts(s, i)?.1)
10183 }
10184 }
10185 DataType::Utf8 => {
10186 time_of_day_nanos_any(base.as_any().downcast_ref::<StringArray>()?.value(i))
10187 }
10188 _ => None,
10189 }
10190 };
10191 let field = |idx: usize, i: usize| -> Option<i64> {
10192 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
10193 (!a.is_null(i)).then(|| a.value(i))
10194 };
10195
10196 let out: Time64NanosecondArray = (0..rows)
10197 .map(|i| {
10198 let u = units?;
10199 if u.is_null(i) {
10200 return None;
10201 }
10202 let truncated = truncate_time_nanos(base_nanos(i)?, u.value(i))?;
10203 let overrides = LocalTimeOverrides {
10204 hour: field(0, i),
10205 minute: field(1, i),
10206 second: field(2, i),
10207 millisecond: field(3, i),
10208 microsecond: field(4, i),
10209 nanosecond: field(5, i),
10210 };
10211 project_localtime(truncated, &overrides)
10212 })
10213 .collect();
10214 Ok(ColumnarValue::Array(std::sync::Arc::new(out)))
10215 }
10216}
10217
10218fn date_fields() -> datafusion::arrow::datatypes::Fields {
10227 graphforge_storage::schemas::date_struct_fields()
10228}
10229
10230fn is_date_struct(dt: &DataType) -> bool {
10233 matches!(dt, DataType::Struct(fields)
10234 if fields.len() == 1
10235 && fields[0].name() == "epoch_day"
10236 && *fields[0].data_type() == DataType::Int64)
10237}
10238
10239fn build_date_struct(rows: &[Option<i64>]) -> datafusion::arrow::array::StructArray {
10242 use datafusion::arrow::array::Int64Array;
10243 use datafusion::arrow::buffer::NullBuffer;
10244 let days: Int64Array = rows.iter().copied().collect();
10245 let nulls = rows.iter().map(Option::is_some).collect::<NullBuffer>();
10246 datafusion::arrow::array::StructArray::new(
10247 date_fields(),
10248 vec![std::sync::Arc::new(days)],
10249 Some(nulls),
10250 )
10251}
10252
10253fn date_scalar(days: Option<i64>) -> ScalarValue {
10255 ScalarValue::Struct(std::sync::Arc::new(build_date_struct(&[days])))
10256}
10257
10258fn date_struct_value(arr: &datafusion::arrow::array::StructArray, i: usize) -> Option<i64> {
10260 use datafusion::arrow::array::{Array, Int64Array};
10261 if arr.is_null(i) {
10262 return None;
10263 }
10264 let days = arr.column(0).as_any().downcast_ref::<Int64Array>()?;
10265 days.is_valid(i).then(|| days.value(i))
10266}
10267
10268fn localdatetime_fields() -> datafusion::arrow::datatypes::Fields {
10272 graphforge_storage::schemas::localdatetime_struct_fields()
10273}
10274
10275fn is_localdatetime_struct(dt: &DataType) -> bool {
10279 use datafusion::arrow::datatypes::TimeUnit;
10280 matches!(dt, DataType::Struct(fields)
10281 if fields.len() == 2
10282 && fields[0].name() == "date"
10283 && *fields[0].data_type() == DataType::Int64
10284 && fields[1].name() == "time"
10285 && *fields[1].data_type() == DataType::Time64(TimeUnit::Nanosecond))
10286}
10287
10288fn build_localdatetime_struct(
10291 rows: &[Option<(i64, i64)>],
10292) -> datafusion::arrow::array::StructArray {
10293 use datafusion::arrow::array::{Int64Array, Time64NanosecondArray};
10294 use datafusion::arrow::buffer::NullBuffer;
10295 let days: Int64Array = rows.iter().map(|r| r.map(|(d, _)| d)).collect();
10296 let nanos: Time64NanosecondArray = rows.iter().map(|r| r.map(|(_, n)| n)).collect();
10297 let nulls = rows.iter().map(Option::is_some).collect::<NullBuffer>();
10298 datafusion::arrow::array::StructArray::new(
10299 localdatetime_fields(),
10300 vec![std::sync::Arc::new(days), std::sync::Arc::new(nanos)],
10301 Some(nulls),
10302 )
10303}
10304
10305fn localdatetime_scalar(parts: Option<(i64, i64)>) -> ScalarValue {
10307 ScalarValue::Struct(std::sync::Arc::new(build_localdatetime_struct(&[parts])))
10308}
10309
10310fn is_duration_struct(dt: &DataType) -> bool {
10313 matches!(dt, DataType::Struct(fields)
10314 if fields.len() == 4
10315 && fields[0].name() == "months"
10316 && fields[1].name() == "days"
10317 && fields[2].name() == "seconds"
10318 && fields[3].name() == "nanos")
10319}
10320
10321fn is_temporal_clock_fn(name: &str) -> bool {
10325 matches!(
10327 name.to_ascii_lowercase().split_once('.'),
10328 Some((
10329 "date" | "localtime" | "time" | "localdatetime" | "datetime",
10330 "transaction" | "statement" | "realtime",
10331 ))
10332 )
10333}
10334
10335fn temporal_null_scalar(name: &str) -> ScalarValue {
10341 let lower = name.to_ascii_lowercase();
10342 let base = lower.split('.').next().unwrap_or(&lower);
10343 match base {
10344 "date" => date_scalar(None),
10345 "localtime" => ScalarValue::Time64Nanosecond(None),
10346 "time" => time_scalar(None),
10347 "localdatetime" => localdatetime_scalar(None),
10348 "datetime" => datetime_scalar(None),
10349 "duration" => duration_scalar(None),
10350 _ => ScalarValue::Null,
10351 }
10352}
10353
10354fn build_duration_struct(
10359 rows: &[Option<crate::temporal::DurationValue>],
10360) -> datafusion::arrow::array::StructArray {
10361 use datafusion::arrow::array::Int64Array;
10362 use datafusion::arrow::buffer::NullBuffer;
10363 let months: Int64Array = rows.iter().map(|r| r.map(|d| d.months)).collect();
10364 let days: Int64Array = rows.iter().map(|r| r.map(|d| d.days)).collect();
10365 let seconds: Int64Array = rows.iter().map(|r| r.map(|d| d.seconds)).collect();
10366 let nanos: Int64Array = rows.iter().map(|r| r.map(|d| d.nanos)).collect();
10367 let nulls = rows.iter().map(Option::is_some).collect::<NullBuffer>();
10368 datafusion::arrow::array::StructArray::new(
10369 graphforge_storage::schemas::duration_struct_fields(),
10370 vec![
10371 std::sync::Arc::new(months),
10372 std::sync::Arc::new(days),
10373 std::sync::Arc::new(seconds),
10374 std::sync::Arc::new(nanos),
10375 ],
10376 Some(nulls),
10377 )
10378}
10379
10380fn duration_struct_parts(
10383 arr: &datafusion::arrow::array::StructArray,
10384 i: usize,
10385) -> Option<crate::temporal::DurationValue> {
10386 use datafusion::arrow::array::{Array, Int64Array};
10387 if arr.is_null(i) {
10388 return None;
10389 }
10390 let col = |idx: usize| arr.column(idx).as_any().downcast_ref::<Int64Array>();
10391 Some(crate::temporal::DurationValue {
10392 months: col(0)?.value(i),
10393 days: col(1)?.value(i),
10394 seconds: col(2)?.value(i),
10395 nanos: col(3)?.value(i),
10396 })
10397}
10398
10399fn duration_scalar(parts: Option<crate::temporal::DurationValue>) -> ScalarValue {
10401 ScalarValue::Struct(std::sync::Arc::new(build_duration_struct(&[parts])))
10402}
10403
10404fn dur_secs_nanos(seconds: i64, nanos: i64) -> crate::temporal::DurationValue {
10407 crate::temporal::DurationValue {
10408 months: 0,
10409 days: 0,
10410 seconds,
10411 nanos,
10412 }
10413}
10414
10415fn duration_value_to_ir(d: crate::temporal::DurationValue) -> IrLiteral {
10417 IrLiteral::Duration {
10418 months: d.months,
10419 days: d.days,
10420 seconds: d.seconds,
10421 nanos: d.nanos,
10422 }
10423}
10424
10425fn localdatetime_struct_parts(
10431 arr: &datafusion::arrow::array::StructArray,
10432 i: usize,
10433) -> Option<(i64, i64)> {
10434 use datafusion::arrow::array::{Array, Int64Array, Time64NanosecondArray};
10435 if arr.is_null(i) {
10436 return None;
10437 }
10438 let d = arr.column(0).as_any().downcast_ref::<Int64Array>()?;
10439 let t = arr
10440 .column(1)
10441 .as_any()
10442 .downcast_ref::<Time64NanosecondArray>()?;
10443 (!d.is_null(i) && !t.is_null(i)).then(|| (d.value(i), t.value(i)))
10444}
10445
10446static CYPHER_LOCALDATETIME_PROJECT: LazyLock<ScalarUDF> =
10455 LazyLock::new(|| ScalarUDF::new_from_impl(CypherLocalDateTimeProject::new()));
10456
10457#[derive(Debug, PartialEq, Eq, Hash)]
10458struct CypherLocalDateTimeProject {
10459 signature: Signature,
10460}
10461
10462impl CypherLocalDateTimeProject {
10463 fn new() -> Self {
10464 Self {
10465 signature: Signature::any(16, Volatility::Immutable),
10466 }
10467 }
10468}
10469
10470impl ScalarUDFImpl for CypherLocalDateTimeProject {
10471 fn as_any(&self) -> &dyn Any {
10472 self
10473 }
10474
10475 fn name(&self) -> &'static str {
10476 "cypher_localdatetime_project"
10477 }
10478
10479 fn signature(&self) -> &Signature {
10480 &self.signature
10481 }
10482
10483 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
10484 Ok(DataType::Struct(localdatetime_fields()))
10485 }
10486
10487 #[allow(
10488 clippy::too_many_lines,
10489 reason = "one cohesive per-row projection: two typed base extractions \
10490 (date + time) plus 14 component overrides — splitting it would \
10491 scatter the row logic across helpers without aiding clarity"
10492 )]
10493 fn invoke_with_args(
10494 &self,
10495 args: ScalarFunctionArgs,
10496 ) -> datafusion::error::Result<ColumnarValue> {
10497 use crate::temporal::{
10498 DateOverrides, LocalTimeOverrides, parse_date_or_datetime_prefix, project_date,
10499 project_localtime, time_of_day_nanos_any,
10500 };
10501 use datafusion::arrow::array::{
10502 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
10503 };
10504 use datafusion::arrow::compute::cast;
10505 use datafusion::arrow::datatypes::TimeUnit;
10506 use datafusion::error::DataFusionError;
10507
10508 let rows = args.number_rows;
10509 let cols: Vec<_> = args
10510 .args
10511 .iter()
10512 .map(|a| a.to_array(rows))
10513 .collect::<datafusion::error::Result<_>>()?;
10514 let typed_or_utf8 = |a: &ArrayRef| -> datafusion::error::Result<ArrayRef> {
10518 if matches!(a.data_type(), DataType::Time64(TimeUnit::Nanosecond))
10519 || is_date_struct(a.data_type())
10520 || is_localdatetime_struct(a.data_type())
10521 || is_time_struct(a.data_type())
10522 || is_datetime_struct(a.data_type())
10523 {
10524 Ok(std::sync::Arc::clone(a))
10525 } else {
10526 cast(a, &DataType::Utf8).map_err(DataFusionError::from)
10527 }
10528 };
10529 let date_src = typed_or_utf8(&cols[0])?;
10530 let time_src = typed_or_utf8(&cols[1])?;
10531 let ov: Vec<_> = cols[2..16]
10532 .iter()
10533 .map(|c| cast(c, &DataType::Int64))
10534 .collect::<std::result::Result<Vec<_>, _>>()
10535 .map_err(DataFusionError::from)?;
10536
10537 let base_date = |i: usize| -> Option<i64> {
10538 if date_src.is_null(i) {
10539 return Some(0); }
10541 match date_src.data_type() {
10542 DataType::Struct(_) => {
10543 let s = date_src.as_any().downcast_ref::<StructArray>()?;
10544 if is_date_struct(date_src.data_type()) {
10545 date_struct_value(s, i)
10546 } else {
10547 Some(localdatetime_struct_parts(s, i)?.0)
10549 }
10550 }
10551 DataType::Utf8 => parse_date_or_datetime_prefix(
10552 date_src.as_any().downcast_ref::<StringArray>()?.value(i),
10553 ),
10554 _ => None,
10555 }
10556 };
10557 let base_time = |i: usize| -> Option<i64> {
10558 if time_src.is_null(i) {
10559 return Some(0); }
10561 match time_src.data_type() {
10562 DataType::Time64(TimeUnit::Nanosecond) => Some(
10563 time_src
10564 .as_any()
10565 .downcast_ref::<Time64NanosecondArray>()?
10566 .value(i),
10567 ),
10568 DataType::Struct(_) => {
10569 let s = time_src.as_any().downcast_ref::<StructArray>()?;
10570 if is_date_struct(time_src.data_type()) {
10575 Some(0)
10576 } else if is_time_struct(time_src.data_type()) {
10577 Some(time_struct_parts(s, i)?.0)
10578 } else {
10579 Some(localdatetime_struct_parts(s, i)?.1)
10580 }
10581 }
10582 DataType::Utf8 => {
10583 time_of_day_nanos_any(time_src.as_any().downcast_ref::<StringArray>()?.value(i))
10584 }
10585 _ => None,
10586 }
10587 };
10588 let field = |idx: usize, i: usize| -> Option<i64> {
10589 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
10590 (!a.is_null(i)).then(|| a.value(i))
10591 };
10592
10593 let parts: Vec<Option<(i64, i64)>> = (0..rows)
10594 .map(|i| {
10595 let date_overrides = DateOverrides {
10596 year: field(0, i),
10597 month: field(1, i),
10598 day: field(2, i),
10599 week: field(3, i),
10600 day_of_week: field(4, i),
10601 ordinal_day: field(5, i),
10602 quarter: field(6, i),
10603 day_of_quarter: field(7, i),
10604 };
10605 let time_overrides = LocalTimeOverrides {
10606 hour: field(8, i),
10607 minute: field(9, i),
10608 second: field(10, i),
10609 millisecond: field(11, i),
10610 microsecond: field(12, i),
10611 nanosecond: field(13, i),
10612 };
10613 let date = project_date(base_date(i)?, &date_overrides)?;
10614 let time = project_localtime(base_time(i)?, &time_overrides)?;
10615 Some((date, time))
10616 })
10617 .collect();
10618 Ok(ColumnarValue::Array(std::sync::Arc::new(
10619 build_localdatetime_struct(&parts),
10620 )))
10621 }
10622}
10623
10624static CYPHER_LOCALDATETIME_TRUNCATE: LazyLock<ScalarUDF> =
10636 LazyLock::new(|| ScalarUDF::new_from_impl(CypherLocalDateTimeTruncate::new()));
10637
10638#[derive(Debug, PartialEq, Eq, Hash)]
10639struct CypherLocalDateTimeTruncate {
10640 signature: Signature,
10641}
10642
10643impl CypherLocalDateTimeTruncate {
10644 fn new() -> Self {
10645 Self {
10646 signature: Signature::any(16, Volatility::Immutable),
10647 }
10648 }
10649}
10650
10651impl ScalarUDFImpl for CypherLocalDateTimeTruncate {
10652 fn as_any(&self) -> &dyn Any {
10653 self
10654 }
10655
10656 fn name(&self) -> &'static str {
10657 "cypher_localdatetime_truncate"
10658 }
10659
10660 fn signature(&self) -> &Signature {
10661 &self.signature
10662 }
10663
10664 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
10665 Ok(DataType::Struct(localdatetime_fields()))
10666 }
10667
10668 #[allow(
10669 clippy::too_many_lines,
10670 reason = "one cohesive per-row truncation: typed source extraction, the \
10671 date/time granularity split, and 14 component overrides"
10672 )]
10673 fn invoke_with_args(
10674 &self,
10675 args: ScalarFunctionArgs,
10676 ) -> datafusion::error::Result<ColumnarValue> {
10677 use crate::temporal::{
10678 DateOverrides, LocalTimeOverrides, parse_date_or_datetime_prefix, project_date,
10679 project_localtime, time_of_day_nanos_any, truncate_date, truncate_time_nanos,
10680 };
10681 use datafusion::arrow::array::{
10682 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
10683 };
10684 use datafusion::arrow::compute::cast;
10685 use datafusion::arrow::datatypes::TimeUnit;
10686 use datafusion::error::DataFusionError;
10687
10688 let rows = args.number_rows;
10689 let cols: Vec<_> = args
10690 .args
10691 .iter()
10692 .map(|a| a.to_array(rows))
10693 .collect::<datafusion::error::Result<_>>()?;
10694 let value: ArrayRef =
10696 if matches!(cols[0].data_type(), DataType::Time64(TimeUnit::Nanosecond))
10697 || is_date_struct(cols[0].data_type())
10698 || is_localdatetime_struct(cols[0].data_type())
10699 || is_time_struct(cols[0].data_type())
10700 || is_datetime_struct(cols[0].data_type())
10701 {
10702 std::sync::Arc::clone(&cols[0])
10703 } else {
10704 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
10705 };
10706 let units_arr = cast(&cols[1], &DataType::Utf8).map_err(DataFusionError::from)?;
10707 let units = units_arr.as_any().downcast_ref::<StringArray>();
10708 let ov: Vec<_> = cols[2..16]
10709 .iter()
10710 .map(|c| cast(c, &DataType::Int64))
10711 .collect::<std::result::Result<Vec<_>, _>>()
10712 .map_err(DataFusionError::from)?;
10713
10714 let base_date = |i: usize| -> Option<i64> {
10715 if value.is_null(i) {
10716 return None;
10717 }
10718 match value.data_type() {
10719 DataType::Struct(_) => {
10720 let s = value.as_any().downcast_ref::<StructArray>()?;
10721 if is_date_struct(value.data_type()) {
10722 date_struct_value(s, i)
10723 } else {
10724 Some(localdatetime_struct_parts(s, i)?.0)
10725 }
10726 }
10727 DataType::Utf8 => parse_date_or_datetime_prefix(
10728 value.as_any().downcast_ref::<StringArray>()?.value(i),
10729 ),
10730 _ => None,
10731 }
10732 };
10733 let base_time = |i: usize| -> Option<i64> {
10734 if value.is_null(i) {
10735 return None;
10736 }
10737 match value.data_type() {
10738 DataType::Time64(TimeUnit::Nanosecond) => Some(
10739 value
10740 .as_any()
10741 .downcast_ref::<Time64NanosecondArray>()?
10742 .value(i),
10743 ),
10744 DataType::Struct(_) => {
10745 let s = value.as_any().downcast_ref::<StructArray>()?;
10746 if is_date_struct(value.data_type()) {
10747 Some(0) } else if is_time_struct(value.data_type()) {
10749 Some(time_struct_parts(s, i)?.0)
10750 } else {
10751 Some(localdatetime_struct_parts(s, i)?.1)
10752 }
10753 }
10754 DataType::Utf8 => {
10755 time_of_day_nanos_any(value.as_any().downcast_ref::<StringArray>()?.value(i))
10756 }
10757 _ => None,
10758 }
10759 };
10760 let field = |idx: usize, i: usize| -> Option<i64> {
10761 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
10762 (!a.is_null(i)).then(|| a.value(i))
10763 };
10764
10765 let parts: Vec<Option<(i64, i64)>> = (0..rows)
10766 .map(|i| {
10767 let u = units?;
10768 if u.is_null(i) {
10769 return None;
10770 }
10771 let (date, time) = match truncate_date(base_date(i)?, u.value(i)) {
10774 Some(d) => (d, 0i64),
10775 None => (
10776 base_date(i)?,
10777 truncate_time_nanos(base_time(i)?, u.value(i))?,
10778 ),
10779 };
10780 let date_overrides = DateOverrides {
10781 year: field(0, i),
10782 month: field(1, i),
10783 day: field(2, i),
10784 week: field(3, i),
10785 day_of_week: field(4, i),
10786 ordinal_day: field(5, i),
10787 quarter: field(6, i),
10788 day_of_quarter: field(7, i),
10789 };
10790 let time_overrides = LocalTimeOverrides {
10791 hour: field(8, i),
10792 minute: field(9, i),
10793 second: field(10, i),
10794 millisecond: field(11, i),
10795 microsecond: field(12, i),
10796 nanosecond: field(13, i),
10797 };
10798 let date = project_date(date, &date_overrides)?;
10799 let time = project_localtime(time, &time_overrides)?;
10800 Some((date, time))
10801 })
10802 .collect();
10803 Ok(ColumnarValue::Array(std::sync::Arc::new(
10804 build_localdatetime_struct(&parts),
10805 )))
10806 }
10807}
10808
10809fn time_fields() -> datafusion::arrow::datatypes::Fields {
10816 graphforge_storage::schemas::time_struct_fields()
10817}
10818
10819fn is_time_struct(dt: &DataType) -> bool {
10822 use datafusion::arrow::datatypes::TimeUnit;
10823 matches!(dt, DataType::Struct(fields)
10824 if fields.len() == 2
10825 && fields[0].name() == "time"
10826 && *fields[0].data_type() == DataType::Time64(TimeUnit::Nanosecond)
10827 && fields[1].name() == "offset"
10828 && *fields[1].data_type() == DataType::Int32)
10829}
10830
10831fn build_time_struct(rows: &[Option<(i64, i32)>]) -> datafusion::arrow::array::StructArray {
10834 use datafusion::arrow::array::{Int32Array, Time64NanosecondArray};
10835 use datafusion::arrow::buffer::NullBuffer;
10836 let nanos: Time64NanosecondArray = rows.iter().map(|r| r.map(|(n, _)| n)).collect();
10837 let offset: Int32Array = rows.iter().map(|r| r.map(|(_, o)| o)).collect();
10838 let nulls = rows.iter().map(Option::is_some).collect::<NullBuffer>();
10839 datafusion::arrow::array::StructArray::new(
10840 time_fields(),
10841 vec![std::sync::Arc::new(nanos), std::sync::Arc::new(offset)],
10842 Some(nulls),
10843 )
10844}
10845
10846fn time_scalar(parts: Option<(i64, i32)>) -> ScalarValue {
10848 ScalarValue::Struct(std::sync::Arc::new(build_time_struct(&[parts])))
10849}
10850
10851fn time_struct_parts(arr: &datafusion::arrow::array::StructArray, i: usize) -> Option<(i64, i32)> {
10854 use datafusion::arrow::array::{Array, Int32Array, Time64NanosecondArray};
10855 if arr.is_null(i) {
10856 return None;
10857 }
10858 let t = arr
10859 .column(0)
10860 .as_any()
10861 .downcast_ref::<Time64NanosecondArray>()?;
10862 let o = arr.column(1).as_any().downcast_ref::<Int32Array>()?;
10863 (!t.is_null(i) && !o.is_null(i)).then(|| (t.value(i), o.value(i)))
10864}
10865
10866static CYPHER_TIME_PROJECT: LazyLock<ScalarUDF> =
10874 LazyLock::new(|| ScalarUDF::new_from_impl(CypherTimeProject::new()));
10875
10876#[derive(Debug, PartialEq, Eq, Hash)]
10877struct CypherTimeProject {
10878 signature: Signature,
10879}
10880
10881impl CypherTimeProject {
10882 fn new() -> Self {
10883 Self {
10884 signature: Signature::any(8, Volatility::Immutable),
10885 }
10886 }
10887}
10888
10889impl ScalarUDFImpl for CypherTimeProject {
10890 fn as_any(&self) -> &dyn Any {
10891 self
10892 }
10893
10894 fn name(&self) -> &'static str {
10895 "cypher_time_project"
10896 }
10897
10898 fn signature(&self) -> &Signature {
10899 &self.signature
10900 }
10901
10902 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
10903 Ok(DataType::Struct(time_fields()))
10904 }
10905
10906 fn invoke_with_args(
10907 &self,
10908 args: ScalarFunctionArgs,
10909 ) -> datafusion::error::Result<ColumnarValue> {
10910 use crate::temporal::{
10911 LocalTimeOverrides, parse_offset_seconds, project_localtime, project_time,
10912 time_of_day_with_offset,
10913 };
10914 use datafusion::arrow::array::{
10915 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
10916 };
10917 use datafusion::arrow::compute::cast;
10918 use datafusion::arrow::datatypes::TimeUnit;
10919 use datafusion::error::DataFusionError;
10920
10921 let rows = args.number_rows;
10922 let cols: Vec<_> = args
10923 .args
10924 .iter()
10925 .map(|a| a.to_array(rows))
10926 .collect::<datafusion::error::Result<_>>()?;
10927 let base: ArrayRef =
10928 if matches!(cols[0].data_type(), DataType::Time64(TimeUnit::Nanosecond))
10929 || is_time_struct(cols[0].data_type())
10930 || is_localdatetime_struct(cols[0].data_type())
10931 || is_datetime_struct(cols[0].data_type())
10932 {
10933 std::sync::Arc::clone(&cols[0])
10934 } else {
10935 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
10936 };
10937 let ov: Vec<_> = cols[1..7]
10938 .iter()
10939 .map(|c| cast(c, &DataType::Int64))
10940 .collect::<std::result::Result<Vec<_>, _>>()
10941 .map_err(DataFusionError::from)?;
10942 let tz_arr = cast(&cols[7], &DataType::Utf8).map_err(DataFusionError::from)?;
10943 let tz = tz_arr.as_any().downcast_ref::<StringArray>();
10944
10945 let base_parts = |i: usize| -> Option<(i64, Option<i32>)> {
10948 if base.is_null(i) {
10949 return None;
10950 }
10951 match base.data_type() {
10952 DataType::Time64(TimeUnit::Nanosecond) => Some((
10953 base.as_any()
10954 .downcast_ref::<Time64NanosecondArray>()?
10955 .value(i),
10956 None,
10957 )),
10958 DataType::Struct(_) => {
10959 let s = base.as_any().downcast_ref::<StructArray>()?;
10960 if is_time_struct(base.data_type()) {
10961 let (n, o) = time_struct_parts(s, i)?;
10962 Some((n, Some(o)))
10963 } else if is_datetime_struct(base.data_type()) {
10964 let (_, n, o, _) = datetime_struct_parts(s, i)?;
10967 Some((n, Some(o)))
10968 } else {
10969 Some((localdatetime_struct_parts(s, i)?.1, None))
10971 }
10972 }
10973 DataType::Utf8 => {
10974 time_of_day_with_offset(base.as_any().downcast_ref::<StringArray>()?.value(i))
10975 }
10976 _ => None,
10977 }
10978 };
10979 let field = |idx: usize, i: usize| -> Option<i64> {
10980 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
10981 (!a.is_null(i)).then(|| a.value(i))
10982 };
10983
10984 let parts: Vec<Option<(i64, i32)>> = (0..rows)
10985 .map(|i| {
10986 let (base_nanos, base_offset) = base_parts(i)?;
10987 let overrides = LocalTimeOverrides {
10988 hour: field(0, i),
10989 minute: field(1, i),
10990 second: field(2, i),
10991 millisecond: field(3, i),
10992 microsecond: field(4, i),
10993 nanosecond: field(5, i),
10994 };
10995 let nanos = project_localtime(base_nanos, &overrides)?;
10996 let new_offset = match tz {
10998 Some(a) if !a.is_null(i) => Some(parse_offset_seconds(a.value(i))?),
10999 _ => None,
11000 };
11001 Some(project_time(nanos, base_offset, new_offset))
11002 })
11003 .collect();
11004 Ok(ColumnarValue::Array(std::sync::Arc::new(
11005 build_time_struct(&parts),
11006 )))
11007 }
11008}
11009
11010static CYPHER_TIME_TRUNCATE: LazyLock<ScalarUDF> =
11019 LazyLock::new(|| ScalarUDF::new_from_impl(CypherTimeTruncate::new()));
11020
11021#[derive(Debug, PartialEq, Eq, Hash)]
11022struct CypherTimeTruncate {
11023 signature: Signature,
11024}
11025
11026impl CypherTimeTruncate {
11027 fn new() -> Self {
11028 Self {
11029 signature: Signature::any(9, Volatility::Immutable),
11030 }
11031 }
11032}
11033
11034impl ScalarUDFImpl for CypherTimeTruncate {
11035 fn as_any(&self) -> &dyn Any {
11036 self
11037 }
11038
11039 fn name(&self) -> &'static str {
11040 "cypher_time_truncate"
11041 }
11042
11043 fn signature(&self) -> &Signature {
11044 &self.signature
11045 }
11046
11047 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
11048 Ok(DataType::Struct(time_fields()))
11049 }
11050
11051 fn invoke_with_args(
11052 &self,
11053 args: ScalarFunctionArgs,
11054 ) -> datafusion::error::Result<ColumnarValue> {
11055 use crate::temporal::{
11056 LocalTimeOverrides, parse_offset_seconds, project_localtime, project_time,
11057 time_of_day_with_offset, truncate_time_nanos,
11058 };
11059 use datafusion::arrow::array::{
11060 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
11061 };
11062 use datafusion::arrow::compute::cast;
11063 use datafusion::arrow::datatypes::TimeUnit;
11064 use datafusion::error::DataFusionError;
11065
11066 let rows = args.number_rows;
11067 let cols: Vec<_> = args
11068 .args
11069 .iter()
11070 .map(|a| a.to_array(rows))
11071 .collect::<datafusion::error::Result<_>>()?;
11072 let base: ArrayRef =
11073 if matches!(cols[0].data_type(), DataType::Time64(TimeUnit::Nanosecond))
11074 || is_time_struct(cols[0].data_type())
11075 || is_localdatetime_struct(cols[0].data_type())
11076 || is_datetime_struct(cols[0].data_type())
11077 {
11078 std::sync::Arc::clone(&cols[0])
11079 } else {
11080 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
11081 };
11082 let units_arr = cast(&cols[1], &DataType::Utf8).map_err(DataFusionError::from)?;
11083 let units = units_arr.as_any().downcast_ref::<StringArray>();
11084 let ov: Vec<_> = cols[2..8]
11085 .iter()
11086 .map(|c| cast(c, &DataType::Int64))
11087 .collect::<std::result::Result<Vec<_>, _>>()
11088 .map_err(DataFusionError::from)?;
11089 let tz_arr = cast(&cols[8], &DataType::Utf8).map_err(DataFusionError::from)?;
11090 let tz = tz_arr.as_any().downcast_ref::<StringArray>();
11091
11092 let base_parts = |i: usize| -> Option<(i64, Option<i32>)> {
11093 if base.is_null(i) {
11094 return None;
11095 }
11096 match base.data_type() {
11097 DataType::Time64(TimeUnit::Nanosecond) => Some((
11098 base.as_any()
11099 .downcast_ref::<Time64NanosecondArray>()?
11100 .value(i),
11101 None,
11102 )),
11103 DataType::Struct(_) => {
11104 let s = base.as_any().downcast_ref::<StructArray>()?;
11105 if is_time_struct(base.data_type()) {
11106 let (n, o) = time_struct_parts(s, i)?;
11107 Some((n, Some(o)))
11108 } else if is_datetime_struct(base.data_type()) {
11109 let (_, n, o, _) = datetime_struct_parts(s, i)?;
11110 Some((n, Some(o)))
11111 } else {
11112 Some((localdatetime_struct_parts(s, i)?.1, None))
11113 }
11114 }
11115 DataType::Utf8 => {
11116 time_of_day_with_offset(base.as_any().downcast_ref::<StringArray>()?.value(i))
11117 }
11118 _ => None,
11119 }
11120 };
11121 let field = |idx: usize, i: usize| -> Option<i64> {
11122 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
11123 (!a.is_null(i)).then(|| a.value(i))
11124 };
11125
11126 let parts: Vec<Option<(i64, i32)>> = (0..rows)
11127 .map(|i| {
11128 let u = units?;
11129 if u.is_null(i) {
11130 return None;
11131 }
11132 let (base_nanos, base_offset) = base_parts(i)?;
11133 let truncated = truncate_time_nanos(base_nanos, u.value(i))?;
11134 let overrides = LocalTimeOverrides {
11135 hour: field(0, i),
11136 minute: field(1, i),
11137 second: field(2, i),
11138 millisecond: field(3, i),
11139 microsecond: field(4, i),
11140 nanosecond: field(5, i),
11141 };
11142 let nanos = project_localtime(truncated, &overrides)?;
11143 let new_offset = match tz {
11144 Some(a) if !a.is_null(i) => Some(parse_offset_seconds(a.value(i))?),
11145 _ => None,
11146 };
11147 let eff_offset = if new_offset.is_some() {
11152 None
11153 } else {
11154 base_offset
11155 };
11156 Some(project_time(nanos, eff_offset, new_offset))
11157 })
11158 .collect();
11159 Ok(ColumnarValue::Array(std::sync::Arc::new(
11160 build_time_struct(&parts),
11161 )))
11162 }
11163}
11164
11165type DateTimeRow = Option<(i64, i64, i32, Option<String>)>;
11172
11173fn datetime_fields() -> datafusion::arrow::datatypes::Fields {
11178 graphforge_storage::schemas::datetime_struct_fields()
11179}
11180
11181fn is_datetime_struct(dt: &DataType) -> bool {
11183 use datafusion::arrow::datatypes::TimeUnit;
11184 matches!(dt, DataType::Struct(fields)
11185 if fields.len() == 4
11186 && fields[0].name() == "date" && *fields[0].data_type() == DataType::Int64
11187 && fields[1].name() == "time"
11188 && *fields[1].data_type() == DataType::Time64(TimeUnit::Nanosecond)
11189 && fields[2].name() == "offset" && *fields[2].data_type() == DataType::Int32
11190 && fields[3].name() == "zone" && *fields[3].data_type() == DataType::Utf8)
11191}
11192
11193fn build_datetime_struct(rows: &[DateTimeRow]) -> datafusion::arrow::array::StructArray {
11196 use datafusion::arrow::array::{Int32Array, Int64Array, StringArray, Time64NanosecondArray};
11197 use datafusion::arrow::buffer::NullBuffer;
11198 let days: Int64Array = rows.iter().map(|r| r.as_ref().map(|t| t.0)).collect();
11199 let nanos: Time64NanosecondArray = rows.iter().map(|r| r.as_ref().map(|t| t.1)).collect();
11200 let offset: Int32Array = rows.iter().map(|r| r.as_ref().map(|t| t.2)).collect();
11201 let zone: StringArray = rows
11205 .iter()
11206 .map(|r| r.as_ref().map(|t| t.3.clone().unwrap_or_default()))
11207 .collect();
11208 let nulls = rows.iter().map(Option::is_some).collect::<NullBuffer>();
11209 datafusion::arrow::array::StructArray::new(
11210 datetime_fields(),
11211 vec![
11212 std::sync::Arc::new(days),
11213 std::sync::Arc::new(nanos),
11214 std::sync::Arc::new(offset),
11215 std::sync::Arc::new(zone),
11216 ],
11217 Some(nulls),
11218 )
11219}
11220
11221fn datetime_scalar(parts: DateTimeRow) -> ScalarValue {
11223 ScalarValue::Struct(std::sync::Arc::new(build_datetime_struct(&[parts])))
11224}
11225
11226fn datetime_struct_parts(arr: &datafusion::arrow::array::StructArray, i: usize) -> DateTimeRow {
11229 use datafusion::arrow::array::{
11230 Array, Int32Array, Int64Array, StringArray, Time64NanosecondArray,
11231 };
11232 if arr.is_null(i) {
11233 return None;
11234 }
11235 let days = arr.column(0).as_any().downcast_ref::<Int64Array>()?;
11236 let nanos = arr
11237 .column(1)
11238 .as_any()
11239 .downcast_ref::<Time64NanosecondArray>()?;
11240 let offset = arr.column(2).as_any().downcast_ref::<Int32Array>()?;
11241 let zone = arr.column(3).as_any().downcast_ref::<StringArray>()?;
11242 if days.is_null(i) || nanos.is_null(i) || offset.is_null(i) {
11243 return None;
11244 }
11245 let zone_label =
11247 (!zone.is_null(i) && !zone.value(i).is_empty()).then(|| zone.value(i).to_string());
11248 Some((days.value(i), nanos.value(i), offset.value(i), zone_label))
11249}
11250
11251static CYPHER_DATETIME_PROJECT: LazyLock<ScalarUDF> =
11259 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDateTimeProject::new()));
11260
11261#[derive(Debug, PartialEq, Eq, Hash)]
11262struct CypherDateTimeProject {
11263 signature: Signature,
11264}
11265
11266impl CypherDateTimeProject {
11267 fn new() -> Self {
11268 Self {
11269 signature: Signature::any(17, Volatility::Immutable),
11270 }
11271 }
11272}
11273
11274impl ScalarUDFImpl for CypherDateTimeProject {
11275 fn as_any(&self) -> &dyn Any {
11276 self
11277 }
11278
11279 fn name(&self) -> &'static str {
11280 "cypher_datetime_project"
11281 }
11282
11283 fn signature(&self) -> &Signature {
11284 &self.signature
11285 }
11286
11287 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
11288 Ok(DataType::Struct(datetime_fields()))
11289 }
11290
11291 #[allow(
11292 clippy::too_many_lines,
11293 reason = "one cohesive per-row projection: typed date/time/zone source \
11294 extraction, 14 component overrides, and zone re-resolution"
11295 )]
11296 fn invoke_with_args(
11297 &self,
11298 args: ScalarFunctionArgs,
11299 ) -> datafusion::error::Result<ColumnarValue> {
11300 use crate::temporal::{
11301 DateOverrides, LocalTimeOverrides, parse_date_or_datetime_prefix, project_date,
11302 project_datetime, project_localtime, time_offset_zone,
11303 };
11304 use datafusion::arrow::array::{
11305 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
11306 };
11307 use datafusion::arrow::compute::cast;
11308 use datafusion::arrow::datatypes::TimeUnit;
11309 use datafusion::error::DataFusionError;
11310
11311 let rows = args.number_rows;
11312 let cols: Vec<_> = args
11313 .args
11314 .iter()
11315 .map(|a| a.to_array(rows))
11316 .collect::<datafusion::error::Result<_>>()?;
11317 let typed_or_utf8 = |a: &ArrayRef| -> datafusion::error::Result<ArrayRef> {
11318 if matches!(a.data_type(), DataType::Time64(TimeUnit::Nanosecond))
11319 || is_date_struct(a.data_type())
11320 || is_localdatetime_struct(a.data_type())
11321 || is_time_struct(a.data_type())
11322 || is_datetime_struct(a.data_type())
11323 {
11324 Ok(std::sync::Arc::clone(a))
11325 } else {
11326 cast(a, &DataType::Utf8).map_err(DataFusionError::from)
11327 }
11328 };
11329 let date_src = typed_or_utf8(&cols[0])?;
11330 let time_src = typed_or_utf8(&cols[1])?;
11331 let ov: Vec<_> = cols[2..16]
11332 .iter()
11333 .map(|c| cast(c, &DataType::Int64))
11334 .collect::<std::result::Result<Vec<_>, _>>()
11335 .map_err(DataFusionError::from)?;
11336 let tz_arr = cast(&cols[16], &DataType::Utf8).map_err(DataFusionError::from)?;
11337 let tz = tz_arr.as_any().downcast_ref::<StringArray>();
11338
11339 let base_date = |i: usize| -> Option<i64> {
11340 if date_src.is_null(i) {
11341 return Some(0); }
11343 match date_src.data_type() {
11344 DataType::Struct(_) => {
11345 let s = date_src.as_any().downcast_ref::<StructArray>()?;
11346 if is_date_struct(date_src.data_type()) {
11347 date_struct_value(s, i)
11348 } else if is_datetime_struct(date_src.data_type()) {
11349 Some(datetime_struct_parts(s, i)?.0)
11350 } else {
11351 Some(localdatetime_struct_parts(s, i)?.0)
11352 }
11353 }
11354 DataType::Utf8 => parse_date_or_datetime_prefix(
11355 date_src.as_any().downcast_ref::<StringArray>()?.value(i),
11356 ),
11357 _ => None,
11358 }
11359 };
11360 let base_time = |i: usize| -> Option<(i64, Option<i32>, Option<String>)> {
11362 if time_src.is_null(i) {
11363 return Some((0, None, None));
11364 }
11365 match time_src.data_type() {
11366 DataType::Time64(TimeUnit::Nanosecond) => Some((
11367 time_src
11368 .as_any()
11369 .downcast_ref::<Time64NanosecondArray>()?
11370 .value(i),
11371 None,
11372 None,
11373 )),
11374 DataType::Struct(_) => {
11375 let s = time_src.as_any().downcast_ref::<StructArray>()?;
11376 if is_date_struct(time_src.data_type()) {
11377 None
11380 } else if is_datetime_struct(time_src.data_type()) {
11381 let (_, n, o, z) = datetime_struct_parts(s, i)?;
11382 Some((n, Some(o), z))
11383 } else if is_time_struct(time_src.data_type()) {
11384 let (n, o) = time_struct_parts(s, i)?;
11385 Some((n, Some(o), None))
11386 } else {
11387 Some((localdatetime_struct_parts(s, i)?.1, None, None))
11388 }
11389 }
11390 DataType::Utf8 => {
11391 time_offset_zone(time_src.as_any().downcast_ref::<StringArray>()?.value(i))
11392 }
11393 _ => None,
11394 }
11395 };
11396 let field = |idx: usize, i: usize| -> Option<i64> {
11397 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
11398 (!a.is_null(i)).then(|| a.value(i))
11399 };
11400
11401 let parts: Vec<DateTimeRow> = (0..rows)
11402 .map(|i| {
11403 let date_overrides = DateOverrides {
11404 year: field(0, i),
11405 month: field(1, i),
11406 day: field(2, i),
11407 week: field(3, i),
11408 day_of_week: field(4, i),
11409 ordinal_day: field(5, i),
11410 quarter: field(6, i),
11411 day_of_quarter: field(7, i),
11412 };
11413 let time_overrides = LocalTimeOverrides {
11414 hour: field(8, i),
11415 minute: field(9, i),
11416 second: field(10, i),
11417 millisecond: field(11, i),
11418 microsecond: field(12, i),
11419 nanosecond: field(13, i),
11420 };
11421 let (base_nanos, src_offset, src_zone) = base_time(i)?;
11422 let date = project_date(base_date(i)?, &date_overrides)?;
11423 let nanos = project_localtime(base_nanos, &time_overrides)?;
11424 let new_tz = tz.and_then(|a| (!a.is_null(i)).then(|| a.value(i)));
11425 let (date, nanos, offset, zone) =
11426 project_datetime(date, nanos, src_offset, src_zone.as_deref(), new_tz)?;
11427 Some((date, nanos, offset, zone))
11428 })
11429 .collect();
11430 Ok(ColumnarValue::Array(std::sync::Arc::new(
11431 build_datetime_struct(&parts),
11432 )))
11433 }
11434}
11435
11436static CYPHER_DATETIME_TRUNCATE: LazyLock<ScalarUDF> =
11447 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDateTimeTruncate::new()));
11448
11449#[derive(Debug, PartialEq, Eq, Hash)]
11450struct CypherDateTimeTruncate {
11451 signature: Signature,
11452}
11453
11454impl CypherDateTimeTruncate {
11455 fn new() -> Self {
11456 Self {
11457 signature: Signature::any(17, Volatility::Immutable),
11458 }
11459 }
11460}
11461
11462impl ScalarUDFImpl for CypherDateTimeTruncate {
11463 fn as_any(&self) -> &dyn Any {
11464 self
11465 }
11466
11467 fn name(&self) -> &'static str {
11468 "cypher_datetime_truncate"
11469 }
11470
11471 fn signature(&self) -> &Signature {
11472 &self.signature
11473 }
11474
11475 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
11476 Ok(DataType::Struct(datetime_fields()))
11477 }
11478
11479 #[allow(
11480 clippy::too_many_lines,
11481 reason = "one cohesive per-row truncation: typed source extraction, the \
11482 date/time granularity split, 14 overrides, and zone re-resolution"
11483 )]
11484 fn invoke_with_args(
11485 &self,
11486 args: ScalarFunctionArgs,
11487 ) -> datafusion::error::Result<ColumnarValue> {
11488 use crate::temporal::{
11489 DateOverrides, LocalTimeOverrides, parse_date_or_datetime_prefix, project_date,
11490 project_datetime, project_localtime, time_offset_zone, truncate_date,
11491 truncate_time_nanos,
11492 };
11493 use datafusion::arrow::array::{
11494 Array, ArrayRef, Int64Array, StringArray, StructArray, Time64NanosecondArray,
11495 };
11496 use datafusion::arrow::compute::cast;
11497 use datafusion::arrow::datatypes::TimeUnit;
11498 use datafusion::error::DataFusionError;
11499
11500 let rows = args.number_rows;
11501 let cols: Vec<_> = args
11502 .args
11503 .iter()
11504 .map(|a| a.to_array(rows))
11505 .collect::<datafusion::error::Result<_>>()?;
11506 let value: ArrayRef =
11508 if matches!(cols[0].data_type(), DataType::Time64(TimeUnit::Nanosecond))
11509 || is_date_struct(cols[0].data_type())
11510 || is_localdatetime_struct(cols[0].data_type())
11511 || is_time_struct(cols[0].data_type())
11512 || is_datetime_struct(cols[0].data_type())
11513 {
11514 std::sync::Arc::clone(&cols[0])
11515 } else {
11516 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
11517 };
11518 let units_arr = cast(&cols[1], &DataType::Utf8).map_err(DataFusionError::from)?;
11519 let units = units_arr.as_any().downcast_ref::<StringArray>();
11520 let ov: Vec<_> = cols[2..16]
11521 .iter()
11522 .map(|c| cast(c, &DataType::Int64))
11523 .collect::<std::result::Result<Vec<_>, _>>()
11524 .map_err(DataFusionError::from)?;
11525 let tz_arr = cast(&cols[16], &DataType::Utf8).map_err(DataFusionError::from)?;
11526 let tz = tz_arr.as_any().downcast_ref::<StringArray>();
11527
11528 let base_date = |i: usize| -> Option<i64> {
11529 if value.is_null(i) {
11530 return None;
11531 }
11532 match value.data_type() {
11533 DataType::Struct(_) => {
11534 let s = value.as_any().downcast_ref::<StructArray>()?;
11535 if is_date_struct(value.data_type()) {
11536 date_struct_value(s, i)
11537 } else if is_datetime_struct(value.data_type()) {
11538 Some(datetime_struct_parts(s, i)?.0)
11539 } else {
11540 Some(localdatetime_struct_parts(s, i)?.0)
11541 }
11542 }
11543 DataType::Utf8 => parse_date_or_datetime_prefix(
11544 value.as_any().downcast_ref::<StringArray>()?.value(i),
11545 ),
11546 _ => None,
11547 }
11548 };
11549 let base_time = |i: usize| -> Option<(i64, Option<i32>, Option<String>)> {
11551 if value.is_null(i) {
11552 return None;
11553 }
11554 match value.data_type() {
11555 DataType::Time64(TimeUnit::Nanosecond) => Some((
11556 value
11557 .as_any()
11558 .downcast_ref::<Time64NanosecondArray>()?
11559 .value(i),
11560 None,
11561 None,
11562 )),
11563 DataType::Struct(_) => {
11564 let s = value.as_any().downcast_ref::<StructArray>()?;
11565 if is_date_struct(value.data_type()) {
11566 Some((0, None, None)) } else if is_datetime_struct(value.data_type()) {
11568 let (_, n, o, z) = datetime_struct_parts(s, i)?;
11569 Some((n, Some(o), z))
11570 } else if is_time_struct(value.data_type()) {
11571 let (n, o) = time_struct_parts(s, i)?;
11572 Some((n, Some(o), None))
11573 } else {
11574 Some((localdatetime_struct_parts(s, i)?.1, None, None))
11575 }
11576 }
11577 DataType::Utf8 => {
11578 time_offset_zone(value.as_any().downcast_ref::<StringArray>()?.value(i))
11579 }
11580 _ => None,
11581 }
11582 };
11583 let field = |idx: usize, i: usize| -> Option<i64> {
11584 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
11585 (!a.is_null(i)).then(|| a.value(i))
11586 };
11587
11588 let parts: Vec<DateTimeRow> = (0..rows)
11589 .map(|i| {
11590 let u = units?;
11591 if u.is_null(i) {
11592 return None;
11593 }
11594 let (bt_nanos, src_offset, src_zone) = base_time(i)?;
11595 let (date0, nanos0) = match truncate_date(base_date(i)?, u.value(i)) {
11598 Some(d) => (d, 0i64),
11599 None => (base_date(i)?, truncate_time_nanos(bt_nanos, u.value(i))?),
11600 };
11601 let date_overrides = DateOverrides {
11602 year: field(0, i),
11603 month: field(1, i),
11604 day: field(2, i),
11605 week: field(3, i),
11606 day_of_week: field(4, i),
11607 ordinal_day: field(5, i),
11608 quarter: field(6, i),
11609 day_of_quarter: field(7, i),
11610 };
11611 let time_overrides = LocalTimeOverrides {
11612 hour: field(8, i),
11613 minute: field(9, i),
11614 second: field(10, i),
11615 millisecond: field(11, i),
11616 microsecond: field(12, i),
11617 nanosecond: field(13, i),
11618 };
11619 let date = project_date(date0, &date_overrides)?;
11620 let nanos = project_localtime(nanos0, &time_overrides)?;
11621 let new_tz = tz.and_then(|a| (!a.is_null(i)).then(|| a.value(i)));
11622 let src_offset = if new_tz.is_some() { None } else { src_offset };
11630 let (date, nanos, offset, zone) =
11631 project_datetime(date, nanos, src_offset, src_zone.as_deref(), new_tz)?;
11632 Some((date, nanos, offset, zone))
11633 })
11634 .collect();
11635 Ok(ColumnarValue::Array(std::sync::Arc::new(
11636 build_datetime_struct(&parts),
11637 )))
11638 }
11639}
11640
11641static CYPHER_TO_STRING: LazyLock<ScalarUDF> =
11653 LazyLock::new(|| ScalarUDF::new_from_impl(CypherToString::new()));
11654
11655#[derive(Debug, PartialEq, Eq, Hash)]
11656struct CypherToString {
11657 signature: Signature,
11658}
11659
11660impl CypherToString {
11661 fn new() -> Self {
11662 Self {
11663 signature: Signature::any(1, Volatility::Immutable),
11664 }
11665 }
11666}
11667
11668impl ScalarUDFImpl for CypherToString {
11669 fn as_any(&self) -> &dyn Any {
11670 self
11671 }
11672
11673 fn name(&self) -> &'static str {
11674 "cypher_to_string"
11675 }
11676
11677 fn signature(&self) -> &Signature {
11678 &self.signature
11679 }
11680
11681 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
11682 Ok(DataType::Utf8)
11683 }
11684
11685 fn invoke_with_args(
11686 &self,
11687 args: ScalarFunctionArgs,
11688 ) -> datafusion::error::Result<ColumnarValue> {
11689 use crate::temporal::{
11690 format_date, render_localdatetime, render_localtime_nanos, render_time_value,
11691 };
11692 use datafusion::arrow::array::{Array, StringArray, StructArray, Time64NanosecondArray};
11693 use datafusion::arrow::compute::cast;
11694 use datafusion::arrow::datatypes::TimeUnit;
11695
11696 let arr = args.args[0].to_array(args.number_rows)?;
11697 let rows = arr.len();
11698 let render = |f: &dyn Fn(usize) -> Option<String>| -> ColumnarValue {
11700 let out: StringArray = (0..rows)
11701 .map(|i| if arr.is_null(i) { None } else { f(i) })
11702 .collect();
11703 ColumnarValue::Array(std::sync::Arc::new(out))
11704 };
11705
11706 let result = match arr.data_type() {
11707 DataType::Struct(_) if is_date_struct(arr.data_type()) => {
11708 let s = arr.as_any().downcast_ref::<StructArray>().unwrap();
11709 render(&|i| date_struct_value(s, i).map(format_date))
11710 }
11711 DataType::Time64(TimeUnit::Nanosecond) => {
11712 let a = arr
11713 .as_any()
11714 .downcast_ref::<Time64NanosecondArray>()
11715 .unwrap();
11716 render(&|i| Some(render_localtime_nanos(a.value(i))))
11717 }
11718 DataType::Struct(_) if is_localdatetime_struct(arr.data_type()) => {
11719 let s = arr.as_any().downcast_ref::<StructArray>().unwrap();
11720 render(&|i| {
11721 localdatetime_struct_parts(s, i).map(|(d, n)| render_localdatetime(d, n))
11722 })
11723 }
11724 DataType::Struct(_) if is_time_struct(arr.data_type()) => {
11725 let s = arr.as_any().downcast_ref::<StructArray>().unwrap();
11726 render(&|i| time_struct_parts(s, i).map(|(n, o)| render_time_value(n, o)))
11727 }
11728 DataType::Struct(_) if is_datetime_struct(arr.data_type()) => {
11729 let s = arr.as_any().downcast_ref::<StructArray>().unwrap();
11730 render(&|i| {
11731 datetime_struct_parts(s, i).map(|(d, n, o, z)| {
11732 crate::temporal::render_datetime_value(d, n, o, z.as_deref())
11733 })
11734 })
11735 }
11736 DataType::Struct(_) if is_duration_struct(arr.data_type()) => {
11737 let s = arr.as_any().downcast_ref::<StructArray>().unwrap();
11738 render(&|i| {
11739 duration_struct_parts(s, i).map(|d| crate::temporal::render_duration_value(&d))
11740 })
11741 }
11742 DataType::Struct(_) if is_het_struct_type(Some(arr.data_type())) => {
11743 let out: datafusion::error::Result<StringArray> = (0..rows)
11744 .map(|i| {
11745 let value = decoded_scalar_at(&arr, i)?;
11746 to_cypher_string(&value)
11747 })
11748 .collect();
11749 ColumnarValue::Array(std::sync::Arc::new(out?))
11750 }
11751 _ => ColumnarValue::Array(
11753 cast(&arr, &DataType::Utf8).map_err(datafusion::error::DataFusionError::from)?,
11754 ),
11755 };
11756 Ok(result)
11757 }
11758}
11759
11760fn scalar_as_i128(v: &ScalarValue) -> Option<i128> {
11762 match v {
11763 ScalarValue::Int8(Some(n)) => Some(i128::from(*n)),
11764 ScalarValue::Int16(Some(n)) => Some(i128::from(*n)),
11765 ScalarValue::Int32(Some(n)) => Some(i128::from(*n)),
11766 ScalarValue::Int64(Some(n)) => Some(i128::from(*n)),
11767 ScalarValue::UInt8(Some(n)) => Some(i128::from(*n)),
11768 ScalarValue::UInt16(Some(n)) => Some(i128::from(*n)),
11769 ScalarValue::UInt32(Some(n)) => Some(i128::from(*n)),
11770 ScalarValue::UInt64(Some(n)) => Some(i128::from(*n)),
11771 _ => None,
11772 }
11773}
11774
11775fn scalar_as_f64(v: &ScalarValue) -> Option<f64> {
11777 #[allow(
11778 clippy::cast_precision_loss,
11779 reason = "only used for mixed integer/float numeric semantics; pure integers use i128"
11780 )]
11781 match v {
11782 ScalarValue::Int8(Some(n)) => Some(f64::from(*n)),
11783 ScalarValue::Int16(Some(n)) => Some(f64::from(*n)),
11784 ScalarValue::Int32(Some(n)) => Some(f64::from(*n)),
11785 ScalarValue::Int64(Some(n)) => Some(*n as f64),
11786 ScalarValue::UInt8(Some(n)) => Some(f64::from(*n)),
11787 ScalarValue::UInt16(Some(n)) => Some(f64::from(*n)),
11788 ScalarValue::UInt32(Some(n)) => Some(f64::from(*n)),
11789 ScalarValue::UInt64(Some(n)) => Some(*n as f64),
11790 ScalarValue::Float32(Some(f)) => Some(f64::from(*f)),
11791 ScalarValue::Float64(Some(f)) => Some(*f),
11792 _ => None,
11793 }
11794}
11795
11796fn cypher_seq_eq(
11798 a: &datafusion::arrow::array::ArrayRef,
11799 b: &datafusion::arrow::array::ArrayRef,
11800) -> Option<bool> {
11801 if a.len() != b.len() {
11802 return Some(false); }
11804 let mut saw_null = false;
11805 for i in 0..a.len() {
11806 let av = ScalarValue::try_from_array(a, i).ok()?;
11807 let bv = ScalarValue::try_from_array(b, i).ok()?;
11808 match cypher_value_eq(&av, &bv) {
11809 Some(false) => return Some(false),
11810 None => saw_null = true,
11811 Some(true) => {}
11812 }
11813 }
11814 if saw_null { None } else { Some(true) }
11815}
11816
11817fn cypher_struct_eq(
11819 a: &datafusion::arrow::array::StructArray,
11820 b: &datafusion::arrow::array::StructArray,
11821) -> Option<bool> {
11822 let mut ka: Vec<&str> = a.fields().iter().map(|f| f.name().as_str()).collect();
11824 let mut kb: Vec<&str> = b.fields().iter().map(|f| f.name().as_str()).collect();
11825 ka.sort_unstable();
11826 kb.sort_unstable();
11827 if ka != kb {
11828 return Some(false);
11829 }
11830 let mut saw_null = false;
11831 for key in ka {
11832 let av = ScalarValue::try_from_array(a.column_by_name(key)?, 0).ok()?;
11833 let bv = ScalarValue::try_from_array(b.column_by_name(key)?, 0).ok()?;
11834 match cypher_value_eq(&av, &bv) {
11835 Some(false) => return Some(false),
11836 None => saw_null = true,
11837 Some(true) => {}
11838 }
11839 }
11840 if saw_null { None } else { Some(true) }
11841}
11842
11843static CYPHER_DATE_TRUNCATE: LazyLock<ScalarUDF> =
11853 LazyLock::new(|| ScalarUDF::new_from_impl(CypherDateTruncate::new()));
11854
11855#[derive(Debug, PartialEq, Eq, Hash)]
11856struct CypherDateTruncate {
11857 signature: Signature,
11858}
11859
11860impl CypherDateTruncate {
11861 fn new() -> Self {
11862 Self {
11863 signature: Signature::any(10, Volatility::Immutable),
11864 }
11865 }
11866}
11867
11868impl ScalarUDFImpl for CypherDateTruncate {
11869 fn as_any(&self) -> &dyn Any {
11870 self
11871 }
11872
11873 fn name(&self) -> &'static str {
11874 "cypher_date_truncate"
11875 }
11876
11877 fn signature(&self) -> &Signature {
11878 &self.signature
11879 }
11880
11881 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
11882 Ok(DataType::Struct(
11883 graphforge_storage::schemas::date_struct_fields(),
11884 ))
11885 }
11886
11887 fn invoke_with_args(
11888 &self,
11889 args: ScalarFunctionArgs,
11890 ) -> datafusion::error::Result<ColumnarValue> {
11891 use crate::temporal::{
11892 DateOverrides, parse_date_or_datetime_prefix, project_date, truncate_date,
11893 };
11894 use datafusion::arrow::array::{Array, ArrayRef, Int64Array, StringArray, StructArray};
11895 use datafusion::arrow::compute::cast;
11896 use datafusion::error::DataFusionError;
11897
11898 let rows = args.number_rows;
11899 let cols: Vec<_> = args
11900 .args
11901 .iter()
11902 .map(|a| a.to_array(rows))
11903 .collect::<datafusion::error::Result<_>>()?;
11904 let value: ArrayRef = if is_date_struct(cols[0].data_type())
11909 || is_localdatetime_struct(cols[0].data_type())
11910 || is_time_struct(cols[0].data_type())
11911 || is_datetime_struct(cols[0].data_type())
11912 {
11913 std::sync::Arc::clone(&cols[0])
11914 } else {
11915 cast(&cols[0], &DataType::Utf8).map_err(DataFusionError::from)?
11916 };
11917 let units_arr = cast(&cols[1], &DataType::Utf8).map_err(DataFusionError::from)?;
11918 let units = units_arr.as_any().downcast_ref::<StringArray>();
11919 let ov: Vec<_> = cols[2..10]
11920 .iter()
11921 .map(|c| cast(c, &DataType::Int64))
11922 .collect::<std::result::Result<Vec<_>, _>>()
11923 .map_err(DataFusionError::from)?;
11924
11925 let base_date = |i: usize| -> Option<i64> {
11926 if value.is_null(i) {
11927 return None;
11928 }
11929 match value.data_type() {
11930 DataType::Struct(_) => {
11931 let s = value.as_any().downcast_ref::<StructArray>()?;
11932 if is_date_struct(value.data_type()) {
11933 date_struct_value(s, i)
11934 } else {
11935 Some(localdatetime_struct_parts(s, i)?.0)
11937 }
11938 }
11939 DataType::Utf8 => parse_date_or_datetime_prefix(
11940 value.as_any().downcast_ref::<StringArray>()?.value(i),
11941 ),
11942 _ => None,
11943 }
11944 };
11945 let field = |idx: usize, i: usize| -> Option<i64> {
11946 let a = ov[idx].as_any().downcast_ref::<Int64Array>()?;
11947 (!a.is_null(i)).then(|| a.value(i))
11948 };
11949
11950 let out: Vec<Option<i64>> = (0..rows)
11951 .map(|i| {
11952 let u = units?;
11953 if u.is_null(i) {
11954 return None;
11955 }
11956 let truncated = truncate_date(base_date(i)?, u.value(i))?;
11957 let overrides = DateOverrides {
11958 year: field(0, i),
11959 month: field(1, i),
11960 day: field(2, i),
11961 week: field(3, i),
11962 day_of_week: field(4, i),
11963 ordinal_day: field(5, i),
11964 quarter: field(6, i),
11965 day_of_quarter: field(7, i),
11966 };
11967 project_date(truncated, &overrides)
11968 })
11969 .collect();
11970 Ok(ColumnarValue::Array(std::sync::Arc::new(
11971 build_date_struct(&out),
11972 )))
11973 }
11974}
11975
11976static CYPHER_PATH_NODES: LazyLock<ScalarUDF> =
11994 LazyLock::new(|| ScalarUDF::new_from_impl(CypherPathNodes::new()));
11995
11996fn path_node_struct_fields() -> datafusion::arrow::datatypes::Fields {
12002 use datafusion::arrow::datatypes::Field;
12003 vec![Field::new(
12004 "node_uuid",
12005 DataType::FixedSizeBinary(16),
12006 false,
12007 )]
12008 .into()
12009}
12010
12011#[derive(Debug, PartialEq, Eq, Hash)]
12015struct PathNodeHydration {
12016 dir: std::path::PathBuf,
12018 labels_by_type: Vec<(u32, String)>,
12020 prop_stems: Vec<String>,
12023 fields: datafusion::arrow::datatypes::Fields,
12025}
12026
12027#[derive(Debug, PartialEq, Eq, Hash)]
12028struct CypherPathNodes {
12029 signature: Signature,
12030 hydrate: Option<PathNodeHydration>,
12031}
12032
12033impl CypherPathNodes {
12034 fn new() -> Self {
12035 Self {
12037 signature: Signature::any(2, Volatility::Immutable),
12038 hydrate: None,
12039 }
12040 }
12041
12042 fn with_hydration(hydrate: PathNodeHydration) -> Self {
12043 Self {
12044 signature: Signature::any(2, Volatility::Immutable),
12045 hydrate: Some(hydrate),
12046 }
12047 }
12048
12049 fn element_fields(&self) -> datafusion::arrow::datatypes::Fields {
12050 self.hydrate
12051 .as_ref()
12052 .map_or_else(path_node_struct_fields, |h| h.fields.clone())
12053 }
12054}
12055
12056impl ScalarUDFImpl for CypherPathNodes {
12057 fn as_any(&self) -> &dyn Any {
12058 self
12059 }
12060
12061 fn name(&self) -> &'static str {
12062 "cypher_path_nodes"
12063 }
12064
12065 fn signature(&self) -> &Signature {
12066 &self.signature
12067 }
12068
12069 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
12070 Ok(DataType::new_list(
12071 DataType::Struct(self.element_fields()),
12072 true,
12073 ))
12074 }
12075
12076 fn invoke_with_args(
12077 &self,
12078 args: ScalarFunctionArgs,
12079 ) -> datafusion::error::Result<ColumnarValue> {
12080 use datafusion::arrow::array::{
12081 Array, ArrayRef, FixedSizeBinaryArray, ListArray, StructArray, new_empty_array,
12082 };
12083 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer};
12084 use datafusion::arrow::datatypes::Field;
12085 use datafusion::common::cast::as_list_array;
12086 use datafusion::error::DataFusionError;
12087 use std::sync::Arc;
12088
12089 let exec_err = |m: String| DataFusionError::Execution(m);
12090 let as_fsb16 =
12091 |array: &dyn Array, what: &str| -> datafusion::error::Result<FixedSizeBinaryArray> {
12092 array
12093 .as_any()
12094 .downcast_ref::<FixedSizeBinaryArray>()
12095 .filter(|a| a.value_length() == 16)
12096 .cloned()
12097 .ok_or_else(|| {
12098 exec_err(format!(
12099 "cypher_path_nodes: expected FixedSizeBinary(16) {what}, got {:?}",
12100 array.data_type()
12101 ))
12102 })
12103 };
12104
12105 let seeds = args.args[0].to_array(args.number_rows)?;
12106 let seeds = as_fsb16(seeds.as_ref(), "start-node uuid")?;
12107 let rels = args.args[1].to_array(args.number_rows)?;
12108 let rels = as_list_array(&rels)?.clone();
12109
12110 let mut flat: Vec<[u8; 16]> = Vec::new();
12113 let mut lengths: Vec<usize> = Vec::with_capacity(rels.len());
12114 let mut valid: Vec<bool> = Vec::with_capacity(rels.len());
12115 for row in 0..rels.len() {
12116 if seeds.is_null(row) || rels.is_null(row) {
12117 lengths.push(0);
12118 valid.push(false);
12119 continue;
12120 }
12121 let start = flat.len();
12122 let mut cur = [0u8; 16];
12123 cur.copy_from_slice(seeds.value(row));
12124 flat.push(cur);
12125
12126 let edges = rels.value(row);
12127 let edges = edges
12128 .as_any()
12129 .downcast_ref::<StructArray>()
12130 .ok_or_else(|| {
12131 exec_err("cypher_path_nodes: relationship-list items must be structs".into())
12132 })?;
12133 let src = edges.column_by_name("src_uuid").ok_or_else(|| {
12136 exec_err("cypher_path_nodes: relationship struct has no src_uuid".into())
12137 })?;
12138 let src = as_fsb16(src.as_ref(), "src_uuid")?;
12139 let dst = edges.column_by_name("dst_uuid").ok_or_else(|| {
12140 exec_err("cypher_path_nodes: relationship struct has no dst_uuid".into())
12141 })?;
12142 let dst = as_fsb16(dst.as_ref(), "dst_uuid")?;
12143
12144 for i in 0..edges.len() {
12145 let s = src.value(i);
12146 let d = dst.value(i);
12147 let next = if cur == s {
12148 d
12149 } else if cur == d {
12150 s
12151 } else {
12152 return Err(exec_err(format!(
12153 "cypher_path_nodes: edge {i} is disconnected from the \
12154 path (corrupt traversal emission)"
12155 )));
12156 };
12157 cur.copy_from_slice(next);
12158 flat.push(cur);
12159 }
12160 lengths.push(flat.len() - start);
12161 valid.push(true);
12162 }
12163
12164 let uuid_child: ArrayRef = if flat.is_empty() {
12167 new_empty_array(&DataType::FixedSizeBinary(16))
12168 } else {
12169 Arc::new(
12170 FixedSizeBinaryArray::try_from_iter(flat.iter())
12171 .map_err(|e| exec_err(e.to_string()))?,
12172 )
12173 };
12174 let fields = self.element_fields();
12175 let mut children: Vec<ArrayRef> = vec![uuid_child];
12176 if let Some(h) = self.hydrate.as_ref() {
12177 children.extend(hydrate_path_node_children(h, &flat)?);
12178 }
12179
12180 let struct_arr = StructArray::try_new(fields.clone(), children, None)
12181 .map_err(|e| exec_err(e.to_string()))?;
12182 let offsets = OffsetBuffer::<i32>::from_lengths(lengths);
12183 let item = Arc::new(Field::new("item", DataType::Struct(fields), true));
12184 let list = ListArray::try_new(
12185 item,
12186 offsets,
12187 Arc::new(struct_arr),
12188 Some(NullBuffer::from(valid)),
12189 )
12190 .map_err(|e| exec_err(e.to_string()))?;
12191 Ok(ColumnarValue::Array(Arc::new(list)))
12192 }
12193}
12194
12195fn hydrate_path_node_children(
12198 h: &PathNodeHydration,
12199 flat: &[[u8; 16]],
12200) -> datafusion::error::Result<Vec<datafusion::arrow::array::ArrayRef>> {
12201 let mut children = vec![path_node_labels_child(h, flat)?];
12202 children.extend(path_node_prop_children(h, flat)?);
12203 Ok(children)
12204}
12205
12206fn hydration_fsb16(
12208 b: &datafusion::arrow::array::RecordBatch,
12209 name: &str,
12210) -> datafusion::error::Result<datafusion::arrow::array::FixedSizeBinaryArray> {
12211 use datafusion::arrow::array::FixedSizeBinaryArray;
12212 b.column_by_name(name)
12213 .and_then(|c| c.as_any().downcast_ref::<FixedSizeBinaryArray>().cloned())
12214 .filter(|a| a.value_length() == 16)
12215 .ok_or_else(|| {
12216 datafusion::error::DataFusionError::Execution(format!(
12217 "cypher_path_nodes: no FixedSizeBinary(16) {name} column"
12218 ))
12219 })
12220}
12221
12222fn path_node_labels_child(
12227 h: &PathNodeHydration,
12228 flat: &[[u8; 16]],
12229) -> datafusion::error::Result<datafusion::arrow::array::ArrayRef> {
12230 use datafusion::arrow::array::{Array, ListBuilder, StringBuilder};
12231 use datafusion::error::DataFusionError;
12232 use std::collections::HashMap;
12233
12234 let exec_err = |m: String| DataFusionError::Execution(m);
12235 let node_batches =
12236 graphforge_storage::read_nodes(&h.dir).map_err(|e| exec_err(e.to_string()))?;
12237 let mut label_of: HashMap<[u8; 16], usize> = HashMap::new();
12238 for b in &node_batches {
12239 let uuids = hydration_fsb16(b, "node_uuid")?;
12240 let type_ids = b
12241 .column_by_name("type_id")
12242 .and_then(|c| {
12243 c.as_any()
12244 .downcast_ref::<datafusion::arrow::array::UInt32Array>()
12245 .cloned()
12246 })
12247 .ok_or_else(|| exec_err("cypher_path_nodes: no UInt32 type_id column".into()))?;
12248 for r in 0..b.num_rows() {
12249 if uuids.is_null(r) || type_ids.is_null(r) {
12250 continue;
12251 }
12252 let mut u = [0u8; 16];
12253 u.copy_from_slice(uuids.value(r));
12254 if let Ok(i) = h
12255 .labels_by_type
12256 .binary_search_by_key(&type_ids.value(r), |(id, _)| *id)
12257 {
12258 label_of.insert(u, i);
12259 }
12260 }
12261 }
12262 let mut labels_b = ListBuilder::new(StringBuilder::new());
12263 for u in flat {
12264 match label_of.get(u) {
12265 Some(&i) => labels_b.values().append_value(&h.labels_by_type[i].1),
12266 None => labels_b.values().append_null(),
12267 }
12268 labels_b.append(true);
12269 }
12270 Ok(std::sync::Arc::new(labels_b.finish()))
12271}
12272
12273fn path_node_prop_children(
12278 h: &PathNodeHydration,
12279 flat: &[[u8; 16]],
12280) -> datafusion::error::Result<Vec<datafusion::arrow::array::ArrayRef>> {
12281 use datafusion::arrow::array::{Array, ArrayRef, UInt32Array, new_null_array};
12282 use datafusion::arrow::compute::kernels::zip::zip;
12283 use datafusion::arrow::compute::{concat_batches, is_not_null, take};
12284 use datafusion::error::DataFusionError;
12285 use std::collections::HashMap;
12286
12287 let exec_err = |m: String| DataFusionError::Execution(m);
12288
12289 let mut batches = Vec::with_capacity(h.prop_stems.len());
12293 for stem in &h.prop_stems {
12294 let bs = graphforge_storage::read_properties(&h.dir, stem)
12295 .map_err(|e| exec_err(e.to_string()))?;
12296 if let Some(first) = bs.first() {
12297 batches
12298 .push(concat_batches(&first.schema(), &bs).map_err(|e| exec_err(e.to_string()))?);
12299 }
12300 }
12301 let mut uuid_to_loc: HashMap<[u8; 16], (usize, u32)> = HashMap::new();
12302 for (bi, b) in batches.iter().enumerate() {
12303 let key = hydration_fsb16(b, "node_uuid")?;
12304 for r in 0..key.len() {
12305 if key.is_null(r) {
12306 continue;
12307 }
12308 let mut u = [0u8; 16];
12309 u.copy_from_slice(key.value(r));
12310 uuid_to_loc.entry(u).or_insert((
12311 bi,
12312 u32::try_from(r).map_err(|_| exec_err(format!("property row {r} exceeds u32")))?,
12313 ));
12314 }
12315 }
12316 let take_by_batch: Vec<UInt32Array> = (0..batches.len())
12317 .map(|bi| {
12318 flat.iter()
12319 .map(|u| match uuid_to_loc.get(u) {
12320 Some(&(owner, row)) if owner == bi => Some(row),
12321 _ => None,
12322 })
12323 .collect()
12324 })
12325 .collect();
12326
12327 let mut children = Vec::with_capacity(h.fields.len().saturating_sub(2));
12328 for field in h.fields.iter().skip(2) {
12329 let mut child: ArrayRef = new_null_array(field.data_type(), flat.len());
12330 for (bi, b) in batches.iter().enumerate() {
12331 let Some(col) = b.column_by_name(field.name()) else {
12334 continue;
12335 };
12336 let taken = take(col, &take_by_batch[bi], None).map_err(|e| exec_err(e.to_string()))?;
12337 child = if batches.len() == 1 {
12338 taken
12339 } else {
12340 let mask = is_not_null(&taken).map_err(|e| exec_err(e.to_string()))?;
12341 zip(&mask, &taken, &child).map_err(|e| exec_err(e.to_string()))?
12342 };
12343 }
12344 children.push(child);
12345 }
12346 Ok(children)
12347}
12348
12349#[cfg(test)]
12354mod tests {
12355 use super::*;
12356 use graphforge_core::PropId;
12357 use graphforge_ir::expr::{BinaryOpKind, IrExpr, IrLiteral, UnaryOpKind};
12358 use graphforge_ir::{ExprArena, VarId};
12359 use std::sync::atomic::{AtomicUsize, Ordering};
12360
12361 static VOLATILE_CALLS: AtomicUsize = AtomicUsize::new(0);
12362 static VOLATILE_ROWS: AtomicUsize = AtomicUsize::new(0);
12363
12364 #[derive(Debug, PartialEq, Eq, Hash)]
12365 struct CountingVolatilePredicate {
12366 signature: Signature,
12367 }
12368
12369 impl CountingVolatilePredicate {
12370 fn new() -> Self {
12371 Self {
12372 signature: Signature::nullary(Volatility::Volatile),
12373 }
12374 }
12375 }
12376
12377 impl ScalarUDFImpl for CountingVolatilePredicate {
12378 fn as_any(&self) -> &dyn Any {
12379 self
12380 }
12381
12382 fn name(&self) -> &'static str {
12383 "counting_volatile_predicate"
12384 }
12385
12386 fn signature(&self) -> &Signature {
12387 &self.signature
12388 }
12389
12390 fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
12391 Ok(DataType::Boolean)
12392 }
12393
12394 fn invoke_with_args(
12395 &self,
12396 args: ScalarFunctionArgs,
12397 ) -> datafusion::error::Result<ColumnarValue> {
12398 use datafusion::arrow::array::BooleanArray;
12399 VOLATILE_CALLS.fetch_add(1, Ordering::SeqCst);
12400 VOLATILE_ROWS.fetch_add(args.number_rows, Ordering::SeqCst);
12401 Ok(ColumnarValue::Array(std::sync::Arc::new(
12402 BooleanArray::from(vec![true; args.number_rows]),
12403 )))
12404 }
12405 }
12406
12407 #[test]
12408 fn temporal_clock_fn_recognition() {
12409 for base in ["date", "localtime", "time", "localdatetime", "datetime"] {
12411 for clock in ["transaction", "statement", "realtime"] {
12412 assert!(is_temporal_clock_fn(&format!("{base}.{clock}")));
12413 }
12414 }
12415 assert!(is_temporal_clock_fn("Date.Realtime"));
12417 assert!(is_temporal_clock_fn("DATETIME.TRANSACTION"));
12418 assert!(!is_temporal_clock_fn("datetime.truncate"));
12420 assert!(!is_temporal_clock_fn("duration.realtime")); assert!(!is_temporal_clock_fn("date"));
12422 assert!(!is_temporal_clock_fn("foo.realtime"));
12423
12424 assert_eq!(
12426 temporal_null_scalar("date").data_type(),
12427 date_scalar(None).data_type()
12428 );
12429 assert_eq!(
12430 temporal_null_scalar("localtime"),
12431 ScalarValue::Time64Nanosecond(None)
12432 );
12433 assert_eq!(
12434 temporal_null_scalar("datetime.realtime").data_type(),
12435 datetime_scalar(None).data_type()
12436 );
12437 }
12438
12439 #[test]
12440 fn quantifier_three_valued_reduce() {
12441 use datafusion::arrow::array::BooleanArray;
12442 use graphforge_ir::QuantifierKind::{All, Any, None, Single};
12443 let b = |v: Vec<Option<bool>>| BooleanArray::from(v);
12444 let r = |k, v: Vec<Option<bool>>| {
12445 let arr = b(v.clone());
12446 reduce_quantifier(k, &arr, v.len())
12447 };
12448 assert_eq!(r(All, vec![]), Some(true));
12450 assert_eq!(r(None, vec![]), Some(true));
12451 assert_eq!(r(Any, vec![]), Some(false));
12452 assert_eq!(r(Single, vec![]), Some(false));
12453 assert_eq!(r(All, vec![Some(true), Some(true)]), Some(true));
12455 assert_eq!(r(All, vec![Some(true), Some(false)]), Some(false));
12456 assert_eq!(r(Any, vec![Some(false), Some(true)]), Some(true));
12457 assert_eq!(r(None, vec![Some(false), Some(false)]), Some(true));
12458 assert_eq!(r(Single, vec![Some(true), Some(false)]), Some(true));
12459 assert_eq!(r(Single, vec![Some(true), Some(true)]), Some(false));
12460 assert_eq!(r(All, vec![Some(true), Option::None]), Option::None); assert_eq!(r(All, vec![Some(false), Option::None]), Some(false)); assert_eq!(r(Any, vec![Some(false), Option::None]), Option::None);
12464 assert_eq!(r(Any, vec![Some(true), Option::None]), Some(true)); assert_eq!(
12466 r(Single, vec![Some(true), Some(true), Option::None]),
12467 Some(false)
12468 ); assert_eq!(r(Single, vec![Some(true), Option::None]), Option::None);
12470 }
12471
12472 #[test]
12473 fn invariant_quantifier_truth_matrix_preserves_cardinality_and_nulls() {
12474 use graphforge_ir::QuantifierKind::{All, Any, None as NoneQ, Single};
12475
12476 for kind in [All, Any, NoneQ, Single] {
12477 for predicate in [Some(true), Some(false), Option::None] {
12478 for length in [0, 1, 4] {
12479 assert_eq!(
12480 reduce_invariant_quantifier(kind, predicate, length),
12481 match predicate {
12482 Some(value) => {
12483 let values = datafusion::arrow::array::BooleanArray::from(vec![
12484 value;
12485 length
12486 ]);
12487 reduce_quantifier(kind, &values, length)
12488 }
12489 Option::None => {
12490 let values =
12491 datafusion::arrow::array::BooleanArray::new_null(length);
12492 reduce_quantifier(kind, &values, length)
12493 }
12494 },
12495 "{kind:?}, predicate={predicate:?}, length={length}"
12496 );
12497 }
12498 }
12499 }
12500 }
12501
12502 #[test]
12503 fn invariant_quantifier_scaling_counts_rows_not_heterogeneous_elements() {
12504 use datafusion::arrow::array::{Array, ArrayRef, BooleanArray, ListArray};
12505 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
12506 use datafusion::arrow::datatypes::Field;
12507 use datafusion::config::ConfigOptions;
12508 use datafusion::scalar::ScalarValue as S;
12509 use graphforge_ir::QuantifierKind::{All, Any, None as NoneQ, Single};
12510 use std::sync::Arc;
12511
12512 let pattern = [
12513 S::Int64(Some(1)),
12514 S::Null,
12515 S::Boolean(Some(true)),
12516 S::Utf8(Some("x".to_owned())),
12517 ];
12518 let flat = pattern
12519 .iter()
12520 .cloned()
12521 .chain(pattern.iter().cloned().cycle().take(40))
12522 .collect::<Vec<_>>();
12523 let values: ArrayRef = Arc::new(build_het_struct(&flat, 0).unwrap());
12524 let item = Arc::new(Field::new("item", values.data_type().clone(), true));
12525 let list: ArrayRef = Arc::new(ListArray::new(
12526 item,
12527 OffsetBuffer::new(ScalarBuffer::from(vec![0i32, 4, 44, 44])),
12528 values,
12529 Some(NullBuffer::from(vec![true, true, false])),
12530 ));
12531 INVARIANT_QUANTIFIER_ROWS.store(0, Ordering::SeqCst);
12532
12533 for kind in [All, Any, NoneQ, Single] {
12534 for predicate in [Some(true), Some(false)] {
12535 let udf = CypherInvariantQuantifier::new(kind, predicate);
12536 let output = udf
12537 .invoke_with_args(ScalarFunctionArgs {
12538 args: vec![ColumnarValue::Array(Arc::clone(&list))],
12539 arg_fields: vec![Arc::new(Field::new(
12540 "list",
12541 list.data_type().clone(),
12542 true,
12543 ))],
12544 number_rows: 3,
12545 return_field: Arc::new(Field::new("out", DataType::Boolean, true)),
12546 config_options: Arc::new(ConfigOptions::default()),
12547 })
12548 .unwrap()
12549 .into_array(3)
12550 .unwrap();
12551 let output = output.as_any().downcast_ref::<BooleanArray>().unwrap();
12552 assert_eq!(
12553 output.value(0),
12554 reduce_invariant_quantifier(kind, predicate, 4).unwrap()
12555 );
12556 assert_eq!(
12557 output.value(1),
12558 reduce_invariant_quantifier(kind, predicate, 40).unwrap()
12559 );
12560 assert!(output.is_null(2));
12561 }
12562 }
12563 assert_eq!(
12564 INVARIANT_QUANTIFIER_ROWS.load(Ordering::SeqCst),
12565 4 * 2 * 3,
12566 "1x and 10x element counts must keep invariant work at one fold per list row"
12567 );
12568 }
12569
12570 #[test]
12571 fn invariant_quantifier_lowering_uses_cardinality_only_udf() {
12572 use graphforge_ir::QuantifierKind::None as NoneQ;
12573
12574 for (predicate, expected) in [
12575 (IrLiteral::Bool(true), Some(true)),
12576 (IrLiteral::Bool(false), Some(false)),
12577 (IrLiteral::Null, Option::None),
12578 ] {
12579 let mut arena = ExprArena::new();
12580 let list = arena.push(IrExpr::VarRef(VarId(0)));
12581 let predicate = arena.push(IrExpr::Literal(predicate));
12582 let quantifier = arena.push(IrExpr::Quantifier {
12583 kind: NoneQ,
12584 loop_var: VarId(1),
12585 list,
12586 predicate,
12587 });
12588 let mut vars = VarMap::new();
12589 vars.insert(VarId(0), "list");
12590 let lowered = make_lowerer(&arena, &vars).lower(quantifier).unwrap();
12591 let DfExpr::ScalarFunction(function) = lowered else {
12592 panic!("invariant quantifier must lower to a scalar UDF")
12593 };
12594 let invariant = function
12595 .func
12596 .inner()
12597 .as_any()
12598 .downcast_ref::<CypherInvariantQuantifier>()
12599 .expect("cardinality-only quantifier UDF");
12600 assert_eq!(invariant.predicate, expected);
12601 }
12602 }
12603
12604 #[test]
12605 fn uncorrelated_list_comprehension_batches_volatile_predicate_once() {
12606 use datafusion::arrow::array::{Array, Int64Builder, ListArray, ListBuilder};
12607 use datafusion::arrow::datatypes::Field;
12608 use datafusion::config::ConfigOptions;
12609 use std::sync::Arc;
12610
12611 VOLATILE_CALLS.store(0, Ordering::SeqCst);
12612 VOLATILE_ROWS.store(0, Ordering::SeqCst);
12613 let mut builder = ListBuilder::new(Int64Builder::new());
12614 builder.append_null();
12615 builder.append(true);
12616 builder.values().append_value(1);
12617 builder.values().append_value(2);
12618 builder.append(true);
12619 builder.values().append_value(3);
12620 builder.append(true);
12621 let input = Arc::new(builder.finish()) as datafusion::arrow::array::ArrayRef;
12622 let predicate = ScalarUDF::new_from_impl(CountingVolatilePredicate::new()).call(vec![]);
12623 let udf = CypherListComp::new(Some(predicate), None, "__gf_elem".to_owned(), vec![]);
12624 let return_type = udf.return_type(&[input.data_type().clone()]).unwrap();
12625 let output = udf
12626 .invoke_with_args(ScalarFunctionArgs {
12627 args: vec![ColumnarValue::Array(input)],
12628 arg_fields: vec![Arc::new(Field::new(
12629 "list",
12630 DataType::new_list(DataType::Int64, true),
12631 true,
12632 ))],
12633 number_rows: 4,
12634 return_field: Arc::new(Field::new("out", return_type, true)),
12635 config_options: Arc::new(ConfigOptions::default()),
12636 })
12637 .unwrap()
12638 .into_array(4)
12639 .unwrap();
12640 let output = output.as_any().downcast_ref::<ListArray>().unwrap();
12641
12642 assert!(output.is_null(0));
12643 assert_eq!(output.value(1).len(), 0);
12644 assert_eq!(output.value(2).len(), 2);
12645 assert_eq!(output.value(3).len(), 1);
12646 assert_eq!(VOLATILE_CALLS.load(Ordering::SeqCst), 1);
12647 assert_eq!(VOLATILE_ROWS.load(Ordering::SeqCst), 3);
12648 }
12649
12650 fn invoke_cypher_quantifier(
12653 kind: graphforge_ir::QuantifierKind,
12654 predicate: DfExpr,
12655 list: datafusion::arrow::array::ArrayRef,
12656 ) -> datafusion::error::Result<datafusion::arrow::array::BooleanArray> {
12657 use datafusion::arrow::array::BooleanArray;
12658 use datafusion::arrow::datatypes::Field;
12659 use datafusion::config::ConfigOptions;
12660 use std::sync::Arc;
12661
12662 let n = list.len();
12663 let field = Arc::new(Field::new("l", list.data_type().clone(), true));
12664 let ret = Arc::new(Field::new("q", DataType::Boolean, true));
12665 let args = ScalarFunctionArgs {
12666 args: vec![ColumnarValue::Array(list)],
12667 arg_fields: vec![field],
12668 number_rows: n,
12669 return_field: ret,
12670 config_options: Arc::new(ConfigOptions::default()),
12671 };
12672 let udf = CypherQuantifier::new(kind, predicate, "__gf_elem".to_owned(), vec![]);
12673 let arr = match udf.invoke_with_args(args)? {
12674 ColumnarValue::Array(a) => a,
12675 ColumnarValue::Scalar(s) => s.to_array_of_size(n)?,
12676 };
12677 Ok(arr
12678 .as_any()
12679 .downcast_ref::<BooleanArray>()
12680 .expect("boolean verdicts")
12681 .clone())
12682 }
12683
12684 #[test]
12685 fn cypher_quantifier_empty_list_yields_identity_without_predicate() {
12686 use datafusion::arrow::array::{Array, ListArray};
12687 use datafusion::arrow::datatypes::Int64Type;
12688 use graphforge_ir::QuantifierKind::{All, Any, None as NoneQ, Single};
12689
12690 let empty = || {
12692 std::sync::Arc::new(ListArray::from_iter_primitive::<Int64Type, _, _>(vec![
12693 Some(Vec::<Option<i64>>::new()),
12694 ])) as datafusion::arrow::array::ArrayRef
12695 };
12696
12697 let field_pred =
12701 datafusion::functions::core::expr_fn::get_field(col_literal("__gf_elem"), "a")
12702 .eq(DfExpr::Literal(ScalarValue::Int64(Some(2)), Option::None));
12703 let elem_pred = col_literal("__gf_elem");
12704
12705 for (kind, expected) in [(All, true), (NoneQ, true), (Any, false), (Single, false)] {
12706 for pred in [field_pred.clone(), elem_pred.clone()] {
12707 let out = invoke_cypher_quantifier(kind, pred, empty()).expect("no error");
12708 assert!(!out.is_null(0), "{kind:?}: empty list must be definitive");
12709 assert_eq!(out.value(0), expected, "{kind:?} identity");
12710 }
12711 }
12712 }
12713
12714 #[test]
12715 fn cypher_quantifier_mixed_batch_and_unplannable_predicate() {
12716 use datafusion::arrow::array::{Array, BooleanBuilder, ListBuilder};
12717 use graphforge_ir::QuantifierKind::Any;
12718
12719 let mut b = ListBuilder::new(BooleanBuilder::new());
12721 b.append_null();
12722 b.append(true);
12723 b.values().append_value(true);
12724 b.values().append_value(false);
12725 b.append(true);
12726 let list = std::sync::Arc::new(b.finish()) as datafusion::arrow::array::ArrayRef;
12727
12728 let out = invoke_cypher_quantifier(Any, col_literal("__gf_elem"), list.clone())
12730 .expect("no error");
12731 assert!(out.is_null(0), "null list → null");
12732 assert!(!out.is_null(1) && !out.value(1), "any over [] is false");
12733 assert!(out.value(2), "any over [true, false] is true");
12734
12735 let bad = datafusion::functions::core::expr_fn::get_field(col_literal("__gf_elem"), "a");
12737 assert!(
12738 invoke_cypher_quantifier(Any, bad, list).is_err(),
12739 "non-empty row against an unbuildable predicate must error"
12740 );
12741 }
12742
12743 fn make_lowerer<'a>(arena: &'a ExprArena, var_map: &'a VarMap) -> ExprLowerer<'a> {
12745 ExprLowerer::new(arena, None, var_map)
12746 }
12747
12748 #[test]
12753 fn boolean_and_numeric_operators_reject_known_bad_operands() {
12754 use graphforge_ir::expr::{BinaryOpKind as B, IrExpr, IrLiteral, UnaryOpKind as U};
12755 let vm = VarMap::new();
12756
12757 let reject = |build: &dyn Fn(&mut ExprArena) -> ExprId| {
12759 let mut a = ExprArena::new();
12760 let id = build(&mut a);
12761 let err = make_lowerer(&a, &vm).lower(id).expect_err("should reject");
12762 assert!(
12763 matches!(err, LoweringError::InvalidType(_)),
12764 "expected InvalidType, got {err:?}"
12765 );
12766 };
12767 reject(&|a| {
12769 let l = a.push(IrExpr::Literal(IrLiteral::Int(1)));
12770 let r = a.push(IrExpr::Literal(IrLiteral::Bool(true)));
12771 a.push(IrExpr::BinaryOp {
12772 op: B::And,
12773 left: l,
12774 right: r,
12775 })
12776 });
12777 reject(&|a| {
12779 let l = a.push(IrExpr::Literal(IrLiteral::Int(1)));
12780 let r = a.push(IrExpr::Literal(IrLiteral::Bool(true)));
12781 a.push(IrExpr::BinaryOp {
12782 op: B::Xor,
12783 left: l,
12784 right: r,
12785 })
12786 });
12787 reject(&|a| {
12789 let e = a.push(IrExpr::Literal(IrLiteral::Str("x".into())));
12790 a.push(IrExpr::UnaryOp {
12791 op: U::Not,
12792 expr: e,
12793 })
12794 });
12795 reject(&|a| {
12797 let e = a.push(IrExpr::Literal(IrLiteral::Bool(true)));
12798 a.push(IrExpr::UnaryOp {
12799 op: U::Neg,
12800 expr: e,
12801 })
12802 });
12803 reject(&|a| {
12805 let l = a.push(IrExpr::Literal(IrLiteral::Str("a".into())));
12806 let r = a.push(IrExpr::Literal(IrLiteral::Int(2)));
12807 a.push(IrExpr::BinaryOp {
12808 op: B::Mod,
12809 left: l,
12810 right: r,
12811 })
12812 });
12813 reject(&|a| {
12815 let v = a.push(IrExpr::Literal(IrLiteral::Int(1)));
12816 let m = a.push(IrExpr::MapLiteral(vec![("k".into(), v)]));
12817 a.push(IrExpr::UnaryOp {
12818 op: U::Not,
12819 expr: m,
12820 })
12821 });
12822 }
12823
12824 #[test]
12825 fn operator_type_check_accepts_valid_and_unknown_operands() {
12826 use graphforge_ir::VarId;
12827 use graphforge_ir::expr::{BinaryOpKind as B, IrExpr, IrLiteral, UnaryOpKind as U};
12828 let accept = |vm: &VarMap, build: &dyn Fn(&mut ExprArena) -> ExprId| {
12829 let mut a = ExprArena::new();
12830 let id = build(&mut a);
12831 make_lowerer(&a, vm)
12832 .lower(id)
12833 .expect("valid/unknown operands must lower cleanly");
12834 };
12835 accept(&VarMap::new(), &|a| {
12837 let l = a.push(IrExpr::Literal(IrLiteral::Bool(true)));
12838 let r = a.push(IrExpr::Literal(IrLiteral::Bool(false)));
12839 a.push(IrExpr::BinaryOp {
12840 op: B::And,
12841 left: l,
12842 right: r,
12843 })
12844 });
12845 accept(&VarMap::new(), &|a| {
12847 let l = a.push(IrExpr::Literal(IrLiteral::Null));
12848 let r = a.push(IrExpr::Literal(IrLiteral::Bool(true)));
12849 a.push(IrExpr::BinaryOp {
12850 op: B::And,
12851 left: l,
12852 right: r,
12853 })
12854 });
12855 let mut vm = VarMap::new();
12857 vm.insert(VarId(0), "x");
12858 accept(&vm, &|a| {
12859 let e = a.push(IrExpr::VarRef(VarId(0)));
12860 a.push(IrExpr::UnaryOp {
12861 op: U::Not,
12862 expr: e,
12863 })
12864 });
12865 }
12866
12867 #[test]
12868 fn nested_quantifiers_get_per_depth_elem_columns() {
12869 use graphforge_ir::QuantifierKind::{None as NoneQ, Single};
12870
12871 let mut arena = ExprArena::new();
12875 let list_outer = arena.push(IrExpr::VarRef(VarId(0)));
12876 let list_inner = arena.push(IrExpr::VarRef(VarId(0)));
12877 let x = arena.push(IrExpr::VarRef(VarId(1)));
12878 let y = arena.push(IrExpr::VarRef(VarId(2)));
12879 let sum = arena.push(IrExpr::BinaryOp {
12880 op: BinaryOpKind::Add,
12881 left: x,
12882 right: y,
12883 });
12884 let fifteen = arena.push(IrExpr::Literal(IrLiteral::Int(15)));
12885 let eq = arena.push(IrExpr::BinaryOp {
12886 op: BinaryOpKind::Eq,
12887 left: sum,
12888 right: fifteen,
12889 });
12890 let inner = arena.push(IrExpr::Quantifier {
12891 kind: Single,
12892 loop_var: VarId(2),
12893 list: list_inner,
12894 predicate: eq,
12895 });
12896 let outer = arena.push(IrExpr::Quantifier {
12897 kind: NoneQ,
12898 loop_var: VarId(1),
12899 list: list_outer,
12900 predicate: inner,
12901 });
12902
12903 let mut vm = VarMap::new();
12904 vm.insert(VarId(0), "list");
12905 let lowered = make_lowerer(&arena, &vm).lower(outer).expect("lower");
12906
12907 let as_quant = |e: &DfExpr| -> Option<(String, Vec<String>, Vec<DfExpr>)> {
12908 let DfExpr::ScalarFunction(f) = e else {
12909 return Option::None;
12910 };
12911 let q = f.func.inner().as_any().downcast_ref::<CypherQuantifier>()?;
12912 Some((q.elem_name.clone(), q.outer_names.clone(), f.args.clone()))
12913 };
12914
12915 let (outer_elem, outer_outers, _) = as_quant(&lowered).expect("outer quantifier UDF");
12918 assert_eq!(outer_elem, "__gf_elem");
12919 assert_eq!(outer_outers, vec!["list".to_owned()]);
12920
12921 let DfExpr::ScalarFunction(outer_fn) = &lowered else {
12924 panic!("outer is a scalar function")
12925 };
12926 let outer_q = outer_fn
12927 .func
12928 .inner()
12929 .as_any()
12930 .downcast_ref::<CypherQuantifier>()
12931 .unwrap();
12932 let (inner_elem, inner_outers, inner_args) =
12933 as_quant(&outer_q.predicate).expect("inner quantifier UDF");
12934 assert_eq!(inner_elem, "__gf_elem_1");
12935 assert_eq!(
12936 inner_outers,
12937 vec!["__gf_elem".to_owned()],
12938 "the outer element is an outer column of the inner UDF (the list \
12939 argument resolves in the enclosing batch, not via outer_names)"
12940 );
12941 let arg_names: Vec<String> = inner_args.iter().map(|a| a.to_string()).collect();
12943 assert_eq!(arg_names, vec!["list", "__gf_elem"]);
12944 }
12945
12946 #[test]
12947 fn hydrated_path_nodes_return_type_carries_labels_and_props() {
12948 use datafusion::arrow::datatypes::Field;
12949
12950 let bare = CypherPathNodes::new().return_type(&[]).unwrap();
12952 let DataType::List(item) = &bare else {
12953 panic!("list return, got {bare:?}")
12954 };
12955 let DataType::Struct(fields) = item.data_type() else {
12956 panic!("struct element")
12957 };
12958 assert_eq!(fields.len(), 1);
12959 assert_eq!(fields[0].name(), "node_uuid");
12960
12961 let hydrated = CypherPathNodes::with_hydration(PathNodeHydration {
12964 dir: std::path::PathBuf::from("/nonexistent"),
12965 labels_by_type: vec![(0, "A".to_owned())],
12966 prop_stems: vec!["_untyped".to_owned()],
12967 fields: vec![
12968 Field::new("node_uuid", DataType::FixedSizeBinary(16), false),
12969 Field::new("labels", DataType::new_list(DataType::Utf8, true), true),
12970 Field::new("name", DataType::Utf8, true),
12971 ]
12972 .into(),
12973 })
12974 .return_type(&[])
12975 .unwrap();
12976 let DataType::List(item) = &hydrated else {
12977 panic!("list return, got {hydrated:?}")
12978 };
12979 let DataType::Struct(fields) = item.data_type() else {
12980 panic!("struct element")
12981 };
12982 let names: Vec<&str> = fields.iter().map(|f| f.name().as_str()).collect();
12983 assert_eq!(names, vec!["node_uuid", "labels", "name"]);
12984 }
12985
12986 #[test]
12987 fn elem_struct_col_routes_property_to_get_field() {
12988 let mut arena = ExprArena::new();
12995 let base = arena.push(IrExpr::VarRef(VarId(0)));
12996 let access = arena.push(IrExpr::PropertyAccess {
12997 base,
12998 prop: PropId(0),
12999 });
13000 let mut vm = VarMap::new();
13001 vm.insert(VarId(0), "__gf_elem");
13002 let mut prop_names = HashMap::new();
13003 prop_names.insert(0u32, "a".to_owned());
13004
13005 let dotted = ExprLowerer::with_prop_names(&arena, &vm, prop_names.clone())
13009 .lower(access)
13010 .unwrap();
13011 assert!(
13012 matches!(&dotted, DfExpr::Column(_)) && dotted.to_string() == "__gf_elem.a",
13013 "expected dotted column, got {dotted:?}"
13014 );
13015
13016 let via_get_field = ExprLowerer::with_prop_names(&arena, &vm, prop_names.clone())
13019 .with_elem_struct_col("__gf_elem".to_owned())
13020 .lower(access)
13021 .unwrap();
13022 assert!(
13023 !matches!(&via_get_field, DfExpr::Column(_)),
13024 "element field access must not be a dotted column: {via_get_field:?}"
13025 );
13026 assert!(
13027 via_get_field.to_string().contains("get_field"),
13028 "expected a get_field call, got {via_get_field}"
13029 );
13030 }
13031
13032 #[test]
13033 fn map_column_field_access_uses_get_field_via_schema() {
13034 use datafusion::arrow::datatypes::{DataType, Field, Fields, Schema};
13039 use datafusion::common::DFSchema;
13040
13041 let map_ty = DataType::Struct(Fields::from(vec![
13042 Field::new("list", DataType::new_list(DataType::Int64, true), true),
13043 Field::new("fixed", DataType::Boolean, true),
13044 ]));
13045 let schema = Schema::new(vec![Field::new("input", map_ty, true)]);
13046 let df_schema = std::sync::Arc::new(DFSchema::try_from(schema).unwrap());
13047
13048 let mut arena = ExprArena::new();
13049 let base = arena.push(IrExpr::VarRef(VarId(0)));
13050 let access = arena.push(IrExpr::PropertyAccess {
13051 base,
13052 prop: PropId(0),
13053 });
13054 let mut vm = VarMap::new();
13055 vm.insert(VarId(0), "input");
13056 let mut prop_names = HashMap::new();
13057 prop_names.insert(0u32, "list".to_owned());
13058
13059 let out = ExprLowerer::with_prop_names(&arena, &vm, prop_names)
13060 .with_input_schema(df_schema)
13061 .lower(access)
13062 .unwrap();
13063 assert!(
13064 !matches!(&out, DfExpr::Column(_)),
13065 "map field must not be a dotted column: {out:?}"
13066 );
13067 assert!(
13068 out.to_string().contains("get_field"),
13069 "expected a get_field call, got {out}"
13070 );
13071 }
13072
13073 #[test]
13074 fn list_plus_uses_native_ops_for_homogeneous_schema_types() {
13075 use datafusion::arrow::datatypes::{DataType, Field, Schema};
13080 use datafusion::common::DFSchema;
13081 use graphforge_ir::expr::BinaryOpKind;
13082
13083 let schema = Schema::new(vec![
13084 Field::new("xs", DataType::new_list(DataType::Int64, true), true),
13085 Field::new("y", DataType::Int64, true),
13086 Field::new("ys", DataType::new_list(DataType::Int64, true), true),
13087 Field::new("s", DataType::Utf8, true),
13088 Field::new("ss", DataType::new_list(DataType::Utf8, true), true),
13089 ]);
13090 let df_schema = std::sync::Arc::new(DFSchema::try_from(schema).unwrap());
13091
13092 let mut arena = ExprArena::new();
13093 let xs = arena.push(IrExpr::VarRef(VarId(0)));
13094 let y = arena.push(IrExpr::VarRef(VarId(1)));
13095 let ys = arena.push(IrExpr::VarRef(VarId(2)));
13096 let s = arena.push(IrExpr::VarRef(VarId(3)));
13097 let ss = arena.push(IrExpr::VarRef(VarId(4)));
13098 let append = arena.push(IrExpr::BinaryOp {
13099 op: BinaryOpKind::Add,
13100 left: xs,
13101 right: y,
13102 });
13103 let concat = arena.push(IrExpr::BinaryOp {
13104 op: BinaryOpKind::Add,
13105 left: xs,
13106 right: ys,
13107 });
13108 let hetero_append = arena.push(IrExpr::BinaryOp {
13109 op: BinaryOpKind::Add,
13110 left: xs,
13111 right: s,
13112 });
13113 let hetero_concat = arena.push(IrExpr::BinaryOp {
13114 op: BinaryOpKind::Add,
13115 left: xs,
13116 right: ss,
13117 });
13118 let mut vm = VarMap::new();
13119 vm.insert(VarId(0), "xs");
13120 vm.insert(VarId(1), "y");
13121 vm.insert(VarId(2), "ys");
13122 vm.insert(VarId(3), "s");
13123 vm.insert(VarId(4), "ss");
13124
13125 let lowerer =
13126 ExprLowerer::with_prop_names(&arena, &vm, HashMap::new()).with_input_schema(df_schema);
13127 assert!(
13128 lowerer
13129 .lower(append)
13130 .unwrap()
13131 .to_string()
13132 .contains("array_append"),
13133 "list + element should append"
13134 );
13135 assert!(
13136 lowerer
13137 .lower(concat)
13138 .unwrap()
13139 .to_string()
13140 .contains("array_concat"),
13141 "list + list should concat"
13142 );
13143 assert!(
13144 lowerer
13145 .lower(hetero_append)
13146 .unwrap()
13147 .to_string()
13148 .contains("cypher_list_plus"),
13149 "list + heterogeneous element should use tagged list-plus"
13150 );
13151 assert!(
13152 lowerer
13153 .lower(hetero_concat)
13154 .unwrap()
13155 .to_string()
13156 .contains("cypher_list_plus"),
13157 "list + heterogeneous list should use tagged list-plus"
13158 );
13159 }
13160
13161 #[test]
13162 fn cypher_list_plus_concats_decoded_list_element() {
13163 use datafusion::arrow::array::{Array, ArrayRef, ListArray};
13164 use datafusion::arrow::datatypes::Field;
13165 use datafusion::config::ConfigOptions;
13166 use datafusion::scalar::ScalarValue as S;
13167 use std::sync::Arc;
13168
13169 let list = |items: Vec<S>| S::List(S::new_list(&items, &DataType::Int64, true));
13170 let left_items = vec![
13171 list(vec![S::Int64(Some(1))]),
13172 list(vec![S::Int64(Some(2)), S::Int64(Some(3))]),
13173 list(vec![S::Int64(Some(4)), S::Int64(Some(5))]),
13174 ];
13175 let left = S::List(S::new_list(&left_items, &left_items[0].data_type(), true))
13176 .to_array()
13177 .unwrap();
13178
13179 let right_list = list(vec![S::Int64(Some(8)), S::Int64(Some(9))]);
13180 let right: ArrayRef = Arc::new(build_het_struct(&[right_list], 1).unwrap());
13181 let udf = CypherListPlus::new();
13182 let arg_types = vec![left.data_type().clone(), right.data_type().clone()];
13183 let return_type = udf.return_type(&arg_types).unwrap();
13184 let args = ScalarFunctionArgs {
13185 args: vec![ColumnarValue::Array(left), ColumnarValue::Array(right)],
13186 arg_fields: vec![
13187 Arc::new(Field::new(
13188 "l",
13189 DataType::new_list(DataType::Int64, true),
13190 true,
13191 )),
13192 Arc::new(Field::new("r", DataType::Struct(het_fields(1)), true)),
13193 ],
13194 number_rows: 1,
13195 return_field: Arc::new(Field::new("out", return_type, true)),
13196 config_options: Arc::new(ConfigOptions::default()),
13197 };
13198 let out = match udf.invoke_with_args(args).unwrap() {
13199 ColumnarValue::Array(a) => a,
13200 ColumnarValue::Scalar(s) => s.to_array().unwrap(),
13201 };
13202 let out = out.as_any().downcast_ref::<ListArray>().unwrap();
13203 assert!(!out.is_null(0));
13204 let values = out.value(0);
13205 assert_eq!(values.len(), 5);
13206 assert_eq!(
13207 decode_het(&ScalarValue::try_from_array(&values, 3).unwrap()),
13208 Some(S::Int64(Some(8)))
13209 );
13210 assert_eq!(
13211 decode_het(&ScalarValue::try_from_array(&values, 4).unwrap()),
13212 Some(S::Int64(Some(9)))
13213 );
13214 }
13215
13216 #[test]
13217 fn tagged_list_element_plus_preserves_dynamic_concat_and_null_rows() {
13218 use datafusion::arrow::array::{Array, ArrayRef, ListArray};
13219 use datafusion::arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
13220 use datafusion::arrow::datatypes::Field;
13221 use datafusion::scalar::ScalarValue as S;
13222 use std::sync::Arc;
13223
13224 let values: ArrayRef = Arc::new(
13225 build_het_struct(
13226 &[S::Int64(Some(1)), S::Boolean(Some(true)), S::Int64(Some(2))],
13227 1,
13228 )
13229 .unwrap(),
13230 );
13231 let item = Arc::new(Field::new("item", values.data_type().clone(), true));
13232 let left: ArrayRef = Arc::new(ListArray::new(
13233 item.clone(),
13234 OffsetBuffer::new(ScalarBuffer::from(vec![0i32, 2, 3, 3, 3])),
13235 values,
13236 Some(NullBuffer::from(vec![true, true, true, false])),
13237 ));
13238 let nested = S::List(S::new_list(
13239 &[S::Int64(Some(8)), S::Int64(Some(7))],
13240 &DataType::Int64,
13241 true,
13242 ));
13243 let right: ArrayRef = Arc::new(
13244 build_het_struct(&[S::Int64(Some(9)), nested, S::Null, S::Int64(Some(1))], 1).unwrap(),
13245 );
13246 let return_type = DataType::List(item);
13247 let output = invoke_tagged_list_element_plus(&left, &right, &return_type)
13248 .unwrap()
13249 .expect("tagged fast path");
13250 let output = output.as_any().downcast_ref::<ListArray>().unwrap();
13251 let decode_row = |row: usize| {
13252 let values = output.value(row);
13253 (0..values.len())
13254 .map(|index| {
13255 decode_het(&ScalarValue::try_from_array(&values, index).unwrap()).unwrap()
13256 })
13257 .collect::<Vec<_>>()
13258 };
13259
13260 assert_eq!(
13261 decode_row(0),
13262 vec![S::Int64(Some(1)), S::Boolean(Some(true)), S::Int64(Some(9))]
13263 );
13264 assert_eq!(
13265 decode_row(1),
13266 vec![S::Int64(Some(2)), S::Int64(Some(8)), S::Int64(Some(7))]
13267 );
13268 assert_eq!(decode_row(2), vec![S::Null]);
13269 assert!(output.is_null(3));
13270 }
13271
13272 #[test]
13273 fn cypher_conversions_decode_tagged_values() {
13274 use datafusion::arrow::array::{Array, ArrayRef, Float64Array, Int64Array, StringArray};
13275 use datafusion::arrow::datatypes::Field;
13276 use datafusion::config::ConfigOptions;
13277 use datafusion::scalar::ScalarValue as S;
13278 use std::sync::Arc;
13279
13280 let values: ArrayRef = Arc::new(
13281 build_het_struct(
13282 &[
13283 S::Int64(Some(2)),
13284 S::Float64(Some(2.9)),
13285 S::Utf8(Some("foo".to_owned())),
13286 ],
13287 0,
13288 )
13289 .unwrap(),
13290 );
13291 let invoke = |kind: CypherConversionKind| {
13292 let udf = CypherConversion::new(kind);
13293 let return_type = udf.return_type(&[values.data_type().clone()]).unwrap();
13294 let args = ScalarFunctionArgs {
13295 args: vec![ColumnarValue::Array(Arc::clone(&values))],
13296 arg_fields: vec![Arc::new(Field::new("v", values.data_type().clone(), true))],
13297 number_rows: values.len(),
13298 return_field: Arc::new(Field::new("out", return_type, true)),
13299 config_options: Arc::new(ConfigOptions::default()),
13300 };
13301 match udf.invoke_with_args(args).unwrap() {
13302 ColumnarValue::Array(a) => a,
13303 ColumnarValue::Scalar(s) => s.to_array_of_size(values.len()).unwrap(),
13304 }
13305 };
13306
13307 let ints = invoke(CypherConversionKind::Integer);
13308 let ints = ints.as_any().downcast_ref::<Int64Array>().unwrap();
13309 assert_eq!(ints.value(0), 2);
13310 assert_eq!(ints.value(1), 2);
13311 assert!(ints.is_null(2));
13312
13313 let floats = invoke(CypherConversionKind::Float);
13314 let floats = floats.as_any().downcast_ref::<Float64Array>().unwrap();
13315 assert_eq!(floats.value(0), 2.0);
13316 assert_eq!(floats.value(1), 2.9);
13317 assert!(floats.is_null(2));
13318
13319 let strings = match CYPHER_TO_STRING
13320 .invoke_with_args(ScalarFunctionArgs {
13321 args: vec![ColumnarValue::Array(values)],
13322 arg_fields: vec![Arc::new(Field::new(
13323 "v",
13324 DataType::Struct(het_fields(0)),
13325 true,
13326 ))],
13327 number_rows: 3,
13328 return_field: Arc::new(Field::new("out", DataType::Utf8, true)),
13329 config_options: Arc::new(ConfigOptions::default()),
13330 })
13331 .unwrap()
13332 {
13333 ColumnarValue::Array(a) => a,
13334 ColumnarValue::Scalar(s) => s.to_array_of_size(3).unwrap(),
13335 };
13336 let strings = strings.as_any().downcast_ref::<StringArray>().unwrap();
13337 assert_eq!(strings.value(0), "2");
13338 assert_eq!(strings.value(1), "2.9");
13339 assert_eq!(strings.value(2), "foo");
13340 }
13341
13342 #[test]
13343 fn het_list_map_roundtrip_and_order() {
13344 use datafusion::scalar::ScalarValue as S;
13345 let map = const_map_scalar(&[
13347 ("a".to_owned(), S::Int64(Some(2))),
13348 ("b".to_owned(), S::Boolean(Some(true))),
13349 ])
13350 .expect("map scalar");
13351 let S::Struct(m) = &map else {
13352 panic!("map is a struct")
13353 };
13354 assert!(is_plain_map_struct(m));
13355 let S::Struct(d) = date_scalar(Some(0)) else {
13356 panic!("date is a struct")
13357 };
13358 assert!(!is_plain_map_struct(&d));
13359
13360 let scalars = vec![S::Int64(Some(1)), map.clone()];
13363 assert_eq!(het_depth(&scalars[0]), Some(0));
13364 assert_eq!(het_depth(&scalars[1]), Some(1));
13365 let depth = scalars.iter().filter_map(het_depth).max().unwrap();
13366 let elem = build_het_struct(&scalars, depth).expect("build tagged struct");
13367 let e0 = ScalarValue::try_from_array(&elem, 0).unwrap();
13368 assert_eq!(decode_het(&e0), Some(S::Int64(Some(1))));
13369 let e1 = ScalarValue::try_from_array(&elem, 1).unwrap();
13370 let decoded = decode_het(&e1).expect("decode map element");
13371 assert_eq!(cypher_value_eq(&decoded, &map), Some(true));
13372
13373 let m1 = const_map_scalar(&[("a".to_owned(), S::Int64(Some(1)))]).unwrap();
13376 let m2 = const_map_scalar(&[("a".to_owned(), S::Int64(Some(2)))]).unwrap();
13377 assert_eq!(cypher_order(&m1, &m2), std::cmp::Ordering::Less);
13378 assert_eq!(
13379 cypher_order(&S::Int64(Some(99)), &m1),
13380 std::cmp::Ordering::Less
13381 );
13382 }
13383
13384 #[test]
13385 fn empty_map_scalar_has_one_row_and_no_fields() {
13386 use datafusion::arrow::array::Array;
13387 use datafusion::scalar::ScalarValue as S;
13388
13389 let map = const_map_scalar(&[]).expect("empty map scalar");
13390 let S::Struct(values) = map else {
13391 panic!("empty map is a struct")
13392 };
13393 assert_eq!(values.len(), 1);
13394 assert_eq!(values.num_columns(), 0);
13395 assert!(is_plain_map_struct(&values));
13396
13397 let scalars = vec![S::Int64(Some(1)), S::Struct(values.clone())];
13398 let encoded = build_het_struct(&scalars, 1).expect("encode empty map");
13399 let tagged = S::try_from_array(&encoded, 1).expect("tagged empty map");
13400 let S::Struct(decoded) = decode_het(&tagged).expect("decode empty map") else {
13401 panic!("decoded empty map is a struct")
13402 };
13403 assert_eq!(decoded.len(), 1);
13404 assert_eq!(decoded.num_columns(), 0);
13405 }
13406
13407 #[test]
13408 fn dictionary_scalar_is_normalized_for_heterogeneous_encoding() {
13409 use datafusion::arrow::datatypes::DataType;
13410 use datafusion::scalar::ScalarValue as S;
13411
13412 let dictionary = S::Dictionary(
13413 Box::new(DataType::Int32),
13414 Box::new(S::Utf8(Some("value".to_owned()))),
13415 );
13416 let scalars = vec![S::Int64(Some(1)), dictionary];
13417 assert_eq!(het_depth(&scalars[1]), Some(0));
13418
13419 let encoded = build_het_struct(&scalars, 0).expect("encode dictionary scalar");
13420 let tagged = S::try_from_array(&encoded, 1).expect("tagged dictionary scalar");
13421 assert_eq!(decode_het(&tagged), Some(S::Utf8(Some("value".to_owned()))));
13422 }
13423
13424 #[test]
13425 fn cypher_value_eq_scalars() {
13426 use datafusion::scalar::ScalarValue as S;
13427 let i = |n| S::Int64(Some(n));
13428
13429 assert_eq!(cypher_value_eq(&i(1), &i(1)), Some(true));
13430 assert_eq!(cypher_value_eq(&i(1), &i(2)), Some(false));
13431 assert_eq!(
13433 cypher_value_eq(&i(1), &S::Utf8(Some("1".into()))),
13434 Some(false)
13435 );
13436 assert_eq!(cypher_value_eq(&i(1), &S::Float64(Some(1.0))), Some(true));
13438 assert_eq!(cypher_value_eq(&i(1), &S::UInt64(Some(1))), Some(true));
13439 assert_eq!(
13440 cypher_value_eq(&S::Float64(Some(f64::NAN)), &S::Float64(Some(f64::NAN))),
13441 Some(false)
13442 );
13443 assert_eq!(cypher_value_eq(&i(1), &S::Int64(None)), None);
13445 assert_eq!(cypher_value_eq(&S::Null, &i(1)), None);
13446 }
13447
13448 #[test]
13449 fn cypher_comparison_predicate_handles_nan_and_cross_type() {
13450 use datafusion::scalar::ScalarValue as S;
13451
13452 assert_eq!(
13453 cypher_compare_pred(&S::Float64(Some(f64::NAN)), &S::Int64(Some(1)), 2),
13454 Some(false)
13455 );
13456 assert_eq!(
13457 cypher_compare_pred(&S::Utf8(Some("1".to_owned())), &S::Int64(Some(1)), 0,),
13458 None
13459 );
13460 assert_eq!(
13461 cypher_compare_pred(&S::Int64(Some(1)), &S::Float64(Some(2.0)), 0),
13462 Some(true)
13463 );
13464 assert_eq!(
13465 cypher_compare_pred(&S::UInt64(Some(1)), &S::Int64(Some(2)), 0),
13466 Some(true)
13467 );
13468 }
13469
13470 #[test]
13475 fn literal_null() {
13476 let mut arena = ExprArena::new();
13477 let id = arena.push(IrExpr::Literal(IrLiteral::Null));
13478 let vm = VarMap::new();
13479 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13480 assert!(matches!(result, DfExpr::Literal(ScalarValue::Null, _)));
13481 }
13482
13483 #[test]
13484 fn literal_bool() {
13485 let mut arena = ExprArena::new();
13486 let id = arena.push(IrExpr::Literal(IrLiteral::Bool(true)));
13487 let vm = VarMap::new();
13488 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13489 assert!(matches!(
13490 result,
13491 DfExpr::Literal(ScalarValue::Boolean(Some(true)), _)
13492 ));
13493 }
13494
13495 #[test]
13496 fn literal_int() {
13497 let mut arena = ExprArena::new();
13498 let id = arena.push(IrExpr::Literal(IrLiteral::Int(42)));
13499 let vm = VarMap::new();
13500 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13501 assert!(matches!(
13502 result,
13503 DfExpr::Literal(ScalarValue::Int64(Some(42)), _)
13504 ));
13505 }
13506
13507 #[test]
13508 fn literal_float() {
13509 let mut arena = ExprArena::new();
13510 let id = arena.push(IrExpr::Literal(IrLiteral::Float(2.71)));
13511 let vm = VarMap::new();
13512 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13513 assert!(matches!(
13514 result,
13515 DfExpr::Literal(ScalarValue::Float64(Some(_)), _)
13516 ));
13517 }
13518
13519 #[test]
13520 fn literal_str() {
13521 let mut arena = ExprArena::new();
13522 let id = arena.push(IrExpr::Literal(IrLiteral::Str("hello".into())));
13523 let vm = VarMap::new();
13524 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13525 assert!(matches!(
13526 result,
13527 DfExpr::Literal(ScalarValue::Utf8(Some(_)), _)
13528 ));
13529 }
13530
13531 #[test]
13532 fn literal_duration() {
13533 let mut arena = ExprArena::new();
13534 let id = arena.push(IrExpr::Literal(IrLiteral::Duration {
13535 months: 0,
13536 days: 0,
13537 seconds: 1,
13538 nanos: 0,
13539 }));
13540 let vm = VarMap::new();
13541 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13542 assert!(matches!(result, DfExpr::Literal(ScalarValue::Struct(_), _)));
13543 }
13544
13545 #[test]
13546 fn literal_datetime() {
13547 let mut arena = ExprArena::new();
13548 let id = arena.push(IrExpr::Literal(IrLiteral::DateTime(0)));
13549 let vm = VarMap::new();
13550 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13551 assert!(matches!(
13552 result,
13553 DfExpr::Literal(ScalarValue::TimestampMicrosecond(Some(0), Some(_)), _)
13554 ));
13555 }
13556
13557 #[test]
13562 fn var_ref_bound() {
13563 let mut arena = ExprArena::new();
13564 let id = arena.push(IrExpr::VarRef(VarId(0)));
13565 let mut vm = VarMap::new();
13566 vm.insert(VarId(0), "node_id");
13567 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13568 assert!(matches!(result, DfExpr::Column(_)));
13569 if let DfExpr::Column(col) = result {
13570 assert_eq!(col.name, "node_id");
13571 }
13572 }
13573
13574 #[test]
13575 fn var_ref_unbound_returns_error() {
13576 let mut arena = ExprArena::new();
13577 let id = arena.push(IrExpr::VarRef(VarId(99)));
13578 let vm = VarMap::new();
13579 let result = make_lowerer(&arena, &vm).lower(id);
13580 assert!(matches!(result, Err(LoweringError::UnboundVar(99))));
13581 }
13582
13583 #[test]
13588 fn unary_not() {
13589 let mut arena = ExprArena::new();
13590 let inner = arena.push(IrExpr::Literal(IrLiteral::Bool(true)));
13591 let id = arena.push(IrExpr::UnaryOp {
13592 op: UnaryOpKind::Not,
13593 expr: inner,
13594 });
13595 let vm = VarMap::new();
13596 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13597 assert!(matches!(result, DfExpr::Not(_)));
13598 }
13599
13600 #[test]
13601 fn unary_is_null() {
13602 let mut arena = ExprArena::new();
13603 let mut vm = VarMap::new();
13604 vm.insert(VarId(0), "x");
13605 let inner = arena.push(IrExpr::VarRef(VarId(0)));
13606 let id = arena.push(IrExpr::UnaryOp {
13607 op: UnaryOpKind::IsNull,
13608 expr: inner,
13609 });
13610 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13611 assert!(matches!(result, DfExpr::IsNull(_)));
13612 }
13613
13614 #[test]
13615 fn unary_is_not_null() {
13616 let mut arena = ExprArena::new();
13617 let mut vm = VarMap::new();
13618 vm.insert(VarId(0), "x");
13619 let inner = arena.push(IrExpr::VarRef(VarId(0)));
13620 let id = arena.push(IrExpr::UnaryOp {
13621 op: UnaryOpKind::IsNotNull,
13622 expr: inner,
13623 });
13624 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13625 assert!(matches!(result, DfExpr::IsNotNull(_)));
13626 }
13627
13628 #[test]
13633 fn binary_eq() {
13634 let mut arena = ExprArena::new();
13635 let l = arena.push(IrExpr::Literal(IrLiteral::Int(1)));
13636 let r = arena.push(IrExpr::Literal(IrLiteral::Int(2)));
13637 let id = arena.push(IrExpr::BinaryOp {
13638 op: BinaryOpKind::Eq,
13639 left: l,
13640 right: r,
13641 });
13642 let vm = VarMap::new();
13643 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13644 let DfExpr::ScalarFunction(sf) = result else {
13647 panic!("expected a cypher_eq scalar-function call, got {result:?}");
13648 };
13649 assert_eq!(sf.func.name(), "cypher_eq");
13650 assert_eq!(sf.args.len(), 2);
13651 }
13652
13653 #[test]
13654 fn binary_in_list() {
13655 let mut arena = ExprArena::new();
13656 let mut vm = VarMap::new();
13657 vm.insert(VarId(0), "x");
13658 let l = arena.push(IrExpr::VarRef(VarId(0)));
13659 let one = arena.push(IrExpr::Literal(IrLiteral::Int(1)));
13660 let two = arena.push(IrExpr::Literal(IrLiteral::Int(2)));
13661 let list = arena.push(IrExpr::ListLiteral(vec![one, two]));
13662 let id = arena.push(IrExpr::BinaryOp {
13663 op: BinaryOpKind::In,
13664 left: l,
13665 right: list,
13666 });
13667 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13668 let DfExpr::ScalarFunction(sf) = result else {
13672 panic!("expected a cypher_in scalar-function call, got {result:?}");
13673 };
13674 assert_eq!(sf.func.name(), "cypher_in");
13675 assert_eq!(sf.args.len(), 2);
13676 }
13677
13678 fn label_membership_expr(arena: &mut ExprArena, left: IrExpr, node_var: VarId) -> ExprId {
13679 let left = arena.push(left);
13680 let node = arena.push(IrExpr::VarRef(node_var));
13681 let labels = arena.push(IrExpr::FunctionCall {
13682 name: "labels".into(),
13683 args: vec![node],
13684 });
13685 arena.push(IrExpr::BinaryOp {
13686 op: BinaryOpKind::In,
13687 left,
13688 right: labels,
13689 })
13690 }
13691
13692 fn label_lowerer<'a>(arena: &'a ExprArena, var_map: &'a VarMap) -> ExprLowerer<'a> {
13693 ExprLowerer::with_prop_names_and_nodes(
13694 arena,
13695 var_map,
13696 HashMap::new(),
13697 HashMap::from([(0, NodeShape { prop_names: vec![] })]),
13698 HashMap::from([(7, "Known".to_owned())]),
13699 false,
13700 )
13701 }
13702
13703 #[test]
13704 fn known_literal_in_labels_lowers_to_direct_type_id_membership() {
13705 let mut arena = ExprArena::new();
13706 let id = label_membership_expr(
13707 &mut arena,
13708 IrExpr::Literal(IrLiteral::Str("Known".into())),
13709 VarId(0),
13710 );
13711 let mut vm = VarMap::new();
13712 vm.insert(VarId(0), "var_0");
13713
13714 let result = label_lowerer(&arena, &vm).lower(id).unwrap();
13715 let DfExpr::ScalarFunction(sf) = &result else {
13716 panic!("expected array_has scalar function, got {result:?}");
13717 };
13718 assert_eq!(sf.func.name(), "array_has");
13719 let rendered = result.to_string();
13720 assert!(rendered.contains("var_0.type_ids"));
13721 assert!(rendered.contains("UInt32(7)"));
13722 assert!(!rendered.contains("cypher_in"));
13723 assert!(!rendered.contains("array_concat"));
13724 }
13725
13726 #[test]
13727 fn unknown_literal_in_labels_retains_generic_membership() {
13728 let mut arena = ExprArena::new();
13729 let id = label_membership_expr(
13730 &mut arena,
13731 IrExpr::Literal(IrLiteral::Str("Unknown".into())),
13732 VarId(0),
13733 );
13734 let mut vm = VarMap::new();
13735 vm.insert(VarId(0), "var_0");
13736
13737 let result = label_lowerer(&arena, &vm).lower(id).unwrap();
13738 let DfExpr::ScalarFunction(sf) = result else {
13739 panic!("expected cypher_in scalar function");
13740 };
13741 assert_eq!(sf.func.name(), "cypher_in");
13742 }
13743
13744 #[test]
13745 fn dynamic_in_labels_retains_generic_membership() {
13746 let mut arena = ExprArena::new();
13747 let id = label_membership_expr(&mut arena, IrExpr::VarRef(VarId(1)), VarId(0));
13748 let mut vm = VarMap::new();
13749 vm.insert(VarId(0), "var_0");
13750 vm.insert(VarId(1), "label_name");
13751
13752 let result = label_lowerer(&arena, &vm).lower(id).unwrap();
13753 let DfExpr::ScalarFunction(sf) = result else {
13754 panic!("expected cypher_in scalar function");
13755 };
13756 assert_eq!(sf.func.name(), "cypher_in");
13757 }
13758
13759 #[test]
13764 fn compound_predicate() {
13765 let mut arena = ExprArena::new();
13766 let mut vm = VarMap::new();
13767 vm.insert(VarId(0), "a.age"); vm.insert(VarId(1), "b.name"); let a_age = arena.push(IrExpr::VarRef(VarId(0)));
13771 let thirty = arena.push(IrExpr::Literal(IrLiteral::Int(30)));
13772 let gt = arena.push(IrExpr::BinaryOp {
13773 op: BinaryOpKind::Gt,
13774 left: a_age,
13775 right: thirty,
13776 });
13777
13778 let b_name = arena.push(IrExpr::VarRef(VarId(1)));
13779 let param = arena.push(IrExpr::Parameter("name".into()));
13780 let eq = arena.push(IrExpr::BinaryOp {
13781 op: BinaryOpKind::Eq,
13782 left: b_name,
13783 right: param,
13784 });
13785
13786 let and = arena.push(IrExpr::BinaryOp {
13787 op: BinaryOpKind::And,
13788 left: gt,
13789 right: eq,
13790 });
13791
13792 let result = make_lowerer(&arena, &vm).lower(and).unwrap();
13793 assert!(matches!(result, DfExpr::ScalarFunction(_)));
13794 if let DfExpr::ScalarFunction(sf) = result {
13795 assert_eq!(sf.func.name(), "cypher_and");
13796 }
13797 }
13798
13799 #[test]
13800 fn xor_chain_lowering_has_one_udf_per_source_operator() {
13801 use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion};
13802
13803 let lower_and_count = |operands: usize| {
13804 let mut arena = ExprArena::new();
13805 let mut root = arena.push(IrExpr::Literal(IrLiteral::Bool(true)));
13806 for index in 1..operands {
13807 let right = match index % 3 {
13808 0 => IrLiteral::Bool(true),
13809 1 => IrLiteral::Bool(false),
13810 _ => IrLiteral::Null,
13811 };
13812 let right = arena.push(IrExpr::Literal(right));
13813 root = arena.push(IrExpr::BinaryOp {
13814 op: BinaryOpKind::Xor,
13815 left: root,
13816 right,
13817 });
13818 }
13819
13820 let lowered = make_lowerer(&arena, &VarMap::new())
13821 .lower(root)
13822 .expect("XOR chain lowers");
13823 let mut udf_count = 0;
13824 lowered
13825 .apply(|expr| {
13826 if matches!(
13827 expr,
13828 DfExpr::ScalarFunction(function)
13829 if function.func.name() == "cypher_xor"
13830 ) {
13831 udf_count += 1;
13832 }
13833 Ok(TreeNodeRecursion::Continue)
13834 })
13835 .expect("expression traversal succeeds");
13836 udf_count
13837 };
13838
13839 let eleven = lower_and_count(11);
13840 let twenty_two = lower_and_count(22);
13841 assert_eq!(eleven, 10);
13842 assert_eq!(twenty_two, 21);
13843 assert!(
13844 twenty_two <= eleven * 3,
13845 "doubling operands must keep deterministic lowering work within 3x"
13846 );
13847 }
13848
13849 #[test]
13850 fn cypher_xor_implements_three_valued_truth_table() {
13851 use datafusion::arrow::array::{Array, BooleanArray};
13852 use datafusion::arrow::datatypes::Field;
13853 use datafusion::config::ConfigOptions;
13854
13855 let left = BooleanArray::from(vec![
13856 Some(false),
13857 Some(false),
13858 Some(false),
13859 Some(true),
13860 Some(true),
13861 Some(true),
13862 None,
13863 None,
13864 None,
13865 ]);
13866 let right = BooleanArray::from(vec![
13867 Some(false),
13868 Some(true),
13869 None,
13870 Some(false),
13871 Some(true),
13872 None,
13873 Some(false),
13874 Some(true),
13875 None,
13876 ]);
13877 let expected = [
13878 Some(false),
13879 Some(true),
13880 None,
13881 Some(true),
13882 Some(false),
13883 None,
13884 None,
13885 None,
13886 None,
13887 ];
13888 let field = Arc::new(Field::new("value", DataType::Boolean, true));
13889 let arguments = ScalarFunctionArgs {
13890 args: vec![
13891 ColumnarValue::Array(Arc::new(left)),
13892 ColumnarValue::Array(Arc::new(right)),
13893 ],
13894 arg_fields: vec![Arc::clone(&field), field],
13895 number_rows: expected.len(),
13896 return_field: Arc::new(Field::new("xor", DataType::Boolean, true)),
13897 config_options: Arc::new(ConfigOptions::default()),
13898 };
13899 let result = CypherBoolOp::new(CypherBoolOpKind::Xor)
13900 .invoke_with_args(arguments)
13901 .expect("XOR evaluates");
13902 let ColumnarValue::Array(result) = result else {
13903 panic!("array inputs must produce an array")
13904 };
13905 let result = result
13906 .as_any()
13907 .downcast_ref::<BooleanArray>()
13908 .expect("XOR result is boolean");
13909
13910 for (index, expected) in expected.into_iter().enumerate() {
13911 let actual = (!result.is_null(index)).then(|| result.value(index));
13912 assert_eq!(actual, expected, "truth-table row {index}");
13913 }
13914 }
13915
13916 #[test]
13921 fn function_call_to_upper() {
13922 let mut arena = ExprArena::new();
13923 let mut vm = VarMap::new();
13924 vm.insert(VarId(0), "n.name");
13925 let arg = arena.push(IrExpr::VarRef(VarId(0)));
13926 let id = arena.push(IrExpr::FunctionCall {
13927 name: "toUpper".into(),
13928 args: vec![arg],
13929 });
13930 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
13931 assert!(matches!(result, DfExpr::ScalarFunction(_)));
13933 }
13934
13935 #[test]
13936 fn function_call_unknown_returns_error() {
13937 let mut arena = ExprArena::new();
13938 let id = arena.push(IrExpr::FunctionCall {
13939 name: "unknownFn".into(),
13940 args: vec![],
13941 });
13942 let vm = VarMap::new();
13943 let result = make_lowerer(&arena, &vm).lower(id);
13944 assert!(matches!(result, Err(LoweringError::UnknownFunction(_))));
13945 }
13946
13947 fn lower_rel_fn(name: &str, int_args: &[i64]) -> String {
13954 let mut arena = ExprArena::new();
13955 let mut vm = VarMap::new();
13956 vm.insert(VarId(0), "var_1.rels");
13957 let mut args = vec![arena.push(IrExpr::VarRef(VarId(0)))];
13958 for &n in int_args {
13959 args.push(arena.push(IrExpr::Literal(IrLiteral::Int(n))));
13960 }
13961 let id = arena.push(IrExpr::FunctionCall {
13962 name: name.into(),
13963 args,
13964 });
13965 let expr = make_lowerer(&arena, &vm).lower(id).expect("lower");
13966 format!("{expr}")
13967 }
13968
13969 #[test]
13970 fn subscript_lowers_to_cypher_value_access() {
13971 let s = lower_rel_fn("_subscript", &[0]);
13974 assert!(s.contains("cypher_value_access"), "got {s}");
13975 assert!(s.contains("var_1.rels"), "got {s}");
13976 }
13977
13978 #[test]
13979 fn head_and_last_lower_to_array_element() {
13980 assert!(lower_rel_fn("head", &[]).contains("array_element"));
13981 assert!(lower_rel_fn("last", &[]).contains("array_element"));
13982 }
13983
13984 #[test]
13985 fn slice_lowers_to_array_slice() {
13986 let s = lower_rel_fn("_slice", &[0, 2]);
13988 assert!(s.contains("array_slice"), "got {s}");
13989 assert!(s.contains("var_1.rels"), "got {s}");
13990 }
13991
13992 #[test]
13993 fn slice_with_null_bounds_uses_array_length() {
13994 let mut arena = ExprArena::new();
13997 let mut vm = VarMap::new();
13998 vm.insert(VarId(0), "var_1.rels");
13999 let list = arena.push(IrExpr::VarRef(VarId(0)));
14000 let start = arena.push(IrExpr::Literal(IrLiteral::Int(1)));
14001 let end_null = arena.push(IrExpr::Literal(IrLiteral::Null));
14002 let id = arena.push(IrExpr::FunctionCall {
14003 name: "_slice".into(),
14004 args: vec![list, start, end_null],
14005 });
14006 let expr = make_lowerer(&arena, &vm).lower(id).expect("lower");
14007 let s = format!("{expr}");
14008 assert!(s.contains("array_slice"), "got {s}");
14009 assert!(
14010 s.contains("array_length"),
14011 "an unbounded end must default to array_length: {s}"
14012 );
14013 }
14014
14015 #[test]
14016 fn type_of_element_lowers_to_runtime_graph_metadata_dispatch() {
14017 let mut arena = ExprArena::new();
14018 let mut vm = VarMap::new();
14019 vm.insert(VarId(0), "var_1.rels");
14020 let list = arena.push(IrExpr::VarRef(VarId(0)));
14021 let idx = arena.push(IrExpr::Literal(IrLiteral::Int(0)));
14022 let elem = arena.push(IrExpr::FunctionCall {
14023 name: "_subscript".into(),
14024 args: vec![list, idx],
14025 });
14026 let id = arena.push(IrExpr::FunctionCall {
14027 name: "type".into(),
14028 args: vec![elem],
14029 });
14030 let expr = make_lowerer(&arena, &vm).lower(id).expect("lower");
14031 let s = format!("{expr}");
14032 assert!(
14033 s.contains("cypher_relationship_type"),
14034 "must dispatch graph metadata by runtime value: {s}"
14035 );
14036 assert!(
14037 s.contains("cypher_value_access"),
14038 "over the indexed element: {s}"
14039 );
14040 }
14041
14042 fn invoke_graph_metadata(
14043 kind: GraphMetadataKind,
14044 value: ScalarValue,
14045 ) -> datafusion::error::Result<ScalarValue> {
14046 use datafusion::config::ConfigOptions;
14047
14048 let udf = CypherGraphMetadata::new(kind);
14049 let return_type = udf.return_type(&[])?;
14050 let result = udf.invoke_with_args(ScalarFunctionArgs {
14051 args: vec![ColumnarValue::Scalar(value.clone())],
14052 arg_fields: vec![Arc::new(Field::new("value", value.data_type(), true))],
14053 number_rows: 1,
14054 return_field: Arc::new(Field::new("metadata", return_type, true)),
14055 config_options: Arc::new(ConfigOptions::default()),
14056 })?;
14057 match result {
14058 ColumnarValue::Array(array) => ScalarValue::try_from_array(&array, 0),
14059 ColumnarValue::Scalar(value) => Ok(value),
14060 }
14061 }
14062
14063 #[test]
14064 fn graph_metadata_runtime_dispatch_validates_entity_kind() {
14065 use datafusion::arrow::array::{Int64Array, StringArray, StructArray};
14066 use datafusion::arrow::datatypes::Fields;
14067
14068 let labels = ScalarValue::List(ScalarValue::new_list(
14069 &[ScalarValue::Utf8(Some("Person".into()))],
14070 &DataType::Utf8,
14071 true,
14072 ));
14073 let node = ScalarValue::Struct(Arc::new(StructArray::new(
14074 Fields::from(vec![
14075 Field::new("node_uuid", DataType::Int64, false),
14076 Field::new("labels", labels.data_type(), true),
14077 Field::new("rel_type", DataType::Utf8, true),
14078 ]),
14079 vec![
14080 Arc::new(Int64Array::from(vec![1])),
14081 match &labels {
14082 ScalarValue::List(array) => Arc::clone(array) as _,
14083 _ => unreachable!(),
14084 },
14085 Arc::new(StringArray::from(vec![Some("property, not metadata")])),
14086 ],
14087 None,
14088 )));
14089 let relationship = ScalarValue::Struct(Arc::new(StructArray::new(
14090 Fields::from(vec![
14091 Field::new("edge_uuid", DataType::Int64, false),
14092 Field::new("rel_type", DataType::Utf8, false),
14093 ]),
14094 vec![
14095 Arc::new(Int64Array::from(vec![2])),
14096 Arc::new(StringArray::from(vec!["KNOWS"])),
14097 ],
14098 None,
14099 )));
14100
14101 let actual_labels =
14102 invoke_graph_metadata(GraphMetadataKind::Labels, node.clone()).expect("labels(node)");
14103 assert_eq!(actual_labels, labels);
14104 assert_eq!(
14105 invoke_graph_metadata(GraphMetadataKind::RelationshipType, relationship.clone())
14106 .expect("type(relationship)"),
14107 ScalarValue::Utf8(Some("KNOWS".into()))
14108 );
14109 assert!(invoke_graph_metadata(GraphMetadataKind::RelationshipType, node).is_err());
14110 assert!(invoke_graph_metadata(GraphMetadataKind::Labels, relationship).is_err());
14111 assert!(
14112 invoke_graph_metadata(GraphMetadataKind::Labels, ScalarValue::Null)
14113 .expect("labels(null)")
14114 .is_null()
14115 );
14116
14117 let colliding_map = ScalarValue::Struct(Arc::new(StructArray::new(
14118 Fields::from(vec![Field::new("labels", labels.data_type(), true)]),
14119 vec![match labels {
14120 ScalarValue::List(array) => array as _,
14121 _ => unreachable!(),
14122 }],
14123 None,
14124 )));
14125 assert!(invoke_graph_metadata(GraphMetadataKind::Labels, colliding_map).is_err());
14126 }
14127
14128 #[test]
14129 fn size_lowers_to_cypher_size_udf() {
14130 let mut arena = ExprArena::new();
14131 let mut vm = VarMap::new();
14132 vm.insert(VarId(0), "var_1.rels");
14133 let list = arena.push(IrExpr::VarRef(VarId(0)));
14134 let id = arena.push(IrExpr::FunctionCall {
14135 name: "size".into(),
14136 args: vec![list],
14137 });
14138 let expr = make_lowerer(&arena, &vm).lower(id).expect("lower");
14139 assert!(format!("{expr}").contains("cypher_size"), "got {expr}");
14140 }
14141
14142 #[test]
14143 fn cypher_in_decodes_tagged_list_rhs() {
14144 use datafusion::arrow::array::{Array, BooleanArray};
14145 use datafusion::scalar::ScalarValue as S;
14146 use std::sync::Arc;
14147
14148 let lhs = Arc::new(BooleanArray::from(vec![
14149 Some(true),
14150 Some(true),
14151 Some(true),
14152 Some(true),
14153 ]));
14154 let rhs_values = vec![
14155 S::List(S::new_list(
14156 &[S::Boolean(Some(true))],
14157 &DataType::Boolean,
14158 true,
14159 )),
14160 S::List(S::new_list(
14161 &[S::Boolean(Some(false))],
14162 &DataType::Boolean,
14163 true,
14164 )),
14165 S::List(S::new_list(&[S::Boolean(None)], &DataType::Boolean, true)),
14166 S::List(S::new_list(&[], &DataType::Boolean, true)),
14167 ];
14168 let depth = rhs_values.iter().filter_map(het_depth).max().unwrap();
14169 let rhs = Arc::new(build_het_struct(&rhs_values, depth).expect("tagged RHS"));
14170 let out = invoke_cypher_in(lhs, rhs);
14171 let bools = out.as_any().downcast_ref::<BooleanArray>().unwrap();
14172
14173 assert!(bools.value(0), "true IN [true]");
14174 assert!(!bools.value(1), "true IN [false]");
14175 assert!(bools.is_null(2), "true IN [null] -> null");
14176 assert!(!bools.value(3), "true IN []");
14177 }
14178
14179 fn invoke_cypher_in(
14180 lhs: datafusion::arrow::array::ArrayRef,
14181 rhs: datafusion::arrow::array::ArrayRef,
14182 ) -> datafusion::arrow::array::ArrayRef {
14183 use std::sync::Arc;
14184
14185 use datafusion::arrow::datatypes::Field;
14186 use datafusion::config::ConfigOptions;
14187
14188 let n = lhs.len();
14189 let lhs_field = Arc::new(Field::new("lhs", lhs.data_type().clone(), true));
14190 let rhs_field = Arc::new(Field::new("rhs", rhs.data_type().clone(), true));
14191 let ret = Arc::new(Field::new("in", DataType::Boolean, true));
14192 let args = ScalarFunctionArgs {
14193 args: vec![ColumnarValue::Array(lhs), ColumnarValue::Array(rhs)],
14194 arg_fields: vec![lhs_field, rhs_field],
14195 number_rows: n,
14196 return_field: ret,
14197 config_options: Arc::new(ConfigOptions::default()),
14198 };
14199 match CypherIn::new().invoke_with_args(args).unwrap() {
14200 ColumnarValue::Array(a) => a,
14201 ColumnarValue::Scalar(s) => s.to_array_of_size(n).unwrap(),
14202 }
14203 }
14204
14205 #[test]
14210 fn cypher_size_counts_list_elements() {
14211 use datafusion::arrow::array::{Int64Array, ListArray};
14212 use datafusion::arrow::datatypes::Int32Type;
14213
14214 let arr = ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
14216 Some(vec![Some(10), Some(20), Some(30)]),
14217 Some(vec![]),
14218 ]);
14219 let out = invoke_cypher_size(std::sync::Arc::new(arr));
14220 let counts = out.as_any().downcast_ref::<Int64Array>().unwrap();
14221 assert_eq!(counts.value(0), 3);
14222 assert_eq!(counts.value(1), 0);
14223 }
14224
14225 #[test]
14226 fn cypher_size_counts_string_chars() {
14227 use datafusion::arrow::array::{Array, Int64Array, StringArray};
14228
14229 let arr = StringArray::from(vec![Some("abc"), Some(""), None]);
14230 let out = invoke_cypher_size(std::sync::Arc::new(arr));
14231 let counts = out.as_any().downcast_ref::<Int64Array>().unwrap();
14232 assert_eq!(counts.value(0), 3);
14233 assert_eq!(counts.value(1), 0);
14234 assert!(counts.is_null(2), "null string → null size");
14235 }
14236
14237 #[test]
14238 fn cypher_size_counts_het_tagged_elements() {
14239 use datafusion::arrow::array::{Array, Int64Array};
14240 use datafusion::scalar::ScalarValue as S;
14241
14242 let scalars = vec![
14246 S::List(S::new_list(
14247 &[S::Int64(Some(1)), S::Int64(Some(2)), S::Int64(Some(3))],
14248 &DataType::Int64,
14249 true,
14250 )),
14251 S::Utf8(Some("ab".to_owned())),
14252 S::Boolean(Some(true)),
14253 S::Null,
14254 ];
14255 let depth = scalars.iter().filter_map(het_depth).max().unwrap();
14256 let elems = build_het_struct(&scalars, depth).expect("build tagged struct");
14257 let out = invoke_cypher_size(std::sync::Arc::new(elems));
14258 let counts = out.as_any().downcast_ref::<Int64Array>().unwrap();
14259 assert_eq!(counts.value(0), 3, "tag-4 list element → element count");
14260 assert_eq!(counts.value(1), 2, "tag-2 string element → char count");
14261 assert!(counts.is_null(2), "non-list/string element → null");
14262 assert!(counts.is_null(3), "null element → null");
14263 }
14264
14265 fn invoke_cypher_size(
14268 array: datafusion::arrow::array::ArrayRef,
14269 ) -> datafusion::arrow::array::ArrayRef {
14270 use std::sync::Arc;
14271
14272 use datafusion::arrow::datatypes::Field;
14273 use datafusion::config::ConfigOptions;
14274
14275 let n = array.len();
14276 let field = Arc::new(Field::new("x", array.data_type().clone(), true));
14277 let ret = Arc::new(Field::new("size", DataType::Int64, true));
14278 let args = ScalarFunctionArgs {
14279 args: vec![ColumnarValue::Array(array)],
14280 arg_fields: vec![field],
14281 number_rows: n,
14282 return_field: ret,
14283 config_options: Arc::new(ConfigOptions::default()),
14284 };
14285 match CypherSize::new().invoke_with_args(args).unwrap() {
14286 ColumnarValue::Array(a) => a,
14287 ColumnarValue::Scalar(s) => s.to_array_of_size(n).unwrap(),
14288 }
14289 }
14290
14291 #[test]
14296 fn path_nodes_lowers_to_udf_over_node_uuid() {
14297 let mut arena = ExprArena::new();
14298 let mut vm = VarMap::new();
14299 vm.insert(VarId(0), "var_0"); vm.insert(VarId(1), "var_1.rels"); let start = arena.push(IrExpr::VarRef(VarId(0)));
14302 let rels = arena.push(IrExpr::VarRef(VarId(1)));
14303 let id = arena.push(IrExpr::FunctionCall {
14304 name: "_path_nodes".into(),
14305 args: vec![start, rels],
14306 });
14307 let expr = make_lowerer(&arena, &vm).lower(id).expect("lower");
14308 let s = format!("{expr}");
14309 assert!(s.contains("cypher_path_nodes"), "got {s}");
14310 assert!(
14311 s.contains("var_0.node_uuid"),
14312 "seed is the uuid column: {s}"
14313 );
14314 }
14315
14316 fn uuid16(b: u8) -> Vec<u8> {
14318 vec![b; 16]
14319 }
14320
14321 fn edge_list(rows: &[Option<&[(u8, u8)]>]) -> datafusion::arrow::array::ArrayRef {
14325 use datafusion::arrow::array::{FixedSizeBinaryBuilder, ListBuilder, StructBuilder};
14326 use datafusion::arrow::datatypes::Field;
14327
14328 let fields: datafusion::arrow::datatypes::Fields = vec![
14329 Field::new("src_uuid", DataType::FixedSizeBinary(16), false),
14330 Field::new("dst_uuid", DataType::FixedSizeBinary(16), false),
14331 ]
14332 .into();
14333 let mut b = ListBuilder::new(StructBuilder::new(
14334 fields,
14335 vec![
14336 Box::new(FixedSizeBinaryBuilder::new(16)),
14337 Box::new(FixedSizeBinaryBuilder::new(16)),
14338 ],
14339 ));
14340 for row in rows {
14341 let Some(edges) = row else {
14342 b.append_null();
14343 continue;
14344 };
14345 for (src, dst) in *edges {
14346 b.values()
14347 .field_builder::<FixedSizeBinaryBuilder>(0)
14348 .unwrap()
14349 .append_value(uuid16(*src))
14350 .unwrap();
14351 b.values()
14352 .field_builder::<FixedSizeBinaryBuilder>(1)
14353 .unwrap()
14354 .append_value(uuid16(*dst))
14355 .unwrap();
14356 b.values().append(true);
14357 }
14358 b.append(true);
14359 }
14360 std::sync::Arc::new(b.finish())
14361 }
14362
14363 fn seed_uuids(vals: &[Option<u8>]) -> datafusion::arrow::array::ArrayRef {
14365 use datafusion::arrow::array::FixedSizeBinaryBuilder;
14366 let mut b = FixedSizeBinaryBuilder::new(16);
14367 for v in vals {
14368 match v {
14369 Some(x) => b.append_value(uuid16(*x)).unwrap(),
14370 None => b.append_null(),
14371 }
14372 }
14373 std::sync::Arc::new(b.finish())
14374 }
14375
14376 fn invoke_path_nodes(
14377 seed: datafusion::arrow::array::ArrayRef,
14378 rels: datafusion::arrow::array::ArrayRef,
14379 ) -> datafusion::error::Result<datafusion::arrow::array::ArrayRef> {
14380 use std::sync::Arc;
14381
14382 use datafusion::arrow::datatypes::Field;
14383 use datafusion::config::ConfigOptions;
14384
14385 let udf = CypherPathNodes::new();
14386 let n = seed.len();
14387 let args = ScalarFunctionArgs {
14388 args: vec![
14389 ColumnarValue::Array(Arc::clone(&seed)),
14390 ColumnarValue::Array(Arc::clone(&rels)),
14391 ],
14392 arg_fields: vec![
14393 Arc::new(Field::new("seed", seed.data_type().clone(), true)),
14394 Arc::new(Field::new("rels", rels.data_type().clone(), true)),
14395 ],
14396 number_rows: n,
14397 return_field: Arc::new(Field::new("nodes", udf.return_type(&[])?, true)),
14398 config_options: Arc::new(ConfigOptions::default()),
14399 };
14400 udf.invoke_with_args(args).map(|v| match v {
14401 ColumnarValue::Array(a) => a,
14402 ColumnarValue::Scalar(s) => s.to_array_of_size(n).unwrap(),
14403 })
14404 }
14405
14406 fn path_node_bytes(out: &datafusion::arrow::array::ArrayRef, i: usize) -> Option<Vec<u8>> {
14409 use datafusion::arrow::array::{Array, FixedSizeBinaryArray, ListArray, StructArray};
14410 let list = out.as_any().downcast_ref::<ListArray>().unwrap();
14411 if list.is_null(i) {
14412 return None;
14413 }
14414 let items = list.value(i);
14415 let items = items.as_any().downcast_ref::<StructArray>().unwrap();
14416 let uuids = items.column_by_name("node_uuid").unwrap();
14417 let uuids = uuids
14418 .as_any()
14419 .downcast_ref::<FixedSizeBinaryArray>()
14420 .unwrap();
14421 Some((0..uuids.len()).map(|j| uuids.value(j)[0]).collect())
14422 }
14423
14424 #[test]
14425 fn path_nodes_walks_forward_chain() {
14426 let out = invoke_path_nodes(
14427 seed_uuids(&[Some(1)]),
14428 edge_list(&[Some(&[(1, 2), (2, 3)])]),
14429 )
14430 .unwrap();
14431 assert_eq!(path_node_bytes(&out, 0), Some(vec![1, 2, 3]));
14432 }
14433
14434 #[test]
14435 fn path_nodes_flips_reversed_storage_orientation() {
14436 let out = invoke_path_nodes(seed_uuids(&[Some(1)]), edge_list(&[Some(&[(2, 1)])])).unwrap();
14439 assert_eq!(path_node_bytes(&out, 0), Some(vec![1, 2]));
14440 }
14441
14442 #[test]
14443 fn path_nodes_mixed_orientation_walk() {
14444 let out = invoke_path_nodes(
14446 seed_uuids(&[Some(1)]),
14447 edge_list(&[Some(&[(1, 2), (3, 2)])]),
14448 )
14449 .unwrap();
14450 assert_eq!(path_node_bytes(&out, 0), Some(vec![1, 2, 3]));
14451 }
14452
14453 #[test]
14454 fn path_nodes_self_loop_stays_put() {
14455 let out = invoke_path_nodes(seed_uuids(&[Some(1)]), edge_list(&[Some(&[(1, 1)])])).unwrap();
14456 assert_eq!(path_node_bytes(&out, 0), Some(vec![1, 1]));
14457 }
14458
14459 #[test]
14460 fn path_nodes_zero_hop_is_seed_only() {
14461 let out = invoke_path_nodes(seed_uuids(&[Some(7)]), edge_list(&[Some(&[])])).unwrap();
14462 assert_eq!(path_node_bytes(&out, 0), Some(vec![7]));
14463 }
14464
14465 #[test]
14466 fn path_nodes_null_seed_or_list_is_null() {
14467 let out = invoke_path_nodes(
14469 seed_uuids(&[None, Some(1)]),
14470 edge_list(&[Some(&[(1, 2)]), None]),
14471 )
14472 .unwrap();
14473 assert_eq!(path_node_bytes(&out, 0), None);
14474 assert_eq!(path_node_bytes(&out, 1), None);
14475 }
14476
14477 #[test]
14478 fn path_nodes_disconnected_edge_errors() {
14479 let err = invoke_path_nodes(seed_uuids(&[Some(1)]), edge_list(&[Some(&[(5, 6)])]))
14480 .expect_err("an edge touching neither endpoint is a corrupt emission");
14481 assert!(err.to_string().contains("disconnected"), "got {err}");
14482 }
14483
14484 #[test]
14485 fn path_nodes_output_matches_declared_return_type() {
14486 let out = invoke_path_nodes(seed_uuids(&[Some(1)]), edge_list(&[Some(&[(1, 2)])])).unwrap();
14489 assert_eq!(
14490 out.data_type(),
14491 &CypherPathNodes::new().return_type(&[]).unwrap()
14492 );
14493 }
14494
14495 #[test]
14500 fn parameter_produces_placeholder() {
14501 let mut arena = ExprArena::new();
14502 let id = arena.push(IrExpr::Parameter("eid".into()));
14503 let vm = VarMap::new();
14504 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
14505 if let DfExpr::Placeholder(p) = result {
14506 assert_eq!(p.id, "$eid");
14508 } else {
14509 panic!("expected Placeholder, got {result:?}");
14510 }
14511 }
14512
14513 #[test]
14518 fn list_literal_of_ints_folds_to_scalar_list() {
14519 use datafusion::arrow::array::Array;
14520 let mut arena = ExprArena::new();
14522 let e1 = arena.push(IrExpr::Literal(IrLiteral::Int(1)));
14523 let e2 = arena.push(IrExpr::Literal(IrLiteral::Int(2)));
14524 let e3 = arena.push(IrExpr::Literal(IrLiteral::Int(3)));
14525 let id = arena.push(IrExpr::ListLiteral(vec![e1, e2, e3]));
14526 let vm = VarMap::new();
14527 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
14528 let DfExpr::Literal(ScalarValue::List(arr), _) = result else {
14529 panic!("expected a ScalarValue::List literal, got {result:?}");
14530 };
14531 assert_eq!(arr.len(), 1);
14533 assert_eq!(arr.value(0).len(), 3);
14534 assert_eq!(arr.value(0).data_type(), &DataType::Int64);
14535 }
14536
14537 #[test]
14538 fn empty_list_literal_folds_to_empty_int64_list() {
14539 use datafusion::arrow::array::Array;
14540 let mut arena = ExprArena::new();
14541 let id = arena.push(IrExpr::ListLiteral(vec![]));
14542 let vm = VarMap::new();
14543 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
14544 let DfExpr::Literal(ScalarValue::List(arr), _) = result else {
14545 panic!("expected an empty ScalarValue::List literal, got {result:?}");
14546 };
14547 assert_eq!(arr.len(), 1);
14548 assert_eq!(arr.value(0).len(), 0, "no elements");
14549 }
14550
14551 #[test]
14552 fn list_literal_with_expression_element_uses_make_array() {
14553 let mut arena = ExprArena::new();
14555 let var = arena.push(IrExpr::VarRef(VarId(0)));
14556 let age = arena.push(IrExpr::PropertyAccess {
14557 base: var,
14558 prop: PropId(0),
14559 });
14560 let one = arena.push(IrExpr::Literal(IrLiteral::Int(1)));
14561 let id = arena.push(IrExpr::ListLiteral(vec![age, one]));
14562 let mut vm = VarMap::new();
14563 vm.insert(VarId(0), "var_0");
14564 let result = make_lowerer(&arena, &vm).lower(id).unwrap();
14565 let DfExpr::ScalarFunction(f) = result else {
14566 panic!("expected a make_array ScalarFunction, got {result:?}");
14567 };
14568 assert_eq!(f.name(), "make_array");
14569 assert_eq!(f.args.len(), 2);
14570 }
14571
14572 #[test]
14577 fn scalar_to_ir_literal_round_trips_each_kind() {
14578 for lit in [
14581 IrLiteral::Bool(true),
14582 IrLiteral::Int(42),
14583 IrLiteral::Float(1.5),
14584 IrLiteral::Str("hi".into()),
14585 IrLiteral::Duration {
14586 months: 14,
14587 days: 3,
14588 seconds: 5,
14589 nanos: 1000,
14590 },
14591 IrLiteral::DateTime(1_700_000_000_000_000),
14592 ] {
14593 let scalar = ir_literal_to_scalar(&lit);
14594 assert_eq!(scalar_to_ir_literal(&scalar).unwrap(), lit);
14595 }
14596 }
14597
14598 #[test]
14599 fn scalar_to_ir_literal_rejects_graph_identity_as_a_property_value() {
14600 let scalar = ir_literal_to_scalar(&IrLiteral::Uuid([0x42; 16]));
14601 assert!(matches!(
14602 scalar_to_ir_literal(&scalar),
14603 Err(LoweringError::InvalidType(message))
14604 if message == "UUID values cannot be stored as graph properties"
14605 ));
14606 }
14607
14608 #[test]
14609 fn scalar_to_ir_literal_null_variants_map_to_null() {
14610 assert_eq!(
14611 scalar_to_ir_literal(&ScalarValue::Null).unwrap(),
14612 IrLiteral::Null
14613 );
14614 assert_eq!(
14615 scalar_to_ir_literal(&ScalarValue::Int64(None)).unwrap(),
14616 IrLiteral::Null
14617 );
14618 }
14619
14620 #[test]
14621 fn scalar_to_ir_literal_widens_smaller_ints() {
14622 assert_eq!(
14623 scalar_to_ir_literal(&ScalarValue::Int32(Some(7))).unwrap(),
14624 IrLiteral::Int(7)
14625 );
14626 assert_eq!(
14627 scalar_to_ir_literal(&ScalarValue::UInt8(Some(255))).unwrap(),
14628 IrLiteral::Int(255)
14629 );
14630 }
14631
14632 #[test]
14633 fn scalar_to_ir_literal_round_trips_a_list() {
14634 for lit in [
14637 IrLiteral::List(vec![IrLiteral::Int(1), IrLiteral::Int(2)]),
14638 IrLiteral::List(vec![IrLiteral::Date(5428), IrLiteral::Date(5429)]),
14639 ] {
14640 let scalar = ir_literal_to_scalar(&lit);
14641 assert_eq!(scalar_to_ir_literal(&scalar).unwrap(), lit);
14642 }
14643 }
14644
14645 #[test]
14646 fn zoned_temporal_order_keys_compare_absolute_instants() {
14647 let hour = 3_600_000_000_000_i64;
14648 let early_time = time_scalar(Some((12 * hour + 35 * 60_000_000_000, 5 * 3_600)));
14649 let late_time = time_scalar(Some((10 * hour + 35 * 60_000_000_000, -8 * 3_600)));
14650 assert!(cypher_order_key(&early_time) < cypher_order_key(&late_time));
14651
14652 let earlier_datetime = datetime_scalar(Some((5_000, 12 * hour, 3_600, None)));
14653 let later_datetime = datetime_scalar(Some((5_000, 12 * hour, 0, None)));
14654 assert!(cypher_order_key(&earlier_datetime) < cypher_order_key(&later_datetime));
14655 }
14656
14657 #[test]
14658 fn zoned_temporal_structs_require_cypher_order_keys() {
14659 assert!(needs_cypher_order_key_type(&time_scalar(None).data_type()));
14660 assert!(needs_cypher_order_key_type(
14661 &datetime_scalar(None).data_type()
14662 ));
14663 }
14664
14665 #[test]
14666 fn dynamic_heterogeneous_list_preserves_graph_value_payloads() {
14667 use datafusion::arrow::array::{Int64Array, StructArray};
14668 use datafusion::arrow::datatypes::{Field, Fields};
14669 use datafusion::config::ConfigOptions;
14670
14671 let node_array = StructArray::new(
14672 Fields::from(vec![Field::new("node_uuid", DataType::Int64, false)]),
14673 vec![Arc::new(Int64Array::from(vec![7]))],
14674 None,
14675 );
14676 let node = ScalarValue::Struct(Arc::new(node_array));
14677 let number = ScalarValue::Int64(Some(42));
14678 let arg_types = vec![node.data_type(), number.data_type()];
14679 let args = ScalarFunctionArgs {
14680 args: vec![
14681 ColumnarValue::Scalar(node.clone()),
14682 ColumnarValue::Scalar(number.clone()),
14683 ],
14684 arg_fields: arg_types
14685 .iter()
14686 .enumerate()
14687 .map(|(i, ty)| Arc::new(Field::new(format!("arg_{i}"), ty.clone(), true)))
14688 .collect(),
14689 number_rows: 1,
14690 return_field: Arc::new(Field::new("out", dynamic_het_type(&arg_types), false)),
14691 config_options: Arc::new(ConfigOptions::default()),
14692 };
14693 let out = match CypherDynamicHetList::new()
14694 .invoke_with_args(args)
14695 .expect("dynamic heterogeneous list")
14696 {
14697 ColumnarValue::Array(array) => array,
14698 ColumnarValue::Scalar(value) => value.to_array().expect("scalar list"),
14699 };
14700 let list = out.as_any().downcast_ref::<ListArray>().expect("List");
14701 let values = list.value(0);
14702 let first = ScalarValue::try_from_array(&values, 0).expect("node element");
14703 let second = ScalarValue::try_from_array(&values, 1).expect("number element");
14704 assert_eq!(decode_het(&first), Some(node));
14705 assert_eq!(decode_het(&second), Some(number));
14706 assert!(cypher_order_key(&first).starts_with("20:node"));
14707 assert!(cypher_order_key(&second).starts_with("80:num"));
14708 }
14709}