Skip to main content

query/dist_plan/
analyzer.rs

1// Copyright 2023 Greptime Team
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::collections::{BTreeMap, BTreeSet, HashSet};
16use std::sync::Arc;
17
18use common_telemetry::debug;
19use datafusion::config::{ConfigExtension, ExtensionOptions};
20use datafusion::datasource::DefaultTableSource;
21use datafusion::error::Result as DfResult;
22use datafusion_common::Column;
23use datafusion_common::config::ConfigOptions;
24use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRewriter};
25use datafusion_expr::expr::{Exists, InSubquery};
26use datafusion_expr::utils::expr_to_columns;
27use datafusion_expr::{Expr, LogicalPlan, LogicalPlanBuilder, Subquery, col as col_fn};
28use datafusion_optimizer::analyzer::AnalyzerRule;
29use datafusion_optimizer::decorrelate_lateral_join::DecorrelateLateralJoin;
30use datafusion_optimizer::decorrelate_predicate_subquery::DecorrelatePredicateSubquery;
31use datafusion_optimizer::eliminate_filter::EliminateFilter;
32use datafusion_optimizer::extract_equijoin_predicate::ExtractEquijoinPredicate;
33use datafusion_optimizer::filter_null_join_keys::FilterNullJoinKeys;
34use datafusion_optimizer::optimizer::Optimizer;
35use datafusion_optimizer::propagate_empty_relation::PropagateEmptyRelation;
36use datafusion_optimizer::push_down_filter::PushDownFilter;
37use datafusion_optimizer::rewrite_set_comparison::RewriteSetComparison;
38use datafusion_optimizer::scalar_subquery_to_join::ScalarSubqueryToJoin;
39use promql::extension_plan::SeriesDivide;
40use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
41use table::metadata::TableType;
42use table::table::adapter::DfTableProviderAdapter;
43
44use crate::dist_plan::RemoteDynFilterProducerId;
45use crate::dist_plan::analyzer::utils::{
46    PatchOptimizerContext, PlanTreeExpressionSimplifier, aliased_columns_for,
47    rewrite_merge_sort_exprs,
48};
49use crate::dist_plan::commutativity::{
50    Categorizer, Commutativity, partial_commutative_transformer,
51};
52use crate::dist_plan::merge_scan::MergeScanLogicalPlan;
53use crate::dist_plan::merge_sort::MergeSortLogicalPlan;
54use crate::metrics::PUSH_DOWN_FALLBACK_ERRORS_TOTAL;
55use crate::plan::ExtractExpr;
56use crate::query_engine::DefaultSerializer;
57
58#[cfg(test)]
59mod test;
60
61mod fallback;
62pub(crate) mod utils;
63
64pub(crate) use utils::AliasMapping;
65
66/// Placeholder for other physical partition columns that are not in logical table
67const OTHER_PHY_PART_COL_PLACEHOLDER: &str = "__OTHER_PHYSICAL_PART_COLS_PLACEHOLDER__";
68
69#[derive(Debug, Clone)]
70pub struct DistPlannerOptions {
71    pub allow_query_fallback: bool,
72}
73
74impl ConfigExtension for DistPlannerOptions {
75    const PREFIX: &'static str = "dist_planner";
76}
77
78impl ExtensionOptions for DistPlannerOptions {
79    fn as_any(&self) -> &dyn std::any::Any {
80        self
81    }
82
83    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
84        self
85    }
86
87    fn cloned(&self) -> Box<dyn ExtensionOptions> {
88        Box::new(self.clone())
89    }
90
91    fn set(&mut self, key: &str, value: &str) -> DfResult<()> {
92        Err(datafusion_common::DataFusionError::NotImplemented(format!(
93            "DistPlannerOptions does not support set key: {key} with value: {value}"
94        )))
95    }
96
97    fn entries(&self) -> Vec<datafusion::config::ConfigEntry> {
98        vec![datafusion::config::ConfigEntry {
99            key: "allow_query_fallback".to_string(),
100            value: Some(self.allow_query_fallback.to_string()),
101            description: "Allow query fallback to fallback plan rewriter",
102        }]
103    }
104}
105
106#[derive(Debug)]
107pub struct DistPlannerAnalyzer;
108
109impl AnalyzerRule for DistPlannerAnalyzer {
110    fn name(&self) -> &str {
111        "DistPlannerAnalyzer"
112    }
113
114    fn analyze(
115        &self,
116        plan: LogicalPlan,
117        config: &ConfigOptions,
118    ) -> datafusion_common::Result<LogicalPlan> {
119        let mut config = config.clone();
120        // Aligned with the behavior in `datafusion_optimizer::OptimizerContext::new()`.
121        config.optimizer.filter_null_join_keys = true;
122        let config = Arc::new(config);
123        let opt = config.extensions.get::<DistPlannerOptions>();
124        let allow_fallback = opt.map(|o| o.allow_query_fallback).unwrap_or(false);
125
126        let optimizer_context = PatchOptimizerContext {
127            inner: datafusion_optimizer::OptimizerContext::new(),
128            config: config.clone(),
129        };
130
131        let plan = plan
132            .rewrite_with_subqueries(&mut PlanTreeExpressionSimplifier::new(optimizer_context))?
133            .data;
134        let fallback_plan = plan.clone();
135
136        // Run a filter-focused optimizer subset before MergeScan wraps remote
137        // inputs. MergeScan intentionally hides its remote_input from later
138        // optimizer passes; this pass only normalizes/decorrelates enough for
139        // DataFusion's PushDownFilter to put side-local predicates into scans.
140        // Keep this narrow: rules like PushDownLimit, OptimizeProjections, and
141        // DISTINCT rewrites can change global distributed-planning boundaries.
142        let optimizer_context = PatchOptimizerContext {
143            inner: datafusion_optimizer::OptimizerContext::new(),
144            config: config.clone(),
145        };
146        let plan = match pre_merge_scan_optimizer().optimize(plan, &optimizer_context, |_, _| {}) {
147            Ok(plan) => plan,
148            Err(err) => {
149                if allow_fallback {
150                    common_telemetry::warn!(err; "Failed to pre-optimize plan, using fallback plan rewriter for plan: {fallback_plan}");
151                    PUSH_DOWN_FALLBACK_ERRORS_TOTAL.inc();
152                    return self.use_fallback(fallback_plan);
153                } else {
154                    return Err(err);
155                }
156            }
157        };
158
159        let result = match self.try_push_down(plan.clone()) {
160            Ok(plan) => plan,
161            Err(err) => {
162                if allow_fallback {
163                    common_telemetry::warn!(err; "Failed to push down plan, using fallback plan rewriter for plan: {plan}");
164                    // if push down failed, use fallback plan rewriter
165                    PUSH_DOWN_FALLBACK_ERRORS_TOTAL.inc();
166                    self.use_fallback(fallback_plan)?
167                } else {
168                    return Err(err);
169                }
170            }
171        };
172
173        Ok(result)
174    }
175}
176
177/// Builds the small optimizer pre-pass that runs before `MergeScan` wrapping.
178///
179/// This is intentionally not DataFusion's full optimizer. After
180/// `PlanRewriter` wraps remote table scans in `MergeScan`,
181/// `MergeScanLogicalPlan::inputs()` hides `remote_input`, so ordinary optimizer
182/// rules can no longer see into the remote side. The main rule we need here is
183/// `PushDownFilter`: it moves side-local join/filter predicates into
184/// `TableScan.filters`, where region pruning and scan-level pruning can use
185/// them.
186///
187/// The rules before `PushDownFilter` are only the minimum cleanup/rewrite steps
188/// needed to make that filter pushdown safe around subqueries and set
189/// comparisons. For example, `RewriteSetComparison` handles ANY/ALL before they
190/// can become scan filters, and the decorrelation/subquery rules expose
191/// supported predicates as joins/filters instead of leaving raw subquery
192/// expressions under a scan.
193///
194/// Keep this list narrow. Do not add broad plan-shaping rules such as
195/// `PushDownLimit`, projection optimization, DISTINCT rewrites, or join-type
196/// rewrites here: those can change the local/remote distributed boundary or
197/// degrade unrelated planning diagnostics. Such rules belong either before this
198/// analyzer or after distributed planning, not in this pre-MergeScan,
199/// filter-focused pass.
200fn pre_merge_scan_optimizer() -> Optimizer {
201    Optimizer::with_rules(vec![
202        Arc::new(RewriteSetComparison::new()),
203        Arc::new(DecorrelatePredicateSubquery::new()),
204        Arc::new(ScalarSubqueryToJoin::new()),
205        Arc::new(DecorrelateLateralJoin::new()),
206        Arc::new(ExtractEquijoinPredicate::new()),
207        Arc::new(EliminateFilter::new()),
208        Arc::new(PropagateEmptyRelation::new()),
209        Arc::new(FilterNullJoinKeys::default()),
210        Arc::new(PushDownFilter::new()),
211    ])
212}
213
214impl DistPlannerAnalyzer {
215    /// Try push down as many nodes as possible
216    fn try_push_down(&self, plan: LogicalPlan) -> DfResult<LogicalPlan> {
217        let plan = plan.transform(&Self::inspect_plan_with_subquery)?;
218        let mut rewriter = PlanRewriter::default();
219        let result = plan.data.rewrite(&mut rewriter)?.data;
220        Self::assign_merge_scan_remote_dyn_filter_producer_ids(result)
221    }
222
223    /// Use fallback plan rewriter to rewrite the plan and only push down table scan nodes
224    fn use_fallback(&self, plan: LogicalPlan) -> DfResult<LogicalPlan> {
225        let mut rewriter = fallback::FallbackPlanRewriter;
226        let result = plan.rewrite(&mut rewriter)?.data;
227        Self::assign_merge_scan_remote_dyn_filter_producer_ids(result)
228    }
229
230    fn inspect_plan_with_subquery(plan: LogicalPlan) -> DfResult<Transformed<LogicalPlan>> {
231        // Workaround for https://github.com/GreptimeTeam/greptimedb/issues/5469 and https://github.com/GreptimeTeam/greptimedb/issues/5799
232        // FIXME(yingwen): Remove the `Limit` plan once we update DataFusion.
233        if let LogicalPlan::Limit(_) | LogicalPlan::Distinct(_) = &plan {
234            return Ok(Transformed::no(plan));
235        }
236
237        let exprs = plan
238            .expressions_consider_join()
239            .into_iter()
240            .map(|e| e.transform(&Self::transform_subquery).map(|x| x.data))
241            .collect::<DfResult<Vec<_>>>()?;
242
243        // Some plans that are special treated (should not call `with_new_exprs` on them)
244        if !matches!(plan, LogicalPlan::Unnest(_)) {
245            let inputs = plan.inputs().into_iter().cloned().collect::<Vec<_>>();
246            Ok(Transformed::yes(plan.with_new_exprs(exprs, inputs)?))
247        } else {
248            Ok(Transformed::no(plan))
249        }
250    }
251
252    fn transform_subquery(expr: Expr) -> DfResult<Transformed<Expr>> {
253        match expr {
254            Expr::Exists(exists) => Ok(Transformed::yes(Expr::Exists(Exists {
255                subquery: Self::handle_subquery(exists.subquery)?,
256                negated: exists.negated,
257            }))),
258            Expr::InSubquery(in_subquery) => Ok(Transformed::yes(Expr::InSubquery(InSubquery {
259                expr: in_subquery.expr,
260                subquery: Self::handle_subquery(in_subquery.subquery)?,
261                negated: in_subquery.negated,
262            }))),
263            Expr::ScalarSubquery(scalar_subquery) => Ok(Transformed::yes(Expr::ScalarSubquery(
264                Self::handle_subquery(scalar_subquery)?,
265            ))),
266
267            _ => Ok(Transformed::no(expr)),
268        }
269    }
270
271    fn handle_subquery(subquery: Subquery) -> DfResult<Subquery> {
272        let mut rewriter = PlanRewriter::default();
273        let mut rewrote_subquery = subquery
274            .subquery
275            .as_ref()
276            .clone()
277            .rewrite(&mut rewriter)?
278            .data;
279        // Workaround. DF doesn't support the first plan in subquery to be an Extension
280        if matches!(rewrote_subquery, LogicalPlan::Extension(_)) {
281            let output_schema = rewrote_subquery.schema().clone();
282            let project_exprs = output_schema
283                .fields()
284                .iter()
285                .map(|f| col_fn(f.name()))
286                .collect::<Vec<_>>();
287            rewrote_subquery = LogicalPlanBuilder::from(rewrote_subquery)
288                .project(project_exprs)?
289                .build()?;
290        }
291
292        Ok(Subquery {
293            subquery: Arc::new(rewrote_subquery),
294            outer_ref_columns: subquery.outer_ref_columns,
295            spans: Default::default(),
296        })
297    }
298
299    fn assign_merge_scan_remote_dyn_filter_producer_ids(
300        plan: LogicalPlan,
301    ) -> DfResult<LogicalPlan> {
302        let mut assigner = MergeScanRemoteDynFilterProducerIdAssigner::default();
303        Ok(plan.rewrite_with_subqueries(&mut assigner)?.data)
304    }
305}
306
307#[derive(Debug, Default)]
308struct RemoteDynFilterProducerIdAllocator {
309    next_remote_dyn_filter_producer_id: u64,
310}
311
312impl RemoteDynFilterProducerIdAllocator {
313    fn allocate(&mut self) -> RemoteDynFilterProducerId {
314        self.next_remote_dyn_filter_producer_id += 1;
315        RemoteDynFilterProducerId::new(self.next_remote_dyn_filter_producer_id)
316    }
317}
318
319/// Assigns query-local RDF producer ids to visible `MergeScan` nodes after plan rewriting.
320#[derive(Debug, Default)]
321struct MergeScanRemoteDynFilterProducerIdAssigner {
322    remote_dyn_filter_producer_id_allocator: RemoteDynFilterProducerIdAllocator,
323}
324
325impl TreeNodeRewriter for MergeScanRemoteDynFilterProducerIdAssigner {
326    type Node = LogicalPlan;
327
328    fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
329        let LogicalPlan::Extension(extension) = &node else {
330            return Ok(Transformed::no(node));
331        };
332        let Some(merge_scan) = extension
333            .node
334            .as_any()
335            .downcast_ref::<MergeScanLogicalPlan>()
336        else {
337            return Ok(Transformed::no(node));
338        };
339
340        Ok(Transformed::yes(
341            merge_scan
342                .clone()
343                .with_remote_dyn_filter_producer_id(
344                    self.remote_dyn_filter_producer_id_allocator.allocate(),
345                )
346                .into_logical_plan(),
347        ))
348    }
349}
350
351/// Status of the rewriter to mark if the current pass is expanded
352#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
353enum RewriterStatus {
354    #[default]
355    Unexpanded,
356    Expanded,
357}
358
359#[derive(Debug, Default)]
360struct PlanRewriter {
361    /// Current level in the tree
362    level: usize,
363    /// Simulated stack for the `rewrite` recursion
364    stack: Vec<(LogicalPlan, usize)>,
365    /// Stages to be expanded, will be added as parent node of merge scan one by one
366    stage: Vec<LogicalPlan>,
367    status: RewriterStatus,
368    /// Partition columns of the table in current pass
369    partition_cols: Option<AliasMapping>,
370    /// use stack count as scope to determine column requirements is needed or not
371    /// i.e for a logical plan like:
372    /// ```ignore
373    /// 1: Projection: t.number
374    /// 2: Sort: t.pk1+t.pk2
375    /// 3. Projection: t.number, t.pk1, t.pk2
376    /// ```
377    /// `Sort` will make a column requirement for `t.pk1+t.pk2` at level 2.
378    /// Which making `Projection` at level 1 need to add a ref to `t.pk1` as well.
379    /// So that the expanded plan will be
380    /// ```ignore
381    /// Projection: t.number
382    ///   MergeSort: t.pk1+t.pk2
383    ///     MergeScan: remote_input=
384    /// Projection: t.number, "t.pk1+t.pk2" <--- the original `Projection` at level 1 get added with `t.pk1+t.pk2`
385    ///  Sort: t.pk1+t.pk2
386    ///    Projection: t.number, t.pk1, t.pk2
387    /// ```
388    /// Making `MergeSort` can have `t.pk1+t.pk2` as input.
389    /// Meanwhile `Projection` at level 3 doesn't need to add any new column because 3 > 2
390    /// and col requirements at level 2 is not applicable for level 3.
391    ///
392    /// see more details in test `expand_proj_step_aggr` and `expand_proj_sort_proj`
393    ///
394    /// TODO(discord9): a simpler solution to track column requirements for merge scan
395    column_requirements: Vec<(HashSet<Column>, usize)>,
396    /// Whether to expand on next call
397    /// This is used to handle the case where a plan is transformed, but need to be expanded from it's
398    /// parent node. For example a Aggregate plan is split into two parts in frontend and datanode, and need
399    /// to be expanded from the parent node of the Aggregate plan.
400    expand_on_next_call: bool,
401    /// Expanding on next partial/conditional/transformed commutative plan
402    /// This is used to handle the case where a plan is transformed, but still
403    /// need to push down as many node as possible before next partial/conditional/transformed commutative
404    /// plan. I.e.
405    /// ```ignore
406    /// Limit:
407    ///     Sort:
408    /// ```
409    /// where `Limit` is partial commutative, and `Sort` is conditional commutative.
410    /// In this case, we need to expand the `Limit` plan,
411    /// so that we can push down the `Sort` plan as much as possible.
412    expand_on_next_part_cond_trans_commutative: bool,
413    new_child_plan: Option<LogicalPlan>,
414}
415
416impl PlanRewriter {
417    fn get_parent(&self) -> Option<&LogicalPlan> {
418        // level starts from 1, it's safe to minus by 1
419        self.stack
420            .iter()
421            .rev()
422            .find(|(_, level)| *level == self.level - 1)
423            .map(|(node, _)| node)
424    }
425
426    /// Return true if should stop and expand. The input plan is the parent node of current node
427    fn should_expand(&mut self, plan: &LogicalPlan) -> DfResult<bool> {
428        debug!(
429            "Check should_expand at level: {}  with Stack:\n{}, ",
430            self.level,
431            self.stack
432                .iter()
433                .map(|(p, l)| format!("{l}:{}{}", "  ".repeat(l - 1), p.display()))
434                .collect::<Vec<String>>()
435                .join("\n"),
436        );
437        if let Err(e) = DFLogicalSubstraitConvertor.encode(plan, DefaultSerializer) {
438            debug!(
439                "PlanRewriter: plan cannot be converted to substrait with error={e:?}, expanding now: {plan}"
440            );
441            return Ok(true);
442        }
443
444        if self.expand_on_next_call {
445            self.expand_on_next_call = false;
446            debug!("PlanRewriter: expand_on_next_call is true, expanding now");
447            return Ok(true);
448        }
449
450        if self.expand_on_next_part_cond_trans_commutative {
451            let comm = Categorizer::check_plan(plan, self.partition_cols.clone())?;
452            match comm {
453                Commutativity::PartialCommutative => {
454                    // a small difference is that for partial commutative, we still need to
455                    // push down it(so `Limit` can be pushed down)
456
457                    // notice how limit needed to be expanded as well to make sure query is correct
458                    // i.e. `Limit fetch=10` need to be pushed down to the leaf node
459                    self.expand_on_next_part_cond_trans_commutative = false;
460                    self.expand_on_next_call = true;
461                }
462                Commutativity::ConditionalCommutative(_)
463                | Commutativity::TransformedCommutative { .. } => {
464                    // again a new node that can be push down, we should just
465                    // do push down now and avoid further expansion
466                    self.expand_on_next_part_cond_trans_commutative = false;
467                    debug!(
468                        "PlanRewriter: meet a new conditional/transformed commutative plan, expanding now: {plan}"
469                    );
470                    return Ok(true);
471                }
472                _ => (),
473            }
474        }
475
476        match Categorizer::check_plan(plan, self.partition_cols.clone())? {
477            Commutativity::Commutative => {
478                // PATCH: we should reconsider SORT's commutativity instead of doing this trick.
479                // explain: for a fully commutative SeriesDivide, its child Sort plan only serves it. I.e., that
480                //   Sort plan is also fully commutative, instead of conditional commutative. So we can remove
481                //   the generated MergeSort from stage safely.
482                if let LogicalPlan::Extension(ext_a) = plan
483                    && ext_a.node.name() == SeriesDivide::name()
484                    && let Some(LogicalPlan::Extension(ext_b)) = self.stage.last()
485                    && ext_b.node.name() == MergeSortLogicalPlan::name()
486                {
487                    // revert last `ConditionalCommutative` result for Sort plan in this case.
488                    // also need to remove any column requirements made by the Sort Plan
489                    // as it may refer to columns later no longer exist(rightfully) like by aggregate or projection
490                    self.stage.pop();
491                    self.expand_on_next_part_cond_trans_commutative = false;
492                    self.column_requirements.clear();
493                }
494            }
495            Commutativity::PartialCommutative => {
496                if let Some(plan) = partial_commutative_transformer(plan) {
497                    // notice this plan is parent of current node, so `self.level - 1` when updating column requirements
498                    self.update_column_requirements(&plan, self.level - 1);
499                    self.expand_on_next_part_cond_trans_commutative = true;
500                    self.stage.push(plan)
501                }
502            }
503            Commutativity::ConditionalCommutative(transformer) => {
504                if let Some(transformer) = transformer
505                    && let Some(plan) = transformer(plan)
506                {
507                    // notice this plan is parent of current node, so `self.level - 1` when updating column requirements
508                    self.update_column_requirements(&plan, self.level - 1);
509                    self.expand_on_next_part_cond_trans_commutative = true;
510                    self.stage.push(plan)
511                }
512            }
513            Commutativity::TransformedCommutative { transformer } => {
514                if let Some(transformer) = transformer {
515                    let transformer_actions = transformer(plan)?;
516                    debug!(
517                        "PlanRewriter: transformed plan: {}\n from {plan}",
518                        transformer_actions
519                            .extra_parent_plans
520                            .iter()
521                            .enumerate()
522                            .map(|(i, p)| format!(
523                                "Extra {i}-th parent plan from parent to child = {}",
524                                p.display()
525                            ))
526                            .collect::<Vec<_>>()
527                            .join("\n")
528                    );
529                    if let Some(new_child_plan) = &transformer_actions.new_child_plan {
530                        debug!("PlanRewriter: new child plan: {}", new_child_plan);
531                    }
532                    if let Some(last_stage) = transformer_actions.extra_parent_plans.last() {
533                        // update the column requirements from the last stage
534                        // notice current plan's parent plan is where we need to apply the column requirements
535                        self.update_column_requirements(last_stage, self.level - 1);
536                    }
537                    self.stage
538                        .extend(transformer_actions.extra_parent_plans.into_iter().rev());
539                    self.expand_on_next_call = true;
540                    self.new_child_plan = transformer_actions.new_child_plan;
541                }
542            }
543            Commutativity::NonCommutative
544            | Commutativity::Unimplemented
545            | Commutativity::Unsupported => {
546                debug!("PlanRewriter: meet a non-commutative plan, expanding now: {plan}");
547                return Ok(true);
548            }
549        }
550
551        Ok(false)
552    }
553
554    /// Update the column requirements for the current plan, plan_level is the level of the plan
555    /// in the stack, which is used to determine if the column requirements are applicable
556    /// for other plans in the stack.
557    fn update_column_requirements(&mut self, plan: &LogicalPlan, plan_level: usize) {
558        debug!(
559            "PlanRewriter: update column requirements for plan: {plan}\n with old column_requirements: {:?}",
560            self.column_requirements
561        );
562        let mut container = HashSet::new();
563        for expr in plan.expressions() {
564            // this method won't fail
565            let _ = expr_to_columns(&expr, &mut container);
566        }
567
568        self.column_requirements.push((container, plan_level));
569        debug!(
570            "PlanRewriter: updated column requirements: {:?}",
571            self.column_requirements
572        );
573    }
574
575    fn is_expanded(&self) -> bool {
576        self.status == RewriterStatus::Expanded
577    }
578
579    fn set_expanded(&mut self) {
580        self.status = RewriterStatus::Expanded;
581    }
582
583    fn set_unexpanded(&mut self) {
584        self.status = RewriterStatus::Unexpanded;
585    }
586
587    fn maybe_set_partitions(&mut self, plan: &LogicalPlan) -> DfResult<()> {
588        if let Some(part_cols) = &mut self.partition_cols {
589            // update partition alias
590            let child = plan.inputs().first().cloned().ok_or_else(|| {
591                datafusion_common::DataFusionError::Internal(format!(
592                    "PlanRewriter: maybe_set_partitions: plan has no child: {plan}"
593                ))
594            })?;
595
596            for (_col_name, alias_set) in part_cols.iter_mut() {
597                let aliased_cols = aliased_columns_for(
598                    &alias_set.clone().into_iter().collect(),
599                    plan,
600                    Some(child),
601                )?;
602                *alias_set = aliased_cols.into_values().flatten().collect();
603            }
604
605            debug!(
606                "PlanRewriter: maybe_set_partitions: updated partition columns: {:?} at plan: {}",
607                part_cols,
608                plan.display()
609            );
610
611            return Ok(());
612        }
613
614        if let LogicalPlan::TableScan(table_scan) = plan
615            && let Some(source) = table_scan
616                .source
617                .as_any()
618                .downcast_ref::<DefaultTableSource>()
619            && let Some(provider) = source
620                .table_provider
621                .as_any()
622                .downcast_ref::<DfTableProviderAdapter>()
623        {
624            let table = provider.table();
625            if table.table_type() == TableType::Base {
626                let info = table.table_info();
627                let partition_key_indices = info.meta.partition_key_indices.clone();
628                let schema = info.meta.schema.clone();
629                let mut partition_cols = partition_key_indices
630                    .iter()
631                    .map(|index| schema.column_name_by_index(*index).to_string())
632                    .collect::<Vec<String>>();
633                debug!(
634                    "PlanRewriter: loaded table partition metadata, table: {}, table_id: {}, partition_key_indices: {:?}, partition_columns: {:?}",
635                    info.name, info.ident.table_id, info.meta.partition_key_indices, partition_cols,
636                );
637
638                let partition_rules = table.partition_rules();
639                let exist_phy_part_cols_not_in_logical_table = partition_rules
640                    .map(|r| !r.extra_phy_cols_not_in_logical_table.is_empty())
641                    .unwrap_or(false);
642
643                if exist_phy_part_cols_not_in_logical_table && partition_cols.is_empty() {
644                    // there are other physical partition columns that are not in logical table and part cols are empty
645                    // so we need to add a placeholder for it to prevent certain optimization
646                    // this is used to make sure the final partition columns(that optimizer see) are not empty
647                    // notice if originally partition_cols is not empty, then there is no need to add this place holder,
648                    // as subset of phy part cols can still be used for certain optimization, and it works as if
649                    // those columns are always null
650                    // This helps with distinguishing between non-partitioned table and partitioned table with all phy part cols not in logical table
651                    partition_cols.push(OTHER_PHY_PART_COL_PLACEHOLDER.to_string());
652                }
653                self.partition_cols = Some(
654                            partition_cols
655                                .into_iter()
656                                .map(|c| {
657                                    if c == OTHER_PHY_PART_COL_PLACEHOLDER {
658                                        // for placeholder, just return a empty alias
659                                        return Ok((c.clone(), BTreeSet::new()));
660                                    }
661                                    let index =
662                                        if let Some(c) = plan.schema().index_of_column_by_name(None, &c){
663                                            c
664                                        } else {
665                                            // the `projection` field of `TableScan` doesn't contain the partition columns,
666                                            // this is similar to not having a alias, hence return empty alias set
667                                            return Ok((c.clone(), BTreeSet::new()))
668                                        };
669                                    let column = plan.schema().columns().get(index).cloned().ok_or_else(|| {
670                                        datafusion_common::DataFusionError::Internal(format!(
671                                            "PlanRewriter: maybe_set_partitions: column index {index} out of bounds in schema of plan: {plan}"
672                                        ))
673                                    })?;
674                                    Ok((c.clone(), BTreeSet::from([column])))
675                                })
676                                .collect::<DfResult<AliasMapping>>()?,
677                        );
678            }
679        }
680
681        Ok(())
682    }
683
684    /// pop one stack item and reduce the level by 1
685    fn pop_stack(&mut self) {
686        self.level -= 1;
687        self.stack.pop();
688    }
689
690    fn expand(&mut self, mut on_node: LogicalPlan) -> DfResult<LogicalPlan> {
691        // store schema before expand, new child plan might have a different schema, so not using it
692        let schema = on_node.schema().clone();
693        if let Some(new_child_plan) = self.new_child_plan.take() {
694            // if there is a new child plan, use it as the new root
695            on_node = new_child_plan;
696        }
697        let mut rewriter = EnforceDistRequirementRewriter::new(
698            std::mem::take(&mut self.column_requirements),
699            self.level,
700        );
701        debug!(
702            "PlanRewriter: enforce column requirements for node: {on_node} with rewriter: {rewriter:?}"
703        );
704        on_node = on_node.rewrite(&mut rewriter)?.data;
705        debug!(
706            "PlanRewriter: after enforced column requirements with rewriter: {rewriter:?} for node:\n{on_node}"
707        );
708
709        debug!(
710            "PlanRewriter: expand on node: {on_node} with partition col alias mapping: {:?}",
711            self.partition_cols
712        );
713
714        // add merge scan as the new root
715        let mut node = MergeScanLogicalPlan::new(
716            on_node.clone(),
717            false,
718            // at this stage, the partition cols should be set
719            // treat it as non-partitioned if None
720            self.partition_cols.clone().unwrap_or_default(),
721        )
722        .into_logical_plan();
723
724        // expand stages
725        for new_stage in self.stage.drain(..) {
726            // tracking alias for merge sort's sort exprs
727            let new_stage = if let LogicalPlan::Extension(ext) = &new_stage
728                && let Some(merge_sort) = ext.node.as_any().downcast_ref::<MergeSortLogicalPlan>()
729            {
730                // TODO(discord9): change `on_node` to `node` once alias tracking is supported for merge scan
731                rewrite_merge_sort_exprs(merge_sort, &on_node)?
732            } else {
733                new_stage
734            };
735            node = new_stage
736                .with_new_exprs(new_stage.expressions_consider_join(), vec![node.clone()])?;
737        }
738        self.set_expanded();
739
740        // recover the schema, this make sure after expand the schema is the same as old node
741        // because after expand the raw top node might have extra columns i.e. sorting columns for `Sort` node
742        let node = LogicalPlanBuilder::from(node)
743            .project(schema.iter().map(|(qualifier, field)| {
744                Expr::Column(Column::new(qualifier.cloned(), field.name()))
745            }))?
746            .build()?;
747
748        Ok(node)
749    }
750}
751
752/// Implementation of the [`TreeNodeRewriter`] trait which is responsible for rewriting
753/// logical plans to enforce various requirement for distributed query.
754///
755/// Requirements enforced by this rewriter:
756/// - Enforce column requirements for `LogicalPlan::Projection` nodes. Makes sure the
757///   required columns are available in the sub plan.
758///
759#[derive(Debug)]
760struct EnforceDistRequirementRewriter {
761    /// only enforce column requirements after the expanding node in question,
762    /// meaning only for node with `cur_level` <= `level` will consider adding those column requirements
763    /// TODO(discord9): a simpler solution to track column requirements for merge scan
764    column_requirements: Vec<(HashSet<Column>, usize)>,
765    /// only apply column requirements >= `cur_level`
766    /// this is used to avoid applying column requirements that are not needed
767    /// for the current node, i.e. the node is not in the scope of the column requirements
768    /// i.e, for this plan:
769    /// ```ignore
770    /// Aggregate: min(t.number)
771    ///   Projection: t.number
772    /// ```
773    /// when on `Projection` node, we don't need to apply the column requirements of `Aggregate` node
774    /// because the `Projection` node is not in the scope of the `Aggregate` node
775    cur_level: usize,
776    plan_per_level: BTreeMap<usize, LogicalPlan>,
777}
778
779impl EnforceDistRequirementRewriter {
780    fn new(column_requirements: Vec<(HashSet<Column>, usize)>, cur_level: usize) -> Self {
781        debug!(
782            "Create EnforceDistRequirementRewriter with column_requirements: {:?} at cur_level: {}",
783            column_requirements, cur_level
784        );
785        Self {
786            column_requirements,
787            cur_level,
788            plan_per_level: BTreeMap::new(),
789        }
790    }
791
792    /// Return a mapping from (original column, level) to aliased columns in current node of all
793    /// applicable column requirements
794    /// i.e. only column requirements with level >= `cur_level` will be considered
795    fn get_current_applicable_column_requirements(
796        &self,
797        node: &LogicalPlan,
798    ) -> DfResult<BTreeMap<(Column, usize), BTreeSet<Column>>> {
799        let col_req_per_level = self
800            .column_requirements
801            .iter()
802            .filter(|(_, level)| *level >= self.cur_level)
803            .collect::<Vec<_>>();
804
805        // track alias for columns and use aliased columns instead
806        // aliased col reqs at current level
807        let mut result_alias_mapping = BTreeMap::new();
808        let Some(child) = node.inputs().first().cloned() else {
809            return Ok(Default::default());
810        };
811        for (col_req, level) in col_req_per_level {
812            if let Some(original) = self.plan_per_level.get(level) {
813                // query for alias in current plan
814                let aliased_cols =
815                    aliased_columns_for(&col_req.iter().cloned().collect(), node, Some(original))?;
816                for original_col in col_req {
817                    let aliased_cols = aliased_cols.get(original_col).cloned();
818                    if let Some(cols) = aliased_cols
819                        && !cols.is_empty()
820                    {
821                        result_alias_mapping.insert((original_col.clone(), *level), cols);
822                    } else {
823                        // if no aliased column found in current node, there should be alias in child node as promised by enforce col reqs
824                        // because it should insert required columns in child node
825                        // so we can find the alias in child node
826                        // if not found, it's an internal error
827                        let aliases_in_child = aliased_columns_for(
828                            &[original_col.clone()].into(),
829                            child,
830                            Some(original),
831                        )?;
832                        let Some(aliases) = aliases_in_child
833                            .get(original_col)
834                            .cloned()
835                            .filter(|a| !a.is_empty())
836                        else {
837                            return Err(datafusion_common::DataFusionError::Internal(format!(
838                                "EnforceDistRequirementRewriter: no alias found for required column {original_col} at level {level} in current node's child plan: \n{child} from original plan: \n{original}",
839                            )));
840                        };
841
842                        result_alias_mapping.insert((original_col.clone(), *level), aliases);
843                    }
844                }
845            }
846        }
847        Ok(result_alias_mapping)
848    }
849}
850
851impl TreeNodeRewriter for EnforceDistRequirementRewriter {
852    type Node = LogicalPlan;
853
854    fn f_down(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
855        // check that node doesn't have multiple children, i.e. join/subquery
856        if node.inputs().len() > 1 {
857            return Err(datafusion_common::DataFusionError::Internal(
858                "EnforceDistRequirementRewriter: node with multiple inputs is not supported"
859                    .to_string(),
860            ));
861        }
862        self.plan_per_level.insert(self.cur_level, node.clone());
863        self.cur_level += 1;
864        Ok(Transformed::no(node))
865    }
866
867    fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
868        self.cur_level -= 1;
869        // first get all applicable column requirements
870
871        // make sure all projection applicable scope has the required columns
872        if let LogicalPlan::Projection(ref projection) = node {
873            let mut applicable_column_requirements =
874                self.get_current_applicable_column_requirements(&node)?;
875
876            debug!(
877                "EnforceDistRequirementRewriter: applicable column requirements at level {} = {:?} for node {}",
878                self.cur_level,
879                applicable_column_requirements,
880                node.display()
881            );
882
883            for expr in &projection.expr {
884                let (qualifier, name) = expr.qualified_name();
885                let column = Column::new(qualifier, name);
886                applicable_column_requirements.retain(|_col_level, alias_set| {
887                    // remove all columns that are already in the projection exprs
888                    !alias_set.contains(&column)
889                });
890            }
891            if applicable_column_requirements.is_empty() {
892                return Ok(Transformed::no(node));
893            }
894
895            let mut new_exprs = projection.expr.clone();
896            for (col, alias_set) in &applicable_column_requirements {
897                // use the first alias in alias set as the column to add
898                new_exprs.push(Expr::Column(alias_set.first().cloned().ok_or_else(
899                    || {
900                        datafusion_common::DataFusionError::Internal(
901                            format!("EnforceDistRequirementRewriter: alias set is empty, for column {col:?} in node {node}"),
902                        )
903                    },
904                )?));
905            }
906            let new_node =
907                node.with_new_exprs(new_exprs, node.inputs().into_iter().cloned().collect())?;
908            debug!(
909                "EnforceDistRequirementRewriter: added missing columns {:?} to projection node from old node: \n{node}\n Making new node: \n{new_node}",
910                applicable_column_requirements
911            );
912
913            // update plan for later use
914            self.plan_per_level.insert(self.cur_level, new_node.clone());
915
916            // still need to continue for next projection if applicable
917            return Ok(Transformed::yes(new_node));
918        }
919        Ok(Transformed::no(node))
920    }
921}
922
923impl TreeNodeRewriter for PlanRewriter {
924    type Node = LogicalPlan;
925
926    /// descend
927    fn f_down<'a>(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
928        self.level += 1;
929        self.stack.push((node.clone(), self.level));
930        // decendening will clear the stage
931        self.stage.clear();
932        self.set_unexpanded();
933        self.partition_cols = None;
934        Ok(Transformed::no(node))
935    }
936
937    /// ascend
938    ///
939    /// Besure to call `pop_stack` before returning
940    fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
941        // only expand once on each ascending
942        if self.is_expanded() {
943            self.pop_stack();
944            return Ok(Transformed::no(node));
945        }
946
947        // only expand when the leaf is table scan
948        if node.inputs().is_empty() && !matches!(node, LogicalPlan::TableScan(_)) {
949            self.set_expanded();
950            self.pop_stack();
951            return Ok(Transformed::no(node));
952        }
953
954        self.maybe_set_partitions(&node)?;
955
956        let Some(parent) = self.get_parent() else {
957            debug!("Plan Rewriter: expand now for no parent found for node: {node}");
958            let node = self.expand(node);
959            debug!(
960                "PlanRewriter: expanded plan: {}",
961                match &node {
962                    Ok(n) => n.to_string(),
963                    Err(e) => format!("Error expanding plan: {e}"),
964                }
965            );
966            let node = node?;
967            self.pop_stack();
968            return Ok(Transformed::yes(node));
969        };
970
971        let parent = parent.clone();
972
973        if self.should_expand(&parent)? {
974            // TODO(ruihang): does this work for nodes with multiple children?;
975            debug!(
976                "PlanRewriter: should expand child:\n {node}\n Of Parent: {}",
977                parent.display()
978            );
979            let node = self.expand(node);
980            debug!(
981                "PlanRewriter: expanded plan: {}",
982                match &node {
983                    Ok(n) => n.to_string(),
984                    Err(e) => format!("Error expanding plan: {e}"),
985                }
986            );
987            let node = node?;
988            self.pop_stack();
989            return Ok(Transformed::yes(node));
990        }
991
992        self.pop_stack();
993        Ok(Transformed::no(node))
994    }
995}