use nodedb_types::Value;
use crate::value_ops::{coerced_eq, is_truthy, to_value_number, value_to_f64};
use super::binary::eval_binary_op;
use super::types::{SqlExpr, WHOLE_ROW_COLUMN};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EvalError {
#[error("division by zero")]
DivisionByZero,
#[error("function {name}() has no evaluator")]
UnknownFunction {
name: String,
},
#[error("{function}(): vector dimension mismatch: expected {expected}, got {got}")]
VectorDimensionMismatch {
function: &'static str,
expected: usize,
got: usize,
},
#[error("{function}(): argument {position} must be {expected}, got {got}")]
ArgumentType {
function: &'static str,
position: usize,
expected: &'static str,
got: &'static str,
},
#[error("{function}(): invalid JSONPath {path:?}: {reason}")]
InvalidJsonPath {
function: &'static str,
path: String,
reason: String,
},
#[error("{function}(): integer total out of range")]
NumericOverflow { function: &'static str },
}
struct RowScope<'a> {
new_doc: &'a Value,
old_doc: Option<&'a Value>,
excluded_doc: Option<&'a Value>,
}
impl<'a> RowScope<'a> {
fn column(&self, name: &str) -> Value {
row_field(self.new_doc, name)
}
fn old_column(&self, name: &str) -> Value {
match self.old_doc {
Some(old) => row_field(old, name),
None => Value::Null,
}
}
fn excluded_column(&self, name: &str) -> Value {
match self.excluded_doc {
Some(excluded) => row_field(excluded, name),
None => Value::Null,
}
}
}
fn row_field(doc: &Value, name: &str) -> Value {
if name == WHOLE_ROW_COLUMN {
return doc.clone();
}
doc.get(name).cloned().unwrap_or(Value::Null)
}
impl SqlExpr {
pub fn eval(&self, doc: &Value) -> Result<Value, EvalError> {
self.eval_scope(&RowScope {
new_doc: doc,
old_doc: None,
excluded_doc: None,
})
}
pub fn eval_with_old(&self, new_doc: &Value, old_doc: &Value) -> Result<Value, EvalError> {
self.eval_scope(&RowScope {
new_doc,
old_doc: Some(old_doc),
excluded_doc: None,
})
}
pub fn eval_with_excluded(&self, doc: &Value, excluded: &Value) -> Result<Value, EvalError> {
self.eval_scope(&RowScope {
new_doc: doc,
old_doc: None,
excluded_doc: Some(excluded),
})
}
fn eval_scope(&self, scope: &RowScope<'_>) -> Result<Value, EvalError> {
match self {
SqlExpr::Column(name) => Ok(scope.column(name)),
SqlExpr::OldColumn(name) => Ok(scope.old_column(name)),
SqlExpr::ExcludedColumn(name) => Ok(scope.excluded_column(name)),
SqlExpr::Literal(v) => Ok(v.clone()),
SqlExpr::BinaryOp { left, op, right } => {
let l = left.eval_scope(scope)?;
let r = right.eval_scope(scope)?;
eval_binary_op(&l, *op, &r)
}
SqlExpr::Negate(inner) => {
let v = inner.eval_scope(scope)?;
if let Some(b) = v.as_bool() {
Ok(Value::Bool(!b))
} else {
match value_to_f64(&v, false) {
Some(n) => Ok(to_value_number(-n)),
None => Ok(Value::Null),
}
}
}
SqlExpr::Function { name, args } => {
let evaluated: Vec<Value> = args
.iter()
.map(|a| a.eval_scope(scope))
.collect::<Result<_, EvalError>>()?;
crate::functions::eval_function(name, &evaluated)
}
SqlExpr::Cast { expr, to_type } => {
let v = expr.eval_scope(scope)?;
Ok(crate::cast::eval_cast(&v, to_type))
}
SqlExpr::Case {
operand,
when_thens,
else_expr,
} => {
let op_val = match operand {
Some(e) => Some(e.eval_scope(scope)?),
None => None,
};
for (when_expr, then_expr) in when_thens {
let when_val = when_expr.eval_scope(scope)?;
let matches = match &op_val {
Some(ov) => coerced_eq(ov, &when_val),
None => is_truthy(&when_val),
};
if matches {
return then_expr.eval_scope(scope);
}
}
match else_expr {
Some(e) => e.eval_scope(scope),
None => Ok(Value::Null),
}
}
SqlExpr::Coalesce(exprs) => {
for expr in exprs {
let v = expr.eval_scope(scope)?;
if !v.is_null() {
return Ok(v);
}
}
Ok(Value::Null)
}
SqlExpr::NullIf(a, b) => {
let va = a.eval_scope(scope)?;
let vb = b.eval_scope(scope)?;
if coerced_eq(&va, &vb) {
Ok(Value::Null)
} else {
Ok(va)
}
}
SqlExpr::IsNull { expr, negated } => {
let v = expr.eval_scope(scope)?;
let is_null = v.is_null();
Ok(Value::Bool(if *negated { !is_null } else { is_null }))
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::types::BinaryOp;
use super::*;
fn doc() -> Value {
Value::Object(
[
("name".to_string(), Value::String("Alice".into())),
("age".to_string(), Value::Integer(30)),
("price".to_string(), Value::Float(10.5)),
("qty".to_string(), Value::Integer(4)),
("active".to_string(), Value::Bool(true)),
("email".to_string(), Value::Null),
]
.into_iter()
.collect(),
)
}
#[test]
fn column_ref() {
let expr = SqlExpr::Column("name".into());
assert_eq!(expr.eval(&doc()).unwrap(), Value::String("Alice".into()));
}
#[test]
fn missing_column() {
let expr = SqlExpr::Column("missing".into());
assert_eq!(expr.eval(&doc()).unwrap(), Value::Null);
}
#[test]
fn whole_row_column_is_the_document() {
let expr = SqlExpr::Column(WHOLE_ROW_COLUMN.into());
assert_eq!(expr.eval(&doc()).unwrap(), doc());
let old = SqlExpr::OldColumn(WHOLE_ROW_COLUMN.into());
let before = Value::Object(Default::default());
assert_eq!(old.eval_with_old(&doc(), &before).unwrap(), before);
}
#[test]
fn to_jsonb_of_the_whole_row_keeps_every_field_type() {
let expr = SqlExpr::Function {
name: "to_jsonb".into(),
args: vec![SqlExpr::Column(WHOLE_ROW_COLUMN.into())],
};
assert_eq!(expr.eval(&doc()).unwrap(), doc());
}
#[test]
fn literal() {
let expr = SqlExpr::Literal(Value::Integer(42));
assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(42));
}
#[test]
fn add() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Column("price".into())),
op: BinaryOp::Add,
right: Box::new(SqlExpr::Literal(Value::Float(1.5))),
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(12));
}
#[test]
fn multiply() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Column("price".into())),
op: BinaryOp::Mul,
right: Box::new(SqlExpr::Column("qty".into())),
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(42));
}
#[test]
fn case_when() {
let expr = SqlExpr::Case {
operand: None,
when_thens: vec![(
SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Column("age".into())),
op: BinaryOp::GtEq,
right: Box::new(SqlExpr::Literal(Value::Integer(18))),
},
SqlExpr::Literal(Value::String("adult".into())),
)],
else_expr: Some(Box::new(SqlExpr::Literal(Value::String("minor".into())))),
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::String("adult".into()));
}
#[test]
fn coalesce() {
let expr = SqlExpr::Coalesce(vec![
SqlExpr::Column("email".into()),
SqlExpr::Literal(Value::String("default@example.com".into())),
]);
assert_eq!(
expr.eval(&doc()).unwrap(),
Value::String("default@example.com".into())
);
}
#[test]
fn is_null() {
let expr = SqlExpr::IsNull {
expr: Box::new(SqlExpr::Column("email".into())),
negated: false,
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::Bool(true));
}
#[test]
fn integer_division_by_zero_errors() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Integer(1))),
op: BinaryOp::Div,
right: Box::new(SqlExpr::Literal(Value::Integer(0))),
};
assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
}
#[test]
fn float_division_by_zero_errors() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Float(1.5))),
op: BinaryOp::Div,
right: Box::new(SqlExpr::Literal(Value::Float(0.0))),
};
assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
}
#[test]
fn decimal_division_by_decimal_zero_errors() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Decimal(
rust_decimal::Decimal::new(15, 1),
))),
op: BinaryOp::Div,
right: Box::new(SqlExpr::Literal(Value::Decimal(
rust_decimal::Decimal::ZERO,
))),
};
assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
}
#[test]
fn modulo_by_zero_errors() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Integer(5))),
op: BinaryOp::Mod,
right: Box::new(SqlExpr::Literal(Value::Integer(0))),
};
assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
}
#[test]
fn scalar_mod_function_by_zero_errors() {
let expr = SqlExpr::Function {
name: "mod".into(),
args: vec![
SqlExpr::Literal(Value::Integer(5)),
SqlExpr::Literal(Value::Integer(0)),
],
};
assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
}
#[test]
fn case_laziness_untaken_branch_not_evaluated() {
let expr = SqlExpr::Case {
operand: None,
when_thens: vec![(
SqlExpr::Literal(Value::Bool(false)),
SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Integer(1))),
op: BinaryOp::Div,
right: Box::new(SqlExpr::Literal(Value::Integer(0))),
},
)],
else_expr: Some(Box::new(SqlExpr::Literal(Value::Integer(0)))),
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(0));
}
#[test]
fn coalesce_laziness_short_circuits_before_division() {
let expr = SqlExpr::Coalesce(vec![
SqlExpr::Literal(Value::Integer(1)),
SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Integer(1))),
op: BinaryOp::Div,
right: Box::new(SqlExpr::Literal(Value::Integer(0))),
},
]);
assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(1));
}
#[test]
fn null_arithmetic_still_folds_to_null() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Null)),
op: BinaryOp::Add,
right: Box::new(SqlExpr::Literal(Value::Integer(1))),
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::Null);
}
#[test]
fn valid_division_still_succeeds() {
let expr = SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Literal(Value::Integer(10))),
op: BinaryOp::Div,
right: Box::new(SqlExpr::Literal(Value::Integer(2))),
};
assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(5));
}
}