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.remote_dyn_filter_producer_id(),
232 self.enable_per_region_metrics,
233 )?;
234 Ok(Some(Arc::new(merge_scan_plan) as _))
235 }
236}
237
238impl DistExtensionPlanner {
239 fn extract_full_table_name(plan: &LogicalPlan) -> Result<Option<TableName>> {
241 let mut extractor = TableScanExtractor::default();
242 let _ = plan.visit(&mut extractor)?;
243 Ok(extractor.table_name)
244 }
245
246 async fn get_regions(
247 &self,
248 table_name: &TableName,
249 logical_plan: &LogicalPlan,
250 ) -> Result<Vec<RegionId>> {
251 let mut extractor = TableScanExtractor::default();
252 let _ = logical_plan.visit(&mut extractor)?;
253 let table = match extractor.captured_table {
255 Some(table) => table,
256 None => self
257 .catalog_manager
258 .table(
259 &table_name.catalog_name,
260 &table_name.schema_name,
261 &table_name.table_name,
262 None,
263 )
264 .await
265 .context(CatalogSnafu)?
266 .with_context(|| TableNotFoundSnafu {
267 table: table_name.to_string(),
268 })?,
269 };
270
271 let table_info = table.table_info();
272 let (physical_table_id, physical_table_route) = self
273 .partition_rule_manager
274 .find_physical_table_route_with_id(table_info.table_id())
275 .await
276 .context(PartitionRuleManagerSnafu)?;
277 let all_regions = physical_table_route
278 .region_routes
279 .iter()
280 .map(|r| RegionId::new(table_info.table_id(), r.region.id.region_number()))
281 .collect::<Vec<_>>();
282 let logical_partition_columns = partition_column_types(&table_info);
283 let partition_columns = logical_partition_columns
284 .iter()
285 .map(|(name, _)| name.clone())
286 .collect::<Vec<_>>();
287 debug!(
288 "DistExtensionPlanner: loaded table partition metadata, table: {}, table_id: {}, partition_key_indices: {:?}, partition_columns: {:?}, all_regions: {:?}",
289 table_name,
290 table_info.table_id(),
291 table_info.meta.partition_key_indices,
292 partition_columns,
293 all_regions,
294 );
295 if partition_columns.is_empty() {
296 return Ok(all_regions);
297 }
298 let partition_expressions = match PredicateExtractor::extract_partition_expressions(
300 logical_plan,
301 &partition_columns,
302 ) {
303 Ok(expressions) => expressions,
304 Err(err) => {
305 common_telemetry::debug!(
306 "Failed to extract partition expressions for table {} (id: {}), using all regions: {:?}",
307 table_name,
308 table.table_info().table_id(),
309 err
310 );
311 return Ok(all_regions);
312 }
313 };
314
315 if partition_expressions.is_empty() {
316 return Ok(all_regions);
317 }
318
319 let Some(partition_column_types) = self
320 .partition_column_types_for_pruning(
321 table_name,
322 table_info.as_ref(),
323 physical_table_id,
324 &partition_expressions,
325 &all_regions,
326 )
327 .await
328 else {
329 return Ok(all_regions);
330 };
331
332 let partitions = match create_partitions_from_region_routes(
334 table_info.table_id(),
335 &physical_table_route.region_routes,
336 ) {
337 Ok(partitions) => partitions,
338 Err(err) => {
339 common_telemetry::debug!(
340 "Failed to get partition information for table {}, using all regions: {:?}",
341 table_name,
342 err
343 );
344 return Ok(all_regions);
345 }
346 };
347 if partitions.is_empty() {
348 return Ok(all_regions);
349 }
350 let pruned_regions = match ConstraintPruner::prune_regions(
352 &partition_expressions,
353 &partitions,
354 partition_column_types,
355 ) {
356 Ok(regions) => regions,
357 Err(err) => {
358 common_telemetry::debug!(
359 "Failed to prune regions for table {}, using all regions: {:?}",
360 table_name,
361 err
362 );
363 return Ok(all_regions);
364 }
365 };
366
367 common_telemetry::debug!(
368 "Region pruning for table {}: {} partition expressions applied, pruned from {} to {} regions",
369 table_name,
370 partition_expressions.len(),
371 all_regions.len(),
372 pruned_regions.len()
373 );
374
375 Ok(pruned_regions)
376 }
377
378 async fn partition_column_types_for_pruning(
386 &self,
387 table_name: &TableName,
388 logical_table_info: &TableInfo,
389 physical_table_id: u32,
390 partition_expressions: &[PartitionExpr],
391 all_regions: &[RegionId],
392 ) -> Option<HashMap<String, ConcreteDataType>> {
393 let physical_partition_columns = if physical_table_id == logical_table_info.table_id() {
394 partition_column_types(logical_table_info)
395 } else {
396 match self
397 .catalog_manager
398 .table_info_by_id(physical_table_id)
399 .await
400 {
401 Ok(Some(physical_table_info)) => {
402 partition_column_types(physical_table_info.as_ref())
403 }
404 Ok(None) => {
405 debug!(
406 "DistExtensionPlanner: physical table info not found for table {} (id: {}), using all regions: {:?}",
407 table_name, physical_table_id, all_regions
408 );
409 return None;
410 }
411 Err(err) => {
412 debug!(
413 "DistExtensionPlanner: failed to load physical table info for table {} (id: {}): {}, using all regions: {:?}",
414 table_name, physical_table_id, err, all_regions
415 );
416 return None;
417 }
418 }
419 };
420 let physical_column_types = physical_partition_columns
421 .into_iter()
422 .collect::<HashMap<_, _>>();
423 let logical_column_types = partition_column_types(logical_table_info)
424 .into_iter()
425 .collect::<HashMap<_, _>>();
426 let mut predicate_column_names = std::collections::HashSet::new();
427 for expression in partition_expressions {
428 expression.collect_column_names(&mut predicate_column_names);
429 }
430 if predicate_column_names
431 .iter()
432 .any(|name| logical_column_types.get(name) != physical_column_types.get(name))
433 {
434 debug!(
435 "DistExtensionPlanner: logical and physical partition metadata mismatch for table {} (physical id: {}), using all regions: {:?}",
436 table_name, physical_table_id, all_regions
437 );
438 return None;
439 }
440
441 Some(physical_column_types)
442 }
443
444 fn optimize_input_logical_plan(
446 &self,
447 session: &dyn Session,
448 plan: &LogicalPlan,
449 ) -> Result<LogicalPlan> {
450 let session_state = session
451 .as_any()
452 .downcast_ref::<SessionState>()
453 .ok_or_else(|| {
454 DataFusionError::Internal(
455 "MergeScan requires a SessionState for logical optimization".to_string(),
456 )
457 })?;
458
459 session_state
460 .optimizer()
461 .optimize(plan.clone(), session_state, |_, _| {})
462 }
463}
464
465fn partition_column_types(table_info: &TableInfo) -> Vec<(String, ConcreteDataType)> {
466 table_info
467 .meta
468 .partition_columns()
469 .map(|column| (column.name.clone(), column.data_type.clone()))
470 .collect()
471}
472
473#[derive(Default)]
475struct TableScanExtractor {
476 pub table_name: Option<TableName>,
477 captured_table: Option<TableRef>,
478}
479
480impl TreeNodeVisitor<'_> for TableScanExtractor {
481 type Node = LogicalPlan;
482
483 fn f_down(&mut self, node: &Self::Node) -> Result<TreeNodeRecursion> {
484 match node {
485 LogicalPlan::TableScan(scan) => {
486 if let Some(source) = scan.source.downcast_ref::<DefaultTableSource>()
487 && let Some(provider) = source
488 .table_provider
489 .downcast_ref::<DfTableProviderAdapter>()
490 {
491 if provider.table().table_type() == TableType::Base {
492 self.captured_table = Some(provider.table());
493 let info = provider.table().table_info();
494 self.table_name = Some(TableName::new(
495 info.catalog_name.clone(),
496 info.schema_name.clone(),
497 info.name.clone(),
498 ));
499 }
500 return Ok(TreeNodeRecursion::Stop);
501 }
502 match &scan.table_name {
503 TableReference::Full {
504 catalog,
505 schema,
506 table,
507 } => {
508 self.table_name = Some(TableName::new(
509 catalog.to_string(),
510 schema.to_string(),
511 table.to_string(),
512 ));
513 Ok(TreeNodeRecursion::Stop)
514 }
515 TableReference::Partial { schema, table } => {
517 self.table_name = Some(TableName::new(
518 DEFAULT_CATALOG_NAME.to_string(),
519 schema.to_string(),
520 table.to_string(),
521 ));
522 Ok(TreeNodeRecursion::Stop)
523 }
524 TableReference::Bare { table } => {
525 self.table_name = Some(TableName::new(
526 DEFAULT_CATALOG_NAME.to_string(),
527 DEFAULT_SCHEMA_NAME.to_string(),
528 table.to_string(),
529 ));
530 Ok(TreeNodeRecursion::Stop)
531 }
532 }
533 }
534 _ => Ok(TreeNodeRecursion::Continue),
535 }
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use std::collections::HashMap;
542 use std::sync::Arc;
543
544 use api::v1::region::{RemoteDynFilterUnregister, RemoteDynFilterUpdate};
545 use async_trait::async_trait;
546 use catalog::memory::MemoryCatalogManager;
547 use catalog::{CatalogManagerRef, RegisterTableRequest};
548 use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
549 use common_meta::cache::new_table_route_cache;
550 use common_meta::key::TableMetadataManager;
551 use common_meta::key::table_route::TableRouteValue;
552 use common_meta::kv_backend::memory::MemoryKvBackend;
553 use common_meta::rpc::router::{Region, RegionRoute};
554 use common_query::request::QueryRequest;
555 use common_recordbatch::SendableRecordBatchStream;
556 use datafusion::datasource::DefaultTableSource;
557 use datafusion_expr::{LogicalPlan, LogicalPlanBuilder, col as df_col, lit};
558 use datatypes::prelude::ConcreteDataType;
559 use datatypes::schema::{ColumnSchema, Schema};
560 use datatypes::value::Value;
561 use moka::future::CacheBuilder;
562 use partition::cache::new_partition_info_cache;
563 use partition::expr::{PartitionExpr, col as partition_col};
564 use partition::manager::PartitionRuleManager;
565 use session::ReadPreference;
566 use store_api::storage::RegionId;
567 use table::metadata::{TableInfo, TableInfoBuilder, TableMeta, TableType};
568 use table::table::adapter::DfTableProviderAdapter;
569 use table::table_name::TableName;
570 use table::test_util::EmptyTable;
571
572 use super::DistExtensionPlanner;
573 use crate::region_query::{RegionQueryHandler, RegionQueryTarget};
574
575 const LOGICAL_TABLE_ID: u32 = 1024;
576 const PHYSICAL_TABLE_ID: u32 = 2048;
577
578 struct UnusedRegionQueryHandler;
579
580 #[async_trait]
581 impl RegionQueryHandler for UnusedRegionQueryHandler {
582 async fn select_target(
583 &self,
584 _read_preference: ReadPreference,
585 _region_id: RegionId,
586 ) -> crate::error::Result<RegionQueryTarget> {
587 unreachable!("get_regions does not select region query targets")
588 }
589
590 async fn do_get(
591 &self,
592 _target: &RegionQueryTarget,
593 _request: QueryRequest,
594 ) -> crate::error::Result<SendableRecordBatchStream> {
595 unreachable!("get_regions does not query regions")
596 }
597
598 async fn handle_remote_dyn_filter_update(
599 &self,
600 _target: &RegionQueryTarget,
601 _query_id: String,
602 _update: RemoteDynFilterUpdate,
603 ) -> crate::error::Result<()> {
604 unreachable!("get_regions does not update dynamic filters")
605 }
606
607 async fn handle_remote_dyn_filter_unregister(
608 &self,
609 _target: &RegionQueryTarget,
610 _query_id: String,
611 _unregister: RemoteDynFilterUnregister,
612 ) -> crate::error::Result<()> {
613 unreachable!("get_regions does not unregister dynamic filters")
614 }
615 }
616
617 fn table_info(
618 table_id: u32,
619 name: &str,
620 columns: &[&str],
621 partition_keys: Vec<usize>,
622 ) -> TableInfo {
623 let schema = Arc::new(Schema::new(
624 columns
625 .iter()
626 .map(|name| ColumnSchema::new(*name, ConcreteDataType::string_datatype(), true))
627 .collect(),
628 ));
629 let meta = TableMeta {
630 schema,
631 primary_key_indices: vec![],
632 value_indices: vec![],
633 engine: "metric".to_string(),
634 next_column_id: columns.len() as u32,
635 options: Default::default(),
636 created_on: Default::default(),
637 updated_on: Default::default(),
638 partition_key_indices: partition_keys,
639 column_ids: (0..columns.len() as u32).collect(),
640 };
641 TableInfoBuilder::default()
642 .table_id(table_id)
643 .table_version(0)
644 .name(name.to_string())
645 .catalog_name(DEFAULT_CATALOG_NAME.to_string())
646 .schema_name(DEFAULT_SCHEMA_NAME.to_string())
647 .desc(None)
648 .table_type(TableType::Base)
649 .meta(meta)
650 .build()
651 .unwrap()
652 }
653
654 fn region_route(region_number: u32, expression: Option<PartitionExpr>) -> RegionRoute {
655 RegionRoute {
656 region: Region {
657 id: RegionId::new(PHYSICAL_TABLE_ID, region_number),
658 partition_expr: expression
659 .map(|expression| expression.as_json_str().unwrap())
660 .unwrap_or_default(),
661 ..Default::default()
662 },
663 ..Default::default()
664 }
665 }
666
667 async fn planner_and_plan(
668 physical_partition_keys: Vec<usize>,
669 expressions: Vec<Option<PartitionExpr>>,
670 ) -> (DistExtensionPlanner, LogicalPlan, TableName) {
671 let logical_info = table_info(LOGICAL_TABLE_ID, "logical", &["host"], vec![0]);
672 let physical_info = table_info(
673 PHYSICAL_TABLE_ID,
674 "physical",
675 &["host", "rack"],
676 physical_partition_keys,
677 );
678 let logical_table = EmptyTable::from_table_info(&logical_info);
679 let physical_table = EmptyTable::from_table_info(&physical_info);
680 let catalog_manager = MemoryCatalogManager::with_default_setup();
681 for table in [&logical_table, &physical_table] {
682 let info = table.table_info();
683 catalog_manager
684 .register_table_sync(RegisterTableRequest {
685 catalog: info.catalog_name.clone(),
686 schema: info.schema_name.clone(),
687 table_name: info.name.clone(),
688 table_id: info.table_id(),
689 table: table.clone(),
690 })
691 .unwrap();
692 }
693
694 let backend = Arc::new(MemoryKvBackend::default());
695 let metadata_manager = TableMetadataManager::new(backend.clone());
696 let routes = expressions
697 .into_iter()
698 .enumerate()
699 .map(|(index, expression)| region_route(index as u32 + 1, expression))
700 .collect();
701 metadata_manager
702 .create_table_metadata(
703 physical_info,
704 TableRouteValue::physical(routes),
705 HashMap::new(),
706 )
707 .await
708 .unwrap();
709 metadata_manager
710 .create_table_metadata(
711 logical_info,
712 TableRouteValue::logical(PHYSICAL_TABLE_ID),
713 HashMap::new(),
714 )
715 .await
716 .unwrap();
717
718 let table_route_cache = Arc::new(new_table_route_cache(
719 "planner-test-routes".to_string(),
720 CacheBuilder::new(16).build(),
721 backend.clone(),
722 ));
723 let partition_info_cache = Arc::new(new_partition_info_cache(
724 "planner-test-partitions".to_string(),
725 CacheBuilder::new(16).build(),
726 table_route_cache.clone(),
727 ));
728 let partition_rule_manager = Arc::new(PartitionRuleManager::new(
729 backend,
730 table_route_cache,
731 partition_info_cache,
732 ));
733 let (resolved_physical_id, physical_route) = partition_rule_manager
734 .find_physical_table_route_with_id(PHYSICAL_TABLE_ID)
735 .await
736 .unwrap();
737 assert_eq!(PHYSICAL_TABLE_ID, resolved_physical_id);
738 let (resolved_logical_id, logical_route) = partition_rule_manager
739 .find_physical_table_route_with_id(LOGICAL_TABLE_ID)
740 .await
741 .unwrap();
742 assert_eq!(PHYSICAL_TABLE_ID, resolved_logical_id);
743 assert_eq!(physical_route.region_routes, logical_route.region_routes);
744 let catalog_manager: CatalogManagerRef = catalog_manager;
745 let planner = DistExtensionPlanner::new(
746 catalog_manager,
747 partition_rule_manager,
748 Arc::new(UnusedRegionQueryHandler),
749 false,
750 );
751 let table_source = Arc::new(DefaultTableSource::new(Arc::new(
752 DfTableProviderAdapter::new(logical_table),
753 )));
754 let plan = LogicalPlanBuilder::scan_with_filters("logical", table_source, None, vec![])
755 .unwrap()
756 .filter(df_col("host").eq(lit("a")))
757 .unwrap()
758 .build()
759 .unwrap();
760 (
761 planner,
762 plan,
763 TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "logical"),
764 )
765 }
766
767 fn physical_partition_expressions() -> Vec<Option<PartitionExpr>> {
768 vec![
769 Some(partition_col("host").lt(Value::String("m".into()))),
770 Some(
771 partition_col("host")
772 .gt_eq(Value::String("m".into()))
773 .and(partition_col("rack").lt(Value::String("n".into()))),
774 ),
775 Some(
776 partition_col("host")
777 .gt_eq(Value::String("m".into()))
778 .and(partition_col("rack").gt_eq(Value::String("n".into()))),
779 ),
780 ]
781 }
782
783 #[tokio::test]
784 async fn region_routing_uses_captured_table() {
785 let (mut planner, plan, table_name) = planner_and_plan(vec![0], vec![None]).await;
786 planner.catalog_manager = MemoryCatalogManager::with_default_setup();
787 assert_eq!(
788 vec![RegionId::new(LOGICAL_TABLE_ID, 1)],
789 planner.get_regions(&table_name, &plan).await.unwrap()
790 );
791 }
792
793 #[tokio::test]
794 async fn logical_table_pruning_uses_physical_partition_datatypes() {
795 let (planner, plan, table_name) =
796 planner_and_plan(vec![0, 1], physical_partition_expressions()).await;
797
798 assert_eq!(
799 vec![RegionId::new(LOGICAL_TABLE_ID, 1)],
800 planner.get_regions(&table_name, &plan).await.unwrap()
801 );
802 }
803
804 #[tokio::test]
805 async fn missing_physical_partition_datatype_falls_back_to_all_logical_regions() {
806 let (planner, plan, table_name) =
807 planner_and_plan(vec![0], physical_partition_expressions()).await;
808
809 assert_all_logical_regions(planner.get_regions(&table_name, &plan).await.unwrap());
810 }
811
812 #[tokio::test]
813 async fn missing_route_partition_expression_falls_back_to_all_logical_regions() {
814 let mut expressions = physical_partition_expressions();
815 expressions[1] = None;
816 let (planner, plan, table_name) = planner_and_plan(vec![0, 1], expressions).await;
817
818 assert_all_logical_regions(planner.get_regions(&table_name, &plan).await.unwrap());
819 }
820
821 fn assert_all_logical_regions(mut regions: Vec<RegionId>) {
822 regions.sort_unstable();
823 assert_eq!(
824 vec![
825 RegionId::new(LOGICAL_TABLE_ID, 1),
826 RegionId::new(LOGICAL_TABLE_ID, 2),
827 RegionId::new(LOGICAL_TABLE_ID, 3),
828 ],
829 regions
830 );
831 }
832}