diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index ff3eac8e0..4763f8b1e 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -15,6 +15,7 @@ use super::plan::{ * but having them separate makes them easier to understand */ pub fn optimize_plan(mut select_plan: Plan) -> Result { + eliminate_between(&mut select_plan.source, &mut select_plan.where_clause)?; if let ConstantConditionEliminationResult::ImpossibleCondition = eliminate_constants(&mut select_plan.source, &mut select_plan.where_clause)? { @@ -498,6 +499,46 @@ fn push_scan_direction(operator: &mut SourceOperator, direction: &Direction) { } } +fn eliminate_between( + operator: &mut SourceOperator, + where_clauses: &mut Option>, +) -> Result<()> { + if let Some(predicates) = where_clauses { + *predicates = predicates.drain(..).map(convert_between_expr).collect(); + } + + match operator { + SourceOperator::Join { + left, + right, + predicates, + .. + } => { + eliminate_between(left, where_clauses)?; + eliminate_between(right, where_clauses)?; + + if let Some(predicates) = predicates { + *predicates = predicates.drain(..).map(convert_between_expr).collect(); + } + } + SourceOperator::Scan { + predicates: Some(preds), + .. + } => { + *preds = preds.drain(..).map(convert_between_expr).collect(); + } + SourceOperator::Search { + predicates: Some(preds), + .. + } => { + *preds = preds.drain(..).map(convert_between_expr).collect(); + } + _ => (), + } + + Ok(()) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConstantPredicate { AlwaysTrue, @@ -808,6 +849,65 @@ pub fn try_extract_index_search_expression( } } +fn convert_between_expr(expr: ast::Expr) -> ast::Expr { + match expr { + ast::Expr::Between { + lhs, + not, + start, + end, + } => { + // Convert `y NOT BETWEEN x AND z` to `x > y OR y > z` + let (lower_op, upper_op) = if not { + (ast::Operator::Greater, ast::Operator::Greater) + } else { + // Convert `y BETWEEN x AND z` to `x <= y AND y <= z` + (ast::Operator::LessEquals, ast::Operator::LessEquals) + }; + + let lower_bound = ast::Expr::Binary(start, lower_op, lhs.clone()); + let upper_bound = ast::Expr::Binary(lhs, upper_op, end); + + if not { + ast::Expr::Binary( + Box::new(lower_bound), + ast::Operator::Or, + Box::new(upper_bound), + ) + } else { + ast::Expr::Binary( + Box::new(lower_bound), + ast::Operator::And, + Box::new(upper_bound), + ) + } + } + ast::Expr::Parenthesized(mut exprs) => { + ast::Expr::Parenthesized(exprs.drain(..).map(convert_between_expr).collect()) + } + // Process other expressions recursively + ast::Expr::Binary(lhs, op, rhs) => ast::Expr::Binary( + Box::new(convert_between_expr(*lhs)), + op, + Box::new(convert_between_expr(*rhs)), + ), + ast::Expr::FunctionCall { + name, + distinctness, + args, + order_by, + filter_over, + } => ast::Expr::FunctionCall { + name, + distinctness, + args: args.map(|args| args.into_iter().map(convert_between_expr).collect()), + order_by, + filter_over, + }, + _ => expr, + } +} + trait TakeOwnership { fn take_ownership(&mut self) -> Self; } diff --git a/testing/where.test b/testing/where.test index 28ce70f8e..264bdfdd8 100755 --- a/testing/where.test +++ b/testing/where.test @@ -317,3 +317,20 @@ do_execsql_test where-age-index-seek-regression-test { do_execsql_test where-age-index-seek-regression-test-2 { select count(1) from users where age > 0; } {10000} + +do_execsql_test where-simple-between { + SELECT * FROM products WHERE price BETWEEN 70 AND 100; +} {1|hat|79.0 +2|cap|82.0 +5|sweatshirt|74.0 +6|shorts|70.0 +7|jeans|78.0 +8|sneakers|82.0 +11|accessories|81.0} + +do_execsql_test between-price-range-with-names { + SELECT * FROM products + WHERE (price BETWEEN 70 AND 100) + AND (name = 'sweatshirt' OR name = 'sneakers'); +} {5|sweatshirt|74.0 +8|sneakers|82.0}