use datafusion_common::tree_node::TreeNode;
use datafusion_expr::{BinaryExpr, Expr, Operator};
pub(crate) fn reorder_predicates(predicates: Vec<Expr>) -> (Vec<Expr>, bool) {
if predicates.len() <= 1 {
return (predicates, false);
}
if predicates.iter().any(Expr::is_volatile) {
return (predicates, false);
}
let classes: Vec<bool> = predicates.iter().map(is_cheap_predicate).collect();
let needs_reorder = classes.windows(2).any(|w| !w[0] && w[1]);
if !needs_reorder {
return (predicates, false);
}
let mut cheap = Vec::with_capacity(predicates.len());
let mut expensive = Vec::new();
for (p, is_cheap) in predicates.into_iter().zip(classes) {
if is_cheap {
cheap.push(p);
} else {
expensive.push(p);
}
}
cheap.extend(expensive);
(cheap, true)
}
fn is_cheap_predicate(expr: &Expr) -> bool {
!expr
.exists(|node| Ok(!is_cheap_node(node)))
.expect("is_cheap_node is infallible")
}
fn is_cheap_node(expr: &Expr) -> bool {
match expr {
Expr::Column(_)
| Expr::Literal(_, _)
| Expr::ScalarVariable(_, _)
| Expr::Placeholder(_)
| Expr::OuterReferenceColumn(_, _)
| Expr::LambdaVariable(_)
| Expr::Alias(_)
| Expr::Not(_)
| Expr::Negative(_)
| Expr::IsNull(_)
| Expr::IsNotNull(_)
| Expr::IsTrue(_)
| Expr::IsFalse(_)
| Expr::IsUnknown(_)
| Expr::IsNotTrue(_)
| Expr::IsNotFalse(_)
| Expr::IsNotUnknown(_)
| Expr::Between(_)
| Expr::Case(_)
| Expr::Cast(_)
| Expr::TryCast(_)
| Expr::InList(_) => true,
Expr::BinaryExpr(BinaryExpr { op, .. }) => !matches!(
op,
Operator::LikeMatch
| Operator::ILikeMatch
| Operator::NotLikeMatch
| Operator::NotILikeMatch
| Operator::RegexMatch
| Operator::RegexIMatch
| Operator::RegexNotMatch
| Operator::RegexNotIMatch
),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use datafusion_expr::{col, lit};
#[test]
fn like_predicate_moves_after_equality() {
let cheap = col("a").eq(lit(1));
let expensive = col("b").like(lit("%foo%"));
let (out, changed) = reorder_predicates(vec![expensive.clone(), cheap.clone()]);
assert_eq!(out, vec![cheap, expensive]);
assert!(changed);
}
#[test]
fn order_among_cheap_predicates_is_preserved() {
let p1 = col("a").eq(lit(1));
let p2 = col("b").eq(lit(2));
let p3 = col("c").eq(lit(3));
let input = vec![p1.clone(), p2.clone(), p3.clone()];
let (out, changed) = reorder_predicates(input.clone());
assert_eq!(out, input);
assert!(!changed);
}
#[test]
fn order_among_expensive_predicates_is_preserved() {
let p1 = col("a").like(lit("%a%"));
let p2 = Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("b")),
Operator::RegexMatch,
Box::new(lit("foo")),
));
let p3 = col("c").like(lit("%c%"));
let input = vec![p1.clone(), p2.clone(), p3.clone()];
let (out, changed) = reorder_predicates(input.clone());
assert_eq!(out, input);
assert!(!changed);
}
#[test]
fn already_cheap_first_reports_no_change() {
let cheap = col("a").eq(lit(1));
let expensive = col("b").like(lit("%a%"));
let input = vec![cheap.clone(), expensive.clone()];
let (out, changed) = reorder_predicates(input.clone());
assert_eq!(out, input);
assert!(!changed);
}
#[test]
fn nested_expensive_under_not_is_expensive() {
let cheap = col("a").eq(lit(1));
let nested = Expr::Not(Box::new(col("b").like(lit("%foo%"))));
let (out, changed) = reorder_predicates(vec![nested.clone(), cheap.clone()]);
assert_eq!(out, vec![cheap, nested]);
assert!(changed);
}
}