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