AkurAI Build
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());
    }
}