1#![forbid(unsafe_code)]
2
3use std::fmt;
10
11pub mod cep;
12pub mod expression;
13pub mod governance;
14mod graph;
15mod lowering;
16pub mod optimizer;
17pub mod task_fragment;
18pub mod udf;
19pub mod window;
20pub use expression::{
21 AggregateFunction as ExprAggregateFunction, BinaryOperator as ExprBinaryOperator,
22 EXPRESSION_FORMAT_VERSION, Expr, ExprDataType, ExprField, IntervalUnit, NullOrdering,
23 ScalarValue, SortDirection, TimeUnit,
24};
25pub use graph::lower_to_physical;
26pub use task_fragment::{
27 TASK_FRAGMENT_VERSION, TypedTaskFragment, encode_typed_task_fragment,
28 execution_kind_from_fragment, task_body_for_profile, validate_job_fragments,
29};
30
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
33pub enum PlanError {
34 #[error("plan parse error: {0}")]
36 Parse(String),
37 #[error("plan encode error: {0}")]
39 Encode(String),
40 #[error("plan validation error: {0}")]
42 Validation(String),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
47pub enum FieldType {
48 Boolean,
49 Int32,
50 Int64,
51 Float64,
52 Utf8,
53 Binary,
54 Timestamp,
55 Variant,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
61pub struct SchemaField {
62 name: String,
63 field_type: FieldType,
64 nullable: bool,
65}
66
67impl SchemaField {
68 pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
70 Self {
71 name: name.into(),
72 field_type,
73 nullable: false,
74 }
75 }
76
77 #[must_use]
79 pub fn with_nullable(mut self, nullable: bool) -> Self {
80 self.nullable = nullable;
81 self
82 }
83
84 pub fn name(&self) -> &str {
86 &self.name
87 }
88
89 pub fn field_type(&self) -> &FieldType {
91 &self.field_type
92 }
93
94 pub fn nullable(&self) -> bool {
96 self.nullable
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
102pub struct PlanSchema {
103 fields: Vec<SchemaField>,
104}
105
106impl PlanSchema {
107 pub fn new(fields: Vec<SchemaField>) -> Self {
109 Self { fields }
110 }
111
112 pub fn fields(&self) -> &[SchemaField] {
114 &self.fields
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.fields.is_empty()
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
129pub enum JoinType {
130 Inner,
131 Left,
132 Right,
133 Full,
134 Semi,
137 Anti,
140 LeftSemi,
144 RightSemi,
146 LeftAnti,
148 RightAnti,
150 Cross,
152 NestedLoop,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
158pub enum NodeOp {
159 Scan { table: String, filters: Vec<String> },
161 Filter { predicate: String },
163 Project { columns: Vec<String> },
165 Aggregate { group_keys: Vec<String> },
167 Join { join_type: JoinType },
169 Exchange { partitioning: Partitioning },
171 Sink { format: String },
173 CoalescePartitions {
178 target_partitions: usize,
180 },
181 CreateLiveTable { name: String, query: String },
183 RefreshLiveTable { name: String },
185 DropLiveTable { name: String },
187 KeyBy { key_column: String },
189 Watermark {
191 event_time_column: String,
192 lag_ms: u64,
193 },
194 Window {
196 spec: Box<window::WindowExecutionSpec>,
197 },
198 StreamSource { source_id: String, bounded: bool },
200 StateTtl { ttl_ms: u64 },
202 GlobalSort {
206 keys: Vec<(String, bool)>,
208 },
209 SortMergeJoin {
211 join_type: JoinType,
212 left_keys: Vec<String>,
214 right_keys: Vec<String>,
215 },
216 WindowJoin {
219 join_type: JoinType,
220 left_keys: Vec<String>,
222 right_keys: Vec<String>,
223 time_column: String,
225 window_ms: u64,
227 },
228 Unnest {
235 array_column: String,
236 output_column: String,
237 with_ordinality: bool,
238 },
239 Cep {
246 key_column: String,
247 event_time_column: String,
248 stage_column: String,
249 },
250 SkewJoin {
258 keys: Vec<String>,
260 factor: u32,
262 join_type: JoinType,
264 },
265 Other { description: String },
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
271pub enum ExecutionKind {
272 Batch,
274 Streaming,
276 DeltaBatch,
282}
283
284impl fmt::Display for ExecutionKind {
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 match self {
287 Self::Batch => f.write_str("batch"),
288 Self::Streaming => f.write_str("streaming"),
289 Self::DeltaBatch => f.write_str("delta-batch"),
290 }
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
296pub enum Partitioning {
297 Unpartitioned,
299 Hash {
301 keys: Vec<String>,
303 buckets: u32,
305 },
306 RoundRobin {
308 buckets: u32,
310 },
311 Broadcast,
313 Range {
318 keys: Vec<(String, bool)>,
320 boundaries: Vec<String>,
323 buckets: u32,
325 },
326}
327
328impl fmt::Display for Partitioning {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 match self {
331 Self::Unpartitioned => f.write_str("unpartitioned"),
332 Self::Hash { keys, buckets } => {
333 write!(f, "hash({}, buckets={})", keys.join(", "), buckets)
334 }
335 Self::RoundRobin { buckets } => write!(f, "round-robin(buckets={})", buckets),
336 Self::Broadcast => f.write_str("broadcast"),
337 Self::Range { keys, buckets, .. } => {
338 let key_str = keys
339 .iter()
340 .map(|(c, asc)| format!("{} {}", c, if *asc { "ASC" } else { "DESC" }))
341 .collect::<Vec<_>>()
342 .join(", ");
343 write!(f, "range({key_str}, buckets={buckets})")
344 }
345 }
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
351pub struct PlanNode {
352 id: String,
353 label: String,
354 kind: ExecutionKind,
355 inputs: Vec<String>,
356 partitioning: Partitioning,
358 broadcast_eligible: bool,
360 estimated_rows: Option<u64>,
362 op: Option<NodeOp>,
364 output_schema: PlanSchema,
366}
367
368impl PlanNode {
369 pub fn new(id: impl Into<String>, label: impl Into<String>, kind: ExecutionKind) -> Self {
371 Self {
372 id: id.into(),
373 label: label.into(),
374 kind,
375 inputs: Vec::new(),
376 partitioning: Partitioning::Unpartitioned,
377 broadcast_eligible: false,
378 estimated_rows: None,
379 op: None,
380 output_schema: PlanSchema::default(),
381 }
382 }
383
384 #[must_use]
386 pub fn with_inputs(mut self, inputs: impl IntoIterator<Item = impl Into<String>>) -> Self {
387 self.inputs = inputs.into_iter().map(Into::into).collect();
388 self
389 }
390
391 #[must_use]
393 pub fn with_label(mut self, label: impl Into<String>) -> Self {
394 self.label = label.into();
395 self
396 }
397
398 #[must_use]
400 pub fn with_partitioning(mut self, partitioning: Partitioning) -> Self {
401 self.partitioning = partitioning;
402 self
403 }
404
405 #[must_use]
407 pub fn with_broadcast_eligible(mut self, broadcast_eligible: bool) -> Self {
408 self.broadcast_eligible = broadcast_eligible;
409 self
410 }
411
412 #[must_use]
418 pub fn with_exchange(
419 self,
420 key_columns: impl IntoIterator<Item = impl Into<String>>,
421 num_partitions: u32,
422 ) -> Self {
423 self.with_partitioning(Partitioning::Hash {
424 keys: key_columns.into_iter().map(Into::into).collect(),
425 buckets: num_partitions,
426 })
427 }
428
429 #[must_use]
431 pub fn with_estimated_rows(mut self, estimated_rows: Option<u64>) -> Self {
432 self.estimated_rows = estimated_rows;
433 self
434 }
435
436 #[must_use]
438 pub fn with_op(mut self, op: NodeOp) -> Self {
439 self.op = Some(op);
440 self
441 }
442
443 #[must_use]
445 pub fn with_output_schema(mut self, schema: PlanSchema) -> Self {
446 self.output_schema = schema;
447 self
448 }
449
450 pub fn id(&self) -> &str {
452 &self.id
453 }
454
455 pub fn label(&self) -> &str {
457 &self.label
458 }
459
460 pub fn kind(&self) -> ExecutionKind {
462 self.kind
463 }
464
465 pub fn inputs(&self) -> &[String] {
467 &self.inputs
468 }
469
470 pub fn partitioning(&self) -> &Partitioning {
472 &self.partitioning
473 }
474
475 pub fn set_partitioning(&mut self, partitioning: Partitioning) {
477 self.partitioning = partitioning;
478 }
479
480 pub fn broadcast_eligible(&self) -> bool {
482 self.broadcast_eligible
483 }
484
485 pub fn estimated_rows(&self) -> Option<u64> {
487 self.estimated_rows
488 }
489
490 pub fn op(&self) -> Option<&NodeOp> {
492 self.op.as_ref()
493 }
494
495 pub fn output_schema(&self) -> &PlanSchema {
497 &self.output_schema
498 }
499}
500
501pub const MAX_PLAN_NODES: usize = 10_000;
506
507#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
509pub(crate) struct PlanCore {
510 pub(crate) name: String,
511 pub(crate) kind: ExecutionKind,
512 pub(crate) nodes: Vec<PlanNode>,
513 shuffle_partitions: Option<u32>,
517}
518
519impl PlanCore {
520 fn new(name: impl Into<String>, kind: ExecutionKind) -> Self {
521 Self {
522 name: name.into(),
523 kind,
524 nodes: Vec::new(),
525 shuffle_partitions: None,
526 }
527 }
528
529 fn add_node(&mut self, node: PlanNode) {
530 self.nodes.push(node);
531 }
532
533 fn with_node(mut self, node: PlanNode) -> Self {
534 self.add_node(node);
535 self
536 }
537
538 fn name(&self) -> &str {
539 &self.name
540 }
541
542 fn kind(&self) -> ExecutionKind {
543 self.kind
544 }
545
546 fn nodes(&self) -> &[PlanNode] {
547 &self.nodes
548 }
549
550 fn nodes_mut(&mut self) -> &mut [PlanNode] {
551 &mut self.nodes
552 }
553
554 fn shuffle_partitions(&self) -> Option<u32> {
555 self.shuffle_partitions
556 }
557
558 fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
559 self.shuffle_partitions = n;
560 self
561 }
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
566pub struct LogicalPlan {
567 pub(crate) core: PlanCore,
568}
569
570impl LogicalPlan {
571 pub fn new(name: impl Into<String>, kind: ExecutionKind) -> Self {
573 Self {
574 core: PlanCore::new(name, kind),
575 }
576 }
577
578 pub fn add_node(&mut self, node: PlanNode) {
580 self.core.add_node(node);
581 }
582
583 #[must_use]
585 pub fn with_node(mut self, node: PlanNode) -> Self {
586 self.core = self.core.with_node(node);
587 self
588 }
589
590 pub fn name(&self) -> &str {
592 self.core.name()
593 }
594
595 pub fn kind(&self) -> ExecutionKind {
597 self.core.kind()
598 }
599
600 pub fn nodes(&self) -> &[PlanNode] {
602 self.core.nodes()
603 }
604
605 pub fn validate(&self) -> Result<(), PlanError> {
607 graph::validate_plan("logical", self.name(), self.nodes())
608 }
609
610 pub fn describe(&self) -> String {
612 describe_plan(
613 "logical",
614 self.core.name(),
615 self.core.kind(),
616 self.core.nodes(),
617 )
618 }
619
620 pub fn shuffle_partitions(&self) -> Option<u32> {
622 self.core.shuffle_partitions()
623 }
624
625 #[must_use]
627 pub fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
628 self.core = self.core.with_shuffle_partitions(n);
629 self
630 }
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
635pub struct PhysicalPlan {
636 pub(crate) core: PlanCore,
637 coalesced_partition_count: Option<usize>,
640}
641
642impl PhysicalPlan {
643 pub fn new(name: impl Into<String>, kind: ExecutionKind) -> Self {
645 Self {
646 core: PlanCore::new(name, kind),
647 coalesced_partition_count: None,
648 }
649 }
650
651 pub fn coalesced_partition_count(&self) -> Option<usize> {
653 self.coalesced_partition_count
654 }
655
656 #[must_use]
658 pub fn with_coalesced_partition_count(mut self, count: usize) -> Self {
659 self.coalesced_partition_count = Some(count);
660 self
661 }
662
663 pub fn add_node(&mut self, node: PlanNode) {
665 self.core.add_node(node);
666 }
667
668 #[must_use]
670 pub fn with_node(mut self, node: PlanNode) -> Self {
671 self.core = self.core.with_node(node);
672 self
673 }
674
675 pub fn name(&self) -> &str {
677 self.core.name()
678 }
679
680 pub fn kind(&self) -> ExecutionKind {
682 self.core.kind()
683 }
684
685 pub fn nodes(&self) -> &[PlanNode] {
687 self.core.nodes()
688 }
689
690 pub fn nodes_mut(&mut self) -> &mut [PlanNode] {
695 self.core.nodes_mut()
696 }
697
698 pub fn shuffle_partitions(&self) -> Option<u32> {
700 self.core.shuffle_partitions()
701 }
702
703 #[must_use]
705 pub fn with_shuffle_partitions(mut self, n: Option<u32>) -> Self {
706 self.core = self.core.with_shuffle_partitions(n);
707 self
708 }
709
710 pub fn validate(&self) -> Result<(), PlanError> {
712 graph::validate_plan("physical", self.name(), self.nodes())
713 }
714
715 pub fn describe(&self) -> String {
717 describe_plan(
718 "physical",
719 self.core.name(),
720 self.core.kind(),
721 self.core.nodes(),
722 )
723 }
724}
725
726fn describe_plan(plan_type: &str, name: &str, kind: ExecutionKind, nodes: &[PlanNode]) -> String {
727 let mut output = format!("{plan_type} plan: {name}\nkind: {kind}\nnodes:");
728 if nodes.is_empty() {
729 output.push_str(" <empty>");
730 return output;
731 }
732
733 for node in nodes {
734 output.push_str(&format!(
735 "\n- {} [{}] {}",
736 node.id(),
737 node.kind(),
738 node.label()
739 ));
740 if !node.inputs().is_empty() {
741 output.push_str(&format!(" <- {}", node.inputs().join(", ")));
742 }
743 if node.partitioning() != &Partitioning::Unpartitioned {
744 output.push_str(&format!(" [partitioning: {}]", node.partitioning()));
745 }
746 if node.broadcast_eligible() {
747 output.push_str(" [broadcast-eligible]");
748 }
749 if let Some(rows) = node.estimated_rows() {
750 output.push_str(&format!(" [est-rows: {rows}]"));
751 }
752 }
753
754 output
755}
756
757#[derive(Debug, Clone, Default, PartialEq, Eq)]
764pub struct PlanDiff {
765 pub added: Vec<String>,
767 pub removed: Vec<String>,
769 pub changed: Vec<String>,
771}
772
773impl PlanDiff {
774 pub fn is_empty(&self) -> bool {
776 self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
777 }
778}
779
780#[must_use]
786pub fn diff_plans(before: &PhysicalPlan, after: &PhysicalPlan) -> PlanDiff {
787 use std::collections::HashMap;
788
789 let before_map: HashMap<&str, &PlanNode> = before.nodes().iter().map(|n| (n.id(), n)).collect();
790 let after_map: HashMap<&str, &PlanNode> = after.nodes().iter().map(|n| (n.id(), n)).collect();
791
792 let mut added = Vec::new();
793 let mut removed = Vec::new();
794 let mut changed = Vec::new();
795
796 for (id, after_node) in &after_map {
797 match before_map.get(id) {
798 None => added.push((*id).to_owned()),
799 Some(before_node) => {
800 let structurally_different = before_node.label() != after_node.label()
801 || before_node.op() != after_node.op()
802 || before_node.inputs() != after_node.inputs()
803 || before_node.partitioning() != after_node.partitioning()
804 || before_node.estimated_rows() != after_node.estimated_rows()
805 || before_node.output_schema() != after_node.output_schema();
806 if structurally_different {
807 changed.push((*id).to_owned());
808 }
809 }
810 }
811 }
812 for id in before_map.keys() {
813 if !after_map.contains_key(id) {
814 removed.push((*id).to_owned());
815 }
816 }
817
818 added.sort();
819 removed.sort();
820 changed.sort();
821
822 PlanDiff {
823 added,
824 removed,
825 changed,
826 }
827}
828
829#[cfg(test)]
830mod gap_tests;
831
832#[cfg(test)]
833mod tests {
834 use super::{
835 ExecutionKind, FieldType, JoinType, LogicalPlan, NodeOp, Partitioning, PhysicalPlan,
836 PlanNode, PlanSchema, SchemaField,
837 };
838
839 #[test]
840 fn describes_logical_plan_with_nodes() {
841 let plan = LogicalPlan::new("demo", ExecutionKind::Batch).with_node(PlanNode::new(
842 "scan",
843 "scan parquet",
844 ExecutionKind::Batch,
845 ));
846
847 let description = plan.describe();
848
849 assert!(description.contains("logical plan: demo"));
850 assert!(description.contains("scan parquet"));
851 }
852
853 #[test]
857 fn join_type_variants_are_distinct() {
858 let all = [
859 JoinType::Inner,
860 JoinType::Left,
861 JoinType::Right,
862 JoinType::Full,
863 JoinType::Semi,
864 JoinType::Anti,
865 JoinType::LeftSemi,
866 JoinType::RightSemi,
867 JoinType::LeftAnti,
868 JoinType::RightAnti,
869 JoinType::Cross,
870 JoinType::NestedLoop,
871 ];
872 assert_ne!(JoinType::LeftSemi, JoinType::Inner);
875 assert_ne!(JoinType::RightSemi, JoinType::Inner);
876 assert_ne!(JoinType::LeftAnti, JoinType::Inner);
877 assert_ne!(JoinType::RightAnti, JoinType::Inner);
878 assert_ne!(JoinType::LeftSemi, JoinType::Semi);
879 assert_ne!(JoinType::LeftAnti, JoinType::Anti);
880 for (i, a) in all.iter().enumerate() {
882 for b in &all[i + 1..] {
883 assert_ne!(a, b, "{a:?} and {b:?} must be distinct");
884 }
885 }
886 }
887
888 #[test]
889 fn plan_node_default_annotations() {
890 let node = PlanNode::new("n1", "label", ExecutionKind::Batch);
891 assert_eq!(node.partitioning(), &Partitioning::Unpartitioned);
892 assert!(!node.broadcast_eligible());
893 assert_eq!(node.estimated_rows(), None);
894 }
895
896 #[test]
897 fn plan_node_builder_methods() {
898 let node = PlanNode::new("n1", "label", ExecutionKind::Batch)
899 .with_partitioning(Partitioning::Hash {
900 keys: vec!["region".to_string()],
901 buckets: 8,
902 })
903 .with_broadcast_eligible(true)
904 .with_estimated_rows(Some(1_000));
905
906 assert_eq!(
907 node.partitioning(),
908 &Partitioning::Hash {
909 keys: vec!["region".to_string()],
910 buckets: 8,
911 }
912 );
913 assert!(node.broadcast_eligible());
914 assert_eq!(node.estimated_rows(), Some(1_000));
915 }
916
917 #[test]
918 fn plan_node_round_robin_partitioning() {
919 let node = PlanNode::new("n1", "label", ExecutionKind::Batch)
920 .with_partitioning(Partitioning::RoundRobin { buckets: 4 });
921 assert_eq!(
922 node.partitioning(),
923 &Partitioning::RoundRobin { buckets: 4 }
924 );
925 }
926
927 #[test]
928 fn plan_node_broadcast_partitioning() {
929 let node = PlanNode::new("n1", "label", ExecutionKind::Batch)
930 .with_partitioning(Partitioning::Broadcast);
931 assert_eq!(node.partitioning(), &Partitioning::Broadcast);
932 }
933
934 #[test]
935 fn describe_shows_partitioning_when_not_unpartitioned() {
936 let plan = LogicalPlan::new("q", ExecutionKind::Batch).with_node(
937 PlanNode::new("agg", "aggregate", ExecutionKind::Batch).with_partitioning(
938 Partitioning::Hash {
939 keys: vec!["city".to_string()],
940 buckets: 16,
941 },
942 ),
943 );
944 let desc = plan.describe();
945 assert!(desc.contains("partitioning: hash(city, buckets=16)"));
946 }
947
948 #[test]
949 fn describe_does_not_show_partitioning_when_unpartitioned() {
950 let plan = LogicalPlan::new("q", ExecutionKind::Batch).with_node(PlanNode::new(
951 "scan",
952 "scan",
953 ExecutionKind::Batch,
954 ));
955 let desc = plan.describe();
956 assert!(!desc.contains("partitioning:"));
957 }
958
959 #[test]
960 fn physical_plan_with_broadcast_node() {
961 let plan = PhysicalPlan::new("p", ExecutionKind::Batch).with_node(
962 PlanNode::new("dim", "dim scan", ExecutionKind::Batch)
963 .with_partitioning(Partitioning::Broadcast)
964 .with_broadcast_eligible(true)
965 .with_estimated_rows(Some(500)),
966 );
967 let node = &plan.nodes()[0];
968 assert_eq!(node.partitioning(), &Partitioning::Broadcast);
969 assert!(node.broadcast_eligible());
970 assert_eq!(node.estimated_rows(), Some(500));
971
972 let desc = plan.describe();
973 assert!(desc.contains("broadcast"));
974 }
975
976 #[test]
977 fn plan_node_with_typed_op() {
978 let node =
979 PlanNode::new("scan", "scan parquet", ExecutionKind::Batch).with_op(NodeOp::Scan {
980 table: String::from("orders"),
981 filters: vec![],
982 });
983 assert!(matches!(node.op(), Some(NodeOp::Scan { table, .. }) if table == "orders"));
984 }
985
986 #[test]
987 fn plan_node_schema_propagation() {
988 let schema = PlanSchema::new(vec![
989 SchemaField::new("id", FieldType::Int64),
990 SchemaField::new("name", FieldType::Utf8).with_nullable(true),
991 ]);
992 let node = PlanNode::new("proj", "project", ExecutionKind::Batch)
993 .with_op(NodeOp::Project {
994 columns: vec![String::from("id"), String::from("name")],
995 })
996 .with_output_schema(schema);
997 assert_eq!(node.output_schema().fields().len(), 2);
998 assert_eq!(node.output_schema().fields()[0].name(), "id");
999 assert_eq!(
1000 node.output_schema().fields()[0].field_type(),
1001 &FieldType::Int64
1002 );
1003 assert!(!node.output_schema().fields()[0].nullable());
1004 assert!(node.output_schema().fields()[1].nullable());
1005 }
1006
1007 #[test]
1008 fn plan_schema_empty_by_default() {
1009 let node = PlanNode::new("n1", "label", ExecutionKind::Batch);
1010 assert!(node.output_schema().is_empty());
1011 }
1012
1013 #[test]
1014 fn node_op_variants_round_trip() {
1015 let ops: Vec<NodeOp> = vec![
1016 NodeOp::Scan {
1017 table: String::from("t1"),
1018 filters: vec![],
1019 },
1020 NodeOp::Filter {
1021 predicate: String::new(),
1022 },
1023 NodeOp::Project {
1024 columns: vec![String::from("a")],
1025 },
1026 NodeOp::Aggregate {
1027 group_keys: vec![String::from("region")],
1028 },
1029 NodeOp::Join {
1030 join_type: JoinType::Inner,
1031 },
1032 NodeOp::Exchange {
1033 partitioning: Partitioning::Broadcast,
1034 },
1035 NodeOp::Sink {
1036 format: String::from("parquet"),
1037 },
1038 NodeOp::CoalescePartitions {
1039 target_partitions: 4,
1040 },
1041 NodeOp::Other {
1042 description: String::from("custom"),
1043 },
1044 ];
1045 for op in &ops {
1046 let cloned = op.clone();
1047 assert_eq!(&cloned, op);
1048 let _ = format!("{cloned:?}");
1050 }
1051 }
1052
1053 #[test]
1054 fn partitioning_display() {
1055 assert_eq!(Partitioning::Unpartitioned.to_string(), "unpartitioned");
1056 assert_eq!(
1057 Partitioning::Hash {
1058 keys: vec!["a".to_string(), "b".to_string()],
1059 buckets: 4
1060 }
1061 .to_string(),
1062 "hash(a, b, buckets=4)"
1063 );
1064 assert_eq!(
1065 Partitioning::RoundRobin { buckets: 2 }.to_string(),
1066 "round-robin(buckets=2)"
1067 );
1068 assert_eq!(Partitioning::Broadcast.to_string(), "broadcast");
1069 }
1070
1071 fn make_plan(nodes: &[(&str, &str)]) -> PhysicalPlan {
1074 let mut plan = PhysicalPlan::new("test", ExecutionKind::Batch);
1075 for (id, label) in nodes {
1076 plan.add_node(PlanNode::new(*id, *label, ExecutionKind::Batch));
1077 }
1078 plan
1079 }
1080
1081 #[test]
1082 fn diff_plans_identical_is_empty() {
1083 let p = make_plan(&[("scan", "Scan"), ("agg", "Aggregate")]);
1084 let diff = super::diff_plans(&p, &p);
1085 assert!(diff.is_empty());
1086 }
1087
1088 #[test]
1089 fn diff_plans_added_node() {
1090 let before = make_plan(&[("scan", "Scan")]);
1091 let after = make_plan(&[("scan", "Scan"), ("filter", "Filter")]);
1092 let diff = super::diff_plans(&before, &after);
1093 assert_eq!(diff.added, vec!["filter"]);
1094 assert!(diff.removed.is_empty());
1095 assert!(diff.changed.is_empty());
1096 }
1097
1098 #[test]
1099 fn diff_plans_removed_node() {
1100 let before = make_plan(&[("scan", "Scan"), ("filter", "Filter")]);
1101 let after = make_plan(&[("scan", "Scan")]);
1102 let diff = super::diff_plans(&before, &after);
1103 assert!(diff.added.is_empty());
1104 assert_eq!(diff.removed, vec!["filter"]);
1105 assert!(diff.changed.is_empty());
1106 }
1107
1108 #[test]
1109 fn diff_plans_changed_label() {
1110 let before = make_plan(&[("n1", "OldLabel")]);
1111 let after = make_plan(&[("n1", "NewLabel")]);
1112 let diff = super::diff_plans(&before, &after);
1113 assert!(diff.added.is_empty());
1114 assert!(diff.removed.is_empty());
1115 assert_eq!(diff.changed, vec!["n1"]);
1116 }
1117
1118 #[test]
1119 fn diff_plans_detects_changed_partitioning() {
1120 let mut before = PhysicalPlan::new("test", ExecutionKind::Batch);
1121 before.add_node(
1122 PlanNode::new("n1", "label", ExecutionKind::Batch)
1123 .with_partitioning(Partitioning::Unpartitioned),
1124 );
1125 let mut after = PhysicalPlan::new("test", ExecutionKind::Batch);
1126 after.add_node(
1127 PlanNode::new("n1", "label", ExecutionKind::Batch)
1128 .with_partitioning(Partitioning::Broadcast),
1129 );
1130 let diff = super::diff_plans(&before, &after);
1131 assert_eq!(diff.changed, vec!["n1"]);
1132 }
1133
1134 #[test]
1135 fn diff_plans_detects_changed_estimated_rows() {
1136 let mut before = PhysicalPlan::new("test", ExecutionKind::Batch);
1137 before.add_node(
1138 PlanNode::new("n1", "label", ExecutionKind::Batch).with_estimated_rows(Some(100)),
1139 );
1140 let mut after = PhysicalPlan::new("test", ExecutionKind::Batch);
1141 after.add_node(
1142 PlanNode::new("n1", "label", ExecutionKind::Batch).with_estimated_rows(Some(200)),
1143 );
1144 let diff = super::diff_plans(&before, &after);
1145 assert_eq!(diff.changed, vec!["n1"]);
1146 }
1147
1148 #[test]
1149 fn diff_plans_detects_changed_inputs() {
1150 let mut before = PhysicalPlan::new("test", ExecutionKind::Batch);
1151 before.add_node(PlanNode::new("src", "source", ExecutionKind::Batch));
1152 before.add_node(PlanNode::new("n1", "label", ExecutionKind::Batch).with_inputs(["src"]));
1153 let mut after = PhysicalPlan::new("test", ExecutionKind::Batch);
1154 after.add_node(PlanNode::new("src", "source", ExecutionKind::Batch));
1155 after.add_node(PlanNode::new("n1", "label", ExecutionKind::Batch)); let diff = super::diff_plans(&before, &after);
1157 assert_eq!(diff.changed, vec!["n1"]);
1158 }
1159
1160 #[test]
1161 fn graph_rejects_duplicate_input_edges() {
1162 let plan = LogicalPlan::new("dup-edges", ExecutionKind::Batch)
1163 .with_node(PlanNode::new("src", "source", ExecutionKind::Batch))
1164 .with_node(
1165 PlanNode::new("n1", "node", ExecutionKind::Batch).with_inputs(["src", "src"]),
1166 );
1167 let err = plan.validate().expect_err("duplicate inputs must fail");
1168 assert!(
1169 err.to_string().contains("duplicate input"),
1170 "unexpected: {err}"
1171 );
1172 }
1173}