Skip to main content

reddb_server/storage/query/
evaluator.rs

1//! Scalar expression evaluator — single owner of value-level
2//! evaluation for SQL scalar `Expr` trees.
3//!
4//! Today scalar expression evaluation is fragmented:
5//!
6//! - `query::filter::Predicate::evaluate` runs comparison predicates
7//!   against a single column value.
8//! - `query::filter_compiled::CompiledFilter::evaluate` runs the
9//!   compiled filter opcode tape over a row slot, but still defers to
10//!   `Predicate::evaluate` per opcode for the actual comparison logic.
11//! - Several inlined arms in `query::core` and `query::executor`
12//!   compute projections, DEFAULT / CHECK expressions, RETURNING
13//!   columns, COMPUTED columns, and ON CONFLICT updates by walking
14//!   the AST and dispatching arithmetic / function calls inline.
15//! - `query::expr_typing` knows how to type an `Expr` against a
16//!   `Scope` but never produces a `Value` — it stops at `TypedExpr`.
17//!
18//! Operator / function / cast resolution is centralised in
19//! `schema::coercion_spine`, but **value-level dispatch** is not — so
20//! adding a new operator overload, a new implicit cast edge, or a
21//! new built-in function requires synchronised edits across each of
22//! the inline evaluators above.
23//!
24//! This module is the first vertical slice of the deep evaluator
25//! interface tracked by `draft-53-scalar-expression-evaluator`. It
26//! exposes:
27//!
28//! - [`Row`]: pluggable column-binding lookup so callers can wire
29//!   either a planner-resolved slot vector or a flat column-name map.
30//! - [`evaluate`]: the single entry point. Walks an `Expr`, resolves
31//!   operators / functions / casts through `coercion_spine`, applies
32//!   the implicit casts the spine asks for, and produces a `Value`.
33//! - [`EvalError`]: typed diagnostic surface — every failure mode the
34//!   evaluator can encounter at runtime is enumerated here so callers
35//!   don't have to parse strings.
36//!
37//! The slice is intentionally additive: callers in `filter.rs`,
38//! `filter_compiled.rs`, and `executor.rs` still own their own
39//! evaluation paths. Subsequent slices migrate those callers onto
40//! `evaluate` once the dispatch surface has proven out under the
41//! focused test set in this module.
42//!
43//! ## Semantics summary
44//!
45//! - **NULL propagation.** Arithmetic, comparison, concat, and CAST
46//!   propagate `Value::Null` — any `Null` operand short-circuits to
47//!   `Null`. `AND` / `OR` follow SQL three-valued logic.
48//! - **Arithmetic overflow.** Signed checked arithmetic; overflow
49//!   surfaces as [`EvalError::ArithmeticOverflow`] rather than wrap
50//!   or panic.
51//! - **Division.** Division by zero surfaces as
52//!   [`EvalError::DivisionByZero`]. Integer `/` always promotes to
53//!   `Float` per the operator catalog; integer `%` stays integer.
54//! - **Implicit cast triggers.** When the operator catalog has no
55//!   exact-type overload, the spine returns the per-operand
56//!   coercions; this evaluator applies them via
57//!   [`coerce::coerce_via_catalog`] before dispatch.
58//! - **Unknown function rejection.** Calls that don't resolve in the
59//!   built-in function catalog produce
60//!   [`EvalError::UnknownFunction`]. Variadic / catalog-resolved
61//!   functions still return through the same dispatch.
62
63use std::sync::Arc;
64
65use super::ast::{BinOp, Expr, FieldRef, UnaryOp};
66use crate::storage::schema::coerce::coerce_via_catalog;
67use crate::storage::schema::coercion_spine;
68use crate::storage::schema::function_catalog::FUNCTION_CATALOG;
69use crate::storage::schema::operator_catalog::OperatorEntry;
70use crate::storage::schema::{DataType, Value};
71
72/// Pluggable row-binding lookup. The evaluator stays agnostic of
73/// whether the caller has a slot vector indexed by planner-assigned
74/// position, a flat `HashMap<String, Value>`, or a graph binding —
75/// every consumer just provides a `Row` impl that resolves a
76/// `FieldRef` to a `Value`.
77pub trait Row {
78    /// Resolve a column / property reference. Returns `None` when the
79    /// reference is unknown (the evaluator surfaces this as
80    /// [`EvalError::UnknownColumn`]) or `Some(Value::Null)` when the
81    /// column exists but the row's value is null.
82    fn get(&self, field: &FieldRef) -> Option<Value>;
83}
84
85/// Trivial `Row` impl over a flat `(table, column) -> Value` closure
86/// — useful for tests and for callers that only have a column-name
87/// map.
88impl<F> Row for F
89where
90    F: Fn(&FieldRef) -> Option<Value>,
91{
92    fn get(&self, field: &FieldRef) -> Option<Value> {
93        self(field)
94    }
95}
96
97/// Errors surfaced by [`evaluate`]. Every variant is a runtime
98/// failure shape — type-resolution failures live in
99/// `expr_typing::TypeError`, not here.
100#[derive(Debug, Clone, PartialEq)]
101pub enum EvalError {
102    /// Column / property not present in the row binding.
103    UnknownColumn(FieldRef),
104    /// Query parameter placeholders are not resolved at this layer
105    /// — the bind phase substitutes a concrete value before
106    /// evaluation.
107    UnboundParameter(usize),
108    /// Operator catalog has no overload that accepts these operand
109    /// types, even after considering implicit coercions.
110    OperatorMismatch {
111        op: BinOp,
112        lhs: DataType,
113        rhs: DataType,
114    },
115    /// Unary operator doesn't accept the operand type.
116    UnaryMismatch { op: UnaryOp, operand: DataType },
117    /// Function catalog has no overload matching this call's
118    /// argument types. Includes user-defined functions because
119    /// today's catalog is the static built-in table only.
120    UnknownFunction { name: String, args: Vec<DataType> },
121    /// Implicit cast required by the spine failed at runtime — the
122    /// catalog said the conversion was legal at `Implicit` context
123    /// but the value's bytes couldn't be converted (e.g. overflow on
124    /// `BigInt` → `Integer`).
125    ImplicitCastFailed {
126        from: DataType,
127        to: DataType,
128        reason: String,
129    },
130    /// Explicit `CAST(x AS T)` failed at runtime.
131    CastFailed {
132        from: DataType,
133        to: DataType,
134        reason: String,
135    },
136    /// Signed-integer overflow during arithmetic.
137    ArithmeticOverflow { op: BinOp },
138    /// `n / 0` or `n % 0`.
139    DivisionByZero,
140    /// Numeric scalar evaluation produced an undefined or non-finite
141    /// float result (domain error, overflow, NaN, or infinity).
142    InvalidNumericResult { function: String, reason: String },
143    /// JSON_PARSE input was not valid JSON text.
144    JsonParseFailed { reason: String },
145    /// `IN (...)` against an empty value list — preserves the legacy
146    /// "always false" semantic but recorded explicitly so the
147    /// optimiser can fold it.
148    EmptyInList,
149}
150
151impl std::fmt::Display for EvalError {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            EvalError::UnknownColumn(field) => write!(f, "unknown column: {field:?}"),
155            EvalError::UnboundParameter(idx) => {
156                write!(f, "unbound query parameter: ${idx}")
157            }
158            EvalError::OperatorMismatch { op, lhs, rhs } => {
159                write!(f, "operator {op:?} not defined for ({lhs:?}, {rhs:?})")
160            }
161            EvalError::UnaryMismatch { op, operand } => {
162                write!(f, "unary {op:?} not defined for {operand:?}")
163            }
164            EvalError::UnknownFunction { name, args } => {
165                write!(f, "unknown function: {name}({args:?})")
166            }
167            EvalError::ImplicitCastFailed { from, to, reason } => {
168                write!(f, "implicit cast {from:?} -> {to:?} failed: {reason}")
169            }
170            EvalError::CastFailed { from, to, reason } => {
171                write!(f, "cast {from:?} -> {to:?} failed: {reason}")
172            }
173            EvalError::ArithmeticOverflow { op } => {
174                write!(f, "arithmetic overflow in {op:?}")
175            }
176            EvalError::DivisionByZero => write!(f, "division by zero"),
177            EvalError::InvalidNumericResult { function, reason } => {
178                write!(f, "invalid numeric result in {function}: {reason}")
179            }
180            EvalError::JsonParseFailed { reason } => {
181                write!(f, "JSON_PARSE failed to parse JSON: {reason}")
182            }
183            EvalError::EmptyInList => write!(f, "IN list is empty"),
184        }
185    }
186}
187
188impl std::error::Error for EvalError {}
189
190/// Evaluate a scalar `Expr` against a row binding. Single entry
191/// point for the deep evaluator interface — every recursive call
192/// folds back through this function so the resolution surface stays
193/// uniform.
194pub fn evaluate(expr: &Expr, row: &dyn Row) -> Result<Value, EvalError> {
195    match expr {
196        Expr::Literal { value, .. } => Ok(value.clone()),
197        Expr::Column { field, .. } => row
198            .get(field)
199            .ok_or_else(|| EvalError::UnknownColumn(field.clone())),
200        Expr::Parameter { index, .. } => Err(EvalError::UnboundParameter(*index)),
201        Expr::UnaryOp { op, operand, .. } => eval_unary(*op, operand, row),
202        Expr::BinaryOp { op, lhs, rhs, .. } => eval_binary(*op, lhs, rhs, row),
203        Expr::Cast { inner, target, .. } => eval_cast(inner, *target, row),
204        Expr::FunctionCall { name, args, .. } => eval_function(name, args, row),
205        Expr::Case {
206            branches, else_, ..
207        } => eval_case(branches, else_.as_deref(), row),
208        Expr::IsNull {
209            operand, negated, ..
210        } => {
211            let v = evaluate(operand, row)?;
212            let is_null = v.is_null();
213            Ok(Value::Boolean(if *negated { !is_null } else { is_null }))
214        }
215        Expr::InList {
216            target,
217            values,
218            negated,
219            ..
220        } => eval_in_list(target, values, *negated, row),
221        Expr::Between {
222            target,
223            low,
224            high,
225            negated,
226            ..
227        } => eval_between(target, low, high, *negated, row),
228        Expr::Subquery { .. } => Err(EvalError::UnknownFunction {
229            name: "SUBQUERY".to_string(),
230            args: Vec::new(),
231        }),
232        Expr::WindowFunctionCall { name, .. } => Err(EvalError::UnknownFunction {
233            name: format!("{name} OVER (...)"),
234            args: Vec::new(),
235        }),
236    }
237}
238
239fn eval_unary(op: UnaryOp, operand: &Expr, row: &dyn Row) -> Result<Value, EvalError> {
240    let v = evaluate(operand, row)?;
241    if v.is_null() {
242        return Ok(Value::Null);
243    }
244    match op {
245        UnaryOp::Neg => match &v {
246            Value::Integer(n) => n
247                .checked_neg()
248                .map(Value::Integer)
249                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
250            Value::BigInt(n) => n
251                .checked_neg()
252                .map(Value::BigInt)
253                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
254            Value::Float(n) => Ok(Value::Float(-*n)),
255            Value::Decimal(n) => n
256                .checked_neg()
257                .map(Value::Decimal)
258                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
259            other => Err(EvalError::UnaryMismatch {
260                op,
261                operand: other.data_type(),
262            }),
263        },
264        UnaryOp::Not => match &v {
265            Value::Boolean(b) => Ok(Value::Boolean(!b)),
266            other => Err(EvalError::UnaryMismatch {
267                op,
268                operand: other.data_type(),
269            }),
270        },
271    }
272}
273
274fn eval_binary(op: BinOp, lhs: &Expr, rhs: &Expr, row: &dyn Row) -> Result<Value, EvalError> {
275    // Logical ops use SQL three-valued logic so we eval both sides
276    // and short-circuit on Null *after* type-checking; arithmetic /
277    // comparison / concat short-circuit before dispatch.
278    let l = evaluate(lhs, row)?;
279    let r = evaluate(rhs, row)?;
280
281    match op {
282        BinOp::And => return three_valued_and(&l, &r),
283        BinOp::Or => return three_valued_or(&l, &r),
284        _ => {}
285    }
286
287    if l.is_null() || r.is_null() {
288        return Ok(Value::Null);
289    }
290
291    let lhs_dt = l.data_type();
292    let rhs_dt = r.data_type();
293    let (entry, coercions) =
294        coercion_spine::resolve_binop(op, lhs_dt, rhs_dt).ok_or(EvalError::OperatorMismatch {
295            op,
296            lhs: lhs_dt,
297            rhs: rhs_dt,
298        })?;
299
300    let l = match coercions.at(0) {
301        Some(target) => apply_implicit_cast(&l, lhs_dt, target)?,
302        None => l,
303    };
304    let r = match coercions.at(1) {
305        Some(target) => apply_implicit_cast(&r, rhs_dt, target)?,
306        None => r,
307    };
308
309    dispatch_binop(op, entry, l, r)
310}
311
312fn dispatch_binop(
313    op: BinOp,
314    entry: &OperatorEntry,
315    l: Value,
316    r: Value,
317) -> Result<Value, EvalError> {
318    match op {
319        BinOp::Add => arith_add(entry, l, r),
320        BinOp::Sub => arith_sub(entry, l, r),
321        BinOp::Mul => arith_mul(entry, l, r),
322        BinOp::Div => arith_div(entry, l, r),
323        BinOp::Mod => arith_mod(entry, l, r),
324        BinOp::Concat => match (&l, &r) {
325            (Value::Text(a), Value::Text(b)) => {
326                let mut s = String::with_capacity(a.len() + b.len());
327                s.push_str(a);
328                s.push_str(b);
329                Ok(Value::Text(Arc::from(s)))
330            }
331            _ => Err(EvalError::OperatorMismatch {
332                op,
333                lhs: l.data_type(),
334                rhs: r.data_type(),
335            }),
336        },
337        BinOp::Eq => Ok(Value::Boolean(values_equal(&l, &r))),
338        BinOp::Ne => Ok(Value::Boolean(!values_equal(&l, &r))),
339        BinOp::Lt => cmp_op(op, l, r, |o| o == std::cmp::Ordering::Less),
340        BinOp::Le => cmp_op(op, l, r, |o| o != std::cmp::Ordering::Greater),
341        BinOp::Gt => cmp_op(op, l, r, |o| o == std::cmp::Ordering::Greater),
342        BinOp::Ge => cmp_op(op, l, r, |o| o != std::cmp::Ordering::Less),
343        BinOp::And | BinOp::Or => unreachable!("handled before dispatch"),
344    }
345}
346
347fn arith_add(entry: &OperatorEntry, l: Value, r: Value) -> Result<Value, EvalError> {
348    match entry.return_type {
349        DataType::Integer => match (l, r) {
350            (Value::Integer(a), Value::Integer(b)) => a
351                .checked_add(b)
352                .map(Value::Integer)
353                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Add }),
354            _ => unreachable_after_coercion("Add", DataType::Integer),
355        },
356        DataType::BigInt => match (l, r) {
357            (Value::BigInt(a), Value::BigInt(b)) => a
358                .checked_add(b)
359                .map(Value::BigInt)
360                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Add }),
361            _ => unreachable_after_coercion("Add", DataType::BigInt),
362        },
363        DataType::Float => checked_float_binop(BinOp::Add, as_f64(&l) + as_f64(&r)),
364        DataType::Decimal => match (l, r) {
365            (Value::Decimal(a), Value::Decimal(b)) => a
366                .checked_add(b)
367                .map(Value::Decimal)
368                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Add }),
369            _ => unreachable_after_coercion("Add", DataType::Decimal),
370        },
371        other => Err(EvalError::OperatorMismatch {
372            op: BinOp::Add,
373            lhs: other,
374            rhs: other,
375        }),
376    }
377}
378
379fn arith_sub(entry: &OperatorEntry, l: Value, r: Value) -> Result<Value, EvalError> {
380    match entry.return_type {
381        DataType::Integer => match (l, r) {
382            (Value::Integer(a), Value::Integer(b)) => a
383                .checked_sub(b)
384                .map(Value::Integer)
385                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
386            _ => unreachable_after_coercion("Sub", DataType::Integer),
387        },
388        DataType::BigInt => match (l, r) {
389            (Value::BigInt(a), Value::BigInt(b)) => a
390                .checked_sub(b)
391                .map(Value::BigInt)
392                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
393            _ => unreachable_after_coercion("Sub", DataType::BigInt),
394        },
395        DataType::Float => checked_float_binop(BinOp::Sub, as_f64(&l) - as_f64(&r)),
396        DataType::Decimal => match (l, r) {
397            (Value::Decimal(a), Value::Decimal(b)) => a
398                .checked_sub(b)
399                .map(Value::Decimal)
400                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
401            _ => unreachable_after_coercion("Sub", DataType::Decimal),
402        },
403        other => Err(EvalError::OperatorMismatch {
404            op: BinOp::Sub,
405            lhs: other,
406            rhs: other,
407        }),
408    }
409}
410
411fn arith_mul(entry: &OperatorEntry, l: Value, r: Value) -> Result<Value, EvalError> {
412    match entry.return_type {
413        DataType::Integer => match (l, r) {
414            (Value::Integer(a), Value::Integer(b)) => a
415                .checked_mul(b)
416                .map(Value::Integer)
417                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Mul }),
418            _ => unreachable_after_coercion("Mul", DataType::Integer),
419        },
420        DataType::BigInt => match (l, r) {
421            (Value::BigInt(a), Value::BigInt(b)) => a
422                .checked_mul(b)
423                .map(Value::BigInt)
424                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Mul }),
425            _ => unreachable_after_coercion("Mul", DataType::BigInt),
426        },
427        DataType::Float => checked_float_binop(BinOp::Mul, as_f64(&l) * as_f64(&r)),
428        other => Err(EvalError::OperatorMismatch {
429            op: BinOp::Mul,
430            lhs: other,
431            rhs: other,
432        }),
433    }
434}
435
436fn arith_div(entry: &OperatorEntry, l: Value, r: Value) -> Result<Value, EvalError> {
437    // Operator catalog promotes integer / integer to Float —
438    // mirror that here so behavior stays identical to the typer.
439    match entry.return_type {
440        DataType::Float => {
441            let denom = as_f64(&r);
442            if denom == 0.0 {
443                return Err(EvalError::DivisionByZero);
444            }
445            checked_float_binop(BinOp::Div, as_f64(&l) / denom)
446        }
447        other => Err(EvalError::OperatorMismatch {
448            op: BinOp::Div,
449            lhs: other,
450            rhs: other,
451        }),
452    }
453}
454
455fn arith_mod(entry: &OperatorEntry, l: Value, r: Value) -> Result<Value, EvalError> {
456    match entry.return_type {
457        DataType::Integer => match (l, r) {
458            (Value::Integer(_), Value::Integer(0)) => Err(EvalError::DivisionByZero),
459            (Value::Integer(a), Value::Integer(b)) => a
460                .checked_rem(b)
461                .map(Value::Integer)
462                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Mod }),
463            _ => unreachable_after_coercion("Mod", DataType::Integer),
464        },
465        DataType::BigInt => match (l, r) {
466            (Value::BigInt(_), Value::BigInt(0)) => Err(EvalError::DivisionByZero),
467            (Value::BigInt(a), Value::BigInt(b)) => a
468                .checked_rem(b)
469                .map(Value::BigInt)
470                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Mod }),
471            _ => unreachable_after_coercion("Mod", DataType::BigInt),
472        },
473        other => Err(EvalError::OperatorMismatch {
474            op: BinOp::Mod,
475            lhs: other,
476            rhs: other,
477        }),
478    }
479}
480
481fn unreachable_after_coercion(op: &'static str, expected: DataType) -> Result<Value, EvalError> {
482    Err(EvalError::OperatorMismatch {
483        op: match op {
484            "Add" => BinOp::Add,
485            "Sub" => BinOp::Sub,
486            "Mul" => BinOp::Mul,
487            "Div" => BinOp::Div,
488            "Mod" => BinOp::Mod,
489            _ => BinOp::Add,
490        },
491        lhs: expected,
492        rhs: expected,
493    })
494}
495
496fn checked_float_binop(op: BinOp, value: f64) -> Result<Value, EvalError> {
497    if value.is_finite() {
498        Ok(Value::Float(value))
499    } else {
500        Err(EvalError::InvalidNumericResult {
501            function: format!("{op:?}"),
502            reason: "result is NaN or infinite".to_string(),
503        })
504    }
505}
506
507fn as_f64(v: &Value) -> f64 {
508    match v {
509        Value::Float(x) => *x,
510        Value::Integer(x) => *x as f64,
511        Value::BigInt(x) => *x as f64,
512        Value::UnsignedInteger(x) => *x as f64,
513        Value::Decimal(x) => crate::storage::schema::decimal_to_f64(*x),
514        _ => 0.0,
515    }
516}
517
518fn cmp_op<F>(op: BinOp, l: Value, r: Value, pick: F) -> Result<Value, EvalError>
519where
520    F: Fn(std::cmp::Ordering) -> bool,
521{
522    let ord = compare_values(&l, &r).ok_or(EvalError::OperatorMismatch {
523        op,
524        lhs: l.data_type(),
525        rhs: r.data_type(),
526    })?;
527    Ok(Value::Boolean(pick(ord)))
528}
529
530/// Total ordering for the numeric and text families that the
531/// catalog declares comparison overloads for. Returns `None` when
532/// the values aren't comparable — callers surface this as
533/// [`EvalError::OperatorMismatch`].
534fn compare_values(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
535    use std::cmp::Ordering;
536    match (a, b) {
537        (Value::Integer(x), Value::Integer(y)) => Some(x.cmp(y)),
538        (Value::BigInt(x), Value::BigInt(y)) => Some(x.cmp(y)),
539        (Value::Float(x), Value::Float(y)) => x.partial_cmp(y),
540        (Value::Text(x), Value::Text(y)) => Some(x.as_ref().cmp(y.as_ref())),
541        (Value::Boolean(x), Value::Boolean(y)) => Some(x.cmp(y)),
542        (Value::Timestamp(x), Value::Timestamp(y)) => Some(x.cmp(y)),
543        (Value::TimestampMs(x), Value::TimestampMs(y)) => Some(x.cmp(y)),
544        (Value::Date(x), Value::Date(y)) => Some(x.cmp(y)),
545        (Value::Time(x), Value::Time(y)) => Some(x.cmp(y)),
546        (Value::Uuid(x), Value::Uuid(y)) => Some(x.cmp(y)),
547        (Value::Decimal(x), Value::Decimal(y)) => Some(x.cmp(y)),
548        // Cross-numeric — operand coercion should have homogenised
549        // these but if a caller invokes the evaluator with mixed
550        // numerics directly, fall back to f64 ordering.
551        (Value::Integer(_) | Value::Float(_) | Value::BigInt(_), _) => {
552            let l = as_f64(a);
553            let r = as_f64(b);
554            l.partial_cmp(&r)
555        }
556        _ => None,
557    }
558}
559
560fn values_equal(a: &Value, b: &Value) -> bool {
561    match (a, b) {
562        (Value::Float(x), Value::Float(y)) => x == y,
563        _ => a == b,
564    }
565}
566
567fn three_valued_and(l: &Value, r: &Value) -> Result<Value, EvalError> {
568    match (l, r) {
569        (Value::Boolean(false), _) | (_, Value::Boolean(false)) => Ok(Value::Boolean(false)),
570        (Value::Boolean(true), Value::Boolean(true)) => Ok(Value::Boolean(true)),
571        (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
572        _ => Err(EvalError::OperatorMismatch {
573            op: BinOp::And,
574            lhs: l.data_type(),
575            rhs: r.data_type(),
576        }),
577    }
578}
579
580fn three_valued_or(l: &Value, r: &Value) -> Result<Value, EvalError> {
581    match (l, r) {
582        (Value::Boolean(true), _) | (_, Value::Boolean(true)) => Ok(Value::Boolean(true)),
583        (Value::Boolean(false), Value::Boolean(false)) => Ok(Value::Boolean(false)),
584        (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
585        _ => Err(EvalError::OperatorMismatch {
586            op: BinOp::Or,
587            lhs: l.data_type(),
588            rhs: r.data_type(),
589        }),
590    }
591}
592
593fn apply_implicit_cast(value: &Value, src: DataType, target: DataType) -> Result<Value, EvalError> {
594    if src == target {
595        return Ok(value.clone());
596    }
597    coerce_via_catalog(value, src, target, None).map_err(|reason| EvalError::ImplicitCastFailed {
598        from: src,
599        to: target,
600        reason,
601    })
602}
603
604fn eval_cast(inner: &Expr, target: DataType, row: &dyn Row) -> Result<Value, EvalError> {
605    let v = evaluate(inner, row)?;
606    if v.is_null() {
607        return Ok(Value::Null);
608    }
609    let src = v.data_type();
610    if src == target {
611        return Ok(v);
612    }
613    coerce_via_catalog(&v, src, target, None).map_err(|reason| EvalError::CastFailed {
614        from: src,
615        to: target,
616        reason,
617    })
618}
619
620fn eval_function(name: &str, args: &[Expr], row: &dyn Row) -> Result<Value, EvalError> {
621    // COALESCE has SQL-special semantics that the catalog can't
622    // express (variadic + uniform arg type unifies poorly with
623    // first-non-null). Handle it before catalog dispatch so we
624    // preserve `COALESCE(int, int) -> int` rather than coercing
625    // every argument to Text.
626    if name.eq_ignore_ascii_case("COALESCE") {
627        for arg in args {
628            let v = evaluate(arg, row)?;
629            if !v.is_null() {
630                return Ok(v);
631            }
632        }
633        return Ok(Value::Null);
634    }
635
636    let arg_values: Vec<Value> = args
637        .iter()
638        .map(|a| evaluate(a, row))
639        .collect::<Result<Vec<_>, _>>()?;
640    let arg_types: Vec<DataType> = arg_values.iter().map(|v| v.data_type()).collect();
641
642    // Strict NULL propagation for built-in scalar functions: any
643    // null arg short-circuits the call to NULL. Only applies when
644    // the function name exists in the catalog so unknown functions
645    // with null args still surface as `UnknownFunction` rather than
646    // silently returning null.
647    if arg_values.iter().any(Value::is_null)
648        && FUNCTION_CATALOG
649            .iter()
650            .any(|e| e.name.eq_ignore_ascii_case(name))
651    {
652        return Ok(Value::Null);
653    }
654
655    let (entry, coercions) =
656        coercion_spine::resolve_function(name, &arg_types).ok_or_else(|| {
657            EvalError::UnknownFunction {
658                name: name.to_string(),
659                args: arg_types.clone(),
660            }
661        })?;
662
663    // Apply per-arg implicit casts.
664    let mut coerced: Vec<Value> = Vec::with_capacity(arg_values.len());
665    for (idx, value) in arg_values.into_iter().enumerate() {
666        let src = arg_types[idx];
667        match coercions.at(idx) {
668            Some(target) if src != target => {
669                coerced.push(apply_implicit_cast(&value, src, target)?);
670            }
671            _ => coerced.push(value),
672        }
673    }
674
675    // NULL propagation for built-ins: if any non-variadic argument
676    // is null, return null. Variadic / aggregate semantics handle
677    // null differently and aren't in scope for this slice.
678    if !entry.variadic && coerced.iter().any(|v| v.is_null()) {
679        return Ok(Value::Null);
680    }
681
682    dispatch_function(entry.name, &coerced)
683}
684
685fn dispatch_function(name: &str, args: &[Value]) -> Result<Value, EvalError> {
686    match name {
687        "UPPER" => match &args[0] {
688            Value::Text(s) => Ok(Value::Text(Arc::from(s.to_uppercase()))),
689            other => Err(EvalError::UnknownFunction {
690                name: name.to_string(),
691                args: vec![other.data_type()],
692            }),
693        },
694        "LOWER" => match &args[0] {
695            Value::Text(s) => Ok(Value::Text(Arc::from(s.to_lowercase()))),
696            other => Err(EvalError::UnknownFunction {
697                name: name.to_string(),
698                args: vec![other.data_type()],
699            }),
700        },
701        "TRIM" => trim_function(name, args, true, true),
702        "LTRIM" => trim_function(name, args, true, false),
703        "RTRIM" => trim_function(name, args, false, true),
704        "LENGTH" | "CHAR_LENGTH" | "CHARACTER_LENGTH" => match &args[0] {
705            Value::Text(s) => Ok(Value::Integer(s.chars().count() as i64)),
706            other => Err(EvalError::UnknownFunction {
707                name: name.to_string(),
708                args: vec![other.data_type()],
709            }),
710        },
711        "OCTET_LENGTH" => match &args[0] {
712            Value::Text(s) => Ok(Value::Integer(s.len() as i64)),
713            Value::Blob(b) => Ok(Value::Integer(b.len() as i64)),
714            other => Err(EvalError::UnknownFunction {
715                name: name.to_string(),
716                args: vec![other.data_type()],
717            }),
718        },
719        "CONCAT" => {
720            let mut out = String::new();
721            for arg in args {
722                let Value::Text(value) = arg else {
723                    return Err(EvalError::UnknownFunction {
724                        name: name.to_string(),
725                        args: args.iter().map(Value::data_type).collect(),
726                    });
727                };
728                out.push_str(value);
729            }
730            Ok(Value::Text(Arc::from(out)))
731        }
732        "JSON_PARSE" => json_parse_value(&args[0]),
733        "JSON_EXTRACT" => Ok(json_extract_value(&args[0], &args[1], false)),
734        "JSON_EXTRACT_TEXT" => Ok(json_extract_value(&args[0], &args[1], true)),
735        "CONTAINS" => Ok(contains_value(&args[0], &args[1])),
736        "ABS" => match &args[0] {
737            Value::Integer(n) => n
738                .checked_abs()
739                .map(Value::Integer)
740                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
741            Value::BigInt(n) => n
742                .checked_abs()
743                .map(Value::BigInt)
744                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
745            Value::Float(n) => Ok(Value::Float(n.abs())),
746            Value::Decimal(n) => n
747                .checked_abs()
748                .map(Value::Decimal)
749                .ok_or(EvalError::ArithmeticOverflow { op: BinOp::Sub }),
750            other => Err(EvalError::UnknownFunction {
751                name: name.to_string(),
752                args: vec![other.data_type()],
753            }),
754        },
755        "SQRT" => unary_math(name, args, |x| {
756            if x < 0.0 {
757                return Err("input must be greater than or equal to zero");
758            }
759            Ok(x.sqrt())
760        }),
761        "POWER" | "POW" => binary_math(name, args, |base, exp| Ok(base.powf(exp))),
762        "EXP" => unary_math(name, args, |x| Ok(x.exp())),
763        "LN" => unary_math(name, args, |x| {
764            if x <= 0.0 {
765                return Err("input must be greater than zero");
766            }
767            Ok(x.ln())
768        }),
769        "LOG" if args.len() == 1 => unary_math(name, args, |x| {
770            if x <= 0.0 {
771                return Err("input must be greater than zero");
772            }
773            Ok(x.log10())
774        }),
775        "LOG" => binary_math(name, args, |base, x| {
776            if base <= 0.0 {
777                return Err("base must be greater than zero");
778            }
779            if base == 1.0 {
780                return Err("base must not equal one");
781            }
782            if x <= 0.0 {
783                return Err("input must be greater than zero");
784            }
785            Ok(x.log(base))
786        }),
787        "LOG10" => unary_math(name, args, |x| {
788            if x <= 0.0 {
789                return Err("input must be greater than zero");
790            }
791            Ok(x.log10())
792        }),
793        "SIN" => unary_math(name, args, |x| Ok(x.sin())),
794        "COS" => unary_math(name, args, |x| Ok(x.cos())),
795        "TAN" => unary_math(name, args, |x| Ok(x.tan())),
796        "ASIN" | "ARCSIN" => unary_math(name, args, |x| {
797            if !(-1.0..=1.0).contains(&x) {
798                return Err("input must be between -1 and 1");
799            }
800            Ok(x.asin())
801        }),
802        "ACOS" | "ARCCOS" => unary_math(name, args, |x| {
803            if !(-1.0..=1.0).contains(&x) {
804                return Err("input must be between -1 and 1");
805            }
806            Ok(x.acos())
807        }),
808        "ATAN" | "ARCTAN" => unary_math(name, args, |x| Ok(x.atan())),
809        "ATAN2" => binary_math(name, args, |y, x| Ok(y.atan2(x))),
810        "COT" => unary_math(name, args, |x| {
811            let tan = x.tan();
812            if tan == 0.0 {
813                return Err("input must not produce zero tangent");
814            }
815            Ok(1.0 / tan)
816        }),
817        "DEGREES" => unary_math(name, args, |x| Ok(x.to_degrees())),
818        "RADIANS" => unary_math(name, args, |x| Ok(x.to_radians())),
819        "PI" => checked_math_result(name, std::f64::consts::PI),
820        // Functions whose runtime body the slice doesn't yet cover
821        // surface as UnknownFunction with the resolved arg types so
822        // callers can see the catalog matched but the dispatch
823        // didn't. Subsequent slices fill in CONCAT, time functions, …
824        other => Err(EvalError::UnknownFunction {
825            name: other.to_string(),
826            args: args.iter().map(|v| v.data_type()).collect(),
827        }),
828    }
829}
830
831fn trim_function(name: &str, args: &[Value], left: bool, right: bool) -> Result<Value, EvalError> {
832    if args.len() != 1 {
833        return Err(EvalError::UnknownFunction {
834            name: name.to_string(),
835            args: args.iter().map(|v| v.data_type()).collect(),
836        });
837    }
838
839    match &args[0] {
840        Value::Text(s) => {
841            let trimmed = match (left, right) {
842                (true, true) => s.trim(),
843                (true, false) => s.trim_start(),
844                (false, true) => s.trim_end(),
845                (false, false) => s,
846            };
847            Ok(Value::Text(Arc::from(trimmed)))
848        }
849        other => Err(EvalError::UnknownFunction {
850            name: name.to_string(),
851            args: vec![other.data_type()],
852        }),
853    }
854}
855
856fn unary_math<F>(name: &str, args: &[Value], op: F) -> Result<Value, EvalError>
857where
858    F: FnOnce(f64) -> Result<f64, &'static str>,
859{
860    let input = math_arg(name, args.first(), 0)?;
861    let value = op(input).map_err(|reason| EvalError::InvalidNumericResult {
862        function: name.to_string(),
863        reason: reason.to_string(),
864    })?;
865    checked_math_result(name, value)
866}
867
868fn binary_math<F>(name: &str, args: &[Value], op: F) -> Result<Value, EvalError>
869where
870    F: FnOnce(f64, f64) -> Result<f64, &'static str>,
871{
872    let left = math_arg(name, args.first(), 0)?;
873    let right = math_arg(name, args.get(1), 1)?;
874    let value = op(left, right).map_err(|reason| EvalError::InvalidNumericResult {
875        function: name.to_string(),
876        reason: reason.to_string(),
877    })?;
878    checked_math_result(name, value)
879}
880
881fn math_arg(name: &str, value: Option<&Value>, index: usize) -> Result<f64, EvalError> {
882    let value = value.ok_or_else(|| EvalError::UnknownFunction {
883        name: name.to_string(),
884        args: Vec::new(),
885    })?;
886    let numeric = as_f64(value);
887    if numeric.is_finite() {
888        Ok(numeric)
889    } else {
890        Err(EvalError::InvalidNumericResult {
891            function: name.to_string(),
892            reason: format!("argument {index} is NaN or infinite"),
893        })
894    }
895}
896
897fn checked_math_result(name: &str, value: f64) -> Result<Value, EvalError> {
898    if value.is_finite() {
899        Ok(Value::Float(value))
900    } else {
901        Err(EvalError::InvalidNumericResult {
902            function: name.to_string(),
903            reason: "result is NaN or infinite".to_string(),
904        })
905    }
906}
907
908fn json_extract_value(input: &Value, path: &Value, as_text: bool) -> Value {
909    let Value::Text(path) = path else {
910        return Value::Null;
911    };
912    let Some(json) = value_to_json(input) else {
913        return Value::Null;
914    };
915    let Some(steps) = parse_json_path(path) else {
916        return Value::Null;
917    };
918    let Some(target) = json_path_get(&json, &steps) else {
919        return Value::Null;
920    };
921
922    if as_text {
923        match target {
924            crate::serde_json::Value::String(value) => Value::text(value.clone()),
925            crate::serde_json::Value::Null => Value::Null,
926            crate::serde_json::Value::Bool(value) => Value::text(value.to_string()),
927            crate::serde_json::Value::Integer(value) => Value::text(value.to_string()),
928            crate::serde_json::Value::Number(value) => Value::text(value.to_string()),
929            other => Value::text(other.to_string_compact()),
930        }
931    } else {
932        Value::text(target.to_string_compact())
933    }
934}
935
936fn json_parse_value(input: &Value) -> Result<Value, EvalError> {
937    let Value::Text(text) = input else {
938        return Err(EvalError::UnknownFunction {
939            name: "JSON_PARSE".to_string(),
940            args: vec![input.data_type()],
941        });
942    };
943    crate::serde_json::from_str::<crate::serde_json::Value>(text)
944        .map(|json| Value::Json(json.to_string_compact().into_bytes()))
945        .map_err(|err| EvalError::JsonParseFailed {
946            reason: err.to_string(),
947        })
948}
949
950fn contains_value(input: &Value, needle: &Value) -> Value {
951    let Value::Text(needle) = needle else {
952        return Value::Null;
953    };
954    Value::Boolean(value_contains(input, needle))
955}
956
957fn value_contains(value: &Value, needle: &str) -> bool {
958    match value {
959        Value::Array(values) => values.iter().any(|value| value_contains(value, needle)),
960        Value::Json(_) => value_to_json(value)
961            .as_ref()
962            .is_some_and(|json| json_value_contains(json, needle)),
963        Value::Text(value) => value.contains(needle),
964        other => other.display_string().contains(needle),
965    }
966}
967
968fn json_value_contains(value: &crate::serde_json::Value, needle: &str) -> bool {
969    match value {
970        crate::serde_json::Value::Array(values) => values
971            .iter()
972            .any(|value| json_value_contains(value, needle)),
973        // Recurse into object *values* so a needle nested inside a JSON
974        // object matches — e.g. `CONTAINS(vision_detections, 'person')`
975        // over a derived `[{"label":"person",...}]` array (#1275). Keys
976        // are deliberately not matched: containment is about the data, not
977        // the field names.
978        crate::serde_json::Value::Object(map) => {
979            map.values().any(|value| json_value_contains(value, needle))
980        }
981        crate::serde_json::Value::String(value) => value == needle,
982        crate::serde_json::Value::Integer(value) => value.to_string() == needle,
983        crate::serde_json::Value::Number(value) => value.to_string() == needle,
984        crate::serde_json::Value::Decimal(value) => value == needle,
985        crate::serde_json::Value::Bool(value) => value.to_string() == needle,
986        crate::serde_json::Value::Null => false,
987    }
988}
989
990fn value_to_json(value: &Value) -> Option<crate::serde_json::Value> {
991    match value {
992        Value::Null => Some(crate::serde_json::Value::Null),
993        // A document body may be the native binary container (PRD-1398); decode
994        // it to JSON so json_extract / CONTAINS see the same shape as a plain
995        // JSON body.
996        Value::Json(bytes) => crate::document_body::decode_container_to_json(bytes)
997            .or_else(|| crate::serde_json::from_slice(bytes).ok()),
998        Value::Text(value) => crate::serde_json::from_str(value).ok(),
999        _ => None,
1000    }
1001}
1002
1003enum JsonPathStep<'a> {
1004    Field(&'a str),
1005    Index(usize),
1006}
1007
1008fn parse_json_path(path: &str) -> Option<Vec<JsonPathStep<'_>>> {
1009    let path = path.trim();
1010    let rest = path.strip_prefix('$').unwrap_or(path);
1011    let mut steps = Vec::new();
1012    let bytes = rest.as_bytes();
1013    let mut index = 0;
1014    while index < bytes.len() {
1015        match bytes[index] {
1016            b'.' => {
1017                index += 1;
1018                let start = index;
1019                while index < bytes.len() && bytes[index] != b'.' && bytes[index] != b'[' {
1020                    index += 1;
1021                }
1022                if start == index {
1023                    return None;
1024                }
1025                steps.push(JsonPathStep::Field(
1026                    std::str::from_utf8(&bytes[start..index]).ok()?,
1027                ));
1028            }
1029            b'[' => {
1030                index += 1;
1031                let start = index;
1032                while index < bytes.len() && bytes[index] != b']' {
1033                    index += 1;
1034                }
1035                if index >= bytes.len() {
1036                    return None;
1037                }
1038                steps.push(JsonPathStep::Index(
1039                    std::str::from_utf8(&bytes[start..index])
1040                        .ok()?
1041                        .parse()
1042                        .ok()?,
1043                ));
1044                index += 1;
1045            }
1046            _ => return None,
1047        }
1048    }
1049    Some(steps)
1050}
1051
1052fn json_path_get<'a>(
1053    root: &'a crate::serde_json::Value,
1054    steps: &[JsonPathStep<'_>],
1055) -> Option<&'a crate::serde_json::Value> {
1056    let mut current = root;
1057    for step in steps {
1058        current = match (step, current) {
1059            (JsonPathStep::Field(name), crate::serde_json::Value::Object(map)) => map.get(*name)?,
1060            (JsonPathStep::Index(index), crate::serde_json::Value::Array(values)) => {
1061                values.get(*index)?
1062            }
1063            _ => return None,
1064        };
1065    }
1066    Some(current)
1067}
1068
1069fn eval_case(
1070    branches: &[(Expr, Expr)],
1071    else_: Option<&Expr>,
1072    row: &dyn Row,
1073) -> Result<Value, EvalError> {
1074    for (cond, value) in branches {
1075        let c = evaluate(cond, row)?;
1076        if matches!(c, Value::Boolean(true)) {
1077            return evaluate(value, row);
1078        }
1079    }
1080    match else_ {
1081        Some(e) => evaluate(e, row),
1082        None => Ok(Value::Null),
1083    }
1084}
1085
1086fn eval_in_list(
1087    target: &Expr,
1088    values: &[Expr],
1089    negated: bool,
1090    row: &dyn Row,
1091) -> Result<Value, EvalError> {
1092    if values.is_empty() {
1093        return Err(EvalError::EmptyInList);
1094    }
1095    let needle = evaluate(target, row)?;
1096    if needle.is_null() {
1097        return Ok(Value::Null);
1098    }
1099    let mut saw_null = false;
1100    for v in values {
1101        let candidate = evaluate(v, row)?;
1102        if candidate.is_null() {
1103            saw_null = true;
1104            continue;
1105        }
1106        if values_equal(&needle, &candidate) {
1107            return Ok(Value::Boolean(!negated));
1108        }
1109    }
1110    if saw_null {
1111        return Ok(Value::Null);
1112    }
1113    Ok(Value::Boolean(negated))
1114}
1115
1116fn eval_between(
1117    target: &Expr,
1118    low: &Expr,
1119    high: &Expr,
1120    negated: bool,
1121    row: &dyn Row,
1122) -> Result<Value, EvalError> {
1123    let v = evaluate(target, row)?;
1124    let lo = evaluate(low, row)?;
1125    let hi = evaluate(high, row)?;
1126    if v.is_null() || lo.is_null() || hi.is_null() {
1127        return Ok(Value::Null);
1128    }
1129    let lo_ok = compare_values(&v, &lo)
1130        .ok_or(EvalError::OperatorMismatch {
1131            op: BinOp::Ge,
1132            lhs: v.data_type(),
1133            rhs: lo.data_type(),
1134        })
1135        .map(|o| o != std::cmp::Ordering::Less)?;
1136    let hi_ok = compare_values(&v, &hi)
1137        .ok_or(EvalError::OperatorMismatch {
1138            op: BinOp::Le,
1139            lhs: v.data_type(),
1140            rhs: hi.data_type(),
1141        })
1142        .map(|o| o != std::cmp::Ordering::Greater)?;
1143    let inside = lo_ok && hi_ok;
1144    Ok(Value::Boolean(if negated { !inside } else { inside }))
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::*;
1150    use crate::storage::query::ast::Span;
1151
1152    fn lit(v: Value) -> Expr {
1153        Expr::Literal {
1154            value: v,
1155            span: Span::synthetic(),
1156        }
1157    }
1158
1159    fn binop(op: BinOp, l: Expr, r: Expr) -> Expr {
1160        Expr::BinaryOp {
1161            op,
1162            lhs: Box::new(l),
1163            rhs: Box::new(r),
1164            span: Span::synthetic(),
1165        }
1166    }
1167
1168    fn empty_row() -> impl Row {
1169        |_field: &FieldRef| -> Option<Value> { None }
1170    }
1171
1172    #[test]
1173    fn contains_descends_into_json_object_values() {
1174        // A derived component-detections array (#1275): CONTAINS must find
1175        // a label nested inside the object elements, not just top-level
1176        // array strings.
1177        let detections = br#"[{"label":"person","confidence":0.9,"bbox":[0,0,1,1]},
1178                              {"label":"car","confidence":0.7,"bbox":[1,1,2,2]}]"#;
1179        let json = Value::Json(detections.to_vec());
1180        assert!(value_contains(&json, "person"));
1181        assert!(value_contains(&json, "car"));
1182        assert!(!value_contains(&json, "bicycle"));
1183        // Keys are not matched — only the data.
1184        assert!(!value_contains(&json, "label"));
1185    }
1186
1187    #[test]
1188    fn integer_addition_overflow_surfaces_as_eval_error() {
1189        let expr = binop(
1190            BinOp::Add,
1191            lit(Value::Integer(i64::MAX)),
1192            lit(Value::Integer(1)),
1193        );
1194        let err = evaluate(&expr, &empty_row()).unwrap_err();
1195        assert_eq!(err, EvalError::ArithmeticOverflow { op: BinOp::Add });
1196    }
1197
1198    #[test]
1199    fn integer_multiplication_overflow_surfaces_as_eval_error() {
1200        let expr = binop(
1201            BinOp::Mul,
1202            lit(Value::Integer(i64::MAX)),
1203            lit(Value::Integer(2)),
1204        );
1205        let err = evaluate(&expr, &empty_row()).unwrap_err();
1206        assert_eq!(err, EvalError::ArithmeticOverflow { op: BinOp::Mul });
1207    }
1208
1209    #[test]
1210    fn integer_subtraction_overflow_surfaces_as_eval_error() {
1211        let expr = binop(
1212            BinOp::Sub,
1213            lit(Value::Integer(i64::MIN)),
1214            lit(Value::Integer(1)),
1215        );
1216        let err = evaluate(&expr, &empty_row()).unwrap_err();
1217        assert_eq!(err, EvalError::ArithmeticOverflow { op: BinOp::Sub });
1218    }
1219
1220    #[test]
1221    fn unary_neg_overflow_on_min_int_surfaces_as_eval_error() {
1222        let expr = Expr::UnaryOp {
1223            op: UnaryOp::Neg,
1224            operand: Box::new(lit(Value::Integer(i64::MIN))),
1225            span: Span::synthetic(),
1226        };
1227        let err = evaluate(&expr, &empty_row()).unwrap_err();
1228        assert_eq!(err, EvalError::ArithmeticOverflow { op: BinOp::Sub });
1229    }
1230
1231    #[test]
1232    fn null_propagates_through_arithmetic() {
1233        let expr = binop(BinOp::Add, lit(Value::Null), lit(Value::Integer(7)));
1234        let v = evaluate(&expr, &empty_row()).unwrap();
1235        assert_eq!(v, Value::Null);
1236    }
1237
1238    #[test]
1239    fn null_propagates_through_comparison() {
1240        let expr = binop(BinOp::Lt, lit(Value::Integer(1)), lit(Value::Null));
1241        let v = evaluate(&expr, &empty_row()).unwrap();
1242        assert_eq!(v, Value::Null);
1243    }
1244
1245    #[test]
1246    fn null_propagates_through_concat() {
1247        let expr = binop(
1248            BinOp::Concat,
1249            lit(Value::Text(Arc::from("hi"))),
1250            lit(Value::Null),
1251        );
1252        let v = evaluate(&expr, &empty_row()).unwrap();
1253        assert_eq!(v, Value::Null);
1254    }
1255
1256    #[test]
1257    fn three_valued_and_returns_false_when_one_side_false_even_with_null() {
1258        let expr = binop(BinOp::And, lit(Value::Null), lit(Value::Boolean(false)));
1259        let v = evaluate(&expr, &empty_row()).unwrap();
1260        assert_eq!(v, Value::Boolean(false));
1261    }
1262
1263    #[test]
1264    fn three_valued_or_returns_true_when_one_side_true_even_with_null() {
1265        let expr = binop(BinOp::Or, lit(Value::Null), lit(Value::Boolean(true)));
1266        let v = evaluate(&expr, &empty_row()).unwrap();
1267        assert_eq!(v, Value::Boolean(true));
1268    }
1269
1270    #[test]
1271    fn three_valued_and_returns_null_for_null_and_true() {
1272        let expr = binop(BinOp::And, lit(Value::Null), lit(Value::Boolean(true)));
1273        let v = evaluate(&expr, &empty_row()).unwrap();
1274        assert_eq!(v, Value::Null);
1275    }
1276
1277    #[test]
1278    fn implicit_cast_triggers_for_decimal_plus_integer() {
1279        // Operator catalog has +(Decimal, Decimal) -> Decimal as
1280        // the only overload that survives coercion (no Decimal ->
1281        // numeric implicit casts exist in the cast catalog). The
1282        // spine therefore inserts an Integer -> Decimal cast on
1283        // the rhs and dispatches the Decimal addition.
1284        // parse_decimal scales by 10^4, so Integer(2) coerces to
1285        // Decimal(20000) and Decimal(10000) + Decimal(20000) =
1286        // Decimal(30000) (fixed-point 3.0000).
1287        let expr = binop(
1288            BinOp::Add,
1289            lit(Value::Decimal(10000)),
1290            lit(Value::Integer(2)),
1291        );
1292        let v = evaluate(&expr, &empty_row()).unwrap();
1293        assert_eq!(v, Value::Decimal(30000));
1294    }
1295
1296    #[test]
1297    fn integer_plus_bigint_resolves_to_preferred_float_overload() {
1298        // (Integer, BigInt) has no exact match. The spine ties
1299        // between +(Integer, Float, Float) (rhs cast BigInt->Float)
1300        // and +(BigInt, BigInt, BigInt) (lhs cast Integer->BigInt).
1301        // Float wins on the preferred-return-type tie-break, so the
1302        // BigInt operand is coerced to Float and the result is Float.
1303        let expr = binop(
1304            BinOp::Add,
1305            lit(Value::Integer(5)),
1306            lit(Value::BigInt(40_000_000_000)),
1307        );
1308        let v = evaluate(&expr, &empty_row()).unwrap();
1309        assert_eq!(v, Value::Float(40_000_000_005.0));
1310    }
1311
1312    #[test]
1313    fn implicit_cast_promotes_integer_to_float_for_float_addition() {
1314        // The catalog has +(Integer, Float) -> Float as a direct
1315        // entry, so no actual coercion is inserted, but the result
1316        // must still be Float.
1317        let expr = binop(BinOp::Add, lit(Value::Integer(2)), lit(Value::Float(0.5)));
1318        let v = evaluate(&expr, &empty_row()).unwrap();
1319        assert_eq!(v, Value::Float(2.5));
1320    }
1321
1322    #[test]
1323    fn integer_division_promotes_to_float() {
1324        let expr = binop(BinOp::Div, lit(Value::Integer(7)), lit(Value::Integer(2)));
1325        let v = evaluate(&expr, &empty_row()).unwrap();
1326        assert_eq!(v, Value::Float(3.5));
1327    }
1328
1329    #[test]
1330    fn division_by_zero_is_eval_error() {
1331        let expr = binop(BinOp::Div, lit(Value::Integer(7)), lit(Value::Integer(0)));
1332        let err = evaluate(&expr, &empty_row()).unwrap_err();
1333        assert_eq!(err, EvalError::DivisionByZero);
1334    }
1335
1336    #[test]
1337    fn unknown_function_surfaces_as_eval_error() {
1338        let expr = Expr::FunctionCall {
1339            name: "no_such_fn".to_string(),
1340            args: vec![lit(Value::Integer(1))],
1341            span: Span::synthetic(),
1342        };
1343        let err = evaluate(&expr, &empty_row()).unwrap_err();
1344        match err {
1345            EvalError::UnknownFunction { name, args } => {
1346                assert_eq!(name, "no_such_fn");
1347                assert_eq!(args, vec![DataType::Integer]);
1348            }
1349            other => panic!("expected UnknownFunction, got {other:?}"),
1350        }
1351    }
1352
1353    #[test]
1354    fn coalesce_returns_first_non_null() {
1355        let expr = Expr::FunctionCall {
1356            name: "COALESCE".to_string(),
1357            args: vec![
1358                lit(Value::Null),
1359                lit(Value::Null),
1360                lit(Value::Integer(42)),
1361                lit(Value::Integer(99)),
1362            ],
1363            span: Span::synthetic(),
1364        };
1365        let v = evaluate(&expr, &empty_row()).unwrap();
1366        assert_eq!(v, Value::Integer(42));
1367    }
1368
1369    #[test]
1370    fn coalesce_all_null_returns_null() {
1371        let expr = Expr::FunctionCall {
1372            name: "COALESCE".to_string(),
1373            args: vec![lit(Value::Null), lit(Value::Null)],
1374            span: Span::synthetic(),
1375        };
1376        let v = evaluate(&expr, &empty_row()).unwrap();
1377        assert_eq!(v, Value::Null);
1378    }
1379
1380    #[test]
1381    fn upper_lower_dispatch_through_function_catalog() {
1382        let expr = Expr::FunctionCall {
1383            name: "UPPER".to_string(),
1384            args: vec![lit(Value::Text(Arc::from("hello")))],
1385            span: Span::synthetic(),
1386        };
1387        let v = evaluate(&expr, &empty_row()).unwrap();
1388        assert_eq!(v, Value::Text(Arc::from("HELLO")));
1389    }
1390
1391    #[test]
1392    fn length_of_null_propagates() {
1393        let expr = Expr::FunctionCall {
1394            name: "LENGTH".to_string(),
1395            args: vec![lit(Value::Null)],
1396            span: Span::synthetic(),
1397        };
1398        let v = evaluate(&expr, &empty_row()).unwrap();
1399        assert_eq!(v, Value::Null);
1400    }
1401
1402    #[test]
1403    fn case_when_picks_first_true_branch() {
1404        let expr = Expr::Case {
1405            branches: vec![
1406                (lit(Value::Boolean(false)), lit(Value::Integer(1))),
1407                (lit(Value::Boolean(true)), lit(Value::Integer(2))),
1408                (lit(Value::Boolean(true)), lit(Value::Integer(3))),
1409            ],
1410            else_: Some(Box::new(lit(Value::Integer(99)))),
1411            span: Span::synthetic(),
1412        };
1413        let v = evaluate(&expr, &empty_row()).unwrap();
1414        assert_eq!(v, Value::Integer(2));
1415    }
1416
1417    #[test]
1418    fn case_falls_through_to_else_when_no_branch_matches() {
1419        let expr = Expr::Case {
1420            branches: vec![(lit(Value::Boolean(false)), lit(Value::Integer(1)))],
1421            else_: Some(Box::new(lit(Value::Integer(99)))),
1422            span: Span::synthetic(),
1423        };
1424        let v = evaluate(&expr, &empty_row()).unwrap();
1425        assert_eq!(v, Value::Integer(99));
1426    }
1427
1428    #[test]
1429    fn case_returns_null_when_no_branch_matches_and_no_else() {
1430        let expr = Expr::Case {
1431            branches: vec![(lit(Value::Boolean(false)), lit(Value::Integer(1)))],
1432            else_: None,
1433            span: Span::synthetic(),
1434        };
1435        let v = evaluate(&expr, &empty_row()).unwrap();
1436        assert_eq!(v, Value::Null);
1437    }
1438
1439    #[test]
1440    fn is_null_handles_null_and_non_null() {
1441        let null_expr = Expr::IsNull {
1442            operand: Box::new(lit(Value::Null)),
1443            negated: false,
1444            span: Span::synthetic(),
1445        };
1446        assert_eq!(
1447            evaluate(&null_expr, &empty_row()).unwrap(),
1448            Value::Boolean(true)
1449        );
1450
1451        let non_null_expr = Expr::IsNull {
1452            operand: Box::new(lit(Value::Integer(7))),
1453            negated: false,
1454            span: Span::synthetic(),
1455        };
1456        assert_eq!(
1457            evaluate(&non_null_expr, &empty_row()).unwrap(),
1458            Value::Boolean(false)
1459        );
1460    }
1461
1462    #[test]
1463    fn between_inclusive_bounds() {
1464        let expr = Expr::Between {
1465            target: Box::new(lit(Value::Integer(5))),
1466            low: Box::new(lit(Value::Integer(1))),
1467            high: Box::new(lit(Value::Integer(10))),
1468            negated: false,
1469            span: Span::synthetic(),
1470        };
1471        assert_eq!(evaluate(&expr, &empty_row()).unwrap(), Value::Boolean(true));
1472    }
1473
1474    #[test]
1475    fn in_list_match_and_miss() {
1476        let hit = Expr::InList {
1477            target: Box::new(lit(Value::Integer(2))),
1478            values: vec![
1479                lit(Value::Integer(1)),
1480                lit(Value::Integer(2)),
1481                lit(Value::Integer(3)),
1482            ],
1483            negated: false,
1484            span: Span::synthetic(),
1485        };
1486        assert_eq!(evaluate(&hit, &empty_row()).unwrap(), Value::Boolean(true));
1487
1488        let miss = Expr::InList {
1489            target: Box::new(lit(Value::Integer(99))),
1490            values: vec![lit(Value::Integer(1)), lit(Value::Integer(2))],
1491            negated: false,
1492            span: Span::synthetic(),
1493        };
1494        assert_eq!(
1495            evaluate(&miss, &empty_row()).unwrap(),
1496            Value::Boolean(false)
1497        );
1498    }
1499
1500    #[test]
1501    fn column_lookup_walks_field_ref() {
1502        let row = |field: &FieldRef| -> Option<Value> {
1503            match field {
1504                FieldRef::TableColumn { table, column } if table == "t" && column == "x" => {
1505                    Some(Value::Integer(11))
1506                }
1507                _ => None,
1508            }
1509        };
1510        let expr = Expr::Column {
1511            field: FieldRef::TableColumn {
1512                table: "t".to_string(),
1513                column: "x".to_string(),
1514            },
1515            span: Span::synthetic(),
1516        };
1517        assert_eq!(evaluate(&expr, &row).unwrap(), Value::Integer(11));
1518    }
1519
1520    #[test]
1521    fn missing_column_surfaces_unknown_column() {
1522        let row = |_: &FieldRef| -> Option<Value> { None };
1523        let expr = Expr::Column {
1524            field: FieldRef::TableColumn {
1525                table: "t".to_string(),
1526                column: "missing".to_string(),
1527            },
1528            span: Span::synthetic(),
1529        };
1530        let err = evaluate(&expr, &row).unwrap_err();
1531        match err {
1532            EvalError::UnknownColumn(_) => {}
1533            other => panic!("expected UnknownColumn, got {other:?}"),
1534        }
1535    }
1536
1537    #[test]
1538    fn parameter_without_bind_is_eval_error() {
1539        let expr = Expr::Parameter {
1540            index: 1,
1541            span: Span::synthetic(),
1542        };
1543        let err = evaluate(&expr, &empty_row()).unwrap_err();
1544        assert_eq!(err, EvalError::UnboundParameter(1));
1545    }
1546
1547    #[test]
1548    fn cast_integer_to_text_uses_explicit_path() {
1549        let expr = Expr::Cast {
1550            inner: Box::new(lit(Value::Integer(42))),
1551            target: DataType::Text,
1552            span: Span::synthetic(),
1553        };
1554        assert_eq!(
1555            evaluate(&expr, &empty_row()).unwrap(),
1556            Value::Text(Arc::from("42"))
1557        );
1558    }
1559
1560    #[test]
1561    fn concat_returns_text() {
1562        let expr = binop(
1563            BinOp::Concat,
1564            lit(Value::Text(Arc::from("foo"))),
1565            lit(Value::Text(Arc::from("bar"))),
1566        );
1567        assert_eq!(
1568            evaluate(&expr, &empty_row()).unwrap(),
1569            Value::Text(Arc::from("foobar"))
1570        );
1571    }
1572}