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