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