use vibesql_ast::{BinaryOperator, CaseWhen, Expression};
use vibesql_types::SqlValue;
use crate::{
errors::ExecutorError,
evaluator::{casting::cast_value, CombinedExpressionEvaluator, ExpressionEvaluator},
};
fn reverse_comparison_op(op: &BinaryOperator) -> Option<BinaryOperator> {
match op {
BinaryOperator::LessThan => Some(BinaryOperator::GreaterThan),
BinaryOperator::LessThanOrEqual => Some(BinaryOperator::GreaterThanOrEqual),
BinaryOperator::GreaterThan => Some(BinaryOperator::LessThan),
BinaryOperator::GreaterThanOrEqual => Some(BinaryOperator::LessThanOrEqual),
BinaryOperator::Equal => Some(BinaryOperator::Equal),
BinaryOperator::NotEqual => Some(BinaryOperator::NotEqual),
_ => None, }
}
fn is_constant_expr(expr: &Expression) -> bool {
match expr {
Expression::Literal(_) => true,
Expression::UnaryOp { expr: inner, .. } => is_constant_expr(inner),
Expression::BinaryOp { left, right, .. } => {
is_constant_expr(left) && is_constant_expr(right)
}
Expression::Cast { expr: inner, .. } => is_constant_expr(inner),
Expression::IsNull { expr: inner, .. } => is_constant_expr(inner),
Expression::IsTruthValue { expr: inner, .. } => is_constant_expr(inner),
Expression::Between { expr, low, high, .. } => {
is_constant_expr(expr) && is_constant_expr(low) && is_constant_expr(high)
}
_ => false,
}
}
pub fn is_constant_false_where(expr: &Expression) -> Option<bool> {
if !is_constant_expr(expr) {
return None;
}
use crate::schema::CombinedSchema;
let schema = CombinedSchema::empty();
let evaluator = CombinedExpressionEvaluator::new(&schema);
match optimize_expression(expr, &evaluator) {
Ok(Expression::Literal(SqlValue::Boolean(false))) => Some(true), Ok(Expression::Literal(SqlValue::Null)) => Some(true), Ok(Expression::Literal(SqlValue::Boolean(true))) => Some(false), _ => None, }
}
#[derive(Debug, PartialEq)]
pub enum WhereOptimization {
AlwaysTrue,
AlwaysFalse,
Optimized(Expression),
Unchanged(Option<Expression>),
}
pub fn optimize_where_clause(
where_expr: Option<&Expression>,
evaluator: &CombinedExpressionEvaluator,
) -> Result<WhereOptimization, ExecutorError> {
match where_expr {
None => Ok(WhereOptimization::Unchanged(None)),
Some(expr) => {
let optimized = optimize_expression(expr, evaluator)?;
match optimized {
Expression::Literal(SqlValue::Boolean(true)) => Ok(WhereOptimization::AlwaysTrue),
Expression::Literal(SqlValue::Boolean(false)) => Ok(WhereOptimization::AlwaysFalse),
Expression::Literal(SqlValue::Null) => {
Ok(WhereOptimization::AlwaysFalse)
}
optimized_expr => {
if optimized_expr == *expr {
Ok(WhereOptimization::Unchanged(Some(optimized_expr)))
} else {
Ok(WhereOptimization::Optimized(optimized_expr))
}
}
}
}
}
}
#[allow(clippy::only_used_in_recursion)]
pub fn optimize_expression(
expr: &Expression,
evaluator: &CombinedExpressionEvaluator,
) -> Result<Expression, ExecutorError> {
match expr {
Expression::Literal(_) => Ok(expr.clone()),
Expression::ColumnRef(_)
| Expression::PseudoVariable { .. }
| Expression::SessionVariable { .. } => Ok(expr.clone()),
Expression::BinaryOp { left, op, right } => {
let left_opt = optimize_expression(left, evaluator)?;
let right_opt = optimize_expression(right, evaluator)?;
if let (Expression::Literal(left_val), Expression::Literal(right_val)) =
(&left_opt, &right_opt)
{
let sql_mode = evaluator.database().map(|db| db.sql_mode()).unwrap_or_default();
match ExpressionEvaluator::eval_binary_op_static(left_val, op, right_val, sql_mode)
{
Ok(result) => return Ok(Expression::Literal(result)),
Err(_) => {
}
}
}
if let Some(reversed_op) = reverse_comparison_op(op) {
let left_is_const = is_constant_expr(&left_opt);
let right_is_const = is_constant_expr(&right_opt);
if left_is_const && !right_is_const {
return Ok(Expression::BinaryOp {
left: Box::new(right_opt),
op: reversed_op,
right: Box::new(left_opt),
});
}
}
Ok(Expression::BinaryOp {
left: Box::new(left_opt),
op: *op,
right: Box::new(right_opt),
})
}
Expression::UnaryOp { op, expr: inner_expr } => {
let inner_opt = optimize_expression(inner_expr, evaluator)?;
if let Expression::Literal(val) = &inner_opt {
let dummy_row = vibesql_storage::Row::new(vec![]);
let unary_expr = Expression::UnaryOp {
op: *op,
expr: Box::new(Expression::Literal(val.clone())),
};
match evaluator.eval(&unary_expr, &dummy_row) {
Ok(result) => Ok(Expression::Literal(result)),
Err(_) => Ok(unary_expr),
}
} else {
Ok(Expression::UnaryOp { op: *op, expr: Box::new(inner_opt) })
}
}
Expression::Function { .. } => Ok(expr.clone()),
Expression::AggregateFunction { .. } => Ok(expr.clone()),
Expression::IsNull { expr: inner_expr, negated } => {
let inner_opt = optimize_expression(inner_expr, evaluator)?;
if let Expression::Literal(val) = &inner_opt {
let is_null = val.is_null();
let result = if *negated { !is_null } else { is_null };
Ok(Expression::Literal(SqlValue::Boolean(result)))
} else {
Ok(Expression::IsNull { expr: Box::new(inner_opt), negated: *negated })
}
}
Expression::IsDistinctFrom { left, right, negated } => {
let left_opt = optimize_expression(left, evaluator)?;
let right_opt = optimize_expression(right, evaluator)?;
if let (Expression::Literal(left_val), Expression::Literal(right_val)) =
(&left_opt, &right_opt)
{
let is_distinct = ExpressionEvaluator::values_are_distinct(left_val, right_val);
let result = if *negated { !is_distinct } else { is_distinct };
Ok(Expression::Literal(SqlValue::Boolean(result)))
} else {
Ok(Expression::IsDistinctFrom {
left: Box::new(left_opt),
right: Box::new(right_opt),
negated: *negated,
})
}
}
Expression::IsTruthValue { expr, truth_value, negated } => {
let inner_opt = optimize_expression(expr, evaluator)?;
if let Expression::Literal(val) = &inner_opt {
let result = match truth_value {
vibesql_ast::TruthValue::True => match val {
SqlValue::Boolean(true) => true,
SqlValue::Integer(n) => *n != 0,
SqlValue::Bigint(n) => *n != 0,
SqlValue::Smallint(n) => *n != 0,
_ => false,
},
vibesql_ast::TruthValue::False => matches!(
val,
SqlValue::Boolean(false)
| SqlValue::Integer(0)
| SqlValue::Bigint(0)
| SqlValue::Smallint(0)
),
vibesql_ast::TruthValue::Unknown => matches!(val, SqlValue::Null),
};
let final_result = if *negated { !result } else { result };
Ok(Expression::Literal(SqlValue::Boolean(final_result)))
} else {
Ok(Expression::IsTruthValue {
expr: Box::new(inner_opt),
truth_value: *truth_value,
negated: *negated,
})
}
}
Expression::Wildcard => Ok(expr.clone()),
Expression::Case { operand, when_clauses, else_result } => {
let operand_opt =
operand.as_ref().map(|op| optimize_expression(op, evaluator)).transpose()?;
let when_clauses_opt: Result<Vec<CaseWhen>, ExecutorError> = when_clauses
.iter()
.map(|wc| {
let conditions: Result<Vec<Expression>, ExecutorError> = wc
.conditions
.iter()
.map(|cond| optimize_expression(cond, evaluator))
.collect();
Ok(CaseWhen {
conditions: conditions?,
result: optimize_expression(&wc.result, evaluator)?,
})
})
.collect();
let else_opt =
else_result.as_ref().map(|er| optimize_expression(er, evaluator)).transpose()?;
Ok(Expression::Case {
operand: operand_opt.map(Box::new),
when_clauses: when_clauses_opt?,
else_result: else_opt.map(Box::new),
})
}
Expression::In { .. } => Ok(expr.clone()),
Expression::InList { .. } => Ok(expr.clone()),
Expression::Between { expr: inner_expr, low, high, negated, symmetric } => {
let expr_opt = optimize_expression(inner_expr, evaluator)?;
let low_opt = optimize_expression(low, evaluator)?;
let high_opt = optimize_expression(high, evaluator)?;
if let (
Expression::Literal(expr_val),
Expression::Literal(low_val),
Expression::Literal(high_val),
) = (&expr_opt, &low_opt, &high_opt)
{
let sql_mode = evaluator.database().map(|db| db.sql_mode()).unwrap_or_default();
match ExpressionEvaluator::eval_between_static(
expr_val, low_val, high_val, *negated, *symmetric, sql_mode,
) {
Ok(result) => return Ok(Expression::Literal(result)),
Err(_) => {
}
}
}
Ok(Expression::Between {
expr: Box::new(expr_opt),
low: Box::new(low_opt),
high: Box::new(high_opt),
negated: *negated,
symmetric: *symmetric,
})
}
Expression::Cast { expr: inner_expr, data_type } => {
let expr_opt = optimize_expression(inner_expr, evaluator)?;
if let Expression::Literal(val) = &expr_opt {
let sql_mode = evaluator.database().map(|db| db.sql_mode()).unwrap_or_default();
match cast_value(val, data_type, &sql_mode) {
Ok(result) => Ok(Expression::Literal(result)),
Err(_) => {
Ok(Expression::Cast {
expr: Box::new(expr_opt),
data_type: data_type.clone(),
})
}
}
} else {
Ok(Expression::Cast { expr: Box::new(expr_opt), data_type: data_type.clone() })
}
}
Expression::Position { .. } | Expression::Trim { .. } | Expression::Extract { .. } => {
Ok(expr.clone())
}
Expression::Like { .. } | Expression::Glob { .. } => Ok(expr.clone()),
Expression::Exists { .. } => Ok(expr.clone()),
Expression::QuantifiedComparison { .. } => Ok(expr.clone()),
Expression::CurrentDate
| Expression::CurrentTime { .. }
| Expression::CurrentTimestamp { .. } => Ok(expr.clone()),
Expression::Interval { value, unit, leading_precision, fractional_precision } => {
let optimized_value = optimize_expression(value, evaluator)?;
Ok(Expression::Interval {
value: Box::new(optimized_value),
unit: unit.clone(),
leading_precision: *leading_precision,
fractional_precision: *fractional_precision,
})
}
Expression::Default => Ok(expr.clone()),
Expression::DuplicateKeyValue { .. } => Ok(expr.clone()),
Expression::WindowFunction { .. } => Ok(expr.clone()),
Expression::NextValue { .. } => Ok(expr.clone()),
Expression::ScalarSubquery(_) => Ok(expr.clone()),
Expression::MatchAgainst { .. } => Ok(expr.clone()),
Expression::Placeholder(_)
| Expression::NumberedPlaceholder(_)
| Expression::NamedPlaceholder(_) => Ok(expr.clone()),
Expression::Conjunction(children) => {
let optimized: Result<Vec<_>, _> =
children.iter().map(|child| optimize_expression(child, evaluator)).collect();
Ok(Expression::Conjunction(optimized?))
}
Expression::Disjunction(children) => {
let optimized: Result<Vec<_>, _> =
children.iter().map(|child| optimize_expression(child, evaluator)).collect();
Ok(Expression::Disjunction(optimized?))
}
Expression::RowValueConstructor(children) => {
let optimized: Result<Vec<_>, _> =
children.iter().map(|child| optimize_expression(child, evaluator)).collect();
Ok(Expression::RowValueConstructor(optimized?))
}
Expression::Collate { expr, collation } => {
let optimized_expr = optimize_expression(expr, evaluator)?;
Ok(Expression::Collate { expr: Box::new(optimized_expr), collation: collation.clone() })
}
Expression::Raise { .. } => Ok(expr.clone()),
}
}
#[cfg(test)]
mod tests {
use vibesql_ast::Expression;
use vibesql_catalog::TableSchema;
use vibesql_types::{DataType, SqlValue};
use super::*;
#[test]
fn test_cast_folding_integer_to_varchar() {
let expr = Expression::Cast {
expr: Box::new(Expression::Literal(SqlValue::Integer(42))),
data_type: DataType::Varchar { max_length: None },
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Varchar(s)) => assert_eq!(s.as_str(), "42"),
_ => panic!("Expected folded VARCHAR literal, got {:?}", optimized),
}
}
#[test]
fn test_cast_folding_varchar_to_integer() {
let expr = Expression::Cast {
expr: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("123")))),
data_type: DataType::Integer,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Integer(n)) => assert_eq!(n, 123),
_ => panic!("Expected folded INTEGER literal, got {:?}", optimized),
}
}
#[test]
fn test_cast_folding_permissive_string_to_integer() {
let expr = Expression::Cast {
expr: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("abc")))),
data_type: DataType::Integer,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Integer(0)) => {} _ => {
panic!("Expected CAST('abc' AS INTEGER) to fold to Integer(0), got {:?}", optimized)
}
}
}
#[test]
fn test_cast_non_literal_not_folded() {
let expr = Expression::Cast {
expr: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
data_type: DataType::Integer,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Cast { .. } => {} _ => panic!("Expected CAST to be preserved for non-literal, got {:?}", optimized),
}
}
#[test]
fn test_cast_null_folding() {
let expr = Expression::Cast {
expr: Box::new(Expression::Literal(SqlValue::Null)),
data_type: DataType::Integer,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Null) => {} _ => panic!("Expected folded NULL literal, got {:?}", optimized),
}
}
#[test]
fn test_between_constant_folding_true() {
let expr = Expression::Between {
expr: Box::new(Expression::Literal(SqlValue::Integer(5))),
low: Box::new(Expression::Literal(SqlValue::Integer(1))),
high: Box::new(Expression::Literal(SqlValue::Integer(10))),
negated: false,
symmetric: false,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Boolean(true)) => {} _ => panic!("Expected folded TRUE literal, got {:?}", optimized),
}
}
#[test]
fn test_between_constant_folding_false() {
let expr = Expression::Between {
expr: Box::new(Expression::Literal(SqlValue::Integer(15))),
low: Box::new(Expression::Literal(SqlValue::Integer(1))),
high: Box::new(Expression::Literal(SqlValue::Integer(10))),
negated: false,
symmetric: false,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Boolean(false)) => {} _ => panic!("Expected folded FALSE literal, got {:?}", optimized),
}
}
#[test]
fn test_between_with_column_not_folded() {
let expr = Expression::Between {
expr: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
low: Box::new(Expression::Literal(SqlValue::Integer(1))),
high: Box::new(Expression::Literal(SqlValue::Integer(10))),
negated: false,
symmetric: false,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Between { .. } => {} _ => panic!("Expected BETWEEN to be preserved for non-literal, got {:?}", optimized),
}
}
#[test]
fn test_not_between_constant_folding() {
let expr = Expression::Between {
expr: Box::new(Expression::Literal(SqlValue::Integer(15))),
low: Box::new(Expression::Literal(SqlValue::Integer(1))),
high: Box::new(Expression::Literal(SqlValue::Integer(10))),
negated: true,
symmetric: false,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Boolean(true)) => {} _ => panic!("Expected folded TRUE literal, got {:?}", optimized),
}
}
#[test]
fn test_between_symmetric_constant_folding() {
let expr = Expression::Between {
expr: Box::new(Expression::Literal(SqlValue::Integer(5))),
low: Box::new(Expression::Literal(SqlValue::Integer(10))),
high: Box::new(Expression::Literal(SqlValue::Integer(1))),
negated: false,
symmetric: true,
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Boolean(true)) => {} _ => panic!("Expected folded TRUE literal, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_greater_than_or_equal() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(9933))),
op: vibesql_ast::BinaryOperator::GreaterThanOrEqual,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col0", false,
))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(
matches!(left.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "col0")
);
assert_eq!(op, vibesql_ast::BinaryOperator::LessThanOrEqual);
assert!(matches!(right.as_ref(), Expression::Literal(SqlValue::Integer(9933))));
}
_ => panic!("Expected normalized BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_less_than_or_equal() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(8524))),
op: vibesql_ast::BinaryOperator::LessThanOrEqual,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col3", false,
))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(
matches!(left.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "col3")
);
assert_eq!(op, vibesql_ast::BinaryOperator::GreaterThanOrEqual);
assert!(matches!(right.as_ref(), Expression::Literal(SqlValue::Integer(8524))));
}
_ => panic!("Expected normalized BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_already_normalized() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col0", false,
))),
op: vibesql_ast::BinaryOperator::LessThanOrEqual,
right: Box::new(Expression::Literal(SqlValue::Integer(9933))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(
matches!(left.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "col0")
);
assert_eq!(op, vibesql_ast::BinaryOperator::LessThanOrEqual);
assert!(matches!(right.as_ref(), Expression::Literal(SqlValue::Integer(9933))));
}
_ => panic!("Expected unchanged BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_equal() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(100))),
op: vibesql_ast::BinaryOperator::Equal,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(
matches!(left.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "x")
);
assert_eq!(op, vibesql_ast::BinaryOperator::Equal);
assert!(matches!(right.as_ref(), Expression::Literal(SqlValue::Integer(100))));
}
_ => panic!("Expected normalized BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_non_comparison_unchanged() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(5))),
op: vibesql_ast::BinaryOperator::Plus,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(matches!(left.as_ref(), Expression::Literal(SqlValue::Integer(5))));
assert_eq!(op, vibesql_ast::BinaryOperator::Plus);
assert!(
matches!(right.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "x")
);
}
_ => panic!("Expected unchanged BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_less_than() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(100))),
op: vibesql_ast::BinaryOperator::LessThan,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(
matches!(left.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "x")
);
assert_eq!(op, vibesql_ast::BinaryOperator::GreaterThan);
assert!(matches!(right.as_ref(), Expression::Literal(SqlValue::Integer(100))));
}
_ => panic!("Expected normalized BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_predicate_normalization_greater_than() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(200))),
op: vibesql_ast::BinaryOperator::GreaterThan,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
};
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::BinaryOp { left, op, right } => {
assert!(
matches!(left.as_ref(), Expression::ColumnRef(col_id) if col_id.column_canonical() == "x")
);
assert_eq!(op, vibesql_ast::BinaryOperator::LessThan);
assert!(matches!(right.as_ref(), Expression::Literal(SqlValue::Integer(200))));
}
_ => panic!("Expected normalized BinaryOp, got {:?}", optimized),
}
}
#[test]
fn test_null_is_not_null_optimization() {
let expr = Expression::IsNull {
expr: Box::new(Expression::Literal(SqlValue::Null)),
negated: true, };
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let optimized = optimize_expression(&expr, &evaluator).unwrap();
match optimized {
Expression::Literal(SqlValue::Boolean(false)) => {} _ => panic!("Expected folded FALSE literal, got {:?}", optimized),
}
}
#[test]
fn test_null_is_not_null_where_optimization() {
let expr = Expression::IsNull {
expr: Box::new(Expression::Literal(SqlValue::Null)),
negated: true, };
let db = vibesql_storage::Database::new();
let schema = TableSchema::new("test".to_string(), vec![]);
let combined = crate::schema::CombinedSchema::from_table("test".to_string(), schema);
let evaluator = CombinedExpressionEvaluator::with_database(&combined, &db);
let result = optimize_where_clause(Some(&expr), &evaluator).unwrap();
assert!(
matches!(result, WhereOptimization::AlwaysFalse),
"Expected AlwaysFalse, got {:?}",
result
);
}
#[test]
fn test_is_constant_false_where_with_not_is_not_null() {
let expr = Expression::UnaryOp {
op: vibesql_ast::UnaryOperator::Not,
expr: Box::new(Expression::IsNull {
expr: Box::new(Expression::UnaryOp {
op: vibesql_ast::UnaryOperator::Plus,
expr: Box::new(Expression::Literal(SqlValue::Integer(35))),
}),
negated: true, }),
};
let result = is_constant_false_where(&expr);
assert_eq!(
result,
Some(true),
"Expected is_constant_false_where to return Some(true), got {:?}",
result
);
}
#[test]
fn test_is_constant_false_where_with_column_ref() {
let expr = Expression::IsNull {
expr: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"x", false,
))),
negated: true,
};
let result = is_constant_false_where(&expr);
assert_eq!(
result, None,
"Expected None for expression with column reference, got {:?}",
result
);
}
#[test]
fn test_is_constant_false_where_true() {
let expr = Expression::BinaryOp {
left: Box::new(Expression::Literal(SqlValue::Integer(1))),
op: vibesql_ast::BinaryOperator::Equal,
right: Box::new(Expression::Literal(SqlValue::Integer(1))),
};
let result = is_constant_false_where(&expr);
assert_eq!(
result,
Some(false),
"Expected Some(false) for always-true expression, got {:?}",
result
);
}
}