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