Skip to main content

risingwave_frontend/optimizer/
mod.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// `PlanRoot` is used to describe a plan. planner will construct a `PlanRoot` with `LogicalNode`.
100/// and required distribution and order. And `PlanRoot` can generate corresponding streaming or
101/// batch plan with optimization. the required Order and Distribution columns might be more than the
102/// output columns. for example:
103/// ```sql
104///    select v1 from t order by id;
105/// ```
106/// the plan will return two columns (id, v1), and the required order column is id. the id
107/// column is required in optimization, but the final generated plan will remove the unnecessary
108/// column in the result.
109#[derive(Educe)]
110#[educe(Debug, Clone)]
111pub struct PlanRoot<P: PlanPhase> {
112    // The current plan node.
113    pub plan: PlanRef<P::Convention>,
114    // The phase of the plan.
115    #[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
123/// `PlanPhase` is used to track the phase of the `PlanRoot`.
124/// Usually, it begins from `Logical` and ends with `Batch` or `Stream`, unless we want to construct a `PlanRoot` from an intermediate phase.
125/// Typical phase transformation are:
126/// - `Logical` -> `OptimizedLogicalForBatch` -> `Batch`
127/// - `Logical` -> `OptimizedLogicalForStream` -> `Stream`
128pub 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    /// Set customized names of the output fields, used for `CREATE [MATERIALIZED VIEW | SINK] r(a,
215    /// b, ..)`.
216    ///
217    /// If the number of names does not match the number of output fields, an error is returned.
218    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    /// Get the plan root's schema, only including the fields to be output.
229    pub fn schema(&self) -> Schema {
230        // The schema can be derived from the `out_fields` and `out_names`, so we don't maintain it
231        // as a field and always construct one on demand here to keep it in sync.
232        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    /// Transform the [`PlanRoot`] back to a [`PlanRef`] suitable to be used as a subplan, for
249    /// example as insert source or subquery. This ignores Order but retains post-Order pruning
250    /// (`out_fields`).
251    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    /// Transform the [`PlanRoot`] wrapped in an array-construction subquery to a [`PlanRef`]
259    /// supported by `ARRAY_AGG`. Similar to the unordered version, this abstracts away internal
260    /// `self.plan` which is further modified by `self.required_order` then `self.out_fields`.
261    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    /// Apply logical optimization to the plan for stream.
308    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    /// Apply logical optimization to the plan for batch.
314    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    /// Optimize and generate a singleton batch physical plan without exchange nodes.
327    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        // Inline session timezone mainly for rewriting now()
338        let mut plan = inline_session_timezone_in_exprs(ctx.clone(), self.plan.clone())?;
339
340        // Const eval of exprs at the last minute, but before `to_batch` to make functional index selection happy.
341        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        // Convert to physical plan node
349        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        // Inline session timezone
362        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        // Inline session timezone mainly for rewriting now()
409        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    /// Optimize and generate a batch query plan for distributed execution.
453    pub fn gen_batch_distributed_plan(mut self) -> Result<BatchPlanRef> {
454        self.required_dist = RequiredDist::single();
455        let mut plan = self.plan;
456
457        // Convert to distributed plan
458        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        // Add Project if the any position of `self.out_fields` is set to zero.
471        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        // Both two phase limit and topn could generate limit on top of the scan, so we push limit here.
477        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    /// Optimize and generate a batch query plan for local execution.
487    pub fn gen_batch_local_plan(self) -> Result<BatchPlanRef> {
488        let mut plan = self.plan;
489
490        // Convert to local plan node
491        plan = plan.to_local_with_order_required(&self.required_order)?;
492
493        // We remark that since the `to_local_with_order_required` does not enforce single
494        // distribution, we enforce at the root if needed.
495        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        // Add Project if the any position of `self.out_fields` is set to zero.
505        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        // Both two phase limit and topn could generate limit on top of the scan, so we push limit here.
517        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    /// Generate optimized stream plan
529    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        // Add Logstore for Unaligned join
569        // Apply this BEFORE delta join rule, because delta join removes
570        // the join
571        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            // TODO: make it a logical optimization.
583            // Rewrite joins with index to delta join
584            plan = plan.optimize_by_rules(&OptimizationStage::new(
585                "To IndexDeltaJoin",
586                vec![IndexDeltaJoinRule::create()],
587                ApplyOrder::BottomUp,
588            ))?;
589        }
590        // Inline session timezone
591        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        // Const eval of exprs at the last minute
599        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            // LocalityProviderCounter is non-zero only when locality backfill is enabled.
625            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            // missed_locality_providers can only be non-zero when locality backfill is disabled.
635            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    /// Generate create index or create materialize view plan.
678    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                        // TODO(st1page): https://github.com/risingwavelabs/risingwave/issues/7234
713                        // assert_eq!(target_size, output_indices.len());
714                        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            // TODO: can be `plan.plan.explain_to_string()`, but should explicitly specify the type due to some limitation of rust compiler
758            ctx.trace(<PlanRef<Stream> as Explain>::explain_to_string(&plan.plan));
759        }
760        Ok(plan)
761    }
762
763    /// Visit the plan root and compute the cardinality.
764    ///
765    /// Panics if not called on a logical plan.
766    fn compute_cardinality(&self) -> Cardinality {
767        CardinalityVisitor.visit(self.plan.clone())
768    }
769
770    /// Optimize and generate a create table plan.
771    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        // Snapshot backfill is not allowed for create table
796        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()) // pk column id must exist in table columns.
809                .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            // Add generated columns.
842            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                    // Dummy DML source planning does not create stream table scans, so this
906                    // required context value is only a placeholder and is not used for backfill
907                    // selection.
908                    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(), // should always be the same as dist of `Union`
963            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        // Add RowIDGen before WatermarkFilter, so filtering always sees a valid row-id key.
1004        if let Some(row_id_index) = row_id_index {
1005            stream_plan = add_row_id_gen(stream_plan, row_id_index);
1006        }
1007
1008        // Add WatermarkFilter node.
1009        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(), &not_null_idxs);
1038        }
1039
1040        // Determine if the table should be refreshable based on the connector type
1041        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        // Validate that refreshable tables have a user-defined primary key (i.e., does not have rowid)
1056        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    /// Optimize and generate a create materialized view plan.
1087    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    /// Optimize and generate a create index plan.
1111    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    /// Optimize and generate a create sink plan.
1161    #[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            // Snapshot backfill on sink-into-table is not allowed
1210            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    /// used when the plan has a target relation such as DML and sink into table, return the mapping from table's columns to the plan's schema
1268    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
1401/// As we always run the root stage locally, for some plan in root stage which need to execute in
1402/// compute node we insert an additional exhchange before it to avoid to include it in the root
1403/// stage.
1404///
1405/// Returns `true` if we must insert an additional exchange to ensure this.
1406fn 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
1417/// The purpose is same as `require_additional_exchange_on_root_in_distributed_mode`. We separate
1418/// them for the different requirement of plan node in different execute mode.
1419fn 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}