mod budget;
#[cfg(test)]
mod expansion_tests;
#[cfg(test)]
mod tests;
use crate::{
db::query::plan::expr::{
BinaryOp, CaseWhenArm, Expr, Function, UnaryOp,
canonicalize::{
normalize_bool_expr,
truth_admission::{TruthAdmission, TruthWrapperScope},
},
},
db::{QueryError, query::preparation::PreparationWork},
value::Value,
};
use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
pub(super) fn normalize_bool_case_expr(
mut expr: Expr,
top_level_where_null_collapse: bool,
work: &PreparationWork<'_>,
) -> Result<Expr, QueryError> {
let Expr::Case {
when_then_arms,
else_expr,
} = &mut expr
else {
return Err(QueryError::invariant());
};
let Some(admitted) = budget::admit_expansion(when_then_arms, else_expr, work)? else {
return Ok(expr);
};
lower_searched_case_to_boolean(
std::mem::take(when_then_arms),
else_expr.take(),
admitted,
top_level_where_null_collapse,
work,
)
}
pub(super) fn canonicalize_normalized_bool_case_in_bool_context(
mut expr: Expr,
top_level_where_null_collapse: bool,
truth_wrapper_scope: Option<TruthWrapperScope>,
work: &PreparationWork<'_>,
) -> Result<Expr, QueryError> {
work.charge(
icydb_diagnostic_code::DiagnosticExecutionBudgetResource::PredicateExpressionSteps,
1,
)?;
match &mut expr {
Expr::Unary {
op: UnaryOp::Not,
expr: child,
} => {
**child = canonicalize_normalized_bool_case_in_bool_context(
child.take(),
false,
truth_wrapper_scope,
work,
)?;
}
Expr::Binary {
op: BinaryOp::And | BinaryOp::Or,
left,
right,
} => {
**left = canonicalize_normalized_bool_case_in_bool_context(
left.take(),
top_level_where_null_collapse,
truth_wrapper_scope,
work,
)?;
**right = canonicalize_normalized_bool_case_in_bool_context(
right.take(),
top_level_where_null_collapse,
truth_wrapper_scope,
work,
)?;
}
Expr::Case {
when_then_arms,
else_expr,
} => {
for arm in when_then_arms.iter_mut() {
let [condition, result] = arm.children_mut();
*condition = canonicalize_normalized_bool_case_in_bool_context(
condition.take(),
true,
truth_wrapper_scope,
work,
)?;
*result = canonicalize_normalized_bool_case_in_bool_context(
result.take(),
top_level_where_null_collapse,
truth_wrapper_scope,
work,
)?;
}
**else_expr = canonicalize_normalized_bool_case_in_bool_context(
else_expr.take(),
top_level_where_null_collapse,
truth_wrapper_scope,
work,
)?;
return normalize_bool_case_expr(expr, top_level_where_null_collapse, work);
}
_ => {
return maybe_collapse_truth_wrapper_in_bool_context(expr, truth_wrapper_scope, work);
}
}
Ok(expr)
}
fn maybe_collapse_truth_wrapper_in_bool_context(
mut expr: Expr,
scope: Option<TruthWrapperScope>,
work: &PreparationWork<'_>,
) -> Result<Expr, QueryError> {
let Some(scope) = scope else {
return Ok(expr);
};
if let Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} = &mut expr
{
let chosen = match (left.as_ref(), right.as_ref()) {
(_, Expr::Literal(Value::Bool(true))) if truth_wrapper_candidate(left, scope) => {
Some((left.take(), true))
}
(Expr::Literal(Value::Bool(true)), _) if truth_wrapper_candidate(right, scope) => {
Some((right.take(), true))
}
(_, Expr::Literal(Value::Bool(false))) if truth_wrapper_candidate(left, scope) => {
Some((left.take(), false))
}
(Expr::Literal(Value::Bool(false)), _) if truth_wrapper_candidate(right, scope) => {
Some((right.take(), false))
}
_ => None,
};
if let Some((child, positive)) = chosen {
if positive {
return Ok(child);
}
work.charge(Resource::PredicateExpressionSteps, 1)?;
work.charge(Resource::TemporaryBytes, size_of::<Expr>() as u64)?;
return Ok(Expr::Unary {
op: UnaryOp::Not,
expr: Box::new(child),
});
}
}
Ok(expr)
}
fn truth_wrapper_candidate(expr: &Expr, scope: TruthWrapperScope) -> bool {
match scope {
TruthWrapperScope::ScalarWhere => TruthAdmission::is_scalar_condition(expr),
TruthWrapperScope::GroupedHaving => TruthAdmission::is_grouped_condition(expr),
}
}
fn lower_searched_case_to_boolean(
arms: Vec<CaseWhenArm>,
else_expr: Expr,
admitted: budget::AdmittedExpansion,
top_level_where_null_collapse: bool,
work: &PreparationWork<'_>,
) -> Result<Expr, QueryError> {
let mut canonical = match (top_level_where_null_collapse, else_expr) {
(true, Expr::Literal(Value::Null)) => {
work.charge(Resource::PredicateExpressionSteps, 1)?;
Expr::Literal(Value::Bool(false))
}
(_, other) => other,
};
for (mut arm, copy) in arms.into_iter().zip(admitted.condition_copies).rev() {
let [condition, result] = arm.children_mut();
copy.charge(work)?;
let duplicate = condition.clone();
let positive = guarded_bool_case_branch(
searched_case_match_guard(condition.take(), work)?,
result.take(),
work,
)?;
let negative_guard = searched_case_match_guard(duplicate, work)?;
work.charge(Resource::PredicateExpressionSteps, 1)?;
work.charge(Resource::TemporaryBytes, size_of::<Expr>() as u64)?;
let negative = guarded_bool_case_branch(
Expr::Unary {
op: UnaryOp::Not,
expr: Box::new(negative_guard),
},
canonical,
work,
)?;
work.charge(Resource::PredicateExpressionSteps, 1)?;
work.charge(Resource::TemporaryBytes, (2 * size_of::<Expr>()) as u64)?;
canonical = normalize_bool_expr(
Expr::Binary {
op: BinaryOp::Or,
left: Box::new(positive),
right: Box::new(negative),
},
work,
)?;
}
Ok(canonical)
}
fn guarded_bool_case_branch(
guard: Expr,
result: Expr,
work: &PreparationWork<'_>,
) -> Result<Expr, QueryError> {
match result {
Expr::Literal(Value::Bool(true)) => Ok(guard),
Expr::Literal(Value::Bool(false)) => Ok(result),
other => {
work.charge(Resource::PredicateExpressionSteps, 1)?;
work.charge(Resource::TemporaryBytes, (2 * size_of::<Expr>()) as u64)?;
Ok(Expr::Binary {
op: BinaryOp::And,
left: Box::new(guard),
right: Box::new(other),
})
}
}
}
fn searched_case_match_guard(
condition: Expr,
work: &PreparationWork<'_>,
) -> Result<Expr, QueryError> {
work.charge(Resource::PredicateExpressionSteps, 2)?;
work.charge(Resource::TemporaryBytes, (2 * size_of::<Expr>()) as u64)?;
Ok(Expr::FunctionCall {
function: Function::Coalesce,
args: vec![condition, Expr::Literal(Value::Bool(false))],
})
}