1use std::sync::Arc;
18
19use ahash::HashMap;
20use arrow_schema::SortOptions;
21use async_trait::async_trait;
22use catalog::CatalogManagerRef;
23use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
24use common_telemetry::debug;
25use datafusion::catalog::Session;
26use datafusion::common::Result;
27use datafusion::datasource::DefaultTableSource;
28use datafusion::execution::context::SessionState;
29use datafusion::logical_expr::physical_planning_context::PhysicalPlanningContext;
30use datafusion::physical_plan::ExecutionPlan;
31use datafusion::physical_planner::{ExtensionPlanner, PhysicalPlanner};
32use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor};
33use datafusion_common::{DataFusionError, TableReference};
34use datafusion_expr::{LogicalPlan, UserDefinedLogicalNode};
35use datafusion_physical_expr::{LexOrdering, PhysicalSortExpr};
36use datatypes::prelude::ConcreteDataType;
37use partition::expr::PartitionExpr;
38use partition::manager::{PartitionRuleManagerRef, create_partitions_from_region_routes};
39use session::context::QueryContext;
40use snafu::{OptionExt, ResultExt};
41use store_api::storage::RegionId;
42use table::TableRef;
43use table::metadata::TableInfo;
44pub use table::metadata::TableType;
45use table::table::adapter::DfTableProviderAdapter;
46use table::table_name::TableName;
47
48use crate::dist_plan::PredicateExtractor;
49use crate::dist_plan::merge_scan::{MergeScanExec, MergeScanLogicalPlan};
50use crate::dist_plan::merge_sort::{MergeSortExec, MergeSortLogicalPlan};
51use crate::dist_plan::region_pruner::ConstraintPruner;
52use crate::error::{CatalogSnafu, PartitionRuleManagerSnafu, TableNotFoundSnafu};
53use crate::region_query::RegionQueryHandlerRef;
54
55pub struct MergeSortExtensionPlanner {}
63
64impl MergeSortExtensionPlanner {
65 fn ordering(
66 planner: &dyn PhysicalPlanner,
67 session: &dyn Session,
68 planning_ctx: &PhysicalPlanningContext,
69 merge_sort: &MergeSortLogicalPlan,
70 ) -> Result<LexOrdering> {
71 let ordering = merge_sort
72 .expr
73 .iter()
74 .map(|sort_expr| {
75 let physical_expr = planner.create_physical_expr(
76 &sort_expr.expr,
77 merge_sort.input.schema(),
78 session,
79 planning_ctx,
80 )?;
81 Ok(PhysicalSortExpr::new(
82 physical_expr,
83 SortOptions {
84 descending: !sort_expr.asc,
85 nulls_first: sort_expr.nulls_first,
86 },
87 ))
88 })
89 .collect::<Result<Vec<_>>>()?;
90
91 LexOrdering::new(ordering).ok_or_else(|| {
92 DataFusionError::Internal(
93 "Expect MergeSort to have non-empty sort expressions".to_string(),
94 )
95 })
96 }
97}
98
99#[async_trait]
100impl ExtensionPlanner for MergeSortExtensionPlanner {
101 async fn plan_extension(
102 &self,
103 planner: &dyn PhysicalPlanner,
104 node: &dyn UserDefinedLogicalNode,
105 _logical_inputs: &[&LogicalPlan],
106 physical_inputs: &[Arc<dyn ExecutionPlan>],
107 session: &dyn Session,
108 planning_ctx: &PhysicalPlanningContext,
109 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
110 if let Some(merge_sort) = node.as_any().downcast_ref::<MergeSortLogicalPlan>() {
111 if let LogicalPlan::Extension(ext) = &merge_sort.input.as_ref()
112 && ext
113 .node
114 .as_any()
115 .downcast_ref::<MergeScanLogicalPlan>()
116 .is_some()
117 {
118 let input = physical_inputs.first().cloned().ok_or_else(|| {
119 DataFusionError::Internal(
120 "Expect MergeSort to have one physical input".to_string(),
121 )
122 })?;
123 if input.downcast_ref::<MergeScanExec>().is_none() {
124 return Err(DataFusionError::Internal(format!(
125 "Expect MergeSort's input is a MergeScanExec, found {:?}",
126 physical_inputs
127 )));
128 }
129
130 let ordering = Self::ordering(planner, session, planning_ctx, merge_sort)?;
131 Ok(Some(Arc::new(MergeSortExec::new(
132 ordering,
133 input,
134 merge_sort.fetch,
135 ))))
136 } else {
137 Ok(None)
138 }
139 } else {
140 Ok(None)
141 }
142 }
143}
144
145pub struct DistExtensionPlanner {
146 catalog_manager: CatalogManagerRef,
147 partition_rule_manager: PartitionRuleManagerRef,
148 region_query_handler: RegionQueryHandlerRef,
149 enable_per_region_metrics: bool,
150}
151
152impl DistExtensionPlanner {
153 pub fn new(
154 catalog_manager: CatalogManagerRef,
155 partition_rule_manager: PartitionRuleManagerRef,
156 region_query_handler: RegionQueryHandlerRef,
157 enable_per_region_metrics: bool,
158 ) -> Self {
159 Self {
160 catalog_manager,
161 partition_rule_manager,
162 region_query_handler,
163 enable_per_region_metrics,
164 }
165 }
166}
167
168#[async_trait]
169impl ExtensionPlanner for DistExtensionPlanner {
170 async fn plan_extension(
171 &self,
172 planner: &dyn PhysicalPlanner,
173 node: &dyn UserDefinedLogicalNode,
174 _logical_inputs: &[&LogicalPlan],
175 _physical_inputs: &[Arc<dyn ExecutionPlan>],
176 session: &dyn Session,
177 _planning_ctx: &PhysicalPlanningContext,
178 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
179 let Some(merge_scan) = node.as_any().downcast_ref::<MergeScanLogicalPlan>() else {
180 return Ok(None);
181 };
182
183 let input_plan = merge_scan.input();
184 let fallback = |logical_plan| async move {
185 let optimized_plan = self.optimize_input_logical_plan(session, logical_plan)?;
186 planner
187 .create_physical_plan(&optimized_plan, session)
188 .await
189 .map(Some)
190 };
191
192 if merge_scan.is_placeholder() {
193 return fallback(input_plan).await;
195 }
196
197 let optimized_plan = input_plan;
198 let Some(table_name) = Self::extract_full_table_name(input_plan)? else {
199 return fallback(optimized_plan).await;
201 };
202
203 let Ok(regions) = self.get_regions(&table_name, input_plan).await else {
204 return fallback(optimized_plan).await;
206 };
207
208 let schema = merge_scan.schema().as_arrow();
210 let session_state = session
211 .as_any()
212 .downcast_ref::<SessionState>()
213 .ok_or_else(|| {
214 DataFusionError::Internal(
215 "MergeScan requires a SessionState for physical planning".to_string(),
216 )
217 })?;
218 let query_ctx = session_state
219 .config()
220 .get_extension()
221 .unwrap_or_else(QueryContext::arc);
222 let merge_scan_plan = MergeScanExec::new(
223 session_state,
224 table_name,
225 regions,
226 input_plan.clone(),
227 schema,
228 self.region_query_handler.clone(),
229 query_ctx,
230 session.config().target_partitions(),
231 merge_scan.partition_cols().clone(),
232 merge_scan.remote_dyn_filter_producer_id(),
233 self.enable_per_region_metrics,
234 )?;
235 Ok(Some(Arc::new(merge_scan_plan) as _))
236 }
237}
238
239impl DistExtensionPlanner {
240 fn extract_full_table_name(plan: &LogicalPlan) -> Result<Option<TableName>> {
242 let mut extractor = TableScanExtractor::default();
243 let _ = plan.visit(&mut extractor)?;
244 Ok(extractor.table_name)
245 }
246
247 async fn get_regions(
248 &self,
249 table_name: &TableName,
250 logical_plan: &LogicalPlan,
251 ) -> Result<Vec<RegionId>> {
252 let mut extractor = TableScanExtractor::default();
253 let _ = logical_plan.visit(&mut extractor)?;
254 let table = match extractor.captured_table {
256 Some(table) => table,
257 None => self
258 .catalog_manager
259 .table(
260 &table_name.catalog_name,
261 &table_name.schema_name,
262 &table_name.table_name,
263 None,
264 )
265 .await
266 .context(CatalogSnafu)?
267 .with_context(|| TableNotFoundSnafu {
268 table: table_name.to_string(),
269 })?,
270 };
271
272 let table_info = table.table_info();
273 let (physical_table_id, physical_table_route) = self
274 .partition_rule_manager
275 .find_physical_table_route_with_id(table_info.table_id())
276 .await
277 .context(PartitionRuleManagerSnafu)?;
278 let all_regions = physical_table_route
279 .region_routes
280 .iter()
281 .map(|r| RegionId::new(table_info.table_id(), r.region.id.region_number()))
282 .collect::<Vec<_>>();
283 let logical_partition_columns = partition_column_types(&table_info);
284 let partition_columns = logical_partition_columns
285 .iter()
286 .map(|(name, _)| name.clone())
287 .collect::<Vec<_>>();
288 debug!(
289 "DistExtensionPlanner: loaded table partition metadata, table: {}, table_id: {}, partition_key_indices: {:?}, partition_columns: {:?}, all_regions: {:?}",
290 table_name,
291 table_info.table_id(),
292 table_info.meta.partition_key_indices,
293 partition_columns,
294 all_regions,
295 );
296 if partition_columns.is_empty() {
297 return Ok(all_regions);
298 }
299 let partition_expressions = match PredicateExtractor::extract_partition_expressions(
301 logical_plan,
302 &partition_columns,
303 ) {
304 Ok(expressions) => expressions,
305 Err(err) => {
306 common_telemetry::debug!(
307 "Failed to extract partition expressions for table {} (id: {}), using all regions: {:?}",
308 table_name,
309 table.table_info().table_id(),
310 err
311 );
312 return Ok(all_regions);
313 }
314 };
315
316 if partition_expressions.is_empty() {
317 return Ok(all_regions);
318 }
319
320 let Some(partition_column_types) = self
321 .partition_column_types_for_pruning(
322 table_name,
323 table_info.as_ref(),
324 physical_table_id,
325 &partition_expressions,
326 &all_regions,
327 )
328 .await
329 else {
330 return Ok(all_regions);
331 };
332
333 let partitions = match create_partitions_from_region_routes(
335 table_info.table_id(),
336 &physical_table_route.region_routes,
337 ) {
338 Ok(partitions) => partitions,
339 Err(err) => {
340 common_telemetry::debug!(
341 "Failed to get partition information for table {}, using all regions: {:?}",
342 table_name,
343 err
344 );
345 return Ok(all_regions);
346 }
347 };
348 if partitions.is_empty() {
349 return Ok(all_regions);
350 }
351 let pruned_regions = match ConstraintPruner::prune_regions(
353 &partition_expressions,
354 &partitions,
355 partition_column_types,
356 ) {
357 Ok(regions) => regions,
358 Err(err) => {
359 common_telemetry::debug!(
360 "Failed to prune regions for table {}, using all regions: {:?}",
361 table_name,
362 err
363 );
364 return Ok(all_regions);
365 }
366 };
367
368 common_telemetry::debug!(
369 "Region pruning for table {}: {} partition expressions applied, pruned from {} to {} regions",
370 table_name,
371 partition_expressions.len(),
372 all_regions.len(),
373 pruned_regions.len()
374 );
375
376 Ok(pruned_regions)
377 }
378
379 async fn partition_column_types_for_pruning(
387 &self,
388 table_name: &TableName,
389 logical_table_info: &TableInfo,
390 physical_table_id: u32,
391 partition_expressions: &[PartitionExpr],
392 all_regions: &[RegionId],
393 ) -> Option<HashMap<String, ConcreteDataType>> {
394 let physical_partition_columns = if physical_table_id == logical_table_info.table_id() {
395 partition_column_types(logical_table_info)
396 } else {
397 match self
398 .catalog_manager
399 .table_info_by_id(physical_table_id)
400 .await
401 {
402 Ok(Some(physical_table_info)) => {
403 partition_column_types(physical_table_info.as_ref())
404 }
405 Ok(None) => {
406 debug!(
407 "DistExtensionPlanner: physical table info not found for table {} (id: {}), using all regions: {:?}",
408 table_name, physical_table_id, all_regions
409 );
410 return None;
411 }
412 Err(err) => {
413 debug!(
414 "DistExtensionPlanner: failed to load physical table info for table {} (id: {}): {}, using all regions: {:?}",
415 table_name, physical_table_id, err, all_regions
416 );
417 return None;
418 }
419 }
420 };
421 let physical_column_types = physical_partition_columns
422 .into_iter()
423 .collect::<HashMap<_, _>>();
424 let logical_column_types = partition_column_types(logical_table_info)
425 .into_iter()
426 .collect::<HashMap<_, _>>();
427 let mut predicate_column_names = std::collections::HashSet::new();
428 for expression in partition_expressions {
429 expression.collect_column_names(&mut predicate_column_names);
430 }
431 if predicate_column_names
432 .iter()
433 .any(|name| logical_column_types.get(name) != physical_column_types.get(name))
434 {
435 debug!(
436 "DistExtensionPlanner: logical and physical partition metadata mismatch for table {} (physical id: {}), using all regions: {:?}",
437 table_name, physical_table_id, all_regions
438 );
439 return None;
440 }
441
442 Some(physical_column_types)
443 }
444
445 fn optimize_input_logical_plan(
447 &self,
448 session: &dyn Session,
449 plan: &LogicalPlan,
450 ) -> Result<LogicalPlan> {
451 let session_state = session
452 .as_any()
453 .downcast_ref::<SessionState>()
454 .ok_or_else(|| {
455 DataFusionError::Internal(
456 "MergeScan requires a SessionState for logical optimization".to_string(),
457 )
458 })?;
459
460 session_state
461 .optimizer()
462 .optimize(plan.clone(), session_state, |_, _| {})
463 }
464}
465
466fn partition_column_types(table_info: &TableInfo) -> Vec<(String, ConcreteDataType)> {
467 table_info
468 .meta
469 .partition_columns()
470 .map(|column| (column.name.clone(), column.data_type.clone()))
471 .collect()
472}
473
474#[derive(Default)]
476struct TableScanExtractor {
477 pub table_name: Option<TableName>,
478 captured_table: Option<TableRef>,
479}
480
481impl TreeNodeVisitor<'_> for TableScanExtractor {
482 type Node = LogicalPlan;
483
484 fn f_down(&mut self, node: &Self::Node) -> Result<TreeNodeRecursion> {
485 match node {
486 LogicalPlan::TableScan(scan) => {
487 if let Some(source) = scan.source.downcast_ref::<DefaultTableSource>()
488 && let Some(provider) = source
489 .table_provider
490 .downcast_ref::<DfTableProviderAdapter>()
491 {
492 if provider.table().table_type() == TableType::Base {
493 self.captured_table = Some(provider.table());
494 let info = provider.table().table_info();
495 self.table_name = Some(TableName::new(
496 info.catalog_name.clone(),
497 info.schema_name.clone(),
498 info.name.clone(),
499 ));
500 }
501 return Ok(TreeNodeRecursion::Stop);
502 }
503 match &scan.table_name {
504 TableReference::Full {
505 catalog,
506 schema,
507 table,
508 } => {
509 self.table_name = Some(TableName::new(
510 catalog.to_string(),
511 schema.to_string(),
512 table.to_string(),
513 ));
514 Ok(TreeNodeRecursion::Stop)
515 }
516 TableReference::Partial { schema, table } => {
518 self.table_name = Some(TableName::new(
519 DEFAULT_CATALOG_NAME.to_string(),
520 schema.to_string(),
521 table.to_string(),
522 ));
523 Ok(TreeNodeRecursion::Stop)
524 }
525 TableReference::Bare { table } => {
526 self.table_name = Some(TableName::new(
527 DEFAULT_CATALOG_NAME.to_string(),
528 DEFAULT_SCHEMA_NAME.to_string(),
529 table.to_string(),
530 ));
531 Ok(TreeNodeRecursion::Stop)
532 }
533 }
534 }
535 _ => Ok(TreeNodeRecursion::Continue),
536 }
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use std::collections::HashMap;
543 use std::sync::Arc;
544
545 use api::v1::region::{RemoteDynFilterUnregister, RemoteDynFilterUpdate};
546 use async_trait::async_trait;
547 use catalog::memory::MemoryCatalogManager;
548 use catalog::{CatalogManagerRef, RegisterTableRequest};
549 use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
550 use common_meta::cache::new_table_route_cache;
551 use common_meta::key::TableMetadataManager;
552 use common_meta::key::table_route::TableRouteValue;
553 use common_meta::kv_backend::memory::MemoryKvBackend;
554 use common_meta::rpc::router::{Region, RegionRoute};
555 use common_query::request::QueryRequest;
556 use common_recordbatch::SendableRecordBatchStream;
557 use datafusion::datasource::DefaultTableSource;
558 use datafusion_expr::{LogicalPlan, LogicalPlanBuilder, col as df_col, lit};
559 use datatypes::prelude::ConcreteDataType;
560 use datatypes::schema::{ColumnSchema, Schema};
561 use datatypes::value::Value;
562 use moka::future::CacheBuilder;
563 use partition::cache::new_partition_info_cache;
564 use partition::expr::{PartitionExpr, col as partition_col};
565 use partition::manager::PartitionRuleManager;
566 use session::ReadPreference;
567 use store_api::storage::RegionId;
568 use table::metadata::{TableInfo, TableInfoBuilder, TableMeta, TableType};
569 use table::table::adapter::DfTableProviderAdapter;
570 use table::table_name::TableName;
571 use table::test_util::EmptyTable;
572
573 use super::DistExtensionPlanner;
574 use crate::region_query::{RegionQueryHandler, RegionQueryTarget};
575
576 const LOGICAL_TABLE_ID: u32 = 1024;
577 const PHYSICAL_TABLE_ID: u32 = 2048;
578
579 struct UnusedRegionQueryHandler;
580
581 #[async_trait]
582 impl RegionQueryHandler for UnusedRegionQueryHandler {
583 async fn select_target(
584 &self,
585 _read_preference: ReadPreference,
586 _region_id: RegionId,
587 ) -> crate::error::Result<RegionQueryTarget> {
588 unreachable!("get_regions does not select region query targets")
589 }
590
591 async fn do_get(
592 &self,
593 _target: &RegionQueryTarget,
594 _request: QueryRequest,
595 ) -> crate::error::Result<SendableRecordBatchStream> {
596 unreachable!("get_regions does not query regions")
597 }
598
599 async fn handle_remote_dyn_filter_update(
600 &self,
601 _target: &RegionQueryTarget,
602 _query_id: String,
603 _update: RemoteDynFilterUpdate,
604 ) -> crate::error::Result<()> {
605 unreachable!("get_regions does not update dynamic filters")
606 }
607
608 async fn handle_remote_dyn_filter_unregister(
609 &self,
610 _target: &RegionQueryTarget,
611 _query_id: String,
612 _unregister: RemoteDynFilterUnregister,
613 ) -> crate::error::Result<()> {
614 unreachable!("get_regions does not unregister dynamic filters")
615 }
616 }
617
618 fn table_info(
619 table_id: u32,
620 name: &str,
621 columns: &[&str],
622 partition_keys: Vec<usize>,
623 ) -> TableInfo {
624 let schema = Arc::new(Schema::new(
625 columns
626 .iter()
627 .map(|name| ColumnSchema::new(*name, ConcreteDataType::string_datatype(), true))
628 .collect(),
629 ));
630 let meta = TableMeta {
631 schema,
632 primary_key_indices: vec![],
633 value_indices: vec![],
634 engine: "metric".to_string(),
635 next_column_id: columns.len() as u32,
636 options: Default::default(),
637 created_on: Default::default(),
638 updated_on: Default::default(),
639 partition_key_indices: partition_keys,
640 column_ids: (0..columns.len() as u32).collect(),
641 };
642 TableInfoBuilder::default()
643 .table_id(table_id)
644 .table_version(0)
645 .name(name.to_string())
646 .catalog_name(DEFAULT_CATALOG_NAME.to_string())
647 .schema_name(DEFAULT_SCHEMA_NAME.to_string())
648 .desc(None)
649 .table_type(TableType::Base)
650 .meta(meta)
651 .build()
652 .unwrap()
653 }
654
655 fn region_route(region_number: u32, expression: Option<PartitionExpr>) -> RegionRoute {
656 RegionRoute {
657 region: Region {
658 id: RegionId::new(PHYSICAL_TABLE_ID, region_number),
659 partition_expr: expression
660 .map(|expression| expression.as_json_str().unwrap())
661 .unwrap_or_default(),
662 ..Default::default()
663 },
664 ..Default::default()
665 }
666 }
667
668 async fn planner_and_plan(
669 physical_partition_keys: Vec<usize>,
670 expressions: Vec<Option<PartitionExpr>>,
671 ) -> (DistExtensionPlanner, LogicalPlan, TableName) {
672 let logical_info = table_info(LOGICAL_TABLE_ID, "logical", &["host"], vec![0]);
673 let physical_info = table_info(
674 PHYSICAL_TABLE_ID,
675 "physical",
676 &["host", "rack"],
677 physical_partition_keys,
678 );
679 let logical_table = EmptyTable::from_table_info(&logical_info);
680 let physical_table = EmptyTable::from_table_info(&physical_info);
681 let catalog_manager = MemoryCatalogManager::with_default_setup();
682 for table in [&logical_table, &physical_table] {
683 let info = table.table_info();
684 catalog_manager
685 .register_table_sync(RegisterTableRequest {
686 catalog: info.catalog_name.clone(),
687 schema: info.schema_name.clone(),
688 table_name: info.name.clone(),
689 table_id: info.table_id(),
690 table: table.clone(),
691 })
692 .unwrap();
693 }
694
695 let backend = Arc::new(MemoryKvBackend::default());
696 let metadata_manager = TableMetadataManager::new(backend.clone());
697 let routes = expressions
698 .into_iter()
699 .enumerate()
700 .map(|(index, expression)| region_route(index as u32 + 1, expression))
701 .collect();
702 metadata_manager
703 .create_table_metadata(
704 physical_info,
705 TableRouteValue::physical(routes),
706 HashMap::new(),
707 )
708 .await
709 .unwrap();
710 metadata_manager
711 .create_table_metadata(
712 logical_info,
713 TableRouteValue::logical(PHYSICAL_TABLE_ID),
714 HashMap::new(),
715 )
716 .await
717 .unwrap();
718
719 let table_route_cache = Arc::new(new_table_route_cache(
720 "planner-test-routes".to_string(),
721 CacheBuilder::new(16).build(),
722 backend.clone(),
723 ));
724 let partition_info_cache = Arc::new(new_partition_info_cache(
725 "planner-test-partitions".to_string(),
726 CacheBuilder::new(16).build(),
727 table_route_cache.clone(),
728 ));
729 let partition_rule_manager = Arc::new(PartitionRuleManager::new(
730 backend,
731 table_route_cache,
732 partition_info_cache,
733 ));
734 let (resolved_physical_id, physical_route) = partition_rule_manager
735 .find_physical_table_route_with_id(PHYSICAL_TABLE_ID)
736 .await
737 .unwrap();
738 assert_eq!(PHYSICAL_TABLE_ID, resolved_physical_id);
739 let (resolved_logical_id, logical_route) = partition_rule_manager
740 .find_physical_table_route_with_id(LOGICAL_TABLE_ID)
741 .await
742 .unwrap();
743 assert_eq!(PHYSICAL_TABLE_ID, resolved_logical_id);
744 assert_eq!(physical_route.region_routes, logical_route.region_routes);
745 let catalog_manager: CatalogManagerRef = catalog_manager;
746 let planner = DistExtensionPlanner::new(
747 catalog_manager,
748 partition_rule_manager,
749 Arc::new(UnusedRegionQueryHandler),
750 false,
751 );
752 let table_source = Arc::new(DefaultTableSource::new(Arc::new(
753 DfTableProviderAdapter::new(logical_table),
754 )));
755 let plan = LogicalPlanBuilder::scan_with_filters("logical", table_source, None, vec![])
756 .unwrap()
757 .filter(df_col("host").eq(lit("a")))
758 .unwrap()
759 .build()
760 .unwrap();
761 (
762 planner,
763 plan,
764 TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "logical"),
765 )
766 }
767
768 fn physical_partition_expressions() -> Vec<Option<PartitionExpr>> {
769 vec![
770 Some(partition_col("host").lt(Value::String("m".into()))),
771 Some(
772 partition_col("host")
773 .gt_eq(Value::String("m".into()))
774 .and(partition_col("rack").lt(Value::String("n".into()))),
775 ),
776 Some(
777 partition_col("host")
778 .gt_eq(Value::String("m".into()))
779 .and(partition_col("rack").gt_eq(Value::String("n".into()))),
780 ),
781 ]
782 }
783
784 #[tokio::test]
785 async fn region_routing_uses_captured_table() {
786 let (mut planner, plan, table_name) = planner_and_plan(vec![0], vec![None]).await;
787 planner.catalog_manager = MemoryCatalogManager::with_default_setup();
788 assert_eq!(
789 vec![RegionId::new(LOGICAL_TABLE_ID, 1)],
790 planner.get_regions(&table_name, &plan).await.unwrap()
791 );
792 }
793
794 #[tokio::test]
795 async fn logical_table_pruning_uses_physical_partition_datatypes() {
796 let (planner, plan, table_name) =
797 planner_and_plan(vec![0, 1], physical_partition_expressions()).await;
798
799 assert_eq!(
800 vec![RegionId::new(LOGICAL_TABLE_ID, 1)],
801 planner.get_regions(&table_name, &plan).await.unwrap()
802 );
803 }
804
805 #[tokio::test]
806 async fn missing_physical_partition_datatype_falls_back_to_all_logical_regions() {
807 let (planner, plan, table_name) =
808 planner_and_plan(vec![0], physical_partition_expressions()).await;
809
810 assert_all_logical_regions(planner.get_regions(&table_name, &plan).await.unwrap());
811 }
812
813 #[tokio::test]
814 async fn missing_route_partition_expression_falls_back_to_all_logical_regions() {
815 let mut expressions = physical_partition_expressions();
816 expressions[1] = None;
817 let (planner, plan, table_name) = planner_and_plan(vec![0, 1], expressions).await;
818
819 assert_all_logical_regions(planner.get_regions(&table_name, &plan).await.unwrap());
820 }
821
822 fn assert_all_logical_regions(mut regions: Vec<RegionId>) {
823 regions.sort_unstable();
824 assert_eq!(
825 vec![
826 RegionId::new(LOGICAL_TABLE_ID, 1),
827 RegionId::new(LOGICAL_TABLE_ID, 2),
828 RegionId::new(LOGICAL_TABLE_ID, 3),
829 ],
830 regions
831 );
832 }
833}