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