Skip to main content

query/dist_plan/
planner.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
15//! [ExtensionPlanner] implementation for distributed planner
16
17use std::sync::Arc;
18
19use ahash::HashMap;
20use async_trait::async_trait;
21use catalog::CatalogManagerRef;
22use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
23use common_telemetry::debug;
24use datafusion::common::Result;
25use datafusion::datasource::DefaultTableSource;
26use datafusion::execution::context::SessionState;
27use datafusion::physical_plan::ExecutionPlan;
28use datafusion::physical_planner::{ExtensionPlanner, PhysicalPlanner};
29use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor};
30use datafusion_common::{DataFusionError, TableReference};
31use datafusion_expr::{LogicalPlan, UserDefinedLogicalNode};
32use partition::manager::{PartitionRuleManagerRef, create_partitions_from_region_routes};
33use session::context::QueryContext;
34use snafu::{OptionExt, ResultExt};
35use store_api::storage::RegionId;
36pub use table::metadata::TableType;
37use table::table::adapter::DfTableProviderAdapter;
38use table::table_name::TableName;
39
40use crate::dist_plan::PredicateExtractor;
41use crate::dist_plan::merge_scan::{MergeScanExec, MergeScanLogicalPlan};
42use crate::dist_plan::merge_sort::MergeSortLogicalPlan;
43use crate::dist_plan::region_pruner::ConstraintPruner;
44use crate::error::{CatalogSnafu, PartitionRuleManagerSnafu, TableNotFoundSnafu};
45use crate::region_query::RegionQueryHandlerRef;
46
47/// Planner for convert merge sort logical plan to physical plan
48///
49/// it is currently a fallback to sort, and doesn't change the execution plan:
50/// `MergeSort(MergeScan) -> Sort(MergeScan) - to physical plan -> ...`
51/// It should be applied after `DistExtensionPlanner`
52///
53/// (Later when actually impl this merge sort)
54///
55/// We should ensure the number of partition is not smaller than the number of region at present. Otherwise this would result in incorrect output.
56pub struct MergeSortExtensionPlanner {}
57
58#[async_trait]
59impl ExtensionPlanner for MergeSortExtensionPlanner {
60    async fn plan_extension(
61        &self,
62        planner: &dyn PhysicalPlanner,
63        node: &dyn UserDefinedLogicalNode,
64        _logical_inputs: &[&LogicalPlan],
65        physical_inputs: &[Arc<dyn ExecutionPlan>],
66        session_state: &SessionState,
67    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
68        if let Some(merge_sort) = node.as_any().downcast_ref::<MergeSortLogicalPlan>() {
69            if let LogicalPlan::Extension(ext) = &merge_sort.input.as_ref()
70                && ext
71                    .node
72                    .as_any()
73                    .downcast_ref::<MergeScanLogicalPlan>()
74                    .is_some()
75            {
76                let merge_scan_exec = physical_inputs
77                    .first()
78                    .and_then(|p| p.as_any().downcast_ref::<MergeScanExec>())
79                    .ok_or(DataFusionError::Internal(format!(
80                        "Expect MergeSort's input is a MergeScanExec, found {:?}",
81                        physical_inputs
82                    )))?;
83
84                let partition_cnt = merge_scan_exec.partition_count();
85                let region_cnt = merge_scan_exec.region_count();
86                // if partition >= region, we know that every partition stream of merge scan is ordered
87                // and we only need to do a merge sort, otherwise fallback to quick sort
88                let can_merge_sort = partition_cnt >= region_cnt;
89                if can_merge_sort {
90                    // TODO(discord9): use `SortPreservingMergeExec here`
91                }
92                // for now merge sort only exist in logical plan, and have the same effect as `Sort`
93                // doesn't change the execution plan, this will change in the future
94                let ret = planner
95                    .create_physical_plan(&merge_sort.clone().into_sort(), session_state)
96                    .await?;
97                Ok(Some(ret))
98            } else {
99                Ok(None)
100            }
101        } else {
102            Ok(None)
103        }
104    }
105}
106
107pub struct DistExtensionPlanner {
108    catalog_manager: CatalogManagerRef,
109    partition_rule_manager: PartitionRuleManagerRef,
110    region_query_handler: RegionQueryHandlerRef,
111    enable_per_region_metrics: bool,
112}
113
114impl DistExtensionPlanner {
115    pub fn new(
116        catalog_manager: CatalogManagerRef,
117        partition_rule_manager: PartitionRuleManagerRef,
118        region_query_handler: RegionQueryHandlerRef,
119        enable_per_region_metrics: bool,
120    ) -> Self {
121        Self {
122            catalog_manager,
123            partition_rule_manager,
124            region_query_handler,
125            enable_per_region_metrics,
126        }
127    }
128}
129
130#[async_trait]
131impl ExtensionPlanner for DistExtensionPlanner {
132    async fn plan_extension(
133        &self,
134        planner: &dyn PhysicalPlanner,
135        node: &dyn UserDefinedLogicalNode,
136        _logical_inputs: &[&LogicalPlan],
137        _physical_inputs: &[Arc<dyn ExecutionPlan>],
138        session_state: &SessionState,
139    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
140        let Some(merge_scan) = node.as_any().downcast_ref::<MergeScanLogicalPlan>() else {
141            return Ok(None);
142        };
143
144        let input_plan = merge_scan.input();
145        let fallback = |logical_plan| async move {
146            let optimized_plan = self.optimize_input_logical_plan(session_state, logical_plan)?;
147            planner
148                .create_physical_plan(&optimized_plan, session_state)
149                .await
150                .map(Some)
151        };
152
153        if merge_scan.is_placeholder() {
154            // ignore placeholder
155            return fallback(input_plan).await;
156        }
157
158        let optimized_plan = input_plan;
159        let Some(table_name) = Self::extract_full_table_name(input_plan)? else {
160            // no relation found in input plan, going to execute them locally
161            return fallback(optimized_plan).await;
162        };
163
164        let Ok(regions) = self.get_regions(&table_name, input_plan).await else {
165            // no peers found, going to execute them locally
166            return fallback(optimized_plan).await;
167        };
168
169        // TODO(ruihang): generate different execution plans for different variant merge operation
170        let schema = optimized_plan.schema().as_arrow();
171        let query_ctx = session_state
172            .config()
173            .get_extension()
174            .unwrap_or_else(QueryContext::arc);
175        let merge_scan_plan = MergeScanExec::new(
176            session_state,
177            table_name,
178            regions,
179            input_plan.clone(),
180            schema,
181            self.region_query_handler.clone(),
182            query_ctx,
183            session_state.config().target_partitions(),
184            merge_scan.partition_cols().clone(),
185            merge_scan.remote_dyn_filter_producer_id(),
186            self.enable_per_region_metrics,
187        )?;
188        Ok(Some(Arc::new(merge_scan_plan) as _))
189    }
190}
191
192impl DistExtensionPlanner {
193    /// Extract fully resolved table name from logical plan
194    fn extract_full_table_name(plan: &LogicalPlan) -> Result<Option<TableName>> {
195        let mut extractor = TableNameExtractor::default();
196        let _ = plan.visit(&mut extractor)?;
197        Ok(extractor.table_name)
198    }
199
200    async fn get_regions(
201        &self,
202        table_name: &TableName,
203        logical_plan: &LogicalPlan,
204    ) -> Result<Vec<RegionId>> {
205        let table = self
206            .catalog_manager
207            .table(
208                &table_name.catalog_name,
209                &table_name.schema_name,
210                &table_name.table_name,
211                None,
212            )
213            .await
214            .context(CatalogSnafu)?
215            .with_context(|| TableNotFoundSnafu {
216                table: table_name.to_string(),
217            })?;
218
219        let table_info = table.table_info();
220        let physical_table_route = self
221            .partition_rule_manager
222            .find_physical_table_route(table_info.table_id())
223            .await
224            .context(PartitionRuleManagerSnafu)?;
225        let all_regions = physical_table_route
226            .region_routes
227            .iter()
228            .map(|r| RegionId::new(table_info.table_id(), r.region.id.region_number()))
229            .collect::<Vec<_>>();
230        // Extract partition columns
231        let partition_columns: Vec<String> =
232            table_info.meta.partition_column_names().cloned().collect();
233        debug!(
234            "DistExtensionPlanner: loaded table partition metadata, table: {}, table_id: {}, partition_key_indices: {:?}, partition_columns: {:?}, all_regions: {:?}",
235            table_name,
236            table_info.table_id(),
237            table_info.meta.partition_key_indices,
238            partition_columns,
239            all_regions,
240        );
241        if partition_columns.is_empty() {
242            return Ok(all_regions);
243        }
244        let partition_column_types = partition_columns
245            .iter()
246            .map(|col_name| {
247                let data_type = table_info
248                    .meta
249                    .schema
250                    .column_schema_by_name(col_name)
251                    // Safety: names are retrieved above from the same table
252                    .unwrap()
253                    .data_type
254                    .clone();
255                (col_name.clone(), data_type)
256            })
257            .collect::<HashMap<_, _>>();
258
259        // Extract predicates from logical plan
260        let partition_expressions = match PredicateExtractor::extract_partition_expressions(
261            logical_plan,
262            &partition_columns,
263        ) {
264            Ok(expressions) => expressions,
265            Err(err) => {
266                common_telemetry::debug!(
267                    "Failed to extract partition expressions for table {} (id: {}), using all regions: {:?}",
268                    table_name,
269                    table.table_info().table_id(),
270                    err
271                );
272                return Ok(all_regions);
273            }
274        };
275
276        if partition_expressions.is_empty() {
277            return Ok(all_regions);
278        }
279
280        // Get partition information for the table if partition rule manager is available
281        let partitions = match create_partitions_from_region_routes(
282            table_info.table_id(),
283            &physical_table_route.region_routes,
284        ) {
285            Ok(partitions) => partitions,
286            Err(err) => {
287                common_telemetry::debug!(
288                    "Failed to get partition information for table {}, using all regions: {:?}",
289                    table_name,
290                    err
291                );
292                return Ok(all_regions);
293            }
294        };
295        if partitions.is_empty() {
296            return Ok(all_regions);
297        }
298
299        // Apply region pruning based on partition rules
300        let pruned_regions = match ConstraintPruner::prune_regions(
301            &partition_expressions,
302            &partitions,
303            partition_column_types,
304        ) {
305            Ok(regions) => regions,
306            Err(err) => {
307                common_telemetry::debug!(
308                    "Failed to prune regions for table {}, using all regions: {:?}",
309                    table_name,
310                    err
311                );
312                return Ok(all_regions);
313            }
314        };
315
316        common_telemetry::debug!(
317            "Region pruning for table {}: {} partition expressions applied, pruned from {} to {} regions",
318            table_name,
319            partition_expressions.len(),
320            all_regions.len(),
321            pruned_regions.len()
322        );
323
324        Ok(pruned_regions)
325    }
326
327    /// Input logical plan is analyzed. Thus only call logical optimizer to optimize it.
328    fn optimize_input_logical_plan(
329        &self,
330        session_state: &SessionState,
331        plan: &LogicalPlan,
332    ) -> Result<LogicalPlan> {
333        let state = session_state.clone();
334        state.optimizer().optimize(plan.clone(), &state, |_, _| {})
335    }
336}
337
338/// Visitor to extract table name from logical plan (TableScan node)
339#[derive(Default)]
340struct TableNameExtractor {
341    pub table_name: Option<TableName>,
342}
343
344impl TreeNodeVisitor<'_> for TableNameExtractor {
345    type Node = LogicalPlan;
346
347    fn f_down(&mut self, node: &Self::Node) -> Result<TreeNodeRecursion> {
348        match node {
349            LogicalPlan::TableScan(scan) => {
350                if let Some(source) = scan.source.as_any().downcast_ref::<DefaultTableSource>()
351                    && let Some(provider) = source
352                        .table_provider
353                        .as_any()
354                        .downcast_ref::<DfTableProviderAdapter>()
355                {
356                    if provider.table().table_type() == TableType::Base {
357                        let info = provider.table().table_info();
358                        self.table_name = Some(TableName::new(
359                            info.catalog_name.clone(),
360                            info.schema_name.clone(),
361                            info.name.clone(),
362                        ));
363                    }
364                    return Ok(TreeNodeRecursion::Stop);
365                }
366                match &scan.table_name {
367                    TableReference::Full {
368                        catalog,
369                        schema,
370                        table,
371                    } => {
372                        self.table_name = Some(TableName::new(
373                            catalog.to_string(),
374                            schema.to_string(),
375                            table.to_string(),
376                        ));
377                        Ok(TreeNodeRecursion::Stop)
378                    }
379                    // TODO(ruihang): Maybe the following two cases should not be valid
380                    TableReference::Partial { schema, table } => {
381                        self.table_name = Some(TableName::new(
382                            DEFAULT_CATALOG_NAME.to_string(),
383                            schema.to_string(),
384                            table.to_string(),
385                        ));
386                        Ok(TreeNodeRecursion::Stop)
387                    }
388                    TableReference::Bare { table } => {
389                        self.table_name = Some(TableName::new(
390                            DEFAULT_CATALOG_NAME.to_string(),
391                            DEFAULT_SCHEMA_NAME.to_string(),
392                            table.to_string(),
393                        ));
394                        Ok(TreeNodeRecursion::Stop)
395                    }
396                }
397            }
398            _ => Ok(TreeNodeRecursion::Continue),
399        }
400    }
401}