use crate::utils::{extract_column_name, extract_literal_value, flip_operator, infix_to_operator};
use radixdb_core::Value;
use radixdb_sql::ast::{self as ast, InfixOperator, PrefixOperator};
use radixdb_storage::expression::{
AndExpr, BetweenExpr, ComparisonExpr, Expression, InListExpr, LikeExpr, NotExpr, NullCheckExpr,
OrExpr,
};
pub fn convert_ast_to_storage_expr(expr: &ast::Expression) -> Option<Box<dyn Expression>> {
match expr {
ast::Expression::Infix(infix) => convert_infix(infix),
ast::Expression::Prefix(prefix) => convert_prefix(prefix),
ast::Expression::In(in_expr) => convert_in(in_expr),
ast::Expression::Between(between) => convert_between(between),
ast::Expression::Like(like) => convert_like(like),
ast::Expression::BooleanLiteral(b) => {
Some(Box::new(radixdb_storage::expression::ConstBoolExpr::new(
b.value,
)))
}
ast::Expression::FunctionCall(_)
| ast::Expression::ScalarSubquery(_)
| ast::Expression::Exists(_)
| ast::Expression::AllAny(_)
| ast::Expression::Case(_)
| ast::Expression::Cast(_)
| ast::Expression::Window(_)
| ast::Expression::Parameter(_)
| ast::Expression::IntervalLiteral(_)
| ast::Expression::BoundValue(_) => None,
ast::Expression::Identifier(_)
| ast::Expression::QualifiedIdentifier(_)
| ast::Expression::IntegerLiteral(_)
| ast::Expression::FloatLiteral(_)
| ast::Expression::StringLiteral(_)
| ast::Expression::NullLiteral(_)
| ast::Expression::List(_)
| ast::Expression::Distinct(_)
| ast::Expression::ExpressionList(_)
| ast::Expression::Aliased(_) => None,
ast::Expression::TableSource(_)
| ast::Expression::JoinSource(_)
| ast::Expression::SubquerySource(_)
| ast::Expression::ValuesSource(_)
| ast::Expression::CteReference(_)
| ast::Expression::FunctionTableSource(_)
| ast::Expression::Star(_)
| ast::Expression::QualifiedStar(_)
| ast::Expression::Default(_)
| ast::Expression::InHashSet(_) => None,
}
}
fn convert_infix(infix: &ast::InfixExpression) -> Option<Box<dyn Expression>> {
match infix.op_type {
InfixOperator::Equal
| InfixOperator::NotEqual
| InfixOperator::LessThan
| InfixOperator::LessEqual
| InfixOperator::GreaterThan
| InfixOperator::GreaterEqual => convert_comparison(infix),
InfixOperator::And => {
let left = convert_ast_to_storage_expr(&infix.left)?;
let right = convert_ast_to_storage_expr(&infix.right)?;
Some(Box::new(AndExpr::and(left, right)))
}
InfixOperator::Or => {
let left = convert_ast_to_storage_expr(&infix.left)?;
let right = convert_ast_to_storage_expr(&infix.right)?;
Some(Box::new(OrExpr::or(left, right)))
}
InfixOperator::Is | InfixOperator::IsNot => convert_is_null(infix),
_ => None,
}
}
fn convert_comparison(infix: &ast::InfixExpression) -> Option<Box<dyn Expression>> {
if let Some((column, value)) = try_extract_column_value(&infix.left, &infix.right) {
let operator = infix_to_operator(infix.op_type)?;
return Some(Box::new(ComparisonExpr::new(column, operator, value)));
}
if let Some((column, value)) = try_extract_column_value(&infix.right, &infix.left) {
let operator = flip_operator(infix_to_operator(infix.op_type)?);
return Some(Box::new(ComparisonExpr::new(column, operator, value)));
}
None
}
fn try_extract_column_value(
maybe_column: &ast::Expression,
maybe_value: &ast::Expression,
) -> Option<(String, Value)> {
let column = extract_column_name(maybe_column)?;
let value = extract_literal_value(maybe_value)?;
Some((column, value))
}
fn convert_is_null(infix: &ast::InfixExpression) -> Option<Box<dyn Expression>> {
let column = extract_column_name(&infix.left)?;
let is_not = infix.op_type == InfixOperator::IsNot;
match &*infix.right {
ast::Expression::NullLiteral(_) => {
if is_not {
Some(Box::new(NullCheckExpr::is_not_null(column)))
} else {
Some(Box::new(NullCheckExpr::is_null(column)))
}
}
_ => None,
}
}
fn convert_prefix(prefix: &ast::PrefixExpression) -> Option<Box<dyn Expression>> {
match prefix.op_type {
PrefixOperator::Not => {
let inner = convert_ast_to_storage_expr(&prefix.right)?;
Some(Box::new(NotExpr::new(inner)))
}
_ => None,
}
}
fn convert_in(in_expr: &ast::InExpression) -> Option<Box<dyn Expression>> {
let column = extract_column_name(&in_expr.left)?;
let values: Option<Vec<Value>> = match &*in_expr.right {
ast::Expression::ExpressionList(list) => {
list.expressions.iter().map(extract_literal_value).collect()
}
ast::Expression::List(list) => list.elements.iter().map(extract_literal_value).collect(),
_ => return None, };
let values = values?;
if values.is_empty() {
return None;
}
let expr = if in_expr.not {
InListExpr::not_in(column, values)
} else {
InListExpr::new(column, values)
};
Some(Box::new(expr))
}
fn convert_between(between: &ast::BetweenExpression) -> Option<Box<dyn Expression>> {
let column = extract_column_name(&between.expr)?;
let low = extract_literal_value(&between.lower)?;
let high = extract_literal_value(&between.upper)?;
let expr = if between.not {
BetweenExpr::not_between(column, low, high)
} else {
BetweenExpr::new(column, low, high)
};
Some(Box::new(expr))
}
fn convert_like(like: &ast::LikeExpression) -> Option<Box<dyn Expression>> {
let column = extract_column_name(&like.left)?;
let pattern = match &*like.pattern {
ast::Expression::StringLiteral(s) => s.value.clone(),
_ => return None, };
let op_upper = like.operator.to_uppercase();
let is_not = op_upper.contains("NOT");
let is_ilike = op_upper.contains("ILIKE");
let expr = match (is_not, is_ilike) {
(true, true) => LikeExpr::not_ilike(column, pattern),
(true, false) => LikeExpr::not_like(column, pattern),
(false, true) => LikeExpr::new_ilike(column, pattern),
(false, false) => LikeExpr::new(column, pattern),
};
Some(Box::new(expr))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::dummy_token;
use radixdb_core::{DataType, Row, Schema, SchemaBuilder};
use radixdb_sql::token::TokenType;
fn test_schema() -> Schema {
SchemaBuilder::new("test")
.add_primary_key("id", DataType::Integer)
.add("name", DataType::Text)
.add("age", DataType::Integer)
.add_nullable("email", DataType::Text)
.build()
}
fn test_row() -> Row {
Row::from_values(vec![
Value::integer(1),
Value::text("Alice"),
Value::integer(30),
Value::text("alice@example.com"),
])
}
fn make_ident(name: &str) -> ast::Expression {
ast::Expression::Identifier(ast::Identifier::new(
dummy_token(name, TokenType::Identifier),
name.to_string(),
))
}
fn make_int(value: i64) -> ast::Expression {
ast::Expression::IntegerLiteral(ast::IntegerLiteral {
token: dummy_token(&value.to_string(), TokenType::Integer),
value,
})
}
fn make_float(value: f64) -> ast::Expression {
ast::Expression::FloatLiteral(ast::FloatLiteral {
token: dummy_token(&value.to_string(), TokenType::Float),
value,
})
}
fn make_str(value: &str) -> ast::Expression {
ast::Expression::StringLiteral(ast::StringLiteral {
token: dummy_token(value, TokenType::String),
value: value.into(),
type_hint: None,
})
}
fn make_infix(left: ast::Expression, op: &str, right: ast::Expression) -> ast::InfixExpression {
ast::InfixExpression::new(
dummy_token(op, TokenType::Operator),
Box::new(left),
op,
Box::new(right),
)
}
#[test]
fn test_convert_simple_equality() {
let ast_expr = ast::Expression::Infix(make_infix(make_ident("id"), "=", make_int(1)));
let storage_expr = convert_ast_to_storage_expr(&ast_expr);
assert!(storage_expr.is_some());
let mut expr = storage_expr.unwrap();
let schema = test_schema();
expr.prepare_for_schema(&schema);
let row = test_row();
assert!(expr.evaluate(&row).unwrap());
}
#[test]
fn test_convert_and_expression() {
let left_cond = ast::Expression::Infix(make_infix(make_ident("id"), "=", make_int(1)));
let right_cond = ast::Expression::Infix(make_infix(make_ident("age"), ">", make_int(20)));
let ast_expr = ast::Expression::Infix(make_infix(left_cond, "AND", right_cond));
let storage_expr = convert_ast_to_storage_expr(&ast_expr);
assert!(storage_expr.is_some());
let mut expr = storage_expr.unwrap();
let schema = test_schema();
expr.prepare_for_schema(&schema);
let row = test_row(); assert!(expr.evaluate(&row).unwrap());
}
#[test]
fn test_convert_value_on_left() {
let ast_expr = ast::Expression::Infix(make_infix(make_int(0), "<", make_ident("id")));
let storage_expr = convert_ast_to_storage_expr(&ast_expr);
assert!(storage_expr.is_some());
let mut expr = storage_expr.unwrap();
let schema = test_schema();
expr.prepare_for_schema(&schema);
let row = test_row(); assert!(expr.evaluate(&row).unwrap()); }
#[test]
fn test_converted_mixed_numeric_filter_keeps_compiled_dynamic_parity() {
const EXACT: i64 = 1_i64 << 53;
let schema = SchemaBuilder::new("numeric_boundary")
.add("value", DataType::Integer)
.build();
let row = Row::from_values(vec![Value::Integer(EXACT + 1)]);
let ast_expr = ast::Expression::Infix(make_infix(
make_ident("value"),
"=",
make_float(EXACT as f64),
));
let mut expression = convert_ast_to_storage_expr(&ast_expr).unwrap();
expression.prepare_for_schema(&schema);
assert!(!expression.evaluate(&row).unwrap());
assert!(!expression.evaluate_fast(&row));
let compiled =
radixdb_storage::expression::CompiledFilter::compile(expression.as_ref(), &schema);
assert!(compiled.is_fully_compiled());
assert!(!compiled.matches(&row));
assert!(!compiled.matches_arc_slice(row.as_slice()));
}
#[test]
fn test_cannot_convert_function_call() {
let ast_expr = ast::Expression::FunctionCall(Box::new(ast::FunctionCall {
token: dummy_token("UPPER", TokenType::Identifier),
function: "UPPER".into(),
arguments: vec![make_ident("name")],
is_distinct: false,
order_by: vec![],
filter: None,
}));
let storage_expr = convert_ast_to_storage_expr(&ast_expr);
assert!(storage_expr.is_none());
}
#[test]
fn test_convert_string_comparison() {
let ast_expr =
ast::Expression::Infix(make_infix(make_ident("name"), "=", make_str("Alice")));
let storage_expr = convert_ast_to_storage_expr(&ast_expr);
assert!(storage_expr.is_some());
let mut expr = storage_expr.unwrap();
let schema = test_schema();
expr.prepare_for_schema(&schema);
let row = test_row();
assert!(expr.evaluate(&row).unwrap());
}
}