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