1use std::num::NonZeroU32;
16use std::ops::DerefMut;
17use std::sync::Arc;
18
19use risingwave_pb::catalog::PbVectorIndexInfo;
20
21pub mod plan_node;
22
23use plan_node::StreamFilter;
24pub use plan_node::{Explain, LogicalPlanRef, PlanRef};
25
26pub mod property;
27
28mod delta_join_solver;
29mod heuristic_optimizer;
30mod plan_rewriter;
31
32mod plan_visitor;
33
34#[cfg(feature = "datafusion")]
35pub use plan_visitor::DataFusionExecuteCheckerExt;
36pub use plan_visitor::{
37 ExecutionModeDecider, PlanVisitor, RelationCollectorVisitor, SysTableVisitor,
38};
39use risingwave_pb::plan_common::source_refresh_mode::RefreshMode;
40
41pub mod backfill_order_strategy;
42mod logical_optimization;
43mod optimizer_context;
44pub mod plan_expr_rewriter;
45mod plan_expr_visitor;
46mod rule;
47pub mod variant_key;
48
49use std::collections::{BTreeMap, HashMap};
50use std::marker::PhantomData;
51
52use educe::Educe;
53use fixedbitset::FixedBitSet;
54use itertools::Itertools;
55pub use logical_optimization::*;
56pub use optimizer_context::*;
57use plan_expr_rewriter::ConstEvalRewriter;
58use property::Order;
59use risingwave_common::bail;
60use risingwave_common::catalog::{
61 ColumnCatalog, ColumnDesc, ConflictBehavior, Field, FieldDisplay, Schema,
62};
63use risingwave_common::session_config::LocalityBackfillMode;
64use risingwave_common::types::DataType;
65use risingwave_common::util::column_index_mapping::ColIndexMapping;
66use risingwave_common::util::iter_util::ZipEqDebug;
67use risingwave_connector::WithPropertiesExt;
68use risingwave_connector::sink::catalog::SinkFormatDesc;
69use risingwave_connector::sink::iceberg::ENABLE_PK_INDEX;
70
71use self::heuristic_optimizer::ApplyOrder;
72use self::plan_node::generic::{self, PhysicalPlanRef};
73use self::plan_node::{
74 BatchProject, LogicalProject, LogicalSource, PartitionComputeInfo, StreamDml,
75 StreamMaterialize, StreamProject, StreamRowIdGen, StreamSink, StreamWatermarkFilter,
76 ToStreamContext, stream_enforce_eowc_requirement,
77};
78#[cfg(debug_assertions)]
79use self::plan_visitor::InputRefValidator;
80use self::plan_visitor::{CardinalityVisitor, StreamKeyChecker, has_batch_exchange};
81use self::property::{Cardinality, RequiredDist};
82use self::rule::*;
83use self::variant_key::variant_key_error;
84use crate::TableCatalog;
85use crate::catalog::table_catalog::TableType;
86use crate::catalog::{DatabaseId, SchemaId};
87use crate::error::{ErrorCode, Result};
88use crate::expr::TimestamptzExprFinder;
89use crate::handler::create_table::{CreateTableInfo, CreateTableProps};
90use crate::optimizer::plan_node::generic::{GenericPlanRef, SourceNodeKind, Union};
91use crate::optimizer::plan_node::{
92 BackfillType, Batch, BatchExchange, BatchPlanNodeType, BatchPlanRef, ConventionMarker,
93 PlanTreeNode, RewriteStreamContext, Stream, StreamExchange, StreamPlanRef, StreamUnion,
94 StreamUpstreamSinkUnion, StreamVectorIndexWrite, ToStream, VisitExprsRecursive,
95};
96use crate::optimizer::plan_visitor::{
97 LocalityBackfillScanEstimator, LocalityProviderCounter, RwTimestampValidator,
98 TemporalJoinValidator,
99};
100use crate::optimizer::property::Distribution;
101use crate::utils::{
102 ColIndexMappingRewriteExt, MV_REFRESH_INTERVAL_SEC_KEY, WithOptionsSecResolved,
103};
104
105#[derive(Educe)]
116#[educe(Debug, Clone)]
117pub struct PlanRoot<P: PlanPhase> {
118 pub plan: PlanRef<P::Convention>,
120 #[educe(Debug(ignore), Clone(method(PhantomData::clone)))]
122 _phase: PhantomData<P>,
123 required_dist: RequiredDist,
124 required_order: Order,
125 out_fields: FixedBitSet,
126 out_names: Vec<String>,
127}
128
129pub trait PlanPhase {
135 type Convention: ConventionMarker;
136}
137
138macro_rules! for_all_phase {
139 () => {
140 for_all_phase! {
141 { Logical, $crate::optimizer::plan_node::Logical },
142 { BatchOptimizedLogical, $crate::optimizer::plan_node::Logical },
143 { StreamOptimizedLogical, $crate::optimizer::plan_node::Stream },
144 { Batch, $crate::optimizer::plan_node::Batch },
145 { Stream, $crate::optimizer::plan_node::Stream }
146 }
147 };
148 ($({$phase:ident, $convention:ty}),+ $(,)?) => {
149 $(
150 paste::paste! {
151 pub struct [< PlanPhase$phase >];
152 impl PlanPhase for [< PlanPhase$phase >] {
153 type Convention = $convention;
154 }
155 pub type [< $phase PlanRoot >] = PlanRoot<[< PlanPhase$phase >]>;
156 }
157 )+
158 }
159}
160
161for_all_phase!();
162
163impl LogicalPlanRoot {
164 pub fn new_with_logical_plan(
165 plan: LogicalPlanRef,
166 required_dist: RequiredDist,
167 required_order: Order,
168 out_fields: FixedBitSet,
169 out_names: Vec<String>,
170 ) -> Self {
171 Self::new_inner(plan, required_dist, required_order, out_fields, out_names)
172 }
173}
174
175impl BatchPlanRoot {
176 pub fn new_with_batch_plan(
177 plan: BatchPlanRef,
178 required_dist: RequiredDist,
179 required_order: Order,
180 out_fields: FixedBitSet,
181 out_names: Vec<String>,
182 ) -> Self {
183 Self::new_inner(plan, required_dist, required_order, out_fields, out_names)
184 }
185}
186
187impl<P: PlanPhase> PlanRoot<P> {
188 fn new_inner(
189 plan: PlanRef<P::Convention>,
190 required_dist: RequiredDist,
191 required_order: Order,
192 out_fields: FixedBitSet,
193 out_names: Vec<String>,
194 ) -> Self {
195 let input_schema = plan.schema();
196 assert_eq!(input_schema.fields().len(), out_fields.len());
197 assert_eq!(out_fields.count_ones(..), out_names.len());
198
199 Self {
200 plan,
201 _phase: PhantomData,
202 required_dist,
203 required_order,
204 out_fields,
205 out_names,
206 }
207 }
208
209 fn into_phase<P2: PlanPhase>(self, plan: PlanRef<P2::Convention>) -> PlanRoot<P2> {
210 PlanRoot {
211 plan,
212 _phase: PhantomData,
213 required_dist: self.required_dist,
214 required_order: self.required_order,
215 out_fields: self.out_fields,
216 out_names: self.out_names,
217 }
218 }
219
220 pub fn set_out_names(&mut self, out_names: Vec<String>) -> Result<()> {
225 if out_names.len() != self.out_fields.count_ones(..) {
226 Err(ErrorCode::InvalidInputSyntax(
227 "number of column names does not match number of columns".to_owned(),
228 ))?
229 }
230 self.out_names = out_names;
231 Ok(())
232 }
233
234 pub fn schema(&self) -> Schema {
236 Schema {
239 fields: self
240 .out_fields
241 .ones()
242 .map(|i| self.plan.schema().fields()[i].clone())
243 .zip_eq_debug(&self.out_names)
244 .map(|(field, name)| Field {
245 name: name.clone(),
246 ..field
247 })
248 .collect(),
249 }
250 }
251}
252
253fn resolve_locality_backfill(
254 ctx: &OptimizerContext,
255 plan: LogicalPlanRef,
256 backfill_type: BackfillType,
257) -> bool {
258 let config = ctx.session_ctx().config();
259 let enabled = config.enable_locality_backfill();
260 let mode = config.locality_backfill_mode();
261 let min_size = config.auto_locality_backfill_min_size();
262 drop(config);
263
264 if !enabled {
265 return false;
266 }
267
268 match mode {
269 LocalityBackfillMode::Always => true,
270 LocalityBackfillMode::Auto => {
271 if backfill_type.without_snapshot()
272 || risingwave_common::license::Feature::LocalityBackfill
273 .check_available()
274 .is_err()
275 {
276 false
277 } else {
278 let catalog_reader = ctx.session_ctx().env().catalog_reader().read_guard();
279 let estimated_backfill_size =
280 LocalityBackfillScanEstimator::estimate(plan, catalog_reader.table_stats());
281 estimated_backfill_size >= min_size
282 }
283 }
284 }
285}
286
287const UNLICENSED_LOCALITY_PROVIDER_LIMIT: usize = 5;
288
289fn locality_backfill_requires_license(
290 mode: LocalityBackfillMode,
291 locality_provider_count: usize,
292) -> bool {
293 locality_provider_count > 0
294 && (mode == LocalityBackfillMode::Auto
295 || locality_provider_count > UNLICENSED_LOCALITY_PROVIDER_LIMIT)
296}
297
298fn rewrite_logical_plan_for_stream(
299 plan: &LogicalPlanRef,
300 backfill_type: BackfillType,
301 locality_backfill_enabled: bool,
302) -> Result<(LogicalPlanRef, ColIndexMapping)> {
303 let (plan, out_col_change) = plan.logical_rewrite_for_stream(
304 &mut RewriteStreamContext::new_with_backfill_type(backfill_type, locality_backfill_enabled),
305 )?;
306 if out_col_change.is_injective() {
307 Ok((plan, out_col_change))
308 } else {
309 let mut output_indices = (0..plan.schema().len()).collect_vec();
310 #[expect(unused_assignments)]
311 let (mut map, mut target_size) = out_col_change.into_parts();
312
313 target_size = plan.schema().len();
316 let mut tar_exists = vec![false; target_size];
317 for i in map.iter_mut().flatten() {
318 if tar_exists[*i] {
319 output_indices.push(*i);
320 *i = target_size;
321 target_size += 1;
322 } else {
323 tar_exists[*i] = true;
324 }
325 }
326 let plan = LogicalProject::with_out_col_idx(plan, output_indices.into_iter());
327 let out_col_change = ColIndexMapping::new(map, target_size);
328 Ok((plan.into(), out_col_change))
329 }
330}
331
332impl LogicalPlanRoot {
333 pub fn into_unordered_subplan(self) -> LogicalPlanRef {
337 if self.out_fields.count_ones(..) == self.out_fields.len() {
338 return self.plan;
339 }
340 LogicalProject::with_out_fields(self.plan, &self.out_fields).into()
341 }
342
343 pub fn into_array_agg(self) -> Result<LogicalPlanRef> {
347 use generic::Agg;
348 use plan_node::PlanAggCall;
349 use risingwave_common::types::ListValue;
350 use risingwave_expr::aggregate::PbAggKind;
351
352 use crate::expr::{ExprImpl, ExprType, FunctionCall, InputRef};
353 use crate::utils::{Condition, IndexSet};
354
355 let Ok(select_idx) = Itertools::exactly_one(self.out_fields.ones()) else {
356 bail!("subquery must return only one column");
357 };
358 let input_column_type = self.plan.schema().fields()[select_idx].data_type();
359 let return_type = DataType::list(input_column_type.clone());
360 let agg = Agg::new(
361 vec![PlanAggCall {
362 agg_type: PbAggKind::ArrayAgg.into(),
363 return_type: return_type.clone(),
364 inputs: vec![InputRef::new(select_idx, input_column_type.clone())],
365 distinct: false,
366 order_by: self.required_order.column_orders,
367 filter: Condition::true_cond(),
368 direct_args: vec![],
369 }],
370 IndexSet::empty(),
371 self.plan,
372 );
373 Ok(LogicalProject::create(
374 agg.into(),
375 vec![
376 FunctionCall::new(
377 ExprType::Coalesce,
378 vec![
379 InputRef::new(0, return_type).into(),
380 ExprImpl::literal_list(
381 ListValue::empty(&input_column_type),
382 input_column_type,
383 ),
384 ],
385 )
386 .unwrap()
387 .into(),
388 ],
389 ))
390 }
391
392 pub fn gen_optimized_logical_plan_for_stream(mut self) -> Result<LogicalPlanRoot> {
394 self.plan = LogicalOptimizer::gen_optimized_logical_plan_for_stream(self.plan.clone())?;
395 Ok(self)
396 }
397
398 pub fn gen_optimized_logical_plan_for_batch(self) -> Result<BatchOptimizedLogicalPlanRoot> {
400 let plan = LogicalOptimizer::gen_optimized_logical_plan_for_batch(self.plan.clone())?;
401 Ok(self.into_phase(plan))
402 }
403
404 pub fn gen_batch_plan(self) -> Result<BatchPlanRoot> {
405 self.gen_optimized_logical_plan_for_batch()?
406 .gen_batch_plan()
407 }
408}
409
410impl BatchOptimizedLogicalPlanRoot {
411 fn reject_variant_keys(&self) -> Result<()> {
414 if let Some(err) = StreamKeyChecker::Variant.visit(self.plan.clone()) {
415 return Err(variant_key_error(err));
416 }
417 let schema = self.plan.schema();
418 for order in &self.required_order.column_orders {
419 let field = &schema[order.column_index];
420 if field.data_type().contains_variant() {
421 return Err(variant_key_error(format!(
422 "VARIANT column \"{}\" should not be in the ORDER BY.",
423 FieldDisplay(field)
424 )));
425 }
426 }
427 Ok(())
428 }
429
430 pub fn gen_batch_plan(self) -> Result<BatchPlanRoot> {
432 self.reject_variant_keys()?;
433 if TemporalJoinValidator::exist_dangling_temporal_scan(self.plan.clone()) {
434 return Err(ErrorCode::NotSupported(
435 "do not support temporal join for batch queries".to_owned(),
436 "please use temporal join in streaming queries".to_owned(),
437 )
438 .into());
439 }
440
441 let ctx = self.plan.ctx();
442 let mut plan = inline_session_timezone_in_exprs(ctx.clone(), self.plan.clone())?;
444
445 plan = const_eval_exprs(plan)?;
447
448 if ctx.is_explain_trace() {
449 ctx.trace("Const eval exprs:");
450 ctx.trace(plan.explain_to_string());
451 }
452
453 let mut plan = plan.to_batch_with_order_required(&self.required_order)?;
455 if ctx.is_explain_trace() {
456 ctx.trace("To Batch Plan:");
457 ctx.trace(plan.explain_to_string());
458 }
459
460 plan = plan.optimize_by_rules(&OptimizationStage::<Batch>::new(
461 "Merge BatchProject",
462 vec![BatchProjectMergeRule::create()],
463 ApplyOrder::BottomUp,
464 ))?;
465
466 plan = inline_session_timezone_in_exprs(ctx.clone(), plan)?;
468
469 if ctx.is_explain_trace() {
470 ctx.trace("Inline Session Timezone:");
471 ctx.trace(plan.explain_to_string());
472 }
473
474 #[cfg(debug_assertions)]
475 InputRefValidator.validate(plan.clone());
476 assert_eq!(
477 *plan.distribution(),
478 Distribution::Single,
479 "{}",
480 plan.explain_to_string()
481 );
482 assert!(
483 !has_batch_exchange(plan.clone()),
484 "{}",
485 plan.explain_to_string()
486 );
487
488 let ctx = plan.ctx();
489 if ctx.is_explain_trace() {
490 ctx.trace("To Batch Physical Plan:");
491 ctx.trace(plan.explain_to_string());
492 }
493
494 Ok(self.into_phase(plan))
495 }
496
497 #[cfg(feature = "datafusion")]
498 pub fn gen_datafusion_logical_plan(
499 &self,
500 ) -> Result<Arc<datafusion::logical_expr::LogicalPlan>> {
501 use datafusion::logical_expr::{Expr as DFExpr, LogicalPlan, Projection, Sort};
502 use datafusion_common::Column;
503 use plan_visitor::LogicalPlanToDataFusionExt;
504
505 use crate::datafusion::{InputColumns, convert_column_order};
506
507 tracing::debug!(
508 "Converting RisingWave logical plan to DataFusion plan:\nRisingWave Plan: {:?}",
509 self.plan
510 );
511
512 let ctx = self.plan.ctx();
513 let mut plan = inline_session_timezone_in_exprs(ctx, self.plan.clone())?;
515 plan = const_eval_exprs(plan)?;
516
517 let mut df_plan = plan.to_datafusion_logical_plan()?;
518
519 if !self.required_order.is_any() {
520 let input_columns = InputColumns::new(df_plan.schema().as_ref(), plan.schema());
521 let expr = self
522 .required_order
523 .column_orders
524 .iter()
525 .map(|column_order| convert_column_order(column_order, &input_columns))
526 .collect_vec();
527 df_plan = Arc::new(LogicalPlan::Sort(Sort {
528 expr,
529 input: df_plan,
530 fetch: None,
531 }));
532 }
533
534 if self.out_names.len() < df_plan.schema().fields().len() {
535 let df_schema = df_plan.schema().as_ref();
536 let projection_exprs = self
537 .out_fields
538 .ones()
539 .zip_eq_debug(self.out_names.iter())
540 .map(|(i, name)| {
541 DFExpr::Column(Column::from(df_schema.qualified_field(i))).alias(name)
542 })
543 .collect_vec();
544 df_plan = Arc::new(LogicalPlan::Projection(Projection::try_new(
545 projection_exprs,
546 df_plan,
547 )?));
548 }
549
550 tracing::debug!("Converted DataFusion plan:\nDataFusion Plan: {:?}", df_plan);
551
552 Ok(df_plan)
553 }
554}
555
556impl BatchPlanRoot {
557 pub fn gen_batch_distributed_plan(mut self) -> Result<BatchPlanRef> {
559 self.required_dist = RequiredDist::single();
560 let mut plan = self.plan;
561
562 plan = plan.to_distributed_with_required(&self.required_order, &self.required_dist)?;
564
565 let ctx = plan.ctx();
566 if ctx.is_explain_trace() {
567 ctx.trace("To Batch Distributed Plan:");
568 ctx.trace(plan.explain_to_string());
569 }
570 if require_additional_exchange_on_root_in_distributed_mode(plan.clone()) {
571 plan =
572 BatchExchange::new(plan, self.required_order.clone(), Distribution::Single).into();
573 }
574
575 if self.out_fields.count_ones(..) != self.out_fields.len() {
577 plan =
578 BatchProject::new(generic::Project::with_out_fields(plan, &self.out_fields)).into();
579 }
580
581 let plan = plan.optimize_by_rules(&OptimizationStage::new(
583 "Push Limit To Scan",
584 vec![BatchPushLimitToScanRule::create()],
585 ApplyOrder::BottomUp,
586 ))?;
587
588 Ok(plan)
589 }
590
591 pub fn gen_batch_local_plan(self) -> Result<BatchPlanRef> {
593 let mut plan = self.plan;
594
595 plan = plan.to_local_with_order_required(&self.required_order)?;
597
598 let insert_exchange = match plan.distribution() {
601 Distribution::Single => require_additional_exchange_on_root_in_local_mode(plan.clone()),
602 _ => true,
603 };
604 if insert_exchange {
605 plan =
606 BatchExchange::new(plan, self.required_order.clone(), Distribution::Single).into()
607 }
608
609 if self.out_fields.count_ones(..) != self.out_fields.len() {
611 plan =
612 BatchProject::new(generic::Project::with_out_fields(plan, &self.out_fields)).into();
613 }
614
615 let ctx = plan.ctx();
616 if ctx.is_explain_trace() {
617 ctx.trace("To Batch Local Plan:");
618 ctx.trace(plan.explain_to_string());
619 }
620
621 let plan = plan.optimize_by_rules(&OptimizationStage::new(
623 "Push Limit To Scan",
624 vec![BatchPushLimitToScanRule::create()],
625 ApplyOrder::BottomUp,
626 ))?;
627
628 Ok(plan)
629 }
630}
631
632impl LogicalPlanRoot {
633 pub(crate) fn derive_backfill_type(&self, allow_snapshot_backfill: bool) -> BackfillType {
635 if allow_snapshot_backfill && self.should_use_snapshot_backfill() {
636 BackfillType::SnapshotBackfill
637 } else {
638 BackfillType::ArrangementBackfill
639 }
640 }
641
642 fn gen_optimized_stream_plan(
643 self,
644 emit_on_window_close: bool,
645 backfill_type: BackfillType,
646 ) -> Result<StreamOptimizedLogicalPlanRoot> {
647 let ctx = self.plan.ctx();
648 let _explain_trace = ctx.is_explain_trace();
649
650 let optimized_plan = self.gen_stream_plan(emit_on_window_close, backfill_type)?;
651
652 let mut plan = optimized_plan
653 .plan
654 .clone()
655 .optimize_by_rules(&OptimizationStage::new(
656 "Merge StreamProject",
657 vec![StreamProjectMergeRule::create()],
658 ApplyOrder::BottomUp,
659 ))?;
660
661 if ctx
662 .session_ctx()
663 .config()
664 .streaming_separate_consecutive_join()
665 {
666 plan = plan.optimize_by_rules(&OptimizationStage::new(
667 "Separate consecutive StreamHashJoin by no-shuffle StreamExchange",
668 vec![SeparateConsecutiveJoinRule::create()],
669 ApplyOrder::BottomUp,
670 ))?;
671 }
672
673 if ctx.session_ctx().config().streaming_enable_unaligned_join() {
677 plan = plan.optimize_by_rules(&OptimizationStage::new(
678 "Add Logstore for Unaligned join",
679 vec![AddLogstoreRule::create()],
680 ApplyOrder::BottomUp,
681 ))?;
682 }
683
684 if ctx.session_ctx().config().streaming_enable_delta_join()
685 && ctx.session_ctx().config().enable_index_selection()
686 {
687 plan = plan.optimize_by_rules(&OptimizationStage::new(
690 "To IndexDeltaJoin",
691 vec![IndexDeltaJoinRule::create()],
692 ApplyOrder::BottomUp,
693 ))?;
694 }
695 plan = inline_session_timezone_in_exprs(ctx.clone(), plan)?;
697
698 if ctx.is_explain_trace() {
699 ctx.trace("Inline session timezone:");
700 ctx.trace(plan.explain_to_string());
701 }
702
703 plan = const_eval_exprs(plan)?;
705
706 if ctx.is_explain_trace() {
707 ctx.trace("Const eval exprs:");
708 ctx.trace(plan.explain_to_string());
709 }
710
711 #[cfg(debug_assertions)]
712 InputRefValidator.validate(plan.clone());
713
714 if TemporalJoinValidator::exist_dangling_temporal_scan(plan.clone()) {
715 return Err(ErrorCode::NotSupported(
716 "exist dangling temporal scan".to_owned(),
717 "please check your temporal join syntax e.g. consider removing the right outer join if it is being used.".to_owned(),
718 ).into());
719 }
720
721 if RwTimestampValidator::select_rw_timestamp_in_stream_query(plan.clone()) {
722 return Err(ErrorCode::NotSupported(
723 "selecting `_rw_timestamp` in a streaming query is not allowed".to_owned(),
724 "please run the sql in batch mode or remove the column `_rw_timestamp` from the streaming query".to_owned(),
725 ).into());
726 }
727
728 Ok(optimized_plan.into_phase(plan))
729 }
730
731 pub(crate) fn require_snapshot_backfill_for_batch_refresh(&self) -> Result<()> {
732 let ctx = self.plan.ctx();
733 let session_ctx = ctx.session_ctx();
734 let snapshot_backfill_enabled = session_ctx
735 .env()
736 .streaming_config()
737 .developer
738 .enable_snapshot_backfill
739 && session_ctx.config().streaming_use_snapshot_backfill();
740 if !snapshot_backfill_enabled {
741 return Err(ErrorCode::NotSupported(
742 "Batch refresh materialized view requires snapshot backfill".to_owned(),
743 format!(
744 "Please enable snapshot backfill or remove `{}` from the WITH clause.",
745 MV_REFRESH_INTERVAL_SEC_KEY
746 ),
747 )
748 .into());
749 }
750 if let Some(reason) = self.plan.forbid_snapshot_backfill() {
751 return Err(ErrorCode::NotSupported(
752 format!("Batch refresh materialized view requires snapshot backfill, but {reason}"),
753 "Please rewrite the query to avoid operators that forbid snapshot backfill."
754 .to_owned(),
755 )
756 .into());
757 }
758 Ok(())
759 }
760
761 fn gen_stream_plan(
763 self,
764 emit_on_window_close: bool,
765 backfill_type: BackfillType,
766 ) -> Result<StreamOptimizedLogicalPlanRoot> {
767 let ctx = self.plan.ctx();
768 let explain_trace = ctx.is_explain_trace();
769
770 let plan = {
771 {
772 if let Some(err) = StreamKeyChecker::Variant.visit(self.plan.clone()) {
773 return Err(variant_key_error(err));
774 }
775 if !ctx
776 .session_ctx()
777 .config()
778 .streaming_allow_jsonb_in_stream_key()
779 && let Some(err) = StreamKeyChecker::Jsonb.visit(self.plan.clone())
780 {
781 return Err(ErrorCode::NotSupported(
782 err,
783 "Using JSONB columns as part of the join or aggregation keys can severely impair performance. \
784 If you intend to proceed, force to enable it with: `set rw_streaming_allow_jsonb_in_stream_key to true`".to_owned(),
785 ).into());
786 }
787 let mut optimized_plan = self.gen_optimized_logical_plan_for_stream()?;
788 let locality_backfill_enabled =
789 resolve_locality_backfill(&ctx, optimized_plan.plan.clone(), backfill_type);
790 let (mut plan, mut out_col_change) = rewrite_logical_plan_for_stream(
791 &optimized_plan.plan,
792 backfill_type,
793 locality_backfill_enabled,
794 )?;
795
796 let locality_provider_count = LocalityProviderCounter::count(plan.clone());
797 let locality_backfill_mode = ctx.session_ctx().config().locality_backfill_mode();
798 if locality_backfill_enabled
799 && locality_backfill_requires_license(
800 locality_backfill_mode,
801 locality_provider_count,
802 )
803 && risingwave_common::license::Feature::LocalityBackfill
804 .check_available()
805 .is_err()
806 {
807 ctx.warn_to_user(format!(
808 "The streaming job would use {locality_provider_count} locality providers, \
809 which are unavailable under the current license. Falling back to regular backfill."
810 ));
811 (plan, out_col_change) = rewrite_logical_plan_for_stream(
812 &optimized_plan.plan,
813 backfill_type,
814 false,
815 )?;
816 }
817 if explain_trace {
818 ctx.trace("Logical Rewrite For Stream:");
819 ctx.trace(plan.explain_to_string());
820 }
821
822 optimized_plan.required_dist =
823 out_col_change.rewrite_required_distribution(&optimized_plan.required_dist);
824 optimized_plan.required_order = out_col_change
825 .rewrite_required_order(&optimized_plan.required_order)
826 .unwrap();
827 optimized_plan.out_fields =
828 out_col_change.rewrite_bitset(&optimized_plan.out_fields);
829 let mut plan = plan.to_stream_with_dist_required(
830 &optimized_plan.required_dist,
831 &mut ToStreamContext::new_with_backfill_type(
832 emit_on_window_close,
833 backfill_type,
834 ),
835 )?;
836 plan = stream_enforce_eowc_requirement(ctx.clone(), plan, emit_on_window_close)?;
837 optimized_plan.into_phase(plan)
838 }
839 };
840
841 if explain_trace {
842 ctx.trace("To Stream Plan:");
843 ctx.trace(<PlanRef<Stream> as Explain>::explain_to_string(&plan.plan));
845 }
846 Ok(plan)
847 }
848
849 fn compute_cardinality(&self) -> Cardinality {
853 CardinalityVisitor.visit(self.plan.clone())
854 }
855
856 pub fn gen_table_plan(
858 self,
859 context: OptimizerContextRef,
860 table_name: String,
861 database_id: DatabaseId,
862 schema_id: SchemaId,
863 CreateTableInfo {
864 columns,
865 pk_column_ids,
866 row_id_index,
867 watermark_descs,
868 source_catalog,
869 version,
870 }: CreateTableInfo,
871 CreateTableProps {
872 definition,
873 append_only,
874 on_conflict,
875 with_version_columns,
876 webhook_info,
877 engine,
878 }: CreateTableProps,
879 ) -> Result<StreamMaterialize> {
880 let backfill_type = self.derive_backfill_type(false);
881 let stream_plan = self.gen_optimized_stream_plan(false, backfill_type)?;
883
884 assert!(!pk_column_ids.is_empty() || row_id_index.is_some());
885
886 let pk_column_indices = {
887 let mut id_to_idx = HashMap::new();
888
889 columns.iter().enumerate().for_each(|(idx, c)| {
890 id_to_idx.insert(c.column_id(), idx);
891 });
892 pk_column_ids
893 .iter()
894 .map(|c| id_to_idx.get(c).copied().unwrap()) .collect_vec()
896 };
897
898 fn inject_project_for_generated_column_if_needed(
899 columns: &[ColumnCatalog],
900 node: StreamPlanRef,
901 ) -> Result<StreamPlanRef> {
902 let exprs = LogicalSource::derive_output_exprs_from_generated_columns(columns)?;
903 if let Some(exprs) = exprs {
904 let logical_project = generic::Project::new(exprs, node);
905 return Ok(StreamProject::new(logical_project).into());
906 }
907 Ok(node)
908 }
909
910 #[derive(PartialEq, Debug, Copy, Clone)]
911 enum PrimaryKeyKind {
912 UserDefinedPrimaryKey,
913 NonAppendOnlyRowIdPk,
914 AppendOnlyRowIdPk,
915 }
916
917 fn inject_dml_node(
918 columns: &[ColumnCatalog],
919 append_only: bool,
920 stream_plan: StreamPlanRef,
921 pk_column_indices: &[usize],
922 kind: PrimaryKeyKind,
923 column_descs: Vec<ColumnDesc>,
924 ) -> Result<StreamPlanRef> {
925 let mut dml_node = StreamDml::new(stream_plan, append_only, column_descs).into();
926
927 dml_node = inject_project_for_generated_column_if_needed(columns, dml_node)?;
929
930 dml_node = match kind {
931 PrimaryKeyKind::UserDefinedPrimaryKey | PrimaryKeyKind::NonAppendOnlyRowIdPk => {
932 RequiredDist::hash_shard(pk_column_indices)
933 .streaming_enforce_if_not_satisfies(dml_node)?
934 }
935 PrimaryKeyKind::AppendOnlyRowIdPk => {
936 StreamExchange::new_no_shuffle(dml_node).into()
937 }
938 };
939
940 Ok(dml_node)
941 }
942
943 let kind = if let Some(row_id_index) = row_id_index {
944 assert_eq!(
945 Itertools::exactly_one(pk_column_indices.iter())
946 .copied()
947 .unwrap(),
948 row_id_index
949 );
950 if append_only {
951 PrimaryKeyKind::AppendOnlyRowIdPk
952 } else {
953 PrimaryKeyKind::NonAppendOnlyRowIdPk
954 }
955 } else {
956 PrimaryKeyKind::UserDefinedPrimaryKey
957 };
958
959 let column_descs: Vec<ColumnDesc> = columns
960 .iter()
961 .filter(|&c| c.can_dml())
962 .map(|c| c.column_desc.clone())
963 .collect();
964
965 let mut not_null_idxs = vec![];
966 for (idx, column) in column_descs.iter().enumerate() {
967 if !column.nullable {
968 not_null_idxs.push(idx);
969 }
970 }
971
972 let version_column_indices = if !with_version_columns.is_empty() {
973 find_version_column_indices(&columns, with_version_columns)?
974 } else {
975 vec![]
976 };
977
978 let with_external_source = source_catalog.is_some();
979 let (dml_source_node, external_source_node) = if with_external_source {
980 let dummy_source_node = LogicalSource::new(
981 None,
982 columns.clone(),
983 row_id_index,
984 SourceNodeKind::CreateTable,
985 context.clone(),
986 None,
987 )
988 .and_then(|s| {
989 s.to_stream(&mut ToStreamContext::new_with_backfill_type(
990 false,
991 BackfillType::ArrangementBackfill,
995 ))
996 })?;
997 let mut external_source_node = stream_plan.plan;
998 external_source_node =
999 inject_project_for_generated_column_if_needed(&columns, external_source_node)?;
1000 external_source_node = match kind {
1001 PrimaryKeyKind::UserDefinedPrimaryKey => {
1002 RequiredDist::hash_shard(&pk_column_indices)
1003 .streaming_enforce_if_not_satisfies(external_source_node)?
1004 }
1005
1006 PrimaryKeyKind::NonAppendOnlyRowIdPk | PrimaryKeyKind::AppendOnlyRowIdPk => {
1007 StreamExchange::new_no_shuffle(external_source_node).into()
1008 }
1009 };
1010 (dummy_source_node, Some(external_source_node))
1011 } else {
1012 (stream_plan.plan, None)
1013 };
1014
1015 let dml_node = inject_dml_node(
1016 &columns,
1017 append_only,
1018 dml_source_node,
1019 &pk_column_indices,
1020 kind,
1021 column_descs,
1022 )?;
1023
1024 let dists = external_source_node
1025 .iter()
1026 .map(|input| input.distribution())
1027 .chain([dml_node.distribution()])
1028 .unique()
1029 .collect_vec();
1030
1031 let dist = match &dists[..] {
1032 &[Distribution::SomeShard, Distribution::HashShard(_)]
1033 | &[Distribution::HashShard(_), Distribution::SomeShard] => Distribution::SomeShard,
1034 &[dist @ Distribution::SomeShard] | &[dist @ Distribution::HashShard(_)] => {
1035 dist.clone()
1036 }
1037 _ => {
1038 unreachable!()
1039 }
1040 };
1041
1042 let generated_column_exprs =
1043 LogicalSource::derive_output_exprs_from_generated_columns(&columns)?;
1044 let upstream_sink_union = StreamUpstreamSinkUnion::new(
1045 context.clone(),
1046 dml_node.schema(),
1047 dml_node.stream_key(),
1048 dist.clone(), append_only,
1050 row_id_index.is_none(),
1051 generated_column_exprs,
1052 );
1053
1054 let union_inputs = external_source_node
1055 .into_iter()
1056 .chain([dml_node, upstream_sink_union.into()])
1057 .collect_vec();
1058
1059 let mut stream_plan: StreamPlanRef = StreamUnion::new_with_dist(
1060 Union {
1061 all: true,
1062 inputs: union_inputs,
1063 source_col: None,
1064 },
1065 dist,
1066 )
1067 .into();
1068
1069 let ttl_watermark_indices = watermark_descs
1070 .iter()
1071 .filter(|d| d.with_ttl)
1072 .map(|d| d.watermark_idx as usize)
1073 .collect_vec();
1074
1075 let add_row_id_gen = |stream_plan: StreamPlanRef, row_id_index| match kind {
1076 PrimaryKeyKind::UserDefinedPrimaryKey => {
1077 unreachable!()
1078 }
1079 PrimaryKeyKind::NonAppendOnlyRowIdPk | PrimaryKeyKind::AppendOnlyRowIdPk => {
1080 StreamRowIdGen::new_with_dist(
1081 stream_plan,
1082 row_id_index,
1083 Distribution::HashShard(vec![row_id_index]),
1084 )
1085 .into()
1086 }
1087 };
1088
1089 if let Some(row_id_index) = row_id_index {
1091 stream_plan = add_row_id_gen(stream_plan, row_id_index);
1092 }
1093
1094 if !watermark_descs.is_empty() {
1096 stream_plan = StreamWatermarkFilter::new(stream_plan, watermark_descs).into();
1097 }
1098
1099 let conflict_behavior = on_conflict.to_behavior(append_only, row_id_index.is_some())?;
1100
1101 if let ConflictBehavior::IgnoreConflict = conflict_behavior
1102 && !version_column_indices.is_empty()
1103 {
1104 Err(ErrorCode::InvalidParameterValue(
1105 "The with version column syntax cannot be used with the ignore behavior of on conflict".to_owned(),
1106 ))?
1107 }
1108
1109 let retention_seconds = context.with_options().retention_seconds();
1110
1111 let table_required_dist = {
1112 let mut bitset = FixedBitSet::with_capacity(columns.len());
1113 for idx in &pk_column_indices {
1114 bitset.insert(*idx);
1115 }
1116 RequiredDist::ShardByKey(bitset)
1117 };
1118
1119 let mut stream_plan = inline_session_timezone_in_exprs(context, stream_plan)?;
1120
1121 if !not_null_idxs.is_empty() {
1122 stream_plan =
1123 StreamFilter::filter_out_any_null_rows(stream_plan.clone(), ¬_null_idxs);
1124 }
1125
1126 let refreshable = source_catalog
1128 .as_ref()
1129 .map(|catalog| {
1130 catalog.with_properties.supports_full_reload_refresh()
1131 && matches!(
1132 catalog
1133 .refresh_mode
1134 .as_ref()
1135 .map(|refresh_mode| refresh_mode.refresh_mode),
1136 Some(Some(RefreshMode::FullReload(_)))
1137 )
1138 })
1139 .unwrap_or(false);
1140
1141 if refreshable && row_id_index.is_some() {
1143 return Err(crate::error::ErrorCode::BindError(
1144 "Refreshable tables must have a PRIMARY KEY. Please define a primary key for the table."
1145 .to_owned(),
1146 )
1147 .into());
1148 }
1149
1150 StreamMaterialize::create_for_table(
1151 stream_plan,
1152 table_name,
1153 database_id,
1154 schema_id,
1155 table_required_dist,
1156 Order::any(),
1157 columns,
1158 definition,
1159 conflict_behavior,
1160 version_column_indices,
1161 pk_column_indices,
1162 ttl_watermark_indices,
1163 row_id_index,
1164 version,
1165 retention_seconds,
1166 webhook_info,
1167 engine,
1168 refreshable,
1169 )
1170 }
1171
1172 pub fn gen_materialize_plan(
1174 self,
1175 database_id: DatabaseId,
1176 schema_id: SchemaId,
1177 mv_name: String,
1178 definition: String,
1179 emit_on_window_close: bool,
1180 backfill_type: BackfillType,
1181 ) -> Result<StreamMaterialize> {
1182 let cardinality = self.compute_cardinality();
1183 let stream_plan = self.gen_optimized_stream_plan(emit_on_window_close, backfill_type)?;
1184 StreamMaterialize::create(
1185 stream_plan,
1186 mv_name,
1187 database_id,
1188 schema_id,
1189 definition,
1190 TableType::MaterializedView,
1191 cardinality,
1192 None,
1193 )
1194 }
1195
1196 pub fn gen_index_plan(
1198 self,
1199 index_name: String,
1200 database_id: DatabaseId,
1201 schema_id: SchemaId,
1202 definition: String,
1203 retention_seconds: Option<NonZeroU32>,
1204 ) -> Result<StreamMaterialize> {
1205 let cardinality = self.compute_cardinality();
1206 let backfill_type = self.derive_backfill_type(false);
1207 let stream_plan = self.gen_optimized_stream_plan(false, backfill_type)?;
1208
1209 StreamMaterialize::create(
1210 stream_plan,
1211 index_name,
1212 database_id,
1213 schema_id,
1214 definition,
1215 TableType::Index,
1216 cardinality,
1217 retention_seconds,
1218 )
1219 }
1220
1221 pub fn gen_vector_index_plan(
1222 self,
1223 index_name: String,
1224 database_id: DatabaseId,
1225 schema_id: SchemaId,
1226 definition: String,
1227 retention_seconds: Option<NonZeroU32>,
1228 vector_index_info: PbVectorIndexInfo,
1229 ) -> Result<StreamVectorIndexWrite> {
1230 let cardinality = self.compute_cardinality();
1231 let backfill_type = self.derive_backfill_type(false);
1232 let stream_plan = self.gen_optimized_stream_plan(false, backfill_type)?;
1233
1234 StreamVectorIndexWrite::create(
1235 stream_plan,
1236 index_name,
1237 database_id,
1238 schema_id,
1239 definition,
1240 cardinality,
1241 retention_seconds,
1242 vector_index_info,
1243 )
1244 }
1245
1246 #[expect(clippy::too_many_arguments)]
1248 pub fn gen_sink_plan(
1249 self,
1250 sink_name: String,
1251 definition: String,
1252 properties: WithOptionsSecResolved,
1253 emit_on_window_close: bool,
1254 db_name: String,
1255 sink_from_table_name: String,
1256 format_desc: Option<SinkFormatDesc>,
1257 without_snapshot: bool,
1258 since_timestamp: bool,
1259 is_iceberg_engine_internal: bool,
1260 target_table: Option<Arc<TableCatalog>>,
1261 partition_info: Option<PartitionComputeInfo>,
1262 user_specified_columns: bool,
1263 auto_refresh_schema_from_table: Option<Arc<TableCatalog>>,
1264 ) -> Result<StreamSink> {
1265 let is_iceberg_v3 = properties.is_iceberg_connector()
1266 && properties
1267 .get(ENABLE_PK_INDEX)
1268 .is_some_and(|value| value.eq_ignore_ascii_case("true"));
1269 let backfill_type = if is_iceberg_v3 {
1270 if without_snapshot || since_timestamp {
1271 return Err(ErrorCode::InvalidInputSyntax(
1272 "Iceberg sinks with `enable_pk_index = 'true'` always require snapshot backfill"
1273 .to_owned(),
1274 )
1275 .into());
1276 }
1277 if target_table.is_some() {
1278 return Err(ErrorCode::InvalidInputSyntax(
1279 "Iceberg sinks with `enable_pk_index = 'true'` cannot bypass snapshot backfill"
1280 .to_owned(),
1281 )
1282 .into());
1283 }
1284 BackfillType::SnapshotBackfill
1285 } else if since_timestamp {
1286 assert!(
1287 target_table.is_none(),
1288 "should not allow since_timestamp for sink-into-table"
1289 );
1290 if is_iceberg_engine_internal {
1291 return Err(ErrorCode::InvalidInputSyntax(
1292 "since_timestamp is not allowed for this sink".to_owned(),
1293 )
1294 .into());
1295 }
1296 BackfillType::SnapshotBackfillSinceTimestamp
1297 } else if without_snapshot {
1298 BackfillType::UpstreamOnlySink
1299 } else if target_table.is_none()
1300 && !is_iceberg_engine_internal
1301 && self.should_use_snapshot_backfill()
1302 && {
1303 if auto_refresh_schema_from_table.is_some() {
1304 self.plan.ctx().session_ctx().notice_to_user("Auto schema change only support for ArrangementBackfill. Switched to use ArrangementBackfill");
1305 false
1306 } else {
1307 true
1308 }
1309 }
1310 {
1311 assert!(
1312 target_table.is_none(),
1313 "should not allow snapshot backfill for sink-into-table"
1314 );
1315 BackfillType::SnapshotBackfill
1317 } else {
1318 BackfillType::ArrangementBackfill
1319 };
1320 if auto_refresh_schema_from_table.is_some()
1321 && backfill_type != BackfillType::ArrangementBackfill
1322 {
1323 return Err(ErrorCode::InvalidInputSyntax(format!(
1324 "auto schema change only support for ArrangementBackfill, but got: {:?}",
1325 backfill_type
1326 ))
1327 .into());
1328 }
1329 let stream_plan = self.gen_optimized_stream_plan(emit_on_window_close, backfill_type)?;
1330 let target_columns_to_plan_mapping = target_table.as_ref().map(|t| {
1331 let columns = t.columns_without_rw_timestamp();
1332 stream_plan.target_columns_to_plan_mapping(&columns, user_specified_columns)
1333 });
1334
1335 StreamSink::create(
1336 stream_plan,
1337 sink_name,
1338 db_name,
1339 sink_from_table_name,
1340 target_table,
1341 target_columns_to_plan_mapping,
1342 definition,
1343 properties,
1344 format_desc,
1345 partition_info,
1346 auto_refresh_schema_from_table,
1347 )
1348 }
1349
1350 pub fn should_use_snapshot_backfill(&self) -> bool {
1351 let ctx = self.plan.ctx();
1352 let session_ctx = ctx.session_ctx();
1353 let use_snapshot_backfill = session_ctx
1354 .env()
1355 .streaming_config()
1356 .developer
1357 .enable_snapshot_backfill
1358 && session_ctx.config().streaming_use_snapshot_backfill();
1359 if use_snapshot_backfill {
1360 if let Some(warning_msg) = self.plan.forbid_snapshot_backfill() {
1361 self.plan.ctx().session_ctx().notice_to_user(warning_msg);
1362 false
1363 } else {
1364 true
1365 }
1366 } else {
1367 false
1368 }
1369 }
1370}
1371
1372impl<P: PlanPhase> PlanRoot<P> {
1373 pub fn target_columns_to_plan_mapping(
1375 &self,
1376 tar_cols: &[ColumnCatalog],
1377 user_specified_columns: bool,
1378 ) -> Vec<Option<usize>> {
1379 #[expect(clippy::disallowed_methods)]
1380 let visible_cols: Vec<(usize, String)> = self
1381 .out_fields
1382 .ones()
1383 .zip_eq(self.out_names.iter().cloned())
1384 .collect_vec();
1385
1386 let visible_col_idxes = visible_cols.iter().map(|(i, _)| *i).collect_vec();
1387 let visible_col_idxes_by_name = visible_cols
1388 .iter()
1389 .map(|(i, name)| (name.as_ref(), *i))
1390 .collect::<BTreeMap<_, _>>();
1391
1392 tar_cols
1393 .iter()
1394 .enumerate()
1395 .filter(|(_, tar_col)| tar_col.can_dml())
1396 .map(|(tar_i, tar_col)| {
1397 if user_specified_columns {
1398 visible_col_idxes_by_name.get(tar_col.name()).cloned()
1399 } else {
1400 (tar_i < visible_col_idxes.len()).then(|| visible_cols[tar_i].0)
1401 }
1402 })
1403 .collect()
1404 }
1405}
1406
1407fn find_version_column_indices(
1408 column_catalog: &Vec<ColumnCatalog>,
1409 version_column_names: Vec<String>,
1410) -> Result<Vec<usize>> {
1411 let mut indices = Vec::new();
1412 for version_column_name in version_column_names {
1413 let mut found = false;
1414 for (index, column) in column_catalog.iter().enumerate() {
1415 if column.column_desc.name == version_column_name {
1416 if let &DataType::Jsonb
1417 | &DataType::Variant
1418 | &DataType::List(_)
1419 | &DataType::Struct(_)
1420 | &DataType::Bytea
1421 | &DataType::Boolean = column.data_type()
1422 {
1423 return Err(ErrorCode::InvalidInputSyntax(format!(
1424 "Version column {} must be of a comparable data type",
1425 version_column_name
1426 ))
1427 .into());
1428 }
1429 indices.push(index);
1430 found = true;
1431 break;
1432 }
1433 }
1434 if !found {
1435 return Err(ErrorCode::InvalidInputSyntax(format!(
1436 "Version column {} not found",
1437 version_column_name
1438 ))
1439 .into());
1440 }
1441 }
1442 Ok(indices)
1443}
1444
1445fn const_eval_exprs<C: ConventionMarker>(plan: PlanRef<C>) -> Result<PlanRef<C>> {
1446 let mut const_eval_rewriter = ConstEvalRewriter { error: None };
1447
1448 let plan = plan.rewrite_exprs_recursive(&mut const_eval_rewriter);
1449 if let Some(error) = const_eval_rewriter.error {
1450 return Err(error);
1451 }
1452 Ok(plan)
1453}
1454
1455fn inline_session_timezone_in_exprs<C: ConventionMarker>(
1456 ctx: OptimizerContextRef,
1457 plan: PlanRef<C>,
1458) -> Result<PlanRef<C>> {
1459 let mut v = TimestamptzExprFinder::default();
1460 plan.visit_exprs_recursive(&mut v);
1461 if v.has() {
1462 Ok(plan.rewrite_exprs_recursive(ctx.session_timezone().deref_mut()))
1463 } else {
1464 Ok(plan)
1465 }
1466}
1467
1468fn exist_and_no_exchange_before(
1469 plan: &BatchPlanRef,
1470 is_candidate: fn(&BatchPlanRef) -> bool,
1471) -> bool {
1472 if plan.node_type() == BatchPlanNodeType::BatchExchange {
1473 return false;
1474 }
1475 is_candidate(plan)
1476 || plan
1477 .inputs()
1478 .iter()
1479 .any(|input| exist_and_no_exchange_before(input, is_candidate))
1480}
1481
1482impl BatchPlanRef {
1483 fn is_user_table_scan(&self) -> bool {
1484 self.node_type() == BatchPlanNodeType::BatchSeqScan
1485 || self.node_type() == BatchPlanNodeType::BatchLogSeqScan
1486 || self.node_type() == BatchPlanNodeType::BatchVectorSearch
1487 }
1488
1489 fn is_source_scan(&self) -> bool {
1490 self.node_type() == BatchPlanNodeType::BatchSource
1491 || self.node_type() == BatchPlanNodeType::BatchKafkaScan
1492 || self.node_type() == BatchPlanNodeType::BatchIcebergScan
1493 }
1494
1495 fn is_lookup_join(&self) -> bool {
1496 self.node_type() == BatchPlanNodeType::BatchLookupJoin
1497 }
1498
1499 fn is_insert(&self) -> bool {
1500 self.node_type() == BatchPlanNodeType::BatchInsert
1501 }
1502
1503 fn is_update(&self) -> bool {
1504 self.node_type() == BatchPlanNodeType::BatchUpdate
1505 }
1506
1507 fn is_delete(&self) -> bool {
1508 self.node_type() == BatchPlanNodeType::BatchDelete
1509 }
1510}
1511
1512fn require_additional_exchange_on_root_in_distributed_mode(plan: BatchPlanRef) -> bool {
1518 assert_eq!(plan.distribution(), &Distribution::Single);
1519 exist_and_no_exchange_before(&plan, |plan| {
1520 plan.is_user_table_scan()
1521 || plan.is_source_scan()
1522 || plan.is_insert()
1523 || plan.is_update()
1524 || plan.is_delete()
1525 || plan.is_lookup_join()
1529 })
1530}
1531
1532fn require_additional_exchange_on_root_in_local_mode(plan: BatchPlanRef) -> bool {
1535 assert_eq!(plan.distribution(), &Distribution::Single);
1536 exist_and_no_exchange_before(&plan, |plan| {
1537 plan.is_user_table_scan() || plan.is_source_scan() || plan.is_insert()
1538 })
1539}
1540
1541#[cfg(test)]
1542mod tests {
1543 use super::*;
1544 use crate::optimizer::plan_node::LogicalValues;
1545
1546 #[tokio::test]
1547 async fn test_as_subplan() {
1548 let ctx = OptimizerContext::mock();
1549 let values = LogicalValues::new(
1550 vec![],
1551 Schema::new(vec![
1552 Field::with_name(DataType::Int32, "v1"),
1553 Field::with_name(DataType::Varchar, "v2"),
1554 ]),
1555 ctx,
1556 )
1557 .into();
1558 let out_fields = FixedBitSet::with_capacity_and_blocks(2, [1]);
1559 let out_names = vec!["v1".into()];
1560 let root = PlanRoot::new_with_logical_plan(
1561 values,
1562 RequiredDist::Any,
1563 Order::any(),
1564 out_fields,
1565 out_names,
1566 );
1567 let subplan = root.into_unordered_subplan();
1568 assert_eq!(
1569 subplan.schema(),
1570 &Schema::new(vec![Field::with_name(DataType::Int32, "v1")])
1571 );
1572 }
1573}