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