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