Merge 'feat(optimizer): eliminate between statement' from KaguraMilet

Rewrite `Y BETWEEN X AND Z` as `X <= Y AND Y <= Z`. And due to the
support of this optimization rule, limbo should now be able to execute
the `BETWEEN AND` statement.

Closes #490
This commit is contained in:
jussisaurio
2024-12-20 17:23:42 +02:00
2 changed files with 117 additions and 0 deletions

View File

@@ -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<Plan> {
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<Vec<ast::Expr>>,
) -> 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;
}

View File

@@ -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}