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