1#![forbid(unsafe_code)]
19#![allow(clippy::unnecessary_literal_bound)]
23
24use std::cmp::Ordering;
25use std::collections::HashMap;
26use std::fmt;
27use std::hash::{Hash, Hasher};
28use std::path::PathBuf;
29use std::sync::Arc;
30
31use datafusion::arrow::datatypes::{DataType, Field, Fields, Schema};
32use datafusion::common::{DFSchema, DFSchemaRef, Result as DfResult, TableReference};
33use datafusion::logical_expr::{Expr, ExprSchemable, LogicalPlan, UserDefinedLogicalNodeCore};
34
35use graphforge_core::OntologyMode;
36use graphforge_ir::{Direction, IrLiteral};
37
38macro_rules! impl_partial_ord {
44 ($t:ty) => {
45 impl PartialOrd for $t {
46 fn partial_cmp(&self, _other: &Self) -> Option<Ordering> {
47 None
48 }
49 }
50 };
51}
52
53fn passthrough_schema(inputs: &[&LogicalPlan]) -> DFSchemaRef {
60 inputs
61 .first()
62 .map_or_else(|| Arc::new(DFSchema::empty()), |p| p.schema().clone())
63}
64
65pub const VAR_LEN_EDGE_LIST_FIELD: &str = "rels";
72
73#[must_use]
92pub fn var_len_edge_list_field(prop_fields: &[Field]) -> Arc<Field> {
93 let mut fields = vec![
94 Field::new("edge_uuid", DataType::FixedSizeBinary(16), false),
95 Field::new("src_uuid", DataType::FixedSizeBinary(16), false),
96 Field::new("dst_uuid", DataType::FixedSizeBinary(16), false),
97 Field::new("rel_type", DataType::Utf8, true),
98 ];
99 fields.extend(prop_fields.iter().cloned());
100 let struct_fields = Fields::from(fields);
101 let item = Arc::new(Field::new("item", DataType::Struct(struct_fields), true));
107 Arc::new(Field::new(
108 VAR_LEN_EDGE_LIST_FIELD,
109 DataType::List(item),
110 true,
111 ))
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
141pub struct VarLenExpandNode {
142 pub input: Arc<LogicalPlan>,
144 pub rel_type_name: String,
146 pub min_hops: u16,
148 pub max_hops: Option<u16>,
150 pub src_var: u32,
152 pub dst_var: u32,
154 pub edge_var: u32,
156 pub direction: Direction,
158 pub rel_ty: Option<u32>,
160 pub dir: PathBuf,
162 pub mode: OntologyMode,
164 schema: DFSchemaRef,
165}
166
167impl VarLenExpandNode {
168 #[allow(clippy::too_many_arguments)]
176 #[must_use]
177 pub fn new(
178 input: Arc<LogicalPlan>,
179 rel_type_name: impl Into<String>,
180 min_hops: u16,
181 max_hops: Option<u16>,
182 src_var: u32,
183 dst_var: u32,
184 edge_var: u32,
185 direction: Direction,
186 rel_ty: Option<u32>,
187 dir: PathBuf,
188 mode: OntologyMode,
189 dst_fields: Vec<Arc<Field>>,
190 edge_field: Arc<Field>,
191 ) -> Self {
192 let schema = Self::build_schema(&input, dst_var, edge_var, dst_fields, edge_field);
193 Self {
194 input,
195 rel_type_name: rel_type_name.into(),
196 min_hops,
197 max_hops,
198 src_var,
199 dst_var,
200 edge_var,
201 direction,
202 rel_ty,
203 dir,
204 mode,
205 schema,
206 }
207 }
208
209 fn build_schema(
213 input: &Arc<LogicalPlan>,
214 dst_var: u32,
215 edge_var: u32,
216 dst_fields: Vec<Arc<Field>>,
217 edge_field: Arc<Field>,
218 ) -> DFSchemaRef {
219 let dst_qualifier = TableReference::bare(format!("var_{dst_var}"));
220 let edge_qualifier = TableReference::bare(format!("var_{edge_var}"));
221 let mut qualified: Vec<(Option<TableReference>, Arc<Field>)> = input
222 .schema()
223 .iter()
224 .map(|(q, f)| (q.cloned(), Arc::clone(f)))
225 .collect();
226 qualified.extend(
227 dst_fields
228 .into_iter()
229 .map(|f| (Some(dst_qualifier.clone()), f)),
230 );
231 qualified.push((Some(edge_qualifier), edge_field));
234 Arc::new(
240 DFSchema::new_with_metadata(qualified, std::collections::HashMap::new())
241 .expect("VarLenExpandNode schema must include qualified destination + edge fields"),
242 )
243 }
244}
245
246impl_partial_ord!(VarLenExpandNode);
247
248impl UserDefinedLogicalNodeCore for VarLenExpandNode {
249 fn name(&self) -> &str {
250 "VarLenExpand"
251 }
252
253 fn inputs(&self) -> Vec<&LogicalPlan> {
254 vec![&self.input]
255 }
256
257 fn schema(&self) -> &DFSchemaRef {
258 &self.schema
259 }
260
261 fn expressions(&self) -> Vec<Expr> {
262 vec![]
263 }
264
265 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
266 let max = self.max_hops.map_or("*".to_owned(), |h| h.to_string());
267 let arrow = match self.direction {
268 Direction::Out => "->",
269 Direction::In => "<-",
270 Direction::Undirected => "--",
271 };
272 write!(
273 f,
274 "VarLenExpand: rel={}, hops={}..{}, dir={arrow}, edge=var_{}",
275 self.rel_type_name, self.min_hops, max, self.edge_var
276 )
277 }
278
279 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
280 let input = Arc::new(
281 inputs
282 .into_iter()
283 .next()
284 .unwrap_or_else(|| (*self.input).clone()),
285 );
286 let dst_qual = format!("var_{}", self.dst_var);
289 let dst_fields: Vec<Arc<Field>> = self
290 .schema
291 .iter()
292 .filter(|(q, _)| q.map(TableReference::table) == Some(dst_qual.as_str()))
293 .map(|(_, f)| Arc::clone(f))
294 .collect();
295 let edge_qual = format!("var_{}", self.edge_var);
297 let edge_field = self
298 .schema
299 .iter()
300 .find(|(q, _)| q.map(TableReference::table) == Some(edge_qual.as_str()))
301 .map_or_else(|| var_len_edge_list_field(&[]), |(_, f)| Arc::clone(f));
302 Ok(Self::new(
303 input,
304 self.rel_type_name.clone(),
305 self.min_hops,
306 self.max_hops,
307 self.src_var,
308 self.dst_var,
309 self.edge_var,
310 self.direction,
311 self.rel_ty,
312 self.dir.clone(),
313 self.mode,
314 dst_fields,
315 edge_field,
316 ))
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Hash)]
346pub struct ExpandNode {
347 pub input: Arc<LogicalPlan>,
349 pub rel_type_name: String,
351 pub src_var: u32,
353 pub dst_var: u32,
355 pub edge_var: u32,
357 pub direction: Direction,
359 pub rel_ty: Option<u32>,
361 pub dir: PathBuf,
363 pub mode: OntologyMode,
365 pub edge_prop_count: usize,
368 schema: DFSchemaRef,
369}
370
371impl ExpandNode {
372 #[allow(clippy::too_many_arguments)]
380 #[must_use]
381 pub fn new(
382 input: Arc<LogicalPlan>,
383 rel_type_name: impl Into<String>,
384 src_var: u32,
385 dst_var: u32,
386 edge_var: u32,
387 direction: Direction,
388 rel_ty: Option<u32>,
389 dir: PathBuf,
390 mode: OntologyMode,
391 edge_fields: Vec<Arc<Field>>,
392 edge_prop_fields: Vec<Arc<Field>>,
393 dst_fields: Vec<Arc<Field>>,
394 ) -> Self {
395 let edge_prop_count = edge_prop_fields.len();
396 let schema = Self::build_schema(
397 &input,
398 dst_var,
399 edge_var,
400 edge_fields,
401 edge_prop_fields,
402 dst_fields,
403 );
404 Self {
405 input,
406 rel_type_name: rel_type_name.into(),
407 src_var,
408 dst_var,
409 edge_var,
410 direction,
411 rel_ty,
412 dir,
413 mode,
414 edge_prop_count,
415 schema,
416 }
417 }
418
419 fn build_schema(
423 input: &Arc<LogicalPlan>,
424 dst_var: u32,
425 edge_var: u32,
426 edge_fields: Vec<Arc<Field>>,
427 edge_prop_fields: Vec<Arc<Field>>,
428 dst_fields: Vec<Arc<Field>>,
429 ) -> DFSchemaRef {
430 let edge_qualifier = TableReference::bare(format!("var_{edge_var}"));
431 let dst_qualifier = TableReference::bare(format!("var_{dst_var}"));
432 let mut qualified: Vec<(Option<TableReference>, Arc<Field>)> = input
433 .schema()
434 .iter()
435 .map(|(q, f)| (q.cloned(), Arc::clone(f)))
436 .collect();
437 qualified.extend(
438 edge_fields
439 .into_iter()
440 .map(|f| (Some(edge_qualifier.clone()), f)),
441 );
442 qualified.extend(edge_prop_fields.into_iter().map(|f| {
446 let f = Arc::new(f.as_ref().clone().with_nullable(true));
447 (Some(edge_qualifier.clone()), f)
448 }));
449 qualified.extend(
450 dst_fields
451 .into_iter()
452 .map(|f| (Some(dst_qualifier.clone()), f)),
453 );
454 Arc::new(
455 DFSchema::new_with_metadata(qualified, std::collections::HashMap::new())
456 .expect("ExpandNode schema must include qualified edge + destination fields"),
457 )
458 }
459}
460
461impl_partial_ord!(ExpandNode);
462
463impl UserDefinedLogicalNodeCore for ExpandNode {
464 fn name(&self) -> &str {
465 "Expand"
466 }
467
468 fn inputs(&self) -> Vec<&LogicalPlan> {
469 vec![&self.input]
470 }
471
472 fn schema(&self) -> &DFSchemaRef {
473 &self.schema
474 }
475
476 fn expressions(&self) -> Vec<Expr> {
477 vec![]
478 }
479
480 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
481 let arrow = match self.direction {
482 Direction::Out => "->",
483 Direction::In => "<-",
484 Direction::Undirected => "--",
485 };
486 write!(
487 f,
488 "Expand: rel={}, dir={arrow}, edge=var_{}",
489 self.rel_type_name, self.edge_var
490 )
491 }
492
493 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
494 let input = Arc::new(
495 inputs
496 .into_iter()
497 .next()
498 .unwrap_or_else(|| (*self.input).clone()),
499 );
500 let edge_qual = format!("var_{}", self.edge_var);
503 let all_edge: Vec<Arc<Field>> = self
504 .schema
505 .iter()
506 .filter(|(q, _)| q.map(TableReference::table) == Some(edge_qual.as_str()))
507 .map(|(_, f)| Arc::clone(f))
508 .collect();
509 let topo_count = all_edge.len().saturating_sub(self.edge_prop_count);
510 let edge_fields = all_edge[..topo_count].to_vec();
511 let edge_prop_fields = all_edge[topo_count..].to_vec();
512 let dst_qual = format!("var_{}", self.dst_var);
513 let dst_fields: Vec<Arc<Field>> = self
514 .schema
515 .iter()
516 .filter(|(q, _)| q.map(TableReference::table) == Some(dst_qual.as_str()))
517 .map(|(_, f)| Arc::clone(f))
518 .collect();
519 Ok(Self::new(
520 input,
521 self.rel_type_name.clone(),
522 self.src_var,
523 self.dst_var,
524 self.edge_var,
525 self.direction,
526 self.rel_ty,
527 self.dir.clone(),
528 self.mode,
529 edge_fields,
530 edge_prop_fields,
531 dst_fields,
532 ))
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Hash)]
555pub struct OptionalMatchNode {
556 pub outer: Arc<LogicalPlan>,
558 pub optional: Arc<LogicalPlan>,
560 pub join_keys: Vec<(usize, usize)>,
563 pub inner_keep_idx: Vec<usize>,
569 schema: DFSchemaRef,
570}
571
572impl OptionalMatchNode {
573 #[must_use]
583 pub fn new(
584 outer: Arc<LogicalPlan>,
585 optional: Arc<LogicalPlan>,
586 join_keys: Vec<(usize, usize)>,
587 inner_keep_idx: Vec<usize>,
588 ) -> Self {
589 let schema = Self::build_schema(&outer, &optional, &inner_keep_idx);
590 Self {
591 outer,
592 optional,
593 join_keys,
594 inner_keep_idx,
595 schema,
596 }
597 }
598
599 fn build_schema(
602 outer: &Arc<LogicalPlan>,
603 optional: &Arc<LogicalPlan>,
604 inner_keep_idx: &[usize],
605 ) -> DFSchemaRef {
606 let mut qualified: Vec<(Option<TableReference>, Arc<Field>)> = outer
607 .schema()
608 .iter()
609 .map(|(q, f)| (q.cloned(), Arc::clone(f)))
610 .collect();
611 let inner: Vec<(Option<TableReference>, Arc<Field>)> = optional
612 .schema()
613 .iter()
614 .map(|(q, f)| (q.cloned(), Arc::clone(f)))
615 .collect();
616 qualified.extend(inner_keep_idx.iter().map(|&i| {
617 let (q, f) = &inner[i];
618 let nullable = Arc::new(f.as_ref().clone().with_nullable(true));
621 (q.clone(), nullable)
622 }));
623 Arc::new(
624 DFSchema::new_with_metadata(qualified, std::collections::HashMap::new())
625 .expect("OptionalMatchNode schema must be constructible from outer + inner fields"),
626 )
627 }
628}
629
630impl_partial_ord!(OptionalMatchNode);
631
632impl UserDefinedLogicalNodeCore for OptionalMatchNode {
633 fn name(&self) -> &str {
634 "OptionalMatch"
635 }
636
637 fn inputs(&self) -> Vec<&LogicalPlan> {
638 vec![&self.outer, &self.optional]
639 }
640
641 fn schema(&self) -> &DFSchemaRef {
642 &self.schema
643 }
644
645 fn expressions(&self) -> Vec<Expr> {
646 vec![]
647 }
648
649 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
650 write!(f, "OptionalMatch: keys={}", self.join_keys.len())
651 }
652
653 fn with_exprs_and_inputs(
654 &self,
655 _exprs: Vec<Expr>,
656 mut inputs: Vec<LogicalPlan>,
657 ) -> DfResult<Self> {
658 let optional = Arc::new(inputs.pop().unwrap_or_else(|| (*self.optional).clone()));
661 let outer = Arc::new(inputs.pop().unwrap_or_else(|| (*self.outer).clone()));
662 Ok(Self::new(
663 outer,
664 optional,
665 self.join_keys.clone(),
666 self.inner_keep_idx.clone(),
667 ))
668 }
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Hash)]
678pub struct PathUniqueNode {
679 pub input: Arc<LogicalPlan>,
681 schema: DFSchemaRef,
682}
683
684impl PathUniqueNode {
685 #[must_use]
687 pub fn new(input: Arc<LogicalPlan>) -> Self {
688 let schema = passthrough_schema(&[&input]);
689 Self { input, schema }
690 }
691}
692
693impl_partial_ord!(PathUniqueNode);
694
695impl UserDefinedLogicalNodeCore for PathUniqueNode {
696 fn name(&self) -> &str {
697 "PathUnique"
698 }
699
700 fn inputs(&self) -> Vec<&LogicalPlan> {
701 vec![&self.input]
702 }
703
704 fn schema(&self) -> &DFSchemaRef {
705 &self.schema
706 }
707
708 fn expressions(&self) -> Vec<Expr> {
709 vec![]
710 }
711
712 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
713 write!(f, "PathUnique")
714 }
715
716 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
717 let input = Arc::new(
718 inputs
719 .into_iter()
720 .next()
721 .unwrap_or_else(|| (*self.input).clone()),
722 );
723 Ok(Self::new(input))
724 }
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Hash)]
734pub struct OntologyInferNode {
735 pub input: Arc<LogicalPlan>,
737 pub rel_type_name: String,
739 pub rule_id: String,
741 pub confidence_model: String,
743 schema: DFSchemaRef,
744}
745
746impl OntologyInferNode {
747 #[must_use]
749 pub fn new(
750 input: Arc<LogicalPlan>,
751 rel_type_name: impl Into<String>,
752 rule_id: impl Into<String>,
753 confidence_model: impl Into<String>,
754 ) -> Self {
755 let schema = passthrough_schema(&[&input]);
756 Self {
757 input,
758 rel_type_name: rel_type_name.into(),
759 rule_id: rule_id.into(),
760 confidence_model: confidence_model.into(),
761 schema,
762 }
763 }
764}
765
766impl_partial_ord!(OntologyInferNode);
767
768impl UserDefinedLogicalNodeCore for OntologyInferNode {
769 fn name(&self) -> &str {
770 "OntologyInfer"
771 }
772
773 fn inputs(&self) -> Vec<&LogicalPlan> {
774 vec![&self.input]
775 }
776
777 fn schema(&self) -> &DFSchemaRef {
778 &self.schema
779 }
780
781 fn expressions(&self) -> Vec<Expr> {
782 vec![]
783 }
784
785 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
786 write!(
787 f,
788 "OntologyInfer: rel={} rule_id={}",
789 self.rel_type_name, self.rule_id
790 )
791 }
792
793 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
794 let input = Arc::new(
795 inputs
796 .into_iter()
797 .next()
798 .unwrap_or_else(|| (*self.input).clone()),
799 );
800 Ok(Self::new(
801 input,
802 self.rel_type_name.clone(),
803 self.rule_id.clone(),
804 self.confidence_model.clone(),
805 ))
806 }
807}
808
809#[derive(Debug, Clone, PartialEq, Eq, Hash)]
815pub struct GraphMergeNode {
816 schema: DFSchemaRef,
817}
818
819impl GraphMergeNode {
820 #[must_use]
822 pub fn new() -> Self {
823 Self {
824 schema: Arc::new(DFSchema::empty()),
825 }
826 }
827}
828
829impl_partial_ord!(GraphMergeNode);
830
831impl Default for GraphMergeNode {
832 fn default() -> Self {
833 Self::new()
834 }
835}
836
837impl UserDefinedLogicalNodeCore for GraphMergeNode {
838 fn name(&self) -> &str {
839 "GraphMerge"
840 }
841
842 fn inputs(&self) -> Vec<&LogicalPlan> {
843 vec![]
844 }
845
846 fn schema(&self) -> &DFSchemaRef {
847 &self.schema
848 }
849
850 fn expressions(&self) -> Vec<Expr> {
851 vec![]
852 }
853
854 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
855 write!(f, "GraphMerge")
856 }
857
858 fn with_exprs_and_inputs(
859 &self,
860 _exprs: Vec<Expr>,
861 _inputs: Vec<LogicalPlan>,
862 ) -> DfResult<Self> {
863 Ok(Self::new())
864 }
865}
866
867#[derive(Debug, Clone, PartialEq)]
878pub struct ResolvedNodeSpec {
879 pub var: u32,
881 pub label_ids: Vec<u32>,
883 pub label_names: Vec<String>,
885 pub properties: Vec<(String, IrLiteral)>,
887 pub computed_properties: Vec<(String, Expr)>,
893 pub is_reference: bool,
897}
898
899#[derive(Debug, Clone, PartialEq)]
901pub struct ResolvedEdgeSpec {
902 pub var: u32,
904 pub src: u32,
906 pub dst: u32,
908 pub rel_type_id: Option<u32>,
910 pub rel_type_name: Option<String>,
912 pub direction: Direction,
914 pub properties: Vec<(String, IrLiteral)>,
922 pub computed_properties: Vec<(String, Expr)>,
925}
926
927#[derive(Debug, Clone)]
946pub struct GraphCreateNode {
947 pub input: Arc<LogicalPlan>,
950 pub nodes: Vec<ResolvedNodeSpec>,
952 pub edges: Vec<ResolvedEdgeSpec>,
954 pub dir: PathBuf,
956 pub mode: OntologyMode,
958 schema: DFSchemaRef,
961 emit_rows: bool,
964}
965
966impl GraphCreateNode {
967 #[must_use]
970 pub fn summary_schema() -> Arc<Schema> {
971 Arc::new(Schema::new(vec![
972 Field::new("nodes_created", DataType::UInt64, false),
973 Field::new("edges_created", DataType::UInt64, false),
974 Field::new("properties_set", DataType::UInt64, false),
975 Field::new("labels_added", DataType::UInt64, false),
976 ]))
977 }
978
979 #[must_use]
982 pub fn new(
983 input: Arc<LogicalPlan>,
984 nodes: Vec<ResolvedNodeSpec>,
985 edges: Vec<ResolvedEdgeSpec>,
986 dir: PathBuf,
987 mode: OntologyMode,
988 ) -> Self {
989 let schema = Arc::new(
990 DFSchema::try_from(Self::summary_schema())
991 .expect("write-summary schema is always valid"),
992 );
993 Self {
994 input,
995 nodes,
996 edges,
997 dir,
998 mode,
999 schema,
1000 emit_rows: false,
1001 }
1002 }
1003
1004 #[must_use]
1011 pub fn new_emitting(
1012 input: Arc<LogicalPlan>,
1013 nodes: Vec<ResolvedNodeSpec>,
1014 edges: Vec<ResolvedEdgeSpec>,
1015 dir: PathBuf,
1016 mode: OntologyMode,
1017 output_schema: DFSchemaRef,
1018 ) -> Self {
1019 Self {
1020 input,
1021 nodes,
1022 edges,
1023 dir,
1024 mode,
1025 schema: output_schema,
1026 emit_rows: true,
1027 }
1028 }
1029
1030 #[must_use]
1033 pub fn emits_rows(&self) -> bool {
1034 self.emit_rows
1035 }
1036}
1037
1038impl_partial_ord!(GraphCreateNode);
1039
1040impl PartialEq for GraphCreateNode {
1047 fn eq(&self, other: &Self) -> bool {
1048 self.input == other.input
1049 && self.nodes == other.nodes
1050 && self.edges == other.edges
1051 && self.dir == other.dir
1052 && self.mode == other.mode
1053 && self.emit_rows == other.emit_rows
1054 }
1055}
1056
1057impl Eq for GraphCreateNode {}
1058
1059fn hash_literal<H: Hasher>(lit: &IrLiteral, state: &mut H) {
1060 match lit {
1061 IrLiteral::Null => 0u8.hash(state),
1062 IrLiteral::Bool(b) => {
1063 1u8.hash(state);
1064 b.hash(state);
1065 }
1066 IrLiteral::Int(i) => {
1067 2u8.hash(state);
1068 i.hash(state);
1069 }
1070 IrLiteral::Float(f) => {
1071 3u8.hash(state);
1072 f.to_bits().hash(state);
1073 }
1074 IrLiteral::Str(s) => {
1075 4u8.hash(state);
1076 s.hash(state);
1077 }
1078 IrLiteral::Uuid(uuid) => {
1079 14u8.hash(state);
1080 uuid.hash(state);
1081 }
1082 IrLiteral::Duration {
1083 months,
1084 days,
1085 seconds,
1086 nanos,
1087 } => {
1088 5u8.hash(state);
1089 months.hash(state);
1090 days.hash(state);
1091 seconds.hash(state);
1092 nanos.hash(state);
1093 }
1094 IrLiteral::DateTime(t) => {
1095 6u8.hash(state);
1096 t.hash(state);
1097 }
1098 IrLiteral::Date(d) => {
1099 7u8.hash(state);
1100 d.hash(state);
1101 }
1102 IrLiteral::LocalDateTime { days, nanos } => {
1103 8u8.hash(state);
1104 days.hash(state);
1105 nanos.hash(state);
1106 }
1107 IrLiteral::Time(n) => {
1108 9u8.hash(state);
1109 n.hash(state);
1110 }
1111 IrLiteral::ZonedTime { nanos, offset } => {
1112 10u8.hash(state);
1113 nanos.hash(state);
1114 offset.hash(state);
1115 }
1116 IrLiteral::ZonedDateTime {
1117 days,
1118 nanos,
1119 offset,
1120 zone,
1121 } => {
1122 11u8.hash(state);
1123 days.hash(state);
1124 nanos.hash(state);
1125 offset.hash(state);
1126 zone.hash(state);
1127 }
1128 IrLiteral::List(items) => {
1129 12u8.hash(state);
1130 items.len().hash(state);
1131 for it in items {
1132 hash_literal(it, state);
1133 }
1134 }
1135 IrLiteral::Map(entries) => {
1136 13u8.hash(state);
1137 entries.len().hash(state);
1138 for (key, value) in entries {
1139 key.hash(state);
1140 hash_literal(value, state);
1141 }
1142 }
1143 }
1144}
1145
1146fn hash_props<H: Hasher>(props: &[(String, IrLiteral)], state: &mut H) {
1147 props.len().hash(state);
1148 for (k, v) in props {
1149 k.hash(state);
1150 hash_literal(v, state);
1151 }
1152}
1153
1154fn hash_computed<H: Hasher>(props: &[(String, Expr)], state: &mut H) {
1157 props.len().hash(state);
1158 for (k, _) in props {
1159 k.hash(state);
1160 }
1161}
1162
1163impl Hash for GraphCreateNode {
1164 fn hash<H: Hasher>(&self, state: &mut H) {
1165 self.input.hash(state);
1166 self.nodes.len().hash(state);
1167 for n in &self.nodes {
1168 n.var.hash(state);
1169 n.label_ids.hash(state);
1170 n.label_names.hash(state);
1171 n.is_reference.hash(state);
1172 hash_props(&n.properties, state);
1173 hash_computed(&n.computed_properties, state);
1174 }
1175 self.edges.len().hash(state);
1176 for e in &self.edges {
1177 e.var.hash(state);
1178 e.src.hash(state);
1179 e.dst.hash(state);
1180 e.rel_type_id.hash(state);
1181 e.rel_type_name.hash(state);
1182 e.direction.hash(state);
1183 hash_props(&e.properties, state);
1184 hash_computed(&e.computed_properties, state);
1185 }
1186 self.dir.hash(state);
1187 self.mode.hash(state);
1188 self.emit_rows.hash(state);
1189 }
1190}
1191
1192impl UserDefinedLogicalNodeCore for GraphCreateNode {
1193 fn name(&self) -> &str {
1194 "GraphCreate"
1195 }
1196
1197 fn inputs(&self) -> Vec<&LogicalPlan> {
1198 vec![&self.input]
1199 }
1200
1201 fn schema(&self) -> &DFSchemaRef {
1202 &self.schema
1203 }
1204
1205 fn expressions(&self) -> Vec<Expr> {
1206 self.nodes
1210 .iter()
1211 .flat_map(|n| n.computed_properties.iter())
1212 .chain(self.edges.iter().flat_map(|e| e.computed_properties.iter()))
1213 .map(|(_, e)| e.clone())
1214 .collect()
1215 }
1216
1217 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
1218 write!(
1219 f,
1220 "GraphCreate: nodes={}, edges={}",
1221 self.nodes.len(),
1222 self.edges.len()
1223 )
1224 }
1225
1226 fn with_exprs_and_inputs(&self, exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
1227 let input = Arc::new(
1228 inputs
1229 .into_iter()
1230 .next()
1231 .unwrap_or_else(|| (*self.input).clone()),
1232 );
1233 let mut nodes = self.nodes.clone();
1237 let mut edges = self.edges.clone();
1238 let total: usize = nodes
1239 .iter()
1240 .map(|n| n.computed_properties.len())
1241 .sum::<usize>()
1242 + edges
1243 .iter()
1244 .map(|e| e.computed_properties.len())
1245 .sum::<usize>();
1246 if exprs.len() == total {
1247 let mut it = exprs.into_iter();
1248 for n in &mut nodes {
1249 for (_, e) in &mut n.computed_properties {
1250 *e = it.next().expect("count checked above");
1251 }
1252 }
1253 for e in &mut edges {
1254 for (_, x) in &mut e.computed_properties {
1255 *x = it.next().expect("count checked above");
1256 }
1257 }
1258 }
1259 if self.emit_rows {
1262 Ok(Self::new_emitting(
1263 input,
1264 nodes,
1265 edges,
1266 self.dir.clone(),
1267 self.mode,
1268 self.schema.clone(),
1269 ))
1270 } else {
1271 Ok(Self::new(input, nodes, edges, self.dir.clone(), self.mode))
1272 }
1273 }
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1287pub struct DeleteTarget {
1288 pub var: u32,
1290 pub is_edge: bool,
1293}
1294
1295#[derive(Debug, Clone)]
1303pub struct GraphDeleteNode {
1304 pub input: Arc<LogicalPlan>,
1306 pub targets: Vec<DeleteTarget>,
1308 pub detach: bool,
1310 pub dir: PathBuf,
1312 pub mode: OntologyMode,
1314 schema: DFSchemaRef,
1315}
1316
1317impl GraphDeleteNode {
1318 #[must_use]
1320 pub fn summary_schema() -> Arc<Schema> {
1321 Arc::new(Schema::new(vec![
1322 Field::new("nodes_deleted", DataType::UInt64, false),
1323 Field::new("edges_deleted", DataType::UInt64, false),
1324 ]))
1325 }
1326
1327 #[must_use]
1329 pub fn new(
1330 input: Arc<LogicalPlan>,
1331 targets: Vec<DeleteTarget>,
1332 detach: bool,
1333 dir: PathBuf,
1334 mode: OntologyMode,
1335 ) -> Self {
1336 let schema = Arc::new(
1337 DFSchema::try_from(Self::summary_schema())
1338 .expect("write-summary schema is always valid"),
1339 );
1340 Self {
1341 input,
1342 targets,
1343 detach,
1344 dir,
1345 mode,
1346 schema,
1347 }
1348 }
1349}
1350
1351impl_partial_ord!(GraphDeleteNode);
1352
1353impl PartialEq for GraphDeleteNode {
1356 fn eq(&self, other: &Self) -> bool {
1357 self.input == other.input
1358 && self.targets == other.targets
1359 && self.detach == other.detach
1360 && self.dir == other.dir
1361 && self.mode == other.mode
1362 }
1363}
1364
1365impl Eq for GraphDeleteNode {}
1366
1367impl Hash for GraphDeleteNode {
1368 fn hash<H: Hasher>(&self, state: &mut H) {
1369 self.input.hash(state);
1370 self.targets.hash(state);
1371 self.detach.hash(state);
1372 self.dir.hash(state);
1373 self.mode.hash(state);
1374 }
1375}
1376
1377impl UserDefinedLogicalNodeCore for GraphDeleteNode {
1378 fn name(&self) -> &str {
1379 "GraphDelete"
1380 }
1381
1382 fn inputs(&self) -> Vec<&LogicalPlan> {
1383 vec![&self.input]
1384 }
1385
1386 fn schema(&self) -> &DFSchemaRef {
1387 &self.schema
1388 }
1389
1390 fn expressions(&self) -> Vec<Expr> {
1391 vec![]
1392 }
1393
1394 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
1395 write!(
1396 f,
1397 "GraphDelete: targets={}, detach={}",
1398 self.targets.len(),
1399 self.detach
1400 )
1401 }
1402
1403 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
1404 let input = Arc::new(
1405 inputs
1406 .into_iter()
1407 .next()
1408 .unwrap_or_else(|| (*self.input).clone()),
1409 );
1410 Ok(Self::new(
1411 input,
1412 self.targets.clone(),
1413 self.detach,
1414 self.dir.clone(),
1415 self.mode,
1416 ))
1417 }
1418}
1419
1420#[derive(Debug, Clone, PartialEq)]
1437pub struct SetTarget {
1438 pub var: u32,
1440 pub is_edge: bool,
1443 pub prop_name: String,
1445 pub value: Expr,
1447}
1448
1449#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1452pub struct RemoveTarget {
1453 pub var: u32,
1455 pub is_edge: bool,
1457 pub prop_name: String,
1459}
1460
1461#[derive(Debug, Clone)]
1472pub struct GraphSetNode {
1473 pub input: Arc<LogicalPlan>,
1475 pub targets: Vec<SetTarget>,
1477 pub type_id_to_entity_name: HashMap<u32, String>,
1480 pub dir: PathBuf,
1482 pub mode: OntologyMode,
1484 schema: DFSchemaRef,
1485}
1486
1487impl GraphSetNode {
1488 #[must_use]
1490 pub fn summary_schema() -> Arc<Schema> {
1491 Arc::new(Schema::new(vec![Field::new(
1492 "properties_set",
1493 DataType::UInt64,
1494 false,
1495 )]))
1496 }
1497
1498 #[must_use]
1500 pub fn new(
1501 input: Arc<LogicalPlan>,
1502 targets: Vec<SetTarget>,
1503 type_id_to_entity_name: HashMap<u32, String>,
1504 dir: PathBuf,
1505 mode: OntologyMode,
1506 ) -> Self {
1507 let schema = Arc::new(
1508 DFSchema::try_from(Self::summary_schema())
1509 .expect("write-summary schema is always valid"),
1510 );
1511 Self {
1512 input,
1513 targets,
1514 type_id_to_entity_name,
1515 dir,
1516 mode,
1517 schema,
1518 }
1519 }
1520}
1521
1522impl_partial_ord!(GraphSetNode);
1523
1524impl PartialEq for GraphSetNode {
1527 fn eq(&self, other: &Self) -> bool {
1528 self.input == other.input
1529 && self.targets == other.targets
1530 && self.dir == other.dir
1531 && self.mode == other.mode
1532 && self.type_id_to_entity_name == other.type_id_to_entity_name
1533 }
1534}
1535
1536impl Eq for GraphSetNode {}
1537
1538impl Hash for GraphSetNode {
1539 fn hash<H: Hasher>(&self, state: &mut H) {
1540 self.input.hash(state);
1541 for t in &self.targets {
1546 t.var.hash(state);
1547 t.is_edge.hash(state);
1548 t.prop_name.hash(state);
1549 }
1550 self.dir.hash(state);
1551 self.mode.hash(state);
1552 }
1553}
1554
1555impl UserDefinedLogicalNodeCore for GraphSetNode {
1556 fn name(&self) -> &str {
1557 "GraphSet"
1558 }
1559
1560 fn inputs(&self) -> Vec<&LogicalPlan> {
1561 vec![&self.input]
1562 }
1563
1564 fn schema(&self) -> &DFSchemaRef {
1565 &self.schema
1566 }
1567
1568 fn expressions(&self) -> Vec<Expr> {
1569 self.targets.iter().map(|t| t.value.clone()).collect()
1572 }
1573
1574 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
1575 write!(f, "GraphSet: targets={}", self.targets.len())
1576 }
1577
1578 fn with_exprs_and_inputs(&self, exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
1579 let input = Arc::new(
1580 inputs
1581 .into_iter()
1582 .next()
1583 .unwrap_or_else(|| (*self.input).clone()),
1584 );
1585 let mut targets = self.targets.clone();
1588 if exprs.len() == targets.len() {
1589 for (t, e) in targets.iter_mut().zip(exprs) {
1590 t.value = e;
1591 }
1592 }
1593 Ok(Self::new(
1594 input,
1595 targets,
1596 self.type_id_to_entity_name.clone(),
1597 self.dir.clone(),
1598 self.mode,
1599 ))
1600 }
1601}
1602
1603#[derive(Debug, Clone)]
1606pub struct GraphRemoveNode {
1607 pub input: Arc<LogicalPlan>,
1609 pub targets: Vec<RemoveTarget>,
1611 pub type_id_to_entity_name: HashMap<u32, String>,
1614 pub dir: PathBuf,
1616 pub mode: OntologyMode,
1618 schema: DFSchemaRef,
1619}
1620
1621impl GraphRemoveNode {
1622 #[must_use]
1624 pub fn summary_schema() -> Arc<Schema> {
1625 Arc::new(Schema::new(vec![Field::new(
1626 "properties_removed",
1627 DataType::UInt64,
1628 false,
1629 )]))
1630 }
1631
1632 #[must_use]
1634 pub fn new(
1635 input: Arc<LogicalPlan>,
1636 targets: Vec<RemoveTarget>,
1637 type_id_to_entity_name: HashMap<u32, String>,
1638 dir: PathBuf,
1639 mode: OntologyMode,
1640 ) -> Self {
1641 let schema = Arc::new(
1642 DFSchema::try_from(Self::summary_schema())
1643 .expect("write-summary schema is always valid"),
1644 );
1645 Self {
1646 input,
1647 targets,
1648 type_id_to_entity_name,
1649 dir,
1650 mode,
1651 schema,
1652 }
1653 }
1654}
1655
1656impl_partial_ord!(GraphRemoveNode);
1657
1658impl PartialEq for GraphRemoveNode {
1659 fn eq(&self, other: &Self) -> bool {
1660 self.input == other.input
1661 && self.targets == other.targets
1662 && self.dir == other.dir
1663 && self.mode == other.mode
1664 && self.type_id_to_entity_name == other.type_id_to_entity_name
1665 }
1666}
1667
1668impl Eq for GraphRemoveNode {}
1669
1670impl Hash for GraphRemoveNode {
1671 fn hash<H: Hasher>(&self, state: &mut H) {
1672 self.input.hash(state);
1673 self.targets.hash(state);
1674 self.dir.hash(state);
1675 self.mode.hash(state);
1676 }
1677}
1678
1679impl UserDefinedLogicalNodeCore for GraphRemoveNode {
1680 fn name(&self) -> &str {
1681 "GraphRemove"
1682 }
1683
1684 fn inputs(&self) -> Vec<&LogicalPlan> {
1685 vec![&self.input]
1686 }
1687
1688 fn schema(&self) -> &DFSchemaRef {
1689 &self.schema
1690 }
1691
1692 fn expressions(&self) -> Vec<Expr> {
1693 vec![]
1694 }
1695
1696 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
1697 write!(f, "GraphRemove: targets={}", self.targets.len())
1698 }
1699
1700 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> DfResult<Self> {
1701 let input = Arc::new(
1702 inputs
1703 .into_iter()
1704 .next()
1705 .unwrap_or_else(|| (*self.input).clone()),
1706 );
1707 Ok(Self::new(
1708 input,
1709 self.targets.clone(),
1710 self.type_id_to_entity_name.clone(),
1711 self.dir.clone(),
1712 self.mode,
1713 ))
1714 }
1715}
1716
1717#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1730pub struct UnwindNode {
1731 pub input: Arc<LogicalPlan>,
1733 pub list_expr: Expr,
1735 pub alias: String,
1737 element_field: Arc<Field>,
1738 schema: DFSchemaRef,
1739}
1740
1741impl UnwindNode {
1742 #[must_use]
1749 pub fn new(
1750 input: Arc<LogicalPlan>,
1751 list_expr: Expr,
1752 alias: impl Into<String>,
1753 element_field: &Field,
1754 ) -> Self {
1755 let alias = alias.into();
1756 let element_field = Arc::new(element_field.clone().with_name(&alias).with_nullable(true));
1760 let schema = Self::build_schema(&input, &alias, Arc::clone(&element_field));
1761 Self {
1762 input,
1763 list_expr,
1764 alias,
1765 element_field,
1766 schema,
1767 }
1768 }
1769
1770 fn build_schema(
1775 input: &Arc<LogicalPlan>,
1776 alias: &str,
1777 element_field: Arc<Field>,
1778 ) -> DFSchemaRef {
1779 let mut qualified: Vec<(Option<TableReference>, Arc<Field>)> = input
1780 .schema()
1781 .iter()
1782 .map(|(q, f)| (q.cloned(), Arc::clone(f)))
1783 .collect();
1784 if let DataType::Struct(fields) = element_field.data_type()
1785 && fields.iter().any(|field| {
1786 matches!(
1787 field.name().as_str(),
1788 "node_uuid" | "edge_uuid" | "src_uuid" | "dst_uuid" | "nodes" | "relationships"
1789 )
1790 })
1791 {
1792 let alias_ref = TableReference::bare(alias.to_owned());
1793 qualified.extend(fields.iter().map(|field| {
1794 (
1795 Some(alias_ref.clone()),
1796 Arc::new(field.as_ref().clone().with_nullable(true)),
1797 )
1798 }));
1799 } else {
1800 qualified.push((None, element_field));
1801 }
1802 Arc::new(
1803 DFSchema::new_with_metadata(qualified, std::collections::HashMap::new())
1804 .expect("UnwindNode schema must include the element column"),
1805 )
1806 }
1807
1808 fn element_field(&self) -> Arc<Field> {
1811 Arc::clone(&self.element_field)
1812 }
1813
1814 fn bound_element_field(&self, list_expr: &Expr, input: &LogicalPlan) -> Arc<Field> {
1815 match list_expr.get_type(input.schema().as_ref()) {
1816 Ok(
1817 DataType::List(field)
1818 | DataType::LargeList(field)
1819 | DataType::FixedSizeList(field, _),
1820 ) => Arc::new(
1821 field
1822 .as_ref()
1823 .clone()
1824 .with_name(&self.alias)
1825 .with_nullable(true),
1826 ),
1827 _ => self.element_field(),
1828 }
1829 }
1830}
1831
1832impl_partial_ord!(UnwindNode);
1833
1834impl UserDefinedLogicalNodeCore for UnwindNode {
1835 fn name(&self) -> &str {
1836 "Unwind"
1837 }
1838
1839 fn inputs(&self) -> Vec<&LogicalPlan> {
1840 vec![&self.input]
1841 }
1842
1843 fn schema(&self) -> &DFSchemaRef {
1844 &self.schema
1845 }
1846
1847 fn expressions(&self) -> Vec<Expr> {
1848 vec![self.list_expr.clone()]
1849 }
1850
1851 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
1852 write!(f, "Unwind: alias={}", self.alias)
1853 }
1854
1855 fn with_exprs_and_inputs(
1856 &self,
1857 mut exprs: Vec<Expr>,
1858 inputs: Vec<LogicalPlan>,
1859 ) -> DfResult<Self> {
1860 let list_expr = exprs.pop().unwrap_or_else(|| self.list_expr.clone());
1861 let input = Arc::new(
1862 inputs
1863 .into_iter()
1864 .next()
1865 .unwrap_or_else(|| (*self.input).clone()),
1866 );
1867 let element_field = self.bound_element_field(&list_expr, &input);
1868 Ok(Self::new(
1869 input,
1870 list_expr,
1871 self.alias.clone(),
1872 &element_field,
1873 ))
1874 }
1875}
1876
1877#[cfg(test)]
1888mod tests {
1889 use super::*;
1890 use datafusion::logical_expr::{Extension, LogicalPlan};
1891
1892 fn empty_plan() -> Arc<LogicalPlan> {
1893 use datafusion::logical_expr::LogicalPlanBuilder;
1894 Arc::new(LogicalPlanBuilder::empty(false).build().unwrap())
1895 }
1896
1897 fn wrap(node: impl UserDefinedLogicalNodeCore) -> LogicalPlan {
1898 LogicalPlan::Extension(Extension {
1899 node: Arc::new(node),
1900 })
1901 }
1902
1903 fn dst_node_fields() -> Vec<Arc<Field>> {
1906 vec![
1907 Arc::new(Field::new("node_id", DataType::UInt64, false)),
1908 Arc::new(Field::new("type_id", DataType::UInt32, false)),
1909 ]
1910 }
1911
1912 fn var_len(rel: &str, min_hops: u16, max_hops: Option<u16>) -> VarLenExpandNode {
1914 VarLenExpandNode::new(
1915 empty_plan(),
1916 rel,
1917 min_hops,
1918 max_hops,
1919 0,
1920 1,
1921 2,
1922 Direction::Out,
1923 Some(7),
1924 PathBuf::from("/tmp/gf"),
1925 OntologyMode::Strict,
1926 dst_node_fields(),
1927 var_len_edge_list_field(&[]),
1928 )
1929 }
1930
1931 fn scan_plan(var: u32, fields: Vec<Field>) -> Arc<LogicalPlan> {
1934 use datafusion::logical_expr::LogicalPlanBuilder;
1935 use datafusion::logical_expr::logical_plan::LogicalTableSource;
1936 let schema = Arc::new(Schema::new(fields));
1937 Arc::new(
1938 LogicalPlanBuilder::scan(
1939 format!("var_{var}"),
1940 Arc::new(LogicalTableSource::new(schema)),
1941 None,
1942 )
1943 .unwrap()
1944 .build()
1945 .unwrap(),
1946 )
1947 }
1948
1949 fn opt_match(
1952 optional: Arc<LogicalPlan>,
1953 join_keys: Vec<(usize, usize)>,
1954 inner_keep_idx: Vec<usize>,
1955 ) -> OptionalMatchNode {
1956 OptionalMatchNode::new(empty_plan(), optional, join_keys, inner_keep_idx)
1957 }
1958
1959 fn unwind(list_expr: datafusion::logical_expr::Expr, alias: &str) -> UnwindNode {
1962 let element_field = Field::new("elem", DataType::Int64, true);
1963 UnwindNode::new(empty_plan(), list_expr, alias, &element_field)
1964 }
1965
1966 #[test]
1967 fn var_len_expand_name() {
1968 let n = var_len("KNOWS", 1, Some(3));
1969 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "VarLenExpand");
1970 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 1);
1971 }
1972
1973 #[test]
1974 fn var_len_expand_schema_extends_input_with_dst_and_edge_fields() {
1975 use datafusion::common::TableReference;
1976 let n = var_len("KNOWS", 1, Some(2));
1978 let schema = UserDefinedLogicalNodeCore::schema(&n);
1979 assert_eq!(schema.fields().len(), 3);
1980 let dst = TableReference::bare("var_1");
1982 assert!(schema.field_with_qualified_name(&dst, "node_id").is_ok());
1983 assert!(schema.field_with_qualified_name(&dst, "type_id").is_ok());
1984 let edge = TableReference::bare("var_2");
1986 let edge_field = schema
1987 .field_with_qualified_name(&edge, VAR_LEN_EDGE_LIST_FIELD)
1988 .expect("edge-list column present");
1989 assert!(matches!(edge_field.data_type(), DataType::List(_)));
1990 }
1991
1992 #[test]
1993 fn optional_match_name() {
1994 let n = opt_match(empty_plan(), vec![], vec![]);
1995 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "OptionalMatch");
1996 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 2);
1997 }
1998
1999 #[test]
2000 fn optional_match_schema_appends_nullable_inner_fields() {
2001 let optional = scan_plan(
2004 1,
2005 vec![
2006 Field::new("node_id", DataType::UInt64, false),
2007 Field::new("type_id", DataType::UInt32, false),
2008 ],
2009 );
2010 let n = opt_match(optional, vec![], vec![0, 1]);
2011 let schema = UserDefinedLogicalNodeCore::schema(&n);
2012 assert_eq!(schema.fields().len(), 2);
2013 assert!(schema.field(0).is_nullable());
2015 assert!(schema.field(1).is_nullable());
2016 let var1 = TableReference::bare("var_1");
2017 assert!(schema.field_with_qualified_name(&var1, "node_id").is_ok());
2018 }
2019
2020 #[test]
2021 fn optional_match_schema_excludes_shared_var_columns() {
2022 let optional = scan_plan(
2026 7,
2027 vec![
2028 Field::new("node_id", DataType::UInt64, false),
2029 Field::new("type_id", DataType::UInt32, false),
2030 Field::new("payload", DataType::UInt64, false),
2031 ],
2032 );
2033 let n = opt_match(optional, vec![(0, 0)], vec![2]);
2034 let schema = UserDefinedLogicalNodeCore::schema(&n);
2035 assert_eq!(schema.fields().len(), 1, "only the non-shared col is kept");
2036 assert_eq!(schema.field(0).name(), "payload");
2037 assert!(schema.field(0).is_nullable());
2038 }
2039
2040 #[test]
2041 fn path_unique_name() {
2042 let n = PathUniqueNode::new(empty_plan());
2043 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "PathUnique");
2044 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 1);
2045 }
2046
2047 #[test]
2048 fn ontology_infer_name() {
2049 let n = OntologyInferNode::new(
2050 empty_plan(),
2051 "MANAGES",
2052 "transitive:MANAGES",
2053 "conservative_min",
2054 );
2055 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "OntologyInfer");
2056 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 1);
2057 }
2058
2059 #[test]
2060 fn graph_merge_name() {
2061 let n = GraphMergeNode::new();
2062 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "GraphMerge");
2063 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 0);
2064 }
2065
2066 #[test]
2067 fn unwind_name() {
2068 use datafusion::logical_expr::lit;
2069 let n = unwind(lit(1i64), "x");
2070 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "Unwind");
2071 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 1);
2072 assert_eq!(UserDefinedLogicalNodeCore::expressions(&n).len(), 1);
2073 }
2074
2075 #[test]
2076 fn unwind_schema_appends_element_column() {
2077 use datafusion::logical_expr::lit;
2078 let n = unwind(lit(1i64), "x");
2080 let schema = UserDefinedLogicalNodeCore::schema(&n);
2081 assert_eq!(schema.fields().len(), 1);
2082 assert!(
2083 schema.field(0).is_nullable(),
2084 "unwound element column is nullable"
2085 );
2086 assert_eq!(schema.field(0).name(), "x");
2089 assert!(schema.field_with_unqualified_name("x").is_ok());
2090 }
2091
2092 #[test]
2093 fn graph_create_name_and_wrap() {
2094 let node = GraphCreateNode::new(
2095 empty_plan(),
2096 vec![ResolvedNodeSpec {
2097 var: 0,
2098 label_ids: vec![3],
2099 label_names: vec!["Person".to_owned()],
2100 properties: vec![("name".to_owned(), IrLiteral::Str("Alice".to_owned()))],
2101 computed_properties: vec![],
2102 is_reference: false,
2103 }],
2104 vec![],
2105 PathBuf::from("/tmp/gf"),
2106 OntologyMode::Strict,
2107 );
2108 assert_eq!(UserDefinedLogicalNodeCore::name(&node), "GraphCreate");
2109 assert_eq!(UserDefinedLogicalNodeCore::inputs(&node).len(), 1);
2111 assert_eq!(UserDefinedLogicalNodeCore::schema(&node).fields().len(), 4);
2113 assert!(matches!(wrap(node), LogicalPlan::Extension(_)));
2115 }
2116
2117 #[test]
2118 fn graph_create_eq_and_hash_are_consistent() {
2119 use std::collections::hash_map::DefaultHasher;
2120
2121 let mk = || {
2122 GraphCreateNode::new(
2123 empty_plan(),
2124 vec![ResolvedNodeSpec {
2125 var: 0,
2126 label_ids: vec![],
2127 label_names: vec![],
2128 properties: vec![("score".to_owned(), IrLiteral::Float(1.5))],
2129 computed_properties: vec![],
2130 is_reference: false,
2131 }],
2132 vec![],
2133 PathBuf::from("/tmp/gf"),
2134 OntologyMode::Exploratory,
2135 )
2136 };
2137 let (a, b) = (mk(), mk());
2138 assert_eq!(a, b);
2139 let mut ha = DefaultHasher::new();
2140 let mut hb = DefaultHasher::new();
2141 a.hash(&mut ha);
2142 b.hash(&mut hb);
2143 assert_eq!(ha.finish(), hb.finish());
2144 }
2145
2146 #[test]
2147 fn graph_create_hashes_every_literal_shape_deterministically() {
2148 use std::collections::hash_map::DefaultHasher;
2149
2150 let literals = vec![
2151 IrLiteral::Null,
2152 IrLiteral::Bool(true),
2153 IrLiteral::Int(7),
2154 IrLiteral::Float(1.5),
2155 IrLiteral::Str("value".into()),
2156 IrLiteral::Uuid([1; 16]),
2157 IrLiteral::Duration {
2158 months: 1,
2159 days: 2,
2160 seconds: 3,
2161 nanos: 4,
2162 },
2163 IrLiteral::DateTime(5),
2164 IrLiteral::Date(6),
2165 IrLiteral::LocalDateTime { days: 7, nanos: 8 },
2166 IrLiteral::Time(9),
2167 IrLiteral::ZonedTime {
2168 nanos: 10,
2169 offset: 3_600,
2170 },
2171 IrLiteral::ZonedDateTime {
2172 days: 11,
2173 nanos: 12,
2174 offset: -3_600,
2175 zone: Some("UTC".into()),
2176 },
2177 IrLiteral::List(vec![IrLiteral::Int(13)]),
2178 IrLiteral::Map(vec![("key".into(), IrLiteral::Int(14))]),
2179 ];
2180 let make = || {
2181 GraphCreateNode::new(
2182 empty_plan(),
2183 vec![ResolvedNodeSpec {
2184 var: 0,
2185 label_ids: vec![1],
2186 label_names: vec!["Person".into()],
2187 properties: literals
2188 .iter()
2189 .cloned()
2190 .enumerate()
2191 .map(|(index, literal)| (index.to_string(), literal))
2192 .collect(),
2193 computed_properties: vec![],
2194 is_reference: false,
2195 }],
2196 vec![],
2197 PathBuf::from("/tmp/gf"),
2198 OntologyMode::Strict,
2199 )
2200 };
2201 let (first, second) = (make(), make());
2202 let mut first_hash = DefaultHasher::new();
2203 let mut second_hash = DefaultHasher::new();
2204 first.hash(&mut first_hash);
2205 second.hash(&mut second_hash);
2206 assert_eq!(first, second);
2207 assert_eq!(first_hash.finish(), second_hash.finish());
2208 }
2209
2210 fn set_node(value: Expr) -> GraphSetNode {
2211 GraphSetNode::new(
2212 empty_plan(),
2213 vec![SetTarget {
2214 var: 0,
2215 is_edge: false,
2216 prop_name: "age".to_owned(),
2217 value,
2218 }],
2219 HashMap::new(),
2220 PathBuf::from("/tmp/gf"),
2221 OntologyMode::Exploratory,
2222 )
2223 }
2224
2225 fn remove_node() -> GraphRemoveNode {
2226 GraphRemoveNode::new(
2227 empty_plan(),
2228 vec![RemoveTarget {
2229 var: 0,
2230 is_edge: false,
2231 prop_name: "age".to_owned(),
2232 }],
2233 HashMap::new(),
2234 PathBuf::from("/tmp/gf"),
2235 OntologyMode::Exploratory,
2236 )
2237 }
2238
2239 #[test]
2240 fn graph_set_name_schema_and_expr_surface() {
2241 use datafusion::logical_expr::lit;
2242 let n = set_node(lit(42i64));
2243 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "GraphSet");
2244 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 1);
2245 assert_eq!(UserDefinedLogicalNodeCore::schema(&n).fields().len(), 1);
2247 assert_eq!(UserDefinedLogicalNodeCore::expressions(&n).len(), 1);
2249 assert!(matches!(wrap(n), LogicalPlan::Extension(_)));
2250 }
2251
2252 #[test]
2253 fn graph_set_round_trips_value_expr() {
2254 use datafusion::logical_expr::{col, lit};
2255 let value = col("var_0.age") + lit(1i64);
2258 let n = set_node(value.clone());
2259 let exprs = UserDefinedLogicalNodeCore::expressions(&n);
2260 assert_eq!(exprs, vec![value.clone()]);
2261 let rebuilt = UserDefinedLogicalNodeCore::with_exprs_and_inputs(&n, exprs, vec![]).unwrap();
2262 assert_eq!(rebuilt.targets[0].value, value);
2263 assert_eq!(n, rebuilt);
2264 }
2265
2266 #[test]
2267 fn graph_set_eq_distinguishes_value_expr() {
2268 use datafusion::logical_expr::lit;
2269 assert_ne!(set_node(lit(1i64)), set_node(lit(2i64)));
2272 }
2273
2274 #[test]
2275 fn graph_remove_name_schema_and_no_exprs() {
2276 let n = remove_node();
2277 assert_eq!(UserDefinedLogicalNodeCore::name(&n), "GraphRemove");
2278 assert_eq!(UserDefinedLogicalNodeCore::inputs(&n).len(), 1);
2279 assert_eq!(UserDefinedLogicalNodeCore::schema(&n).fields().len(), 1);
2280 assert!(UserDefinedLogicalNodeCore::expressions(&n).is_empty());
2282 assert!(matches!(wrap(n), LogicalPlan::Extension(_)));
2283 }
2284
2285 #[test]
2286 fn graph_remove_eq_and_hash_are_consistent() {
2287 use std::collections::hash_map::DefaultHasher;
2288 let (a, b) = (remove_node(), remove_node());
2289 assert_eq!(a, b);
2290 let mut ha = DefaultHasher::new();
2291 let mut hb = DefaultHasher::new();
2292 a.hash(&mut ha);
2293 b.hash(&mut hb);
2294 assert_eq!(ha.finish(), hb.finish());
2295 }
2296
2297 #[test]
2298 fn graph_create_rebuilds_computed_properties_and_preserves_emit_mode() {
2299 use datafusion::logical_expr::lit;
2300
2301 let output_schema = Arc::new(DFSchema::empty());
2302 let node = GraphCreateNode::new_emitting(
2303 empty_plan(),
2304 vec![ResolvedNodeSpec {
2305 var: 0,
2306 label_ids: vec![1],
2307 label_names: vec!["Person".into()],
2308 properties: vec![],
2309 computed_properties: vec![("score".into(), lit(1_i64))],
2310 is_reference: false,
2311 }],
2312 vec![ResolvedEdgeSpec {
2313 var: 1,
2314 src: 0,
2315 dst: 2,
2316 rel_type_id: Some(3),
2317 rel_type_name: Some("KNOWS".into()),
2318 direction: Direction::Out,
2319 properties: vec![],
2320 computed_properties: vec![("weight".into(), lit(2_i64))],
2321 }],
2322 PathBuf::from("/tmp/gf"),
2323 OntologyMode::Strict,
2324 output_schema.clone(),
2325 );
2326 assert!(node.emits_rows());
2327 assert_eq!(UserDefinedLogicalNodeCore::expressions(&node).len(), 2);
2328
2329 let rebuilt = UserDefinedLogicalNodeCore::with_exprs_and_inputs(
2330 &node,
2331 vec![lit(10_i64), lit(20_i64)],
2332 vec![(*empty_plan()).clone()],
2333 )
2334 .unwrap();
2335 assert!(rebuilt.emits_rows());
2336 assert_eq!(rebuilt.schema(), &output_schema);
2337 assert_eq!(rebuilt.nodes[0].computed_properties[0].1, lit(10_i64));
2338 assert_eq!(rebuilt.edges[0].computed_properties[0].1, lit(20_i64));
2339
2340 let short =
2341 UserDefinedLogicalNodeCore::with_exprs_and_inputs(&node, vec![lit(99_i64)], vec![])
2342 .unwrap();
2343 assert_eq!(short.nodes[0].computed_properties[0].1, lit(1_i64));
2344 assert_eq!(short.edges[0].computed_properties[0].1, lit(2_i64));
2345 }
2346
2347 #[test]
2348 fn graph_delete_contract_round_trips_and_distinguishes_detach() {
2349 use std::collections::hash_map::DefaultHasher;
2350
2351 let node = GraphDeleteNode::new(
2352 empty_plan(),
2353 vec![DeleteTarget {
2354 var: 7,
2355 is_edge: false,
2356 }],
2357 true,
2358 PathBuf::from("/tmp/gf"),
2359 OntologyMode::Exploratory,
2360 );
2361 assert_eq!(UserDefinedLogicalNodeCore::name(&node), "GraphDelete");
2362 assert_eq!(UserDefinedLogicalNodeCore::inputs(&node).len(), 1);
2363 assert_eq!(UserDefinedLogicalNodeCore::schema(&node).fields().len(), 2);
2364 assert!(UserDefinedLogicalNodeCore::expressions(&node).is_empty());
2365
2366 let rebuilt = UserDefinedLogicalNodeCore::with_exprs_and_inputs(
2367 &node,
2368 vec![],
2369 vec![(*empty_plan()).clone()],
2370 )
2371 .unwrap();
2372 assert_eq!(node, rebuilt);
2373 assert_ne!(
2374 node,
2375 GraphDeleteNode::new(
2376 empty_plan(),
2377 node.targets.clone(),
2378 false,
2379 node.dir.clone(),
2380 node.mode,
2381 )
2382 );
2383 let mut first = DefaultHasher::new();
2384 let mut second = DefaultHasher::new();
2385 node.hash(&mut first);
2386 rebuilt.hash(&mut second);
2387 assert_eq!(first.finish(), second.finish());
2388 }
2389
2390 #[test]
2391 fn optimizer_nodes_are_intentionally_not_orderable() {
2392 use datafusion::logical_expr::lit;
2393
2394 let set = set_node(lit(1_i64));
2395 let remove = remove_node();
2396 let create = GraphCreateNode::new(
2397 empty_plan(),
2398 vec![],
2399 vec![],
2400 PathBuf::from("/tmp/gf"),
2401 OntologyMode::Strict,
2402 );
2403 let delete = GraphDeleteNode::new(
2404 empty_plan(),
2405 vec![],
2406 false,
2407 PathBuf::from("/tmp/gf"),
2408 OntologyMode::Strict,
2409 );
2410
2411 assert_eq!(create.partial_cmp(&create), None);
2412 assert_eq!(delete.partial_cmp(&delete), None);
2413 assert_eq!(set.partial_cmp(&set), None);
2414 assert_eq!(remove.partial_cmp(&remove), None);
2415 }
2416
2417 #[test]
2418 fn all_stubs_wrap_as_extension() {
2419 use datafusion::logical_expr::lit;
2420 let plans = vec![
2421 wrap(var_len("*", 1, None)),
2422 wrap(opt_match(empty_plan(), vec![], vec![])),
2423 wrap(PathUniqueNode::new(empty_plan())),
2424 wrap(OntologyInferNode::new(
2425 empty_plan(),
2426 "REL",
2427 "transitive:REL",
2428 "conservative_min",
2429 )),
2430 wrap(GraphMergeNode::new()),
2431 wrap(unwind(lit(1i64), "x")),
2432 wrap(set_node(lit(1i64))),
2433 wrap(remove_node()),
2434 ];
2435 for p in &plans {
2436 assert!(
2437 matches!(p, LogicalPlan::Extension(_)),
2438 "expected Extension, got {p:?}"
2439 );
2440 }
2441 }
2442
2443 #[test]
2444 fn fmt_for_explain_is_non_empty() {
2445 use datafusion::logical_expr::lit;
2446
2447 struct ExplainWrapper<'a, T: UserDefinedLogicalNodeCore>(&'a T);
2449 impl<T: UserDefinedLogicalNodeCore> fmt::Display for ExplainWrapper<'_, T> {
2450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2451 self.0.fmt_for_explain(f)
2452 }
2453 }
2454 fn explain<T: UserDefinedLogicalNodeCore>(n: &T) -> String {
2455 ExplainWrapper(n).to_string()
2456 }
2457
2458 assert!(!explain(&var_len("KNOWS", 1, Some(3))).is_empty());
2459 assert!(!explain(&opt_match(empty_plan(), vec![], vec![])).is_empty());
2460 assert!(!explain(&PathUniqueNode::new(empty_plan())).is_empty());
2461 assert!(
2462 !explain(&OntologyInferNode::new(
2463 empty_plan(),
2464 "MANAGES",
2465 "transitive:MANAGES",
2466 "conservative_min"
2467 ))
2468 .is_empty()
2469 );
2470 assert!(!explain(&GraphMergeNode::new()).is_empty());
2471 assert!(!explain(&unwind(lit(1i64), "x")).is_empty());
2472
2473 assert!(explain(&var_len("KNOWS", 1, Some(3))).contains("KNOWS"));
2475 assert!(
2476 explain(&OntologyInferNode::new(
2477 empty_plan(),
2478 "MANAGES",
2479 "transitive:MANAGES",
2480 "conservative_min"
2481 ))
2482 .contains("MANAGES")
2483 );
2484 assert!(explain(&unwind(lit(1i64), "x")).contains("x"));
2485 }
2486
2487 #[test]
2488 fn extension_stub_trait_contracts_cover_rebuilds_directions_and_hashing() {
2489 use datafusion::logical_expr::lit;
2490 use std::collections::hash_map::DefaultHasher;
2491 use std::hash::{Hash, Hasher};
2492
2493 let mut incoming = var_len("KNOWS", 1, None);
2494 incoming.direction = Direction::In;
2495 let mut undirected = var_len("KNOWS", 1, None);
2496 undirected.direction = Direction::Undirected;
2497 struct Explain<'a, T: UserDefinedLogicalNodeCore>(&'a T);
2498 impl<T: UserDefinedLogicalNodeCore> fmt::Display for Explain<'_, T> {
2499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2500 self.0.fmt_for_explain(f)
2501 }
2502 }
2503 assert!(Explain(&incoming).to_string().contains("dir=<-"));
2504 assert!(Explain(&undirected).to_string().contains("dir=--"));
2505
2506 let path_unique = PathUniqueNode::new(empty_plan());
2507 assert!(
2508 UserDefinedLogicalNodeCore::schema(&path_unique)
2509 .fields()
2510 .is_empty()
2511 );
2512 assert!(UserDefinedLogicalNodeCore::expressions(&path_unique).is_empty());
2513 let rebuilt = path_unique
2514 .with_exprs_and_inputs(vec![], vec![empty_plan().as_ref().clone()])
2515 .unwrap();
2516 assert_eq!(UserDefinedLogicalNodeCore::name(&rebuilt), "PathUnique");
2517
2518 let merge = GraphMergeNode::default();
2519 assert!(
2520 UserDefinedLogicalNodeCore::schema(&merge)
2521 .fields()
2522 .is_empty()
2523 );
2524 assert!(UserDefinedLogicalNodeCore::expressions(&merge).is_empty());
2525 assert_eq!(
2526 UserDefinedLogicalNodeCore::name(&merge.with_exprs_and_inputs(vec![], vec![]).unwrap()),
2527 "GraphMerge"
2528 );
2529
2530 let create = GraphCreateNode::new(
2531 empty_plan(),
2532 vec![],
2533 vec![],
2534 PathBuf::from("/tmp/gf"),
2535 OntologyMode::Strict,
2536 );
2537 let delete = GraphDeleteNode::new(
2538 empty_plan(),
2539 vec![],
2540 true,
2541 PathBuf::from("/tmp/gf"),
2542 OntologyMode::Strict,
2543 );
2544 let set = set_node(lit(1_i64));
2545 let remove = remove_node();
2546 for rendered in [
2547 Explain(&create).to_string(),
2548 Explain(&delete).to_string(),
2549 Explain(&set).to_string(),
2550 Explain(&remove).to_string(),
2551 ] {
2552 assert!(!rendered.is_empty());
2553 }
2554 for hashable in [&create as &dyn HashProbe, &set as &dyn HashProbe] {
2555 let mut hasher = DefaultHasher::new();
2556 hashable.hash_into(&mut hasher);
2557 let _ = hasher.finish();
2558 }
2559
2560 trait HashProbe {
2561 fn hash_into(&self, state: &mut DefaultHasher);
2562 }
2563 impl<T: Hash> HashProbe for T {
2564 fn hash_into(&self, state: &mut DefaultHasher) {
2565 self.hash(state);
2566 }
2567 }
2568 }
2569
2570 fn expand_node(edge_prop_fields: Vec<Arc<Field>>) -> ExpandNode {
2575 let edge_fields = vec![
2576 Arc::new(Field::new(
2577 "edge_uuid",
2578 DataType::FixedSizeBinary(16),
2579 false,
2580 )),
2581 Arc::new(Field::new("edge_id", DataType::UInt64, false)),
2582 Arc::new(Field::new("src_id", DataType::UInt64, false)),
2583 Arc::new(Field::new("dst_id", DataType::UInt64, false)),
2584 ];
2585 ExpandNode::new(
2586 empty_plan(),
2587 "KNOWS",
2588 0,
2589 2,
2590 1,
2591 Direction::Out,
2592 Some(7),
2593 PathBuf::from("/tmp/p"),
2594 OntologyMode::Strict,
2595 edge_fields,
2596 edge_prop_fields,
2597 dst_node_fields(),
2598 )
2599 }
2600
2601 #[test]
2602 fn expand_node_schema_order_qualifiers_and_prop_nullability() {
2603 use datafusion::common::TableReference;
2604
2605 let node = expand_node(vec![Arc::new(Field::new("since", DataType::Int64, false))]);
2608 let schema = UserDefinedLogicalNodeCore::schema(&node);
2609 let names: Vec<String> = schema
2611 .iter()
2612 .map(|(q, f)| {
2613 format!(
2614 "{}.{}",
2615 q.map(TableReference::table).unwrap_or("?"),
2616 f.name()
2617 )
2618 })
2619 .collect();
2620 assert_eq!(
2621 names,
2622 [
2623 "var_1.edge_uuid",
2624 "var_1.edge_id",
2625 "var_1.src_id",
2626 "var_1.dst_id",
2627 "var_1.since",
2628 "var_2.node_id",
2629 "var_2.type_id",
2630 ]
2631 );
2632 let edge = TableReference::bare("var_1");
2633 let since = schema.field_with_qualified_name(&edge, "since").unwrap();
2634 assert!(since.is_nullable(), "prop columns are LEFT-join nullable");
2635 let uuid = schema
2636 .field_with_qualified_name(&edge, "edge_uuid")
2637 .unwrap();
2638 assert!(!uuid.is_nullable(), "topology columns keep non-null");
2639 assert_eq!(node.edge_prop_count, 1);
2640 }
2641
2642 #[test]
2643 fn expand_node_with_exprs_and_inputs_round_trips() {
2644 let node = expand_node(vec![Arc::new(Field::new("since", DataType::Int64, true))]);
2645 let rebuilt = UserDefinedLogicalNodeCore::with_exprs_and_inputs(
2646 &node,
2647 vec![],
2648 vec![(*empty_plan()).clone()],
2649 )
2650 .unwrap();
2651 assert_eq!(
2652 UserDefinedLogicalNodeCore::schema(&rebuilt).as_arrow(),
2653 UserDefinedLogicalNodeCore::schema(&node).as_arrow(),
2654 "schema survives optimizer-style input replacement"
2655 );
2656 assert_eq!(rebuilt.edge_prop_count, 1);
2657 assert_eq!(rebuilt.rel_type_name, "KNOWS");
2658 }
2659}