1use std::cmp::min;
16use std::collections::{HashMap, HashSet};
17use std::fmt::{Debug, Display, Formatter};
18use std::num::NonZeroU64;
19
20use anyhow::anyhow;
21use async_recursion::async_recursion;
22use enum_as_inner::EnumAsInner;
23use futures::TryStreamExt;
24use itertools::Itertools;
25use petgraph::{Directed, Graph};
26use pgwire::pg_server::SessionId;
27use risingwave_batch::error::BatchError;
28use risingwave_batch::worker_manager::worker_node_manager::WorkerNodeSelector;
29use risingwave_common::bitmap::{Bitmap, BitmapBuilder};
30use risingwave_common::catalog::Schema;
31use risingwave_common::hash::table_distribution::TableDistribution;
32use risingwave_common::hash::{WorkerSlotId, WorkerSlotMapping};
33use risingwave_common::util::scan_range::ScanRange;
34use risingwave_connector::source::filesystem::opendal_source::opendal_enumerator::OpendalEnumerator;
35use risingwave_connector::source::filesystem::opendal_source::{
36 BatchPosixFsEnumerator, OpendalAzblob, OpendalGcs, OpendalS3,
37};
38use risingwave_connector::source::iceberg::{IcebergFileScanTask, IcebergScanTaskPlanner};
39use risingwave_connector::source::kafka::KafkaSplitEnumerator;
40use risingwave_connector::source::prelude::DatagenSplitEnumerator;
41use risingwave_connector::source::reader::reader::build_opendal_fs_list_for_batch;
42use risingwave_connector::source::{
43 ConnectorProperties, SourceEnumeratorContext, SplitEnumerator, SplitImpl,
44};
45use risingwave_pb::batch_plan::plan_node::NodeBody;
46use risingwave_pb::batch_plan::{ExchangeInfo, ScanRange as ScanRangeProto};
47use risingwave_pb::plan_common::Field as PbField;
48use serde::ser::SerializeStruct;
49use serde::{Serialize, Serializer};
50use uuid::Uuid;
51
52use super::SchedulerError;
53use crate::TableCatalog;
54use crate::catalog::TableId;
55use crate::catalog::catalog_service::CatalogReader;
56use crate::optimizer::plan_node::generic::{GenericPlanRef, PhysicalPlanRef};
57use crate::optimizer::plan_node::{
58 BatchIcebergScan, BatchKafkaScan, BatchPlanNodeType, BatchPlanRef as PlanRef, BatchSource,
59 PlanNodeId,
60};
61use crate::optimizer::property::Distribution;
62use crate::scheduler::SchedulerResult;
63
64#[derive(Clone, Debug, Hash, Eq, PartialEq)]
65pub struct QueryId {
66 pub id: String,
67}
68
69impl std::fmt::Display for QueryId {
70 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
71 write!(f, "QueryId:{}", self.id)
72 }
73}
74
75#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
76pub struct StageId(u32);
77
78impl Display for StageId {
79 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80 write!(f, "{}", self.0)
81 }
82}
83
84impl Debug for StageId {
85 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{:?}", self.0)
87 }
88}
89
90impl From<StageId> for u32 {
91 fn from(value: StageId) -> Self {
92 value.0
93 }
94}
95
96impl From<u32> for StageId {
97 fn from(value: u32) -> Self {
98 StageId(value)
99 }
100}
101
102impl Serialize for StageId {
103 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104 where
105 S: Serializer,
106 {
107 self.0.serialize(serializer)
108 }
109}
110
111impl StageId {
112 pub fn inc(&mut self) {
113 self.0 += 1;
114 }
115}
116
117pub const ROOT_TASK_ID: u64 = 0;
119pub const ROOT_TASK_OUTPUT_ID: u64 = 0;
121pub type TaskId = u64;
122
123#[derive(Debug)]
125#[cfg_attr(test, derive(Clone))]
126pub struct ExecutionPlanNode {
127 pub plan_node_id: PlanNodeId,
128 pub plan_node_type: BatchPlanNodeType,
129 pub node: NodeBody,
130 pub schema: Vec<PbField>,
131
132 pub children: Vec<ExecutionPlanNode>,
133
134 pub source_stage_id: Option<StageId>,
139}
140
141impl Serialize for ExecutionPlanNode {
142 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
143 where
144 S: serde::Serializer,
145 {
146 let mut state = serializer.serialize_struct("QueryStage", 5)?;
147 state.serialize_field("plan_node_id", &self.plan_node_id)?;
148 state.serialize_field("plan_node_type", &self.plan_node_type)?;
149 state.serialize_field("schema", &self.schema)?;
150 state.serialize_field("children", &self.children)?;
151 state.serialize_field("source_stage_id", &self.source_stage_id)?;
152 state.end()
153 }
154}
155
156impl TryFrom<PlanRef> for ExecutionPlanNode {
157 type Error = SchedulerError;
158
159 fn try_from(plan_node: PlanRef) -> Result<Self, Self::Error> {
160 Ok(Self {
161 plan_node_id: plan_node.plan_base().id(),
162 plan_node_type: plan_node.node_type(),
163 node: plan_node.try_to_batch_prost_body()?,
164 children: vec![],
165 schema: plan_node.schema().to_prost(),
166 source_stage_id: None,
167 })
168 }
169}
170
171impl ExecutionPlanNode {
172 pub fn node_type(&self) -> BatchPlanNodeType {
173 self.plan_node_type
174 }
175}
176
177pub struct BatchPlanFragmenter {
179 query_id: QueryId,
180 next_stage_id: StageId,
181 worker_node_manager: WorkerNodeSelector,
182 catalog_reader: CatalogReader,
183
184 batch_parallelism: usize,
185
186 stage_graph_builder: Option<StageGraphBuilder>,
187 stage_graph: Option<StageGraph>,
188}
189
190impl Default for QueryId {
191 fn default() -> Self {
192 Self {
193 id: Uuid::new_v4().to_string(),
194 }
195 }
196}
197
198impl BatchPlanFragmenter {
199 pub fn new(
200 worker_node_manager: WorkerNodeSelector,
201 catalog_reader: CatalogReader,
202 batch_parallelism: Option<NonZeroU64>,
203 batch_node: PlanRef,
204 ) -> SchedulerResult<Self> {
205 let batch_parallelism = if let Some(num) = batch_parallelism {
210 min(
212 num.get() as usize,
213 worker_node_manager.schedule_unit_count(),
214 )
215 } else {
216 worker_node_manager.schedule_unit_count()
218 };
219
220 let mut plan_fragmenter = Self {
221 query_id: Default::default(),
222 next_stage_id: 0.into(),
223 worker_node_manager,
224 catalog_reader,
225 batch_parallelism,
226 stage_graph_builder: Some(StageGraphBuilder::new(batch_parallelism)),
227 stage_graph: None,
228 };
229 plan_fragmenter.split_into_stage(batch_node)?;
230 Ok(plan_fragmenter)
231 }
232
233 fn split_into_stage(&mut self, batch_node: PlanRef) -> SchedulerResult<()> {
235 let root_stage_id = self.new_stage(
236 batch_node,
237 Some(Distribution::Single.to_prost(
238 1,
239 &self.catalog_reader,
240 &self.worker_node_manager,
241 self.batch_parallelism,
242 )?),
243 )?;
244 self.stage_graph = Some(
245 self.stage_graph_builder
246 .take()
247 .unwrap()
248 .build(root_stage_id),
249 );
250 Ok(())
251 }
252}
253
254#[derive(Debug)]
256#[cfg_attr(test, derive(Clone))]
257pub struct Query {
258 pub query_id: QueryId,
260 pub stage_graph: StageGraph,
261}
262
263impl Query {
264 pub fn leaf_stages(&self) -> Vec<StageId> {
265 let mut ret_leaf_stages = Vec::new();
266 for stage_id in self.stage_graph.stages.keys() {
267 if self
268 .stage_graph
269 .get_child_stages_unchecked(stage_id)
270 .is_empty()
271 {
272 ret_leaf_stages.push(*stage_id);
273 }
274 }
275 ret_leaf_stages
276 }
277
278 pub fn get_parents(&self, stage_id: &StageId) -> &HashSet<StageId> {
279 self.stage_graph.parent_edges.get(stage_id).unwrap()
280 }
281
282 pub fn root_stage_id(&self) -> StageId {
283 self.stage_graph.root_stage_id
284 }
285
286 pub fn query_id(&self) -> &QueryId {
287 &self.query_id
288 }
289
290 pub fn stages_with_table_scan(&self) -> HashSet<StageId> {
291 self.stage_graph
292 .stages
293 .iter()
294 .filter_map(|(stage_id, stage_query)| {
295 if stage_query.has_table_scan() {
296 Some(*stage_id)
297 } else {
298 None
299 }
300 })
301 .collect()
302 }
303
304 pub fn has_lookup_join_stage(&self) -> bool {
305 self.stage_graph
306 .stages
307 .iter()
308 .any(|(_stage_id, stage_query)| stage_query.has_lookup_join())
309 }
310
311 pub fn stage(&self, stage_id: StageId) -> &QueryStage {
312 &self.stage_graph.stages[&stage_id]
313 }
314
315 pub fn batch_parallelism(&self) -> usize {
316 self.stage_graph.batch_parallelism
317 }
318}
319
320#[derive(Debug, Clone)]
321pub enum SourceFetchParameters {
322 KafkaTimebound {
323 lower: Option<i64>,
324 upper: Option<i64>,
325 },
326 Empty,
327}
328
329#[derive(Debug, Clone)]
330pub enum UnpartitionedData {
331 Iceberg {
332 task: IcebergFileScanTask,
333 limit: Option<u64>,
334 },
335}
336
337#[derive(Debug, Clone)]
338pub struct SourceFetchInfo {
339 pub schema: Schema,
340 pub connector: ConnectorProperties,
343 pub fetch_parameters: SourceFetchParameters,
346}
347
348#[derive(Clone)]
349pub enum SourceScanInfo {
350 Incomplete(SourceFetchInfo),
352 Unpartitioned(UnpartitionedData),
353 Complete(Vec<SplitImpl>),
354}
355
356impl SourceScanInfo {
357 pub fn new(fetch_info: SourceFetchInfo) -> Self {
358 Self::Incomplete(fetch_info)
359 }
360
361 pub async fn complete(self, batch_parallelism: usize) -> SchedulerResult<Self> {
362 match self {
363 SourceScanInfo::Incomplete(fetch_info) => fetch_info.complete(batch_parallelism).await,
364 SourceScanInfo::Unpartitioned(data) => data.complete(batch_parallelism),
365 SourceScanInfo::Complete(_) => {
366 unreachable!("Never call complete when SourceScanInfo is already complete")
367 }
368 }
369 }
370
371 pub fn split_info(&self) -> SchedulerResult<&Vec<SplitImpl>> {
372 match self {
373 Self::Incomplete(_) => Err(SchedulerError::Internal(anyhow!(
374 "Should not get split info from incomplete source scan info"
375 ))),
376 Self::Unpartitioned(_) => Err(SchedulerError::Internal(anyhow!(
377 "Should not get split info from unpartitioned source scan info"
378 ))),
379 Self::Complete(split_info) => Ok(split_info),
380 }
381 }
382}
383
384impl UnpartitionedData {
385 fn complete(self, batch_parallelism: usize) -> SchedulerResult<SourceScanInfo> {
386 let splits = match self {
387 UnpartitionedData::Iceberg { task, limit } => {
388 IcebergScanTaskPlanner::plan_splits(task, batch_parallelism, limit)?
389 .into_iter()
390 .map(SplitImpl::Iceberg)
391 .collect()
392 }
393 };
394 Ok(SourceScanInfo::Complete(splits))
395 }
396}
397
398impl SourceFetchInfo {
399 async fn complete(self, _batch_parallelism: usize) -> SchedulerResult<SourceScanInfo> {
400 match (self.connector, self.fetch_parameters) {
401 (
402 ConnectorProperties::Kafka(prop),
403 SourceFetchParameters::KafkaTimebound { lower, upper },
404 ) => {
405 let mut kafka_enumerator =
406 KafkaSplitEnumerator::new(*prop, SourceEnumeratorContext::dummy().into())
407 .await?;
408 let split_info = kafka_enumerator
409 .list_splits_batch(lower, upper)
410 .await?
411 .into_iter()
412 .map(SplitImpl::Kafka)
413 .collect_vec();
414
415 Ok(SourceScanInfo::Complete(split_info))
416 }
417 (ConnectorProperties::Datagen(prop), SourceFetchParameters::Empty) => {
418 let mut datagen_enumerator =
419 DatagenSplitEnumerator::new(*prop, SourceEnumeratorContext::dummy().into())
420 .await?;
421 let split_info = datagen_enumerator.list_splits().await?;
422 let res = split_info.into_iter().map(SplitImpl::Datagen).collect_vec();
423
424 Ok(SourceScanInfo::Complete(res))
425 }
426 (ConnectorProperties::OpendalS3(prop), SourceFetchParameters::Empty) => {
427 let lister: OpendalEnumerator<OpendalS3> = OpendalEnumerator::new_s3_source(
428 &prop.s3_properties,
429 prop.assume_role,
430 prop.fs_common.compression_format,
431 )?;
432 let stream = build_opendal_fs_list_for_batch(lister);
433
434 let batch_res: Vec<_> = stream.try_collect().await?;
435 let res = batch_res
436 .into_iter()
437 .map(SplitImpl::OpendalS3)
438 .collect_vec();
439
440 Ok(SourceScanInfo::Complete(res))
441 }
442 (ConnectorProperties::Gcs(prop), SourceFetchParameters::Empty) => {
443 let lister: OpendalEnumerator<OpendalGcs> =
444 OpendalEnumerator::new_gcs_source(*prop)?;
445 let stream = build_opendal_fs_list_for_batch(lister);
446 let batch_res: Vec<_> = stream.try_collect().await?;
447 let res = batch_res.into_iter().map(SplitImpl::Gcs).collect_vec();
448
449 Ok(SourceScanInfo::Complete(res))
450 }
451 (ConnectorProperties::Azblob(prop), SourceFetchParameters::Empty) => {
452 let lister: OpendalEnumerator<OpendalAzblob> =
453 OpendalEnumerator::new_azblob_source(*prop)?;
454 let stream = build_opendal_fs_list_for_batch(lister);
455 let batch_res: Vec<_> = stream.try_collect().await?;
456 let res = batch_res.into_iter().map(SplitImpl::Azblob).collect_vec();
457
458 Ok(SourceScanInfo::Complete(res))
459 }
460 (ConnectorProperties::BatchPosixFs(prop), SourceFetchParameters::Empty) => {
461 use risingwave_connector::source::SplitEnumerator;
462 let mut enumerator = BatchPosixFsEnumerator::new(
463 *prop,
464 risingwave_connector::source::SourceEnumeratorContext::dummy().into(),
465 )
466 .await?;
467 let splits = enumerator.list_splits().await?;
468 let res = splits
469 .into_iter()
470 .map(SplitImpl::BatchPosixFs)
471 .collect_vec();
472
473 Ok(SourceScanInfo::Complete(res))
474 }
475 (connector, _) => Err(SchedulerError::Internal(anyhow!(
476 "Unsupported to query directly from this {} source, \
477 please create a table or streaming job from it",
478 connector.kind()
479 ))),
480 }
481 }
482}
483
484#[derive(Clone, Debug)]
485pub struct TableScanInfo {
486 name: String,
488
489 partitions: Option<HashMap<WorkerSlotId, TablePartitionInfo>>,
497}
498
499impl TableScanInfo {
500 pub fn new(name: String, partitions: HashMap<WorkerSlotId, TablePartitionInfo>) -> Self {
502 Self {
503 name,
504 partitions: Some(partitions),
505 }
506 }
507
508 pub fn system_table(name: String) -> Self {
510 Self {
511 name,
512 partitions: None,
513 }
514 }
515
516 pub fn name(&self) -> &str {
517 self.name.as_ref()
518 }
519
520 pub fn partitions(&self) -> Option<&HashMap<WorkerSlotId, TablePartitionInfo>> {
521 self.partitions.as_ref()
522 }
523}
524
525#[derive(Clone, Debug)]
526pub struct TablePartitionInfo {
527 pub vnode_bitmap: Bitmap,
528 pub scan_ranges: Vec<ScanRangeProto>,
529}
530
531#[derive(Clone, Debug, EnumAsInner)]
532pub enum PartitionInfo {
533 Table(TablePartitionInfo),
534 Source(Vec<SplitImpl>),
535 File(Vec<String>),
536}
537
538#[derive(Clone, Debug)]
539pub struct FileScanInfo {
540 pub file_location: Vec<String>,
541}
542
543#[cfg_attr(test, derive(Clone))]
545pub struct QueryStage {
546 pub id: StageId,
547 pub root: ExecutionPlanNode,
548 pub exchange_info: Option<ExchangeInfo>,
549 pub parallelism: Option<u32>,
550 pub table_scan_info: Option<TableScanInfo>,
552 pub source_info: Option<SourceScanInfo>,
553 pub file_scan_info: Option<FileScanInfo>,
554 pub has_lookup_join: bool,
555 pub dml_table_id: Option<TableId>,
556 pub session_id: SessionId,
557 pub batch_enable_distributed_dml: bool,
558
559 children_exchange_distribution: Option<HashMap<StageId, Distribution>>,
561}
562
563impl QueryStage {
564 pub fn has_table_scan(&self) -> bool {
568 self.table_scan_info.is_some()
569 }
570
571 pub fn has_lookup_join(&self) -> bool {
574 self.has_lookup_join
575 }
576
577 pub fn with_exchange_info(
578 self,
579 exchange_info: Option<ExchangeInfo>,
580 parallelism: Option<u32>,
581 ) -> Self {
582 if let Some(exchange_info) = exchange_info {
583 Self {
584 id: self.id,
585 root: self.root,
586 exchange_info: Some(exchange_info),
587 parallelism,
588 table_scan_info: self.table_scan_info,
589 source_info: self.source_info,
590 file_scan_info: self.file_scan_info,
591 has_lookup_join: self.has_lookup_join,
592 dml_table_id: self.dml_table_id,
593 session_id: self.session_id,
594 batch_enable_distributed_dml: self.batch_enable_distributed_dml,
595 children_exchange_distribution: self.children_exchange_distribution,
596 }
597 } else {
598 self
599 }
600 }
601
602 pub fn with_exchange_info_and_complete_source_info(
603 self,
604 exchange_info: Option<ExchangeInfo>,
605 source_info: SourceScanInfo,
606 task_parallelism: u32,
607 ) -> Self {
608 assert!(matches!(source_info, SourceScanInfo::Complete(_)));
609 let exchange_info = if let Some(exchange_info) = exchange_info {
610 Some(exchange_info)
611 } else {
612 self.exchange_info
613 };
614 Self {
615 id: self.id,
616 root: self.root,
617 exchange_info,
618 parallelism: Some(task_parallelism),
619 table_scan_info: self.table_scan_info,
620 source_info: Some(source_info),
621 file_scan_info: self.file_scan_info,
622 has_lookup_join: self.has_lookup_join,
623 dml_table_id: self.dml_table_id,
624 session_id: self.session_id,
625 batch_enable_distributed_dml: self.batch_enable_distributed_dml,
626 children_exchange_distribution: None,
627 }
628 }
629}
630
631impl Debug for QueryStage {
632 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
633 f.debug_struct("QueryStage")
634 .field("id", &self.id)
635 .field("parallelism", &self.parallelism)
636 .field("exchange_info", &self.exchange_info)
637 .field("has_table_scan", &self.has_table_scan())
638 .finish()
639 }
640}
641
642impl Serialize for QueryStage {
643 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
644 where
645 S: serde::Serializer,
646 {
647 let mut state = serializer.serialize_struct("QueryStage", 3)?;
648 state.serialize_field("root", &self.root)?;
649 state.serialize_field("parallelism", &self.parallelism)?;
650 state.serialize_field("exchange_info", &self.exchange_info)?;
651 state.end()
652 }
653}
654
655struct QueryStageBuilder {
656 id: StageId,
657 root: Option<ExecutionPlanNode>,
658 parallelism: Option<u32>,
659 exchange_info: Option<ExchangeInfo>,
660
661 children_stages: Vec<StageId>,
662 table_scan_info: Option<TableScanInfo>,
664 source_info: Option<SourceScanInfo>,
665 file_scan_file: Option<FileScanInfo>,
666 has_lookup_join: bool,
667 dml_table_id: Option<TableId>,
668 session_id: SessionId,
669 batch_enable_distributed_dml: bool,
670
671 children_exchange_distribution: HashMap<StageId, Distribution>,
672}
673
674impl QueryStageBuilder {
675 fn new(
676 id: StageId,
677 parallelism: Option<u32>,
678 exchange_info: Option<ExchangeInfo>,
679 table_scan_info: Option<TableScanInfo>,
680 source_info: Option<SourceScanInfo>,
681 file_scan_file: Option<FileScanInfo>,
682 has_lookup_join: bool,
683 dml_table_id: Option<TableId>,
684 session_id: SessionId,
685 batch_enable_distributed_dml: bool,
686 ) -> Self {
687 Self {
688 id,
689 root: None,
690 parallelism,
691 exchange_info,
692 children_stages: vec![],
693 table_scan_info,
694 source_info,
695 file_scan_file,
696 has_lookup_join,
697 dml_table_id,
698 session_id,
699 batch_enable_distributed_dml,
700 children_exchange_distribution: HashMap::new(),
701 }
702 }
703
704 fn finish(self, stage_graph_builder: &mut StageGraphBuilder) -> StageId {
705 let children_exchange_distribution = if self.parallelism.is_none() {
706 Some(self.children_exchange_distribution)
707 } else {
708 None
709 };
710 let stage = QueryStage {
711 id: self.id,
712 root: self.root.unwrap(),
713 exchange_info: self.exchange_info,
714 parallelism: self.parallelism,
715 table_scan_info: self.table_scan_info,
716 source_info: self.source_info,
717 file_scan_info: self.file_scan_file,
718 has_lookup_join: self.has_lookup_join,
719 dml_table_id: self.dml_table_id,
720 session_id: self.session_id,
721 batch_enable_distributed_dml: self.batch_enable_distributed_dml,
722 children_exchange_distribution,
723 };
724
725 let stage_id = stage.id;
726 stage_graph_builder.add_node(stage);
727 for child_stage_id in self.children_stages {
728 stage_graph_builder.link_to_child(self.id, child_stage_id);
729 }
730 stage_id
731 }
732}
733
734#[derive(Debug, Serialize)]
736#[cfg_attr(test, derive(Clone))]
737pub struct StageGraph {
738 pub root_stage_id: StageId,
739 pub stages: HashMap<StageId, QueryStage>,
740 child_edges: HashMap<StageId, HashSet<StageId>>,
742 parent_edges: HashMap<StageId, HashSet<StageId>>,
744
745 batch_parallelism: usize,
746}
747
748enum StageCompleteInfo {
749 ExchangeInfo((Option<ExchangeInfo>, Option<u32>)),
750 ExchangeWithSourceInfo((Option<ExchangeInfo>, SourceScanInfo, u32)),
751}
752
753impl StageGraph {
754 pub fn get_child_stages_unchecked(&self, stage_id: &StageId) -> &HashSet<StageId> {
755 self.child_edges.get(stage_id).unwrap()
756 }
757
758 pub fn get_child_stages(&self, stage_id: &StageId) -> Option<&HashSet<StageId>> {
759 self.child_edges.get(stage_id)
760 }
761
762 pub fn stage_ids_by_topo_order(&self) -> impl Iterator<Item = StageId> {
764 let mut stack = Vec::with_capacity(self.stages.len());
765 stack.push(self.root_stage_id);
766 let mut ret = Vec::with_capacity(self.stages.len());
767 let mut existing = HashSet::with_capacity(self.stages.len());
768
769 while let Some(s) = stack.pop() {
770 if !existing.contains(&s) {
771 ret.push(s);
772 existing.insert(s);
773 stack.extend(&self.child_edges[&s]);
774 }
775 }
776
777 ret.into_iter().rev()
778 }
779
780 async fn complete(
781 self,
782 catalog_reader: &CatalogReader,
783 worker_node_manager: &WorkerNodeSelector,
784 ) -> SchedulerResult<StageGraph> {
785 let mut complete_stages = HashMap::new();
786 self.complete_stage(
787 self.root_stage_id,
788 None,
789 &mut complete_stages,
790 catalog_reader,
791 worker_node_manager,
792 )
793 .await?;
794 let mut stages = self.stages;
795 Ok(StageGraph {
796 root_stage_id: self.root_stage_id,
797 stages: complete_stages
798 .into_iter()
799 .map(|(stage_id, info)| {
800 let stage = stages.remove(&stage_id).expect("should exist");
801 let stage = match info {
802 StageCompleteInfo::ExchangeInfo((exchange_info, parallelism)) => {
803 stage.with_exchange_info(exchange_info, parallelism)
804 }
805 StageCompleteInfo::ExchangeWithSourceInfo((
806 exchange_info,
807 source_info,
808 parallelism,
809 )) => stage.with_exchange_info_and_complete_source_info(
810 exchange_info,
811 source_info,
812 parallelism,
813 ),
814 };
815 (stage_id, stage)
816 })
817 .collect(),
818 child_edges: self.child_edges,
819 parent_edges: self.parent_edges,
820 batch_parallelism: self.batch_parallelism,
821 })
822 }
823
824 #[async_recursion]
825 async fn complete_stage(
826 &self,
827 stage_id: StageId,
828 exchange_info: Option<ExchangeInfo>,
829 complete_stages: &mut HashMap<StageId, StageCompleteInfo>,
830 catalog_reader: &CatalogReader,
831 worker_node_manager: &WorkerNodeSelector,
832 ) -> SchedulerResult<()> {
833 let stage = &self.stages[&stage_id];
834 let parallelism = if stage.parallelism.is_some() {
835 complete_stages.insert(
837 stage.id,
838 StageCompleteInfo::ExchangeInfo((exchange_info, stage.parallelism)),
839 );
840 None
841 } else if matches!(
842 stage.source_info,
843 Some(SourceScanInfo::Incomplete(_)) | Some(SourceScanInfo::Unpartitioned(_))
844 ) {
845 let complete_source_info = stage
846 .source_info
847 .as_ref()
848 .unwrap()
849 .clone()
850 .complete(self.batch_parallelism)
851 .await?;
852
853 let task_parallelism = match &stage.source_info {
861 Some(SourceScanInfo::Incomplete(source_fetch_info)) => {
862 match source_fetch_info.connector {
863 ConnectorProperties::Gcs(_)
864 | ConnectorProperties::OpendalS3(_)
865 | ConnectorProperties::Azblob(_) => (min(
866 complete_source_info.split_info().unwrap().len() as u32,
867 (self.batch_parallelism / 2) as u32,
868 ))
869 .max(1),
870 _ => complete_source_info.split_info().unwrap().len() as u32,
871 }
872 }
873 _ => complete_source_info.split_info().unwrap().len() as u32,
874 };
875 let complete_stage_info = StageCompleteInfo::ExchangeWithSourceInfo((
878 exchange_info,
879 complete_source_info,
880 task_parallelism,
881 ));
882 complete_stages.insert(stage.id, complete_stage_info);
883 Some(task_parallelism)
884 } else {
885 assert!(stage.file_scan_info.is_some());
886 let parallelism = min(
887 self.batch_parallelism / 2,
888 stage.file_scan_info.as_ref().unwrap().file_location.len(),
889 );
890 complete_stages.insert(
891 stage.id,
892 StageCompleteInfo::ExchangeInfo((exchange_info, Some(parallelism as u32))),
893 );
894 None
895 };
896
897 for child_stage_id in self
898 .child_edges
899 .get(&stage.id)
900 .map(|edges| edges.iter())
901 .into_iter()
902 .flatten()
903 {
904 let exchange_info = if let Some(parallelism) = parallelism {
905 let exchange_distribution = stage
906 .children_exchange_distribution
907 .as_ref()
908 .unwrap()
909 .get(child_stage_id)
910 .expect("Exchange distribution is not consistent with the stage graph");
911 Some(exchange_distribution.to_prost(
912 parallelism,
913 catalog_reader,
914 worker_node_manager,
915 self.batch_parallelism,
916 )?)
917 } else {
918 None
919 };
920 self.complete_stage(
921 *child_stage_id,
922 exchange_info,
923 complete_stages,
924 catalog_reader,
925 worker_node_manager,
926 )
927 .await?;
928 }
929
930 Ok(())
931 }
932
933 pub fn to_petgraph(&self) -> Graph<String, String, Directed> {
935 let mut graph = Graph::<String, String, Directed>::new();
936
937 let mut node_indices = HashMap::new();
938
939 for (&stage_id, stage_ref) in self.stages.iter().sorted_by_key(|(id, _)| **id) {
941 let node_label = format!("Stage {}: {:?}", stage_id, stage_ref);
942 let node_index = graph.add_node(node_label);
943 node_indices.insert(stage_id, node_index);
944 }
945
946 for (&parent_id, children) in &self.child_edges {
948 if let Some(&parent_index) = node_indices.get(&parent_id) {
949 for &child_id in children {
950 if let Some(&child_index) = node_indices.get(&child_id) {
951 graph.add_edge(parent_index, child_index, "".to_owned());
953 }
954 }
955 }
956 }
957
958 graph
959 }
960}
961
962struct StageGraphBuilder {
963 stages: HashMap<StageId, QueryStage>,
964 child_edges: HashMap<StageId, HashSet<StageId>>,
965 parent_edges: HashMap<StageId, HashSet<StageId>>,
966 batch_parallelism: usize,
967}
968
969impl StageGraphBuilder {
970 pub fn new(batch_parallelism: usize) -> Self {
971 Self {
972 stages: HashMap::new(),
973 child_edges: HashMap::new(),
974 parent_edges: HashMap::new(),
975 batch_parallelism,
976 }
977 }
978
979 pub fn build(self, root_stage_id: StageId) -> StageGraph {
980 StageGraph {
981 root_stage_id,
982 stages: self.stages,
983 child_edges: self.child_edges,
984 parent_edges: self.parent_edges,
985 batch_parallelism: self.batch_parallelism,
986 }
987 }
988
989 pub fn link_to_child(&mut self, parent_id: StageId, child_id: StageId) {
992 self.child_edges
993 .get_mut(&parent_id)
994 .unwrap()
995 .insert(child_id);
996 self.parent_edges
997 .get_mut(&child_id)
998 .unwrap()
999 .insert(parent_id);
1000 }
1001
1002 pub fn add_node(&mut self, stage: QueryStage) {
1003 self.child_edges.insert(stage.id, HashSet::new());
1005 self.parent_edges.insert(stage.id, HashSet::new());
1006 self.stages.insert(stage.id, stage);
1007 }
1008}
1009
1010impl BatchPlanFragmenter {
1011 pub async fn generate_complete_query(self) -> SchedulerResult<Query> {
1017 let stage_graph = self.stage_graph.unwrap();
1018 let new_stage_graph = stage_graph
1019 .complete(&self.catalog_reader, &self.worker_node_manager)
1020 .await?;
1021 Ok(Query {
1022 query_id: self.query_id,
1023 stage_graph: new_stage_graph,
1024 })
1025 }
1026
1027 fn new_stage(
1028 &mut self,
1029 root: PlanRef,
1030 exchange_info: Option<ExchangeInfo>,
1031 ) -> SchedulerResult<StageId> {
1032 let next_stage_id = self.next_stage_id;
1033 self.next_stage_id.inc();
1034
1035 let mut table_scan_info = None;
1036 let mut source_info = None;
1037 let mut file_scan_info = None;
1038
1039 if let Some(info) = self.collect_stage_table_scan(root.clone())? {
1042 table_scan_info = Some(info);
1043 } else if let Some(info) = Self::collect_stage_source(root.clone())? {
1044 source_info = Some(info);
1045 } else if let Some(info) = Self::collect_stage_file_scan(root.clone())? {
1046 file_scan_info = Some(info);
1047 }
1048
1049 let mut has_lookup_join = false;
1050 let parallelism = match root.distribution() {
1051 Distribution::Single => {
1052 has_lookup_join = self
1056 .collect_stage_lookup_join_parallelism(root.clone())?
1057 .is_some();
1058
1059 if let Some(info) = &mut table_scan_info {
1060 if let Some(partitions) = &mut info.partitions {
1061 if partitions.len() != 1 {
1062 tracing::warn!(
1065 "The stage has single distribution, but contains a scan of table `{}` with {} partitions. A single random worker will be assigned",
1066 info.name,
1067 partitions.len()
1068 );
1069
1070 *partitions = partitions
1071 .drain()
1072 .take(1)
1073 .update(|(_, info)| {
1074 info.vnode_bitmap = Bitmap::ones(info.vnode_bitmap.len());
1075 })
1076 .collect();
1077 }
1078 } else {
1079 }
1081 } else if source_info.is_some() {
1082 return Err(SchedulerError::Internal(anyhow!(
1083 "The stage has single distribution, but contains a source operator"
1084 )));
1085 }
1086 1
1087 }
1088 _ => {
1089 if let Some(table_scan_info) = &table_scan_info {
1090 table_scan_info
1091 .partitions
1092 .as_ref()
1093 .map(|m| m.len())
1094 .unwrap_or(1)
1095 } else if let Some(lookup_join_parallelism) =
1096 self.collect_stage_lookup_join_parallelism(root.clone())?
1097 {
1098 has_lookup_join = true;
1099 lookup_join_parallelism
1100 } else if source_info.is_some() {
1101 0
1102 } else if file_scan_info.is_some() {
1103 1
1104 } else {
1105 self.batch_parallelism
1106 }
1107 }
1108 };
1109 if source_info.is_none() && file_scan_info.is_none() && parallelism == 0 {
1110 return Err(BatchError::EmptyWorkerNodes.into());
1111 }
1112 let parallelism = if parallelism == 0 {
1113 None
1114 } else {
1115 Some(parallelism as u32)
1116 };
1117 let dml_table_id = Self::collect_dml_table_id(&root);
1118 let mut builder = QueryStageBuilder::new(
1119 next_stage_id,
1120 parallelism,
1121 exchange_info,
1122 table_scan_info,
1123 source_info,
1124 file_scan_info,
1125 has_lookup_join,
1126 dml_table_id,
1127 root.ctx().session_ctx().session_id(),
1128 root.ctx()
1129 .session_ctx()
1130 .config()
1131 .batch_enable_distributed_dml(),
1132 );
1133
1134 self.visit_node(root, &mut builder, None)?;
1135
1136 Ok(builder.finish(self.stage_graph_builder.as_mut().unwrap()))
1137 }
1138
1139 fn visit_node(
1140 &mut self,
1141 node: PlanRef,
1142 builder: &mut QueryStageBuilder,
1143 parent_exec_node: Option<&mut ExecutionPlanNode>,
1144 ) -> SchedulerResult<()> {
1145 match node.node_type() {
1146 BatchPlanNodeType::BatchExchange => {
1147 self.visit_exchange(node, builder, parent_exec_node)?;
1148 }
1149 _ => {
1150 let mut execution_plan_node = ExecutionPlanNode::try_from(node.clone())?;
1151
1152 for child in node.inputs() {
1153 self.visit_node(child, builder, Some(&mut execution_plan_node))?;
1154 }
1155
1156 if let Some(parent) = parent_exec_node {
1157 parent.children.push(execution_plan_node);
1158 } else {
1159 builder.root = Some(execution_plan_node);
1160 }
1161 }
1162 }
1163 Ok(())
1164 }
1165
1166 fn visit_exchange(
1167 &mut self,
1168 node: PlanRef,
1169 builder: &mut QueryStageBuilder,
1170 parent_exec_node: Option<&mut ExecutionPlanNode>,
1171 ) -> SchedulerResult<()> {
1172 let mut execution_plan_node = ExecutionPlanNode::try_from(node.clone())?;
1173 let child_exchange_info = if let Some(parallelism) = builder.parallelism {
1174 Some(node.distribution().to_prost(
1175 parallelism,
1176 &self.catalog_reader,
1177 &self.worker_node_manager,
1178 self.batch_parallelism,
1179 )?)
1180 } else {
1181 None
1182 };
1183 let child_stage_id = self.new_stage(node.inputs()[0].clone(), child_exchange_info)?;
1184 execution_plan_node.source_stage_id = Some(child_stage_id);
1185 if builder.parallelism.is_none() {
1186 builder
1187 .children_exchange_distribution
1188 .insert(child_stage_id, node.distribution().clone());
1189 }
1190
1191 if let Some(parent) = parent_exec_node {
1192 parent.children.push(execution_plan_node);
1193 } else {
1194 builder.root = Some(execution_plan_node);
1195 }
1196
1197 builder.children_stages.push(child_stage_id);
1198 Ok(())
1199 }
1200
1201 fn collect_stage_source(node: PlanRef) -> SchedulerResult<Option<SourceScanInfo>> {
1206 if node.node_type() == BatchPlanNodeType::BatchExchange {
1207 return Ok(None);
1209 }
1210
1211 if let Some(batch_kafka_node) = node.as_batch_kafka_scan() {
1212 let batch_kafka_scan: &BatchKafkaScan = batch_kafka_node;
1213 let source_catalog = batch_kafka_scan.source_catalog();
1214 if let Some(source_catalog) = source_catalog {
1215 let property =
1216 ConnectorProperties::extract(source_catalog.with_properties.clone(), false)?;
1217 let timestamp_bound = batch_kafka_scan.kafka_timestamp_range_value();
1218 return Ok(Some(SourceScanInfo::new(SourceFetchInfo {
1219 schema: batch_kafka_scan.base.schema().clone(),
1220 connector: property,
1221 fetch_parameters: SourceFetchParameters::KafkaTimebound {
1222 lower: timestamp_bound.0,
1223 upper: timestamp_bound.1,
1224 },
1225 })));
1226 }
1227 } else if let Some(batch_iceberg_scan) = node.as_batch_iceberg_scan() {
1228 let batch_iceberg_scan: &BatchIcebergScan = batch_iceberg_scan;
1229 let task = batch_iceberg_scan.task.clone();
1230 let limit = batch_iceberg_scan.limit();
1231 return Ok(Some(SourceScanInfo::Unpartitioned(
1232 UnpartitionedData::Iceberg { task, limit },
1233 )));
1234 } else if let Some(source_node) = node.as_batch_source() {
1235 let source_node: &BatchSource = source_node;
1237 let source_catalog = source_node.source_catalog();
1238 if let Some(source_catalog) = source_catalog {
1239 let property =
1240 ConnectorProperties::extract(source_catalog.with_properties.clone(), false)?;
1241 return Ok(Some(SourceScanInfo::new(SourceFetchInfo {
1242 schema: source_node.base.schema().clone(),
1243 connector: property,
1244 fetch_parameters: SourceFetchParameters::Empty,
1245 })));
1246 }
1247 }
1248
1249 node.inputs()
1250 .into_iter()
1251 .find_map(|n| Self::collect_stage_source(n).transpose())
1252 .transpose()
1253 }
1254
1255 fn collect_stage_file_scan(node: PlanRef) -> SchedulerResult<Option<FileScanInfo>> {
1256 if node.node_type() == BatchPlanNodeType::BatchExchange {
1257 return Ok(None);
1259 }
1260
1261 if let Some(batch_file_scan) = node.as_batch_file_scan() {
1262 return Ok(Some(FileScanInfo {
1263 file_location: batch_file_scan.core.file_location(),
1264 }));
1265 }
1266
1267 node.inputs()
1268 .into_iter()
1269 .find_map(|n| Self::collect_stage_file_scan(n).transpose())
1270 .transpose()
1271 }
1272
1273 fn collect_stage_table_scan(&self, node: PlanRef) -> SchedulerResult<Option<TableScanInfo>> {
1278 let build_table_scan_info = |name, table_catalog: &TableCatalog, scan_range| {
1279 let vnode_mapping = self
1280 .worker_node_manager
1281 .fragment_mapping(table_catalog.fragment_id, self.batch_parallelism)?;
1282 let partitions = derive_partitions(scan_range, table_catalog, &vnode_mapping)?;
1283 let info = TableScanInfo::new(name, partitions);
1284 Ok(Some(info))
1285 };
1286 if node.node_type() == BatchPlanNodeType::BatchExchange {
1287 return Ok(None);
1289 }
1290 if let Some(scan_node) = node.as_batch_sys_seq_scan() {
1291 let name = scan_node.core().table.name.clone();
1292 Ok(Some(TableScanInfo::system_table(name)))
1293 } else if let Some(scan_node) = node.as_batch_log_seq_scan() {
1294 build_table_scan_info(
1295 scan_node.core().table_name.clone(),
1296 &scan_node.core().table,
1297 &[],
1298 )
1299 } else if let Some(scan_node) = node.as_batch_seq_scan() {
1300 build_table_scan_info(
1301 scan_node.core().table_name().to_owned(),
1302 &scan_node.core().table_catalog,
1303 scan_node.scan_ranges(),
1304 )
1305 } else {
1306 node.inputs()
1307 .into_iter()
1308 .find_map(|n| self.collect_stage_table_scan(n).transpose())
1309 .transpose()
1310 }
1311 }
1312
1313 fn collect_dml_table_id(node: &PlanRef) -> Option<TableId> {
1315 if node.node_type() == BatchPlanNodeType::BatchExchange {
1316 return None;
1317 }
1318 if let Some(insert) = node.as_batch_insert() {
1319 Some(insert.core.table_id)
1320 } else if let Some(update) = node.as_batch_update() {
1321 Some(update.core.table_id)
1322 } else if let Some(delete) = node.as_batch_delete() {
1323 Some(delete.core.table_id)
1324 } else {
1325 node.inputs()
1326 .into_iter()
1327 .find_map(|n| Self::collect_dml_table_id(&n))
1328 }
1329 }
1330
1331 fn collect_stage_lookup_join_parallelism(
1332 &self,
1333 node: PlanRef,
1334 ) -> SchedulerResult<Option<usize>> {
1335 if node.node_type() == BatchPlanNodeType::BatchExchange {
1336 return Ok(None);
1338 }
1339 if let Some(lookup_join) = node.as_batch_lookup_join() {
1340 let table_catalog = lookup_join.right_table();
1341 let vnode_mapping = self
1342 .worker_node_manager
1343 .fragment_mapping(table_catalog.fragment_id, self.batch_parallelism)?;
1344 let parallelism = vnode_mapping.iter().sorted().dedup().count();
1345 Ok(Some(parallelism))
1346 } else {
1347 node.inputs()
1348 .into_iter()
1349 .find_map(|n| self.collect_stage_lookup_join_parallelism(n).transpose())
1350 .transpose()
1351 }
1352 }
1353}
1354
1355fn derive_partitions(
1358 scan_ranges: &[ScanRange],
1359 table_catalog: &TableCatalog,
1360 vnode_mapping: &WorkerSlotMapping,
1361) -> SchedulerResult<HashMap<WorkerSlotId, TablePartitionInfo>> {
1362 let vnode_mapping = if table_catalog.vnode_count.value() != vnode_mapping.len() {
1363 assert_eq!(
1367 table_catalog.vnode_count.value(),
1368 1,
1369 "fragment vnode count {} does not match table vnode count {}",
1370 vnode_mapping.len(),
1371 table_catalog.vnode_count.value(),
1372 );
1373 &WorkerSlotMapping::new_single(vnode_mapping.iter().next().unwrap())
1374 } else {
1375 vnode_mapping
1376 };
1377 let vnode_count = vnode_mapping.len();
1378
1379 let mut partitions: HashMap<WorkerSlotId, (BitmapBuilder, Vec<_>)> = HashMap::new();
1380
1381 if scan_ranges.is_empty() {
1382 return Ok(vnode_mapping
1383 .to_bitmaps()
1384 .into_iter()
1385 .map(|(k, vnode_bitmap)| {
1386 (
1387 k,
1388 TablePartitionInfo {
1389 vnode_bitmap,
1390 scan_ranges: vec![],
1391 },
1392 )
1393 })
1394 .collect());
1395 }
1396
1397 let table_distribution = TableDistribution::new_from_storage_table_desc(
1398 Some(Bitmap::ones(vnode_count).into()),
1399 &table_catalog.table_desc().try_to_protobuf()?,
1400 );
1401
1402 for scan_range in scan_ranges {
1403 let vnode = scan_range.try_compute_vnode(&table_distribution);
1404 match vnode {
1405 None => {
1406 vnode_mapping.to_bitmaps().into_iter().for_each(
1408 |(worker_slot_id, vnode_bitmap)| {
1409 let (bitmap, scan_ranges) = partitions
1410 .entry(worker_slot_id)
1411 .or_insert_with(|| (BitmapBuilder::zeroed(vnode_count), vec![]));
1412 vnode_bitmap
1413 .iter()
1414 .enumerate()
1415 .for_each(|(vnode, b)| bitmap.set(vnode, b));
1416 scan_ranges.push(scan_range.to_protobuf());
1417 },
1418 );
1419 }
1420 Some(vnode) => {
1422 let worker_slot_id = vnode_mapping[vnode];
1423 let (bitmap, scan_ranges) = partitions
1424 .entry(worker_slot_id)
1425 .or_insert_with(|| (BitmapBuilder::zeroed(vnode_count), vec![]));
1426 bitmap.set(vnode.to_index(), true);
1427 scan_ranges.push(scan_range.to_protobuf());
1428 }
1429 }
1430 }
1431
1432 Ok(partitions
1433 .into_iter()
1434 .map(|(k, (bitmap, scan_ranges))| {
1435 (
1436 k,
1437 TablePartitionInfo {
1438 vnode_bitmap: bitmap.finish(),
1439 scan_ranges,
1440 },
1441 )
1442 })
1443 .collect())
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448 use std::collections::{HashMap, HashSet};
1449
1450 use risingwave_pb::batch_plan::plan_node::NodeBody;
1451
1452 use crate::optimizer::plan_node::BatchPlanNodeType;
1453 use crate::scheduler::plan_fragmenter::StageId;
1454
1455 #[tokio::test]
1456 async fn test_fragmenter() {
1457 let query = crate::scheduler::distributed::tests::create_query().await;
1458
1459 assert_eq!(query.stage_graph.root_stage_id, 0.into());
1460 assert_eq!(query.stage_graph.stages.len(), 4);
1461
1462 assert_eq!(
1464 query.stage_graph.child_edges[&0.into()],
1465 HashSet::from_iter([1.into()])
1466 );
1467 assert_eq!(
1468 query.stage_graph.child_edges[&1.into()],
1469 HashSet::from_iter([2.into(), 3.into()])
1470 );
1471 assert_eq!(query.stage_graph.child_edges[&2.into()], HashSet::new());
1472 assert_eq!(query.stage_graph.child_edges[&3.into()], HashSet::new());
1473
1474 assert_eq!(query.stage_graph.parent_edges[&0.into()], HashSet::new());
1476 assert_eq!(
1477 query.stage_graph.parent_edges[&1.into()],
1478 HashSet::from_iter([0.into()])
1479 );
1480 assert_eq!(
1481 query.stage_graph.parent_edges[&2.into()],
1482 HashSet::from_iter([1.into()])
1483 );
1484 assert_eq!(
1485 query.stage_graph.parent_edges[&3.into()],
1486 HashSet::from_iter([1.into()])
1487 );
1488
1489 {
1491 let stage_id_to_pos: HashMap<StageId, usize> = query
1492 .stage_graph
1493 .stage_ids_by_topo_order()
1494 .enumerate()
1495 .map(|(pos, stage_id)| (stage_id, pos))
1496 .collect();
1497
1498 for stage_id in query.stage_graph.stages.keys() {
1499 let stage_pos = stage_id_to_pos[stage_id];
1500 for child_stage_id in &query.stage_graph.child_edges[stage_id] {
1501 let child_pos = stage_id_to_pos[child_stage_id];
1502 assert!(stage_pos > child_pos);
1503 }
1504 }
1505 }
1506
1507 let root_exchange = query.stage_graph.stages.get(&0.into()).unwrap();
1509 assert_eq!(
1510 root_exchange.root.node_type(),
1511 BatchPlanNodeType::BatchExchange
1512 );
1513 assert_eq!(root_exchange.root.source_stage_id, Some(1.into()));
1514 assert!(matches!(root_exchange.root.node, NodeBody::Exchange(_)));
1515 assert_eq!(root_exchange.parallelism, Some(1));
1516 assert!(!root_exchange.has_table_scan());
1517
1518 let join_node = query.stage_graph.stages.get(&1.into()).unwrap();
1519 assert_eq!(join_node.root.node_type(), BatchPlanNodeType::BatchHashJoin);
1520 assert_eq!(join_node.parallelism, Some(24));
1521
1522 assert!(matches!(join_node.root.node, NodeBody::HashJoin(_)));
1523 assert_eq!(join_node.root.source_stage_id, None);
1524 assert_eq!(2, join_node.root.children.len());
1525
1526 assert!(matches!(
1527 join_node.root.children[0].node,
1528 NodeBody::Exchange(_)
1529 ));
1530 assert_eq!(join_node.root.children[0].source_stage_id, Some(2.into()));
1531 assert_eq!(0, join_node.root.children[0].children.len());
1532
1533 assert!(matches!(
1534 join_node.root.children[1].node,
1535 NodeBody::Exchange(_)
1536 ));
1537 assert_eq!(join_node.root.children[1].source_stage_id, Some(3.into()));
1538 assert_eq!(0, join_node.root.children[1].children.len());
1539 assert!(!join_node.has_table_scan());
1540
1541 let scan_node1 = query.stage_graph.stages.get(&2.into()).unwrap();
1542 assert_eq!(scan_node1.root.node_type(), BatchPlanNodeType::BatchSeqScan);
1543 assert_eq!(scan_node1.root.source_stage_id, None);
1544 assert_eq!(0, scan_node1.root.children.len());
1545 assert!(scan_node1.has_table_scan());
1546
1547 let scan_node2 = query.stage_graph.stages.get(&3.into()).unwrap();
1548 assert_eq!(scan_node2.root.node_type(), BatchPlanNodeType::BatchFilter);
1549 assert_eq!(scan_node2.root.source_stage_id, None);
1550 assert_eq!(1, scan_node2.root.children.len());
1551 assert!(scan_node2.has_table_scan());
1552 }
1553}