-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert boolean case expressions to boolean logic #1719
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -442,6 +442,8 @@ impl<'a> ExprRewriter for Simplifier<'a> { | |
use Expr::*; | ||
use Operator::{And, Divide, Eq, Multiply, NotEq, Or}; | ||
|
||
let is_boolean_expr = self.is_boolean_type(&expr); | ||
|
||
let new_expr = match expr { | ||
// | ||
// Rules for Eq | ||
|
@@ -662,6 +664,48 @@ impl<'a> ExprRewriter for Simplifier<'a> { | |
_ => unreachable!(), | ||
}, | ||
|
||
// | ||
// Rules for Case | ||
// | ||
|
||
// CASE | ||
// WHEN X THEN A | ||
// WHEN Y THEN B | ||
// ... | ||
// ELSE Q | ||
// END | ||
// | ||
// ---> (X AND A) OR (Y AND B AND NOT X) OR ... (NOT (X OR Y) AND Q) | ||
tustvold marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Case { | ||
expr: None, | ||
when_then_expr, | ||
else_expr, | ||
} if is_boolean_expr => { | ||
// The disjunction of all the when predicates encountered so far | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the complexity of expression this could potentially produce This would prevent some sort of pathological explosion and would handle the usecase of simple mapping |
||
let mut filter_expr = lit(false); | ||
// The disjunction of all the cases | ||
let mut out_expr = lit(false); | ||
|
||
for (when, then) in when_then_expr { | ||
let case_expr = when | ||
.as_ref() | ||
.clone() | ||
.and(filter_expr.clone().not()) | ||
.and(*then); | ||
|
||
out_expr = out_expr.or(case_expr); | ||
filter_expr = filter_expr.or(*when); | ||
} | ||
|
||
if let Some(else_expr) = else_expr { | ||
let case_expr = filter_expr.not().and(*else_expr); | ||
out_expr = out_expr.or(case_expr); | ||
} | ||
|
||
// Do a first pass at simplification | ||
out_expr.rewrite(self)? | ||
} | ||
|
||
expr => { | ||
// no additional rewrites possible | ||
expr | ||
|
@@ -1175,6 +1219,8 @@ mod tests { | |
.expect("expected to simplify") | ||
.rewrite(&mut const_evaluator) | ||
.expect("expected to const evaluate") | ||
.rewrite(&mut rewriter) | ||
.expect("expected to simplify") | ||
} | ||
|
||
fn expr_test_schema() -> DFSchemaRef { | ||
|
@@ -1291,6 +1337,11 @@ mod tests { | |
|
||
#[test] | ||
fn simplify_expr_case_when_then_else() { | ||
// CASE WHERE c2 != false THEN "ok" == "not_ok" ELSE c2 == true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately the optimizer is not especially brilliant at optimizing boolean expressions - I've created #1716 to track this. I'm not sure if we want to constrain the rewrite until the boolean expression optimizer is able to more effectively reduce down what it produces... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 I do wonder given the nullability concerns, if in addition to "simplify" which seeks to retain the same semantics, there could be something like: /// returns true if this expr *always* returns null or false
/// (aka would filter out all rows from a query)
fn is_null_or_false(expr: &Expr) -> bool {
...
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow, are you suggesting I change something or just musing 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry -- I was musing -- no specific changes suggested There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I implemented something related here: https://github.com/influxdata/influxdb_iox/pull/3557/files#r796984073 |
||
// --> | ||
// CASE WHERE c2 THEN false ELSE c2 | ||
// --> | ||
// false | ||
assert_eq!( | ||
simplify(Expr::Case { | ||
expr: None, | ||
|
@@ -1300,11 +1351,73 @@ mod tests { | |
)], | ||
else_expr: Some(Box::new(col("c2").eq(lit(true)))), | ||
}), | ||
Expr::Case { | ||
col("c2").not().and(col("c2")) // #1716 | ||
); | ||
|
||
// CASE WHERE c2 != false THEN "ok" == "ok" ELSE c2 | ||
// --> | ||
// CASE WHERE c2 THEN true ELSE c2 | ||
// --> | ||
// c2 | ||
assert_eq!( | ||
simplify(Expr::Case { | ||
expr: None, | ||
when_then_expr: vec![(Box::new(col("c2")), Box::new(lit(false)))], | ||
when_then_expr: vec![( | ||
Box::new(col("c2").not_eq(lit(false))), | ||
Box::new(lit("ok").eq(lit("ok"))), | ||
)], | ||
else_expr: Some(Box::new(col("c2").eq(lit(true)))), | ||
}), | ||
col("c2").or(col("c2").not().and(col("c2"))) // #1716 | ||
); | ||
|
||
// CASE WHERE ISNULL(c2) THEN true ELSE c2 | ||
// --> | ||
// ISNULL(c2) OR c2 | ||
assert_eq!( | ||
simplify(Expr::Case { | ||
expr: None, | ||
when_then_expr: vec![( | ||
Box::new(col("c2").is_null()), | ||
Box::new(lit(true)), | ||
)], | ||
else_expr: Some(Box::new(col("c2"))), | ||
} | ||
}), | ||
col("c2") | ||
.is_null() | ||
.or(col("c2").is_null().not().and(col("c2"))) | ||
); | ||
|
||
// CASE WHERE c1 then true WHERE c2 then false ELSE true | ||
// --> c1 OR (NOT(c1) AND c2 AND FALSE) OR (NOT(c1 OR c2) AND TRUE) | ||
// --> c1 OR (NOT(c1 OR c2)) | ||
// --> NOT(c1) AND c2 | ||
assert_eq!( | ||
simplify(Expr::Case { | ||
expr: None, | ||
when_then_expr: vec![ | ||
(Box::new(col("c1")), Box::new(lit(true)),), | ||
(Box::new(col("c2")), Box::new(lit(false)),) | ||
], | ||
else_expr: Some(Box::new(lit(true))), | ||
}), | ||
col("c1").or(col("c1").or(col("c2")).not()) | ||
); | ||
|
||
// CASE WHERE c1 then true WHERE c2 then true ELSE false | ||
// --> c1 OR (NOT(c1) AND c2 AND TRUE) OR (NOT(c1 OR c2) AND FALSE) | ||
// --> c1 OR (NOT(c1) AND c2) | ||
// --> c1 OR c2 | ||
assert_eq!( | ||
simplify(Expr::Case { | ||
expr: None, | ||
when_then_expr: vec![ | ||
(Box::new(col("c1")), Box::new(lit(true)),), | ||
(Box::new(col("c2")), Box::new(lit(false)),) | ||
], | ||
else_expr: Some(Box::new(lit(true))), | ||
}), | ||
col("c1").or(col("c1").or(col("c2")).not()) | ||
); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend putting this into the case statement to avoid evaluating it on each
Expr
(otherwise I think it ends up beingO(N^2)
asis_boolean_type
recursively checks all inputs