1use std::collections::HashSet;
16use std::sync::Arc;
17
18use common_function::aggrs::aggr_wrapper::{StateMergeHelper, is_all_aggr_exprs_steppable};
19use common_telemetry::debug;
20use datafusion::error::Result as DfResult;
21use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
22use datafusion_expr::{Expr, LogicalPlan, UserDefinedLogicalNode};
23use promql::extension_plan::{
24 EmptyMetric, InstantManipulate, RangeManipulate, SeriesDivide, SeriesNormalize,
25};
26use store_api::metric_engine_consts::DATA_SCHEMA_TSID_COLUMN_NAME;
27
28use crate::dist_plan::MergeScanLogicalPlan;
29use crate::dist_plan::analyzer::AliasMapping;
30use crate::dist_plan::merge_sort::{MergeSortLogicalPlan, merge_sort_transformer};
31
32pub struct StepTransformAction {
33 extra_parent_plans: Vec<LogicalPlan>,
34 new_child_plan: Option<LogicalPlan>,
35}
36
37pub fn step_aggr_to_upper_aggr(
47 aggr_plan: &LogicalPlan,
48) -> datafusion_common::Result<StepTransformAction> {
49 let LogicalPlan::Aggregate(input_aggr) = aggr_plan else {
50 return Err(datafusion_common::DataFusionError::Plan(
51 "step_aggr_to_upper_aggr only accepts Aggregate plan".to_string(),
52 ));
53 };
54 if !is_all_aggr_exprs_steppable(&input_aggr.aggr_expr) {
55 return Err(datafusion_common::DataFusionError::NotImplemented(format!(
56 "Some aggregate expressions are not steppable in [{}]",
57 input_aggr
58 .aggr_expr
59 .iter()
60 .map(|e| e.to_string())
61 .collect::<Vec<_>>()
62 .join(", ")
63 )));
64 }
65
66 let step_aggr_plan = StateMergeHelper::split_aggr_node(input_aggr.clone())?;
67
68 let ret = StepTransformAction {
70 extra_parent_plans: vec![step_aggr_plan.upper_merge.clone()],
71 new_child_plan: Some(step_aggr_plan.lower_state.clone()),
72 };
73 Ok(ret)
74}
75
76#[allow(dead_code)]
77pub enum Commutativity {
78 Commutative,
79 PartialCommutative,
80 ConditionalCommutative(Option<Transformer>),
81 TransformedCommutative {
82 transformer: Option<StageTransformer>,
84 },
85 NonCommutative,
86 Unimplemented,
87 Unsupported,
89}
90
91pub struct Categorizer {}
92
93impl Categorizer {
94 pub fn check_plan(
95 plan: &LogicalPlan,
96 partition_cols: Option<AliasMapping>,
97 ) -> DfResult<Commutativity> {
98 if has_subquery(plan)? {
101 return Ok(Commutativity::Unimplemented);
102 }
103
104 let partition_cols = partition_cols.unwrap_or_default();
105
106 let comm = match plan {
107 LogicalPlan::Projection(proj) => {
108 for expr in &proj.expr {
109 let commutativity = Self::check_expr(expr);
110 if !matches!(commutativity, Commutativity::Commutative) {
111 return Ok(commutativity);
112 }
113 }
114 Commutativity::Commutative
115 }
116 LogicalPlan::Filter(filter) => Self::check_expr(&filter.predicate),
117 LogicalPlan::Window(_) => Commutativity::Unimplemented,
118 LogicalPlan::Aggregate(aggr) => {
119 let has_grouping_set = aggr
122 .group_expr
123 .iter()
124 .any(|expr| matches!(expr, Expr::GroupingSet(_)));
125 let is_all_steppable =
126 !has_grouping_set && is_all_aggr_exprs_steppable(&aggr.aggr_expr);
127 let matches_partition = Self::check_partition(&aggr.group_expr, &partition_cols);
128 if !matches_partition && is_all_steppable {
129 debug!("Plan is steppable: {plan}");
130 return Ok(Commutativity::TransformedCommutative {
131 transformer: Some(Arc::new(|plan: &LogicalPlan| {
132 debug!("Before Step optimize: {plan}");
133 let ret = step_aggr_to_upper_aggr(plan);
134 ret.inspect_err(|err| {
135 common_telemetry::error!("Failed to step aggregate plan: {err:?}");
136 })
137 .map(|s| TransformerAction {
138 extra_parent_plans: s.extra_parent_plans,
139 new_child_plan: s.new_child_plan,
140 })
141 })),
142 });
143 }
144 if !matches_partition {
145 return Ok(Commutativity::NonCommutative);
146 }
147 for expr in &aggr.aggr_expr {
148 let commutativity = Self::check_expr(expr);
149 if !matches!(commutativity, Commutativity::Commutative) {
150 return Ok(commutativity);
151 }
152 }
153 Commutativity::ConditionalCommutative(None)
158 }
159 LogicalPlan::Sort(_sort) => {
160 if partition_cols.is_empty() {
161 return Ok(Commutativity::Commutative);
162 }
163
164 Commutativity::ConditionalCommutative(Some(Arc::new(merge_sort_transformer)))
167 }
168 LogicalPlan::Join(_) => Commutativity::NonCommutative,
169 LogicalPlan::Repartition(_) => {
170 Commutativity::Unimplemented
172 }
173 LogicalPlan::Union(_) => Commutativity::Unimplemented,
174 LogicalPlan::TableScan(_) => Commutativity::Commutative,
175 LogicalPlan::EmptyRelation(_) => Commutativity::NonCommutative,
176 LogicalPlan::Subquery(_) => Commutativity::Unimplemented,
177 LogicalPlan::SubqueryAlias(_) => Commutativity::Commutative,
178 LogicalPlan::Limit(limit) => {
179 if partition_cols.is_empty() && limit.fetch.is_some() {
182 Commutativity::Commutative
183 } else if limit.skip.is_none() && limit.fetch.is_some() {
184 Commutativity::PartialCommutative
185 } else {
186 Commutativity::Unimplemented
187 }
188 }
189 LogicalPlan::Extension(extension) => {
190 Self::check_extension_plan(extension.node.as_ref() as _, &partition_cols)
191 }
192 LogicalPlan::Distinct(_) => {
193 if partition_cols.is_empty() {
194 Commutativity::Commutative
195 } else {
196 Commutativity::PartialCommutative
197 }
198 }
199 LogicalPlan::Unnest(_) => Commutativity::Commutative,
200 LogicalPlan::Statement(_) => Commutativity::Unsupported,
201 LogicalPlan::Values(_) => Commutativity::Unsupported,
202 LogicalPlan::Explain(_) => Commutativity::Unsupported,
203 LogicalPlan::Analyze(_) => Commutativity::Unsupported,
204 LogicalPlan::DescribeTable(_) => Commutativity::Unsupported,
205 LogicalPlan::Dml(_) => Commutativity::Unsupported,
206 LogicalPlan::Ddl(_) => Commutativity::Unsupported,
207 LogicalPlan::Copy(_) => Commutativity::Unsupported,
208 LogicalPlan::RecursiveQuery(_) => Commutativity::Unsupported,
209 };
210
211 Ok(comm)
212 }
213
214 pub fn check_extension_plan(
215 plan: &dyn UserDefinedLogicalNode,
216 partition_cols: &AliasMapping,
217 ) -> Commutativity {
218 match plan.name() {
219 name if name == SeriesDivide::name() => {
220 let series_divide = plan.as_any().downcast_ref::<SeriesDivide>().unwrap();
221 if series_divide
224 .tags()
225 .iter()
226 .any(|tag| tag == DATA_SCHEMA_TSID_COLUMN_NAME)
227 {
228 return Commutativity::Commutative;
229 }
230
231 let tags = series_divide.tags().iter().collect::<HashSet<_>>();
232
233 for all_alias in partition_cols.values() {
234 let all_alias = all_alias.iter().map(|c| &c.name).collect::<HashSet<_>>();
235 if tags.intersection(&all_alias).count() == 0 {
236 return Commutativity::NonCommutative;
237 }
238 }
239
240 Commutativity::Commutative
241 }
242 name if name == SeriesNormalize::name()
243 || name == InstantManipulate::name()
244 || name == RangeManipulate::name() =>
245 {
246 Commutativity::Commutative
249 }
250 name if name == EmptyMetric::name()
251 || name == MergeScanLogicalPlan::name()
252 || name == MergeSortLogicalPlan::name() =>
253 {
254 Commutativity::Unimplemented
255 }
256 _ => Commutativity::Unsupported,
257 }
258 }
259
260 pub fn check_expr(expr: &Expr) -> Commutativity {
261 #[allow(deprecated)]
262 match expr {
263 Expr::Column(_)
264 | Expr::ScalarVariable(_, _)
265 | Expr::Literal(_, _)
266 | Expr::BinaryExpr(_)
267 | Expr::Not(_)
268 | Expr::IsNotNull(_)
269 | Expr::IsNull(_)
270 | Expr::IsTrue(_)
271 | Expr::IsFalse(_)
272 | Expr::IsNotTrue(_)
273 | Expr::IsNotFalse(_)
274 | Expr::Negative(_)
275 | Expr::Between(_)
276 | Expr::Exists(_)
277 | Expr::InList(_)
278 | Expr::Like(_)
279 | Expr::Case(_) => Commutativity::Commutative,
280 Expr::ScalarFunction(_) => Commutativity::Commutative,
284 Expr::AggregateFunction(_udaf) => Commutativity::Commutative,
285
286 Expr::SimilarTo(_)
287 | Expr::IsUnknown(_)
288 | Expr::IsNotUnknown(_)
289 | Expr::WindowFunction(_)
290 | Expr::InSubquery(_)
291 | Expr::ScalarSubquery(_)
292 | Expr::HigherOrderFunction(_)
293 | Expr::Lambda(_)
294 | Expr::LambdaVariable(_)
295 | Expr::Wildcard { .. } => Commutativity::Unimplemented,
296
297 Expr::Alias(alias) => Self::check_expr(&alias.expr),
298 Expr::Cast(cast) => Self::check_expr(&cast.expr),
299 Expr::TryCast(try_cast) => Self::check_expr(&try_cast.expr),
300
301 Expr::Unnest(_)
302 | Expr::GroupingSet(_)
303 | Expr::Placeholder(_)
304 | Expr::OuterReferenceColumn(_, _)
305 | Expr::SetComparison(_) => Commutativity::Unimplemented,
306 }
307 }
308
309 fn check_partition(exprs: &[Expr], partition_cols: &AliasMapping) -> bool {
319 let group_cols = exprs
320 .iter()
321 .filter_map(|expr| {
322 let mut expr = expr;
323 while let Expr::Alias(alias) = expr {
324 expr = &alias.expr;
325 }
326 match expr {
327 Expr::Column(column) => Some(column.name.clone()),
328 _ => None,
329 }
330 })
331 .collect::<HashSet<_>>();
332 for all_alias in partition_cols.values() {
333 let all_alias = all_alias
334 .iter()
335 .map(|c| c.name.clone())
336 .collect::<HashSet<_>>();
337 if group_cols.intersection(&all_alias).count() == 0 {
340 return false;
341 }
342 }
343
344 true
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use std::collections::{BTreeMap, BTreeSet};
351
352 use datafusion_common::Column;
353 use datafusion_expr::LogicalPlanBuilder;
354 use datafusion_expr::expr::ScalarFunction;
355 use datafusion_functions::core::coalesce;
356 use promql::functions::{
357 NativeHistogramDrop, NativeHistogramFraction, NativeHistogramQuantile,
358 };
359
360 use super::*;
361
362 #[test]
363 fn series_divide_by_tsid_is_commutative() {
364 let input = LogicalPlanBuilder::empty(false).build().unwrap();
365 let series_divide = SeriesDivide::new(
366 vec![DATA_SCHEMA_TSID_COLUMN_NAME.to_string()],
367 "ts".to_string(),
368 input,
369 );
370
371 let partition_cols: AliasMapping = BTreeMap::from([(
372 "some_partition_col".to_string(),
373 BTreeSet::from([Column::from_name("some_partition_col")]),
374 )]);
375
376 let commutativity = Categorizer::check_extension_plan(&series_divide, &partition_cols);
377 assert!(matches!(commutativity, Commutativity::Commutative));
378 }
379
380 #[test]
381 fn annotated_histogram_helpers_do_not_block_pushdown() {
382 for udf in [
383 NativeHistogramQuantile::scalar_udf(),
384 NativeHistogramFraction::scalar_udf(),
385 NativeHistogramDrop::warning_bool_false_udf(String::new(), None),
386 ] {
387 let helper = Expr::ScalarFunction(ScalarFunction::new_udf(Arc::new(udf), vec![]));
388 let expr = Expr::ScalarFunction(ScalarFunction::new_udf(coalesce(), vec![helper]));
389 assert!(matches!(
390 Categorizer::check_expr(&expr),
391 Commutativity::Commutative
392 ));
393 }
394 }
395}
396
397pub type Transformer = Arc<dyn Fn(&LogicalPlan) -> Option<LogicalPlan>>;
398
399pub type StageTransformer = Arc<dyn Fn(&LogicalPlan) -> DfResult<TransformerAction>>;
401
402pub struct TransformerAction {
404 pub extra_parent_plans: Vec<LogicalPlan>,
413 pub new_child_plan: Option<LogicalPlan>,
415}
416
417pub fn partial_commutative_transformer(plan: &LogicalPlan) -> Option<LogicalPlan> {
418 Some(plan.clone())
419}
420
421fn has_subquery(plan: &LogicalPlan) -> DfResult<bool> {
422 let mut found = false;
423 plan.apply_expressions(|e| {
424 e.apply(|x| {
425 if matches!(
426 x,
427 Expr::Exists(_) | Expr::InSubquery(_) | Expr::ScalarSubquery(_)
428 ) {
429 found = true;
430 Ok(TreeNodeRecursion::Stop)
431 } else {
432 Ok(TreeNodeRecursion::Continue)
433 }
434 })
435 })?;
436 Ok(found)
437}
438
439#[cfg(test)]
440mod test {
441 use datafusion_expr::{LogicalPlanBuilder, Sort};
442
443 use super::*;
444
445 #[test]
446 fn sort_on_empty_partition() {
447 let plan = LogicalPlan::Sort(Sort {
448 expr: vec![],
449 input: Arc::new(LogicalPlanBuilder::empty(false).build().unwrap()),
450 fetch: None,
451 });
452 assert!(matches!(
453 Categorizer::check_plan(&plan, Some(Default::default())).unwrap(),
454 Commutativity::Commutative
455 ));
456 }
457}