Menu
Public repository / 91bd2a37654909c5a3aef6ddd614991bd378ccb8
AkurAI-Framework / crates/sqlexec/src/eval.rs
Latest change d971623579ac6cb425eea3ca33f24af5983c18b2 - feat(sqlexec): SQL executor over the storage B+tree (CREATE/INSERT/SELECT) by Ólafur Búi Ólafsson
//! `WHERE` evaluation: turn an [`Expr`] tree plus a row into a boolean.
//!
//! Comparisons follow three-valued logic loosely: anything that cannot be
//! compared (a `NULL` operand, or mismatched types) makes the comparison
//! false rather than erroring, which keeps a scan from blowing up on a single
//! odd row. `AND`/`OR`/`NOT` then combine those booleans normally.
use std::cmp::Ordering;
use akurai_sql::{CompareOp, Expr};
use crate::catalog::Schema;
use crate::error::ExecError;
use crate::value::Value;
/// Evaluate `expr` against `row` (whose cells are in `schema` column order).
/// Returns the boolean truth of the predicate.
pub fn eval_predicate(expr: &Expr, schema: &Schema, row: &[Value]) -> Result<bool, ExecError> {
match expr {
Expr::And(l, r) => Ok(eval_predicate(l, schema, row)? && eval_predicate(r, schema, row)?),
Expr::Or(l, r) => Ok(eval_predicate(l, schema, row)? || eval_predicate(r, schema, row)?),
Expr::Not(inner) => Ok(!eval_predicate(inner, schema, row)?),
Expr::Comparison { left, op, right } => {
let l = eval_operand(left, schema, row)?;
let r = eval_operand(right, schema, row)?;
Ok(apply_op(&l, *op, &r))
}
// A bare column or literal in boolean position: truthy if non-null and
// not zero/empty. Keeps the evaluator total without inventing syntax.
Expr::Column(_) | Expr::Literal(_) => {
let v = eval_operand(expr, schema, row)?;
Ok(is_truthy(&v))
}
}
}
/// Resolve a leaf expression (column reference or literal) to a [`Value`].
fn eval_operand(expr: &Expr, schema: &Schema, row: &[Value]) -> Result<Value, ExecError> {
match expr {
Expr::Literal(lit) => Ok(Value::from_literal(lit)),
Expr::Column(name) => {
let idx = schema
.index_of(name)
.ok_or_else(|| ExecError::UnknownColumn(name.clone()))?;
Ok(row.get(idx).cloned().unwrap_or(Value::Null))
}
// Nested boolean expressions used as operands collapse to 0/1.
other => Ok(Value::Int(eval_predicate(other, schema, row)? as i64)),
}
}
fn apply_op(l: &Value, op: CompareOp, r: &Value) -> bool {
match l.compare(r) {
Some(ord) => match op {
CompareOp::Eq => ord == Ordering::Equal,
CompareOp::NotEq => ord != Ordering::Equal,
CompareOp::Lt => ord == Ordering::Less,
CompareOp::LtEq => ord != Ordering::Greater,
CompareOp::Gt => ord == Ordering::Greater,
CompareOp::GtEq => ord != Ordering::Less,
},
// Incomparable (NULL or type mismatch): only `!=` of two distinct,
// comparable things is true; here we treat unknown as false except that
// `<>`/`!=` between two non-null incomparable values is true.
None => match op {
CompareOp::NotEq => !l.is_null() && !r.is_null(),
_ => false,
},
}
}
fn is_truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Int(i) => *i != 0,
Value::Float(f) => *f != 0.0,
Value::Str(s) => !s.is_empty(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use akurai_sql::{DataType, Literal};
fn schema() -> Schema {
Schema::from_defs(
"t",
&[
akurai_sql::ColumnDef {
name: "age".into(),
ty: DataType::Integer,
not_null: true,
},
akurai_sql::ColumnDef {
name: "name".into(),
ty: DataType::Text,
not_null: false,
},
],
)
}
fn col(n: &str) -> Expr {
Expr::Column(n.into())
}
fn int(i: i64) -> Expr {
Expr::Literal(Literal::Int(i))
}
fn cmp(l: Expr, op: CompareOp, r: Expr) -> Expr {
Expr::Comparison {
left: Box::new(l),
op,
right: Box::new(r),
}
}
#[test]
fn comparisons_against_a_row() {
let s = schema();
let row = vec![Value::Int(20), Value::Str("bob".into())];
assert!(eval_predicate(&cmp(col("age"), CompareOp::GtEq, int(18)), &s, &row).unwrap());
assert!(!eval_predicate(&cmp(col("age"), CompareOp::Lt, int(18)), &s, &row).unwrap());
assert!(eval_predicate(
&cmp(
col("name"),
CompareOp::Eq,
Expr::Literal(Literal::Str("bob".into()))
),
&s,
&row
)
.unwrap());
}
#[test]
fn and_or_not_combine() {
let s = schema();
let row = vec![Value::Int(20), Value::Str("bob".into())];
let a = cmp(col("age"), CompareOp::Gt, int(18));
let b = cmp(col("age"), CompareOp::Lt, int(10));
assert!(eval_predicate(
&Expr::Or(Box::new(a.clone()), Box::new(b.clone())),
&s,
&row
)
.unwrap());
assert!(!eval_predicate(&Expr::And(Box::new(a.clone()), Box::new(b)), &s, &row).unwrap());
assert!(!eval_predicate(&Expr::Not(Box::new(a)), &s, &row).unwrap());
}
#[test]
fn null_comparisons_are_false() {
let s = schema();
let row = vec![Value::Int(20), Value::Null];
assert!(!eval_predicate(
&cmp(
col("name"),
CompareOp::Eq,
Expr::Literal(Literal::Str("x".into()))
),
&s,
&row
)
.unwrap());
}
#[test]
fn unknown_column_errors() {
let s = schema();
let row = vec![Value::Int(20), Value::Null];
assert!(eval_predicate(&cmp(col("nope"), CompareOp::Eq, int(1)), &s, &row).is_err());
}
}