1use std::sync::Arc;
16
17use datafusion::config::ConfigOptions;
18use datafusion::physical_optimizer::PhysicalOptimizerRule;
19use datafusion::physical_plan::projection::ProjectionExec;
20use datafusion::physical_plan::repartition::RepartitionExec;
21use datafusion::physical_plan::{
22 ChildrenPropertiesMode, ExecutionPlan, Partitioning, ReplaceChildrenOptions,
23};
24use datafusion_common::Result as DfResult;
25use datafusion_physical_expr::Distribution;
26use datafusion_physical_expr::utils::map_columns_before_projection;
27
28use crate::dist_plan::MergeScanExec;
29
30#[derive(Debug)]
38pub struct PassDistribution;
39
40impl PhysicalOptimizerRule for PassDistribution {
41 fn optimize(
42 &self,
43 plan: Arc<dyn ExecutionPlan>,
44 config: &ConfigOptions,
45 ) -> DfResult<Arc<dyn ExecutionPlan>> {
46 Self::do_optimize(plan, config)
47 }
48
49 fn name(&self) -> &str {
50 "PassDistributionRule"
51 }
52
53 fn schema_check(&self) -> bool {
54 false
55 }
56}
57
58impl PassDistribution {
59 fn do_optimize(
60 plan: Arc<dyn ExecutionPlan>,
61 _config: &ConfigOptions,
62 ) -> DfResult<Arc<dyn ExecutionPlan>> {
63 Self::rewrite_with_distribution(plan, None)
65 }
66
67 fn rewrite_with_distribution(
69 plan: Arc<dyn ExecutionPlan>,
70 current_req: Option<Distribution>,
71 ) -> DfResult<Arc<dyn ExecutionPlan>> {
72 if let Some(merge_scan) = plan.downcast_ref::<MergeScanExec>()
74 && let Some(Distribution::KeyPartitioned(hash_exprs)) = current_req.as_ref()
75 {
76 if let Partitioning::Hash(current_hash_exprs, _) = &merge_scan.properties().partitioning
77 && *current_hash_exprs == *hash_exprs
78 {
79 return Ok(plan);
80 }
81
82 if let Some(new_plan) = merge_scan
83 .try_with_new_distribution(Distribution::KeyPartitioned(hash_exprs.clone()))
84 {
85 return Ok(Arc::new(new_plan) as _);
87 }
88
89 let partitioning = Partitioning::Hash(
90 hash_exprs.clone(),
91 merge_scan.properties().partitioning.partition_count(),
92 );
93 return Ok(Arc::new(RepartitionExec::try_new(plan, partitioning)?) as _);
94 }
95
96 let children = plan.children();
98 if children.is_empty() {
99 return Ok(plan);
100 }
101
102 let required = plan.input_distribution_requirements();
103 let mut new_children = Vec::with_capacity(children.len());
104 for (idx, child) in children.into_iter().enumerate() {
105 let child_req = match required.child_distribution(idx) {
106 Some(Distribution::UnspecifiedDistribution) if idx == 0 => {
107 Self::map_hash_requirement_through_projection(plan.as_ref(), ¤t_req)
108 }
109 Some(Distribution::UnspecifiedDistribution) => None,
110 None => current_req.clone(),
111 Some(req) => Some(req.clone()),
112 };
113 let new_child = Self::rewrite_with_distribution(child.clone(), child_req)?;
114 new_children.push(new_child);
115 }
116
117 let unchanged = plan
119 .children()
120 .into_iter()
121 .zip(new_children.iter())
122 .all(|(old, new)| Arc::ptr_eq(old, new));
123 if unchanged {
124 Ok(plan)
125 } else {
126 plan.replace_children(
127 new_children,
128 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
129 )
130 }
131 }
132
133 fn map_hash_requirement_through_projection(
134 plan: &dyn ExecutionPlan,
135 current_req: &Option<Distribution>,
136 ) -> Option<Distribution> {
137 let Some(Distribution::KeyPartitioned(required_exprs)) = current_req else {
138 return None;
139 };
140
141 let projection = plan.downcast_ref::<ProjectionExec>()?;
142 let proj_exprs = projection
143 .expr()
144 .iter()
145 .map(|expr| (Arc::clone(&expr.expr), expr.alias.clone()))
146 .collect::<Vec<_>>();
147 let mapped = map_columns_before_projection(required_exprs, &proj_exprs);
148
149 (mapped.len() == required_exprs.len()).then_some(Distribution::KeyPartitioned(mapped))
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use std::collections::{BTreeMap, BTreeSet};
156
157 use api::v1::region::{RemoteDynFilterUnregister, RemoteDynFilterUpdate};
158 use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit};
159 use async_trait::async_trait;
160 use common_query::request::QueryRequest;
161 use datafusion::common::NullEquality;
162 use datafusion::execution::SessionStateBuilder;
163 use datafusion::physical_optimizer::PhysicalOptimizerRule;
164 use datafusion::physical_plan::joins::{HashJoinExec, PartitionMode};
165 use datafusion::physical_plan::projection::{ProjectionExec, ProjectionExpr};
166 use datafusion::physical_plan::{ExecutionPlanProperties, Partitioning};
167 use datafusion_expr::{JoinType, LogicalPlanBuilder};
168 use datafusion_physical_expr::PhysicalExpr;
169 use datafusion_physical_expr::expressions::Column as PhysicalColumn;
170 use session::ReadPreference;
171 use session::context::QueryContext;
172 use store_api::metric_engine_consts::DATA_SCHEMA_TSID_COLUMN_NAME;
173 use store_api::storage::RegionId;
174 use table::table_name::TableName;
175
176 use super::*;
177 use crate::dist_plan::RemoteDynFilterProducerId;
178 use crate::error::Result as QueryResult;
179 use crate::region_query::RegionQueryHandler;
180
181 struct NoopRegionQueryHandler;
182
183 #[async_trait]
184 impl RegionQueryHandler for NoopRegionQueryHandler {
185 async fn select_target(
186 &self,
187 _read_preference: ReadPreference,
188 _region_id: RegionId,
189 ) -> QueryResult<crate::region_query::RegionQueryTarget> {
190 unreachable!("pass distribution tests should not execute remote queries")
191 }
192
193 async fn do_get(
194 &self,
195 _target: &crate::region_query::RegionQueryTarget,
196 _request: QueryRequest,
197 ) -> QueryResult<common_recordbatch::SendableRecordBatchStream> {
198 unreachable!("pass distribution tests should not execute remote queries")
199 }
200
201 async fn handle_remote_dyn_filter_update(
202 &self,
203 _target: &crate::region_query::RegionQueryTarget,
204 _query_id: String,
205 _update: RemoteDynFilterUpdate,
206 ) -> QueryResult<()> {
207 unreachable!("pass distribution tests should not send remote dyn filter updates")
208 }
209
210 async fn handle_remote_dyn_filter_unregister(
211 &self,
212 _target: &crate::region_query::RegionQueryTarget,
213 _query_id: String,
214 _unregister: RemoteDynFilterUnregister,
215 ) -> QueryResult<()> {
216 unreachable!("pass distribution tests should not send remote dyn filter unregisters")
217 }
218 }
219
220 #[test]
221 fn passes_hash_requirement_through_projection_to_merge_scan() {
222 let schema = test_schema();
223 let left_merge_scan = Arc::new(test_merge_scan_exec(schema.clone()));
224 let right_merge_scan = Arc::new(test_merge_scan_exec(schema.clone()));
225 let left_projection = Arc::new(
226 ProjectionExec::try_new(
227 vec![
228 ProjectionExpr::new(partition_column("greptime_value", 3), "greptime_value"),
229 ProjectionExpr::new(
230 partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
231 DATA_SCHEMA_TSID_COLUMN_NAME,
232 ),
233 ProjectionExpr::new(
234 partition_column("greptime_timestamp", 2),
235 "greptime_timestamp",
236 ),
237 ],
238 left_merge_scan,
239 )
240 .unwrap(),
241 ) as Arc<dyn datafusion::physical_plan::ExecutionPlan>;
242 let join = Arc::new(
243 HashJoinExec::try_new(
244 left_projection,
245 right_merge_scan,
246 vec![
247 (
248 partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
249 partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
250 ),
251 (
252 partition_column("greptime_timestamp", 2),
253 partition_column("greptime_timestamp", 2),
254 ),
255 ],
256 None,
257 &JoinType::Inner,
258 None,
259 PartitionMode::Partitioned,
260 NullEquality::NullEqualsNull,
261 false,
262 )
263 .unwrap(),
264 ) as Arc<dyn datafusion::physical_plan::ExecutionPlan>;
265
266 let optimized = PassDistribution
267 .optimize(join, &ConfigOptions::default())
268 .unwrap();
269 let hash_join = optimized.downcast_ref::<HashJoinExec>().unwrap();
270 let left_projection = hash_join.left().downcast_ref::<ProjectionExec>().unwrap();
271 let left_partitioning = left_projection.input().output_partitioning();
272 let right_partitioning = hash_join.right().output_partitioning();
273
274 let Partitioning::Hash(left_exprs, left_count) = left_partitioning else {
275 panic!("expected left merge scan hash partitioning");
276 };
277 let Partitioning::Hash(right_exprs, right_count) = right_partitioning else {
278 panic!("expected right merge scan hash partitioning");
279 };
280
281 assert_eq!(*left_count, 2);
282 assert_eq!(*right_count, 2);
283 assert_eq!(
284 column_names(left_exprs),
285 vec![DATA_SCHEMA_TSID_COLUMN_NAME, "greptime_timestamp"]
286 );
287 assert_eq!(
288 column_names(right_exprs),
289 vec![DATA_SCHEMA_TSID_COLUMN_NAME, "greptime_timestamp"]
290 );
291 }
292
293 #[test]
294 fn merge_scan_rejects_hash_requirement_on_partition_key_subset() {
295 let merge_scan = test_merge_scan_exec(test_schema());
296
297 let new_plan = merge_scan.try_with_new_distribution(Distribution::KeyPartitioned(vec![
298 partition_column(DATA_SCHEMA_TSID_COLUMN_NAME, 1),
299 ]));
300
301 assert!(
302 new_plan.is_none(),
303 "partitioning by a subset of multi-column partition keys is not sufficient"
304 );
305 }
306
307 fn test_merge_scan_exec(schema: SchemaRef) -> MergeScanExec {
308 let session_state = SessionStateBuilder::new().with_default_features().build();
309 let partition_cols = BTreeMap::from([
310 (
311 DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
312 BTreeSet::from([datafusion_common::Column::from_name(
313 DATA_SCHEMA_TSID_COLUMN_NAME,
314 )]),
315 ),
316 (
317 "greptime_timestamp".to_string(),
318 BTreeSet::from([datafusion_common::Column::from_name("greptime_timestamp")]),
319 ),
320 ]);
321 let plan = LogicalPlanBuilder::empty(false).build().unwrap();
322
323 MergeScanExec::new(
324 &session_state,
325 TableName::new("greptime", "public", "test"),
326 vec![RegionId::new(1, 0), RegionId::new(1, 1)],
327 plan,
328 schema.as_ref(),
329 Arc::new(NoopRegionQueryHandler),
330 QueryContext::arc(),
331 32,
332 partition_cols,
333 Some(RemoteDynFilterProducerId::new(1)),
334 false,
335 )
336 .unwrap()
337 }
338
339 fn test_schema() -> SchemaRef {
340 Arc::new(Schema::new(vec![
341 Field::new("host", DataType::Utf8, true),
342 Field::new(DATA_SCHEMA_TSID_COLUMN_NAME, DataType::UInt64, false),
343 Field::new(
344 "greptime_timestamp",
345 DataType::Timestamp(TimeUnit::Millisecond, None),
346 false,
347 ),
348 Field::new("greptime_value", DataType::Float64, true),
349 ]))
350 }
351
352 fn partition_column(name: &str, index: usize) -> Arc<dyn PhysicalExpr> {
353 Arc::new(PhysicalColumn::new(name, index))
354 }
355
356 fn column_names(exprs: &[Arc<dyn PhysicalExpr>]) -> Vec<&str> {
357 exprs
358 .iter()
359 .map(|expr| expr.downcast_ref::<PhysicalColumn>().unwrap().name())
360 .collect()
361 }
362}