Skip to main content

uqa_sql/expr/
evaluator.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! AST scalar evaluation orchestration.
8
9use uqa_core::{ArrayValue, Value};
10
11use crate::ast::{Expr, FunctionResolutionError};
12use crate::error::{Result, SQLError};
13use crate::params::SQLParam;
14
15use super::binary::{compare_nullable, eval_binary, truthy, values_equal, values_equal_nullable};
16use super::builtin::eval_bound_builtin_function_call;
17use super::call_arguments::evaluate_call_args;
18use super::call_dispatch::eval_function_call;
19use super::casting::negate_value;
20use super::context::{cast_value_with_type_resolution, EvalContext};
21
22/// Evaluate a value-producing AST expression against one row and parameter context.
23#[expect(
24    clippy::too_many_lines,
25    reason = "builtin dispatch preserves arity, NULL, and error precedence"
26)]
27pub fn eval(expr: &Expr, ctx: &EvalContext<'_>) -> Result<Value> {
28    match expr {
29        Expr::Default => Err(SQLError::Internal(
30            "DEFAULT reached scalar expression evaluation without a mutation target".into(),
31        )),
32        Expr::Literal(v) | Expr::TypedLiteral { value: v, .. } => Ok(v.clone()),
33        Expr::Param(i) => match i.checked_sub(1).and_then(|index| ctx.params.get(index)) {
34            Some(SQLParam::Scalar(v) | SQLParam::TypedScalar { value: v, .. }) => Ok(v.clone()),
35            Some(SQLParam::Vector(v)) => Ok(Value::List(
36                v.iter().map(|x| Value::Float(f64::from(*x))).collect(),
37            )),
38            Some(SQLParam::Tensor(vectors)) => Ok(Value::List(
39                vectors
40                    .iter()
41                    .map(|vector| {
42                        Value::List(vector.iter().map(|x| Value::Float(f64::from(*x))).collect())
43                    })
44                    .collect(),
45            )),
46            None => Err(SQLError::MissingParam(*i)),
47        },
48        Expr::Column(name) => {
49            // Plain column refs match either an unqualified key or the
50            // suffix of a qualified `table.col` key, so the same row
51            // shape works for single-table SELECTs and JOIN tuples.
52            if ctx.row_lookup()?.column_is_ambiguous(name) {
53                return Err(SQLError::AmbiguousColumn(name.clone()));
54            }
55            Ok(ctx
56                .row_lookup()?
57                .column(name)
58                .cloned()
59                .unwrap_or(Value::Null))
60        }
61        Expr::QualifiedColumn { qualifier, column } => {
62            if ctx
63                .row_lookup()?
64                .qualified_column_is_ambiguous(qualifier, column)
65            {
66                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
67            }
68            Ok(ctx
69                .row_lookup()?
70                .qualified_column(qualifier, column)
71                .cloned()
72                .unwrap_or(Value::Null))
73        }
74        Expr::InternalColumn(column) => ctx
75            .row_lookup()?
76            .internal_column(*column)
77            .cloned()
78            .ok_or_else(|| {
79                SQLError::Internal(format!(
80                    "internal relation attribute {column:?} is unavailable"
81                ))
82            }),
83        Expr::Array(elements) => {
84            let mut out = Vec::with_capacity(elements.len());
85            for e in elements {
86                out.push(eval(e, ctx)?);
87            }
88            ArrayValue::try_new(out).map(Value::Array).ok_or_else(|| {
89                SQLError::TypeMismatch(
90                    "multidimensional arrays must have matching dimensions".into(),
91                )
92            })
93        }
94        Expr::Row(elements) => {
95            let mut out = Vec::with_capacity(elements.len());
96            for element in elements {
97                out.push(eval(element, ctx)?);
98            }
99            Ok(Value::Row(out))
100        }
101        Expr::Star | Expr::QualifiedStar(_) => {
102            Err(SQLError::Internal("`*` cannot be evaluated".into()))
103        }
104        Expr::Func {
105            name,
106            binding,
107            args,
108            ..
109        } => {
110            if name.eq_ignore_ascii_case("coalesce")
111                && binding.as_ref().is_none_or(|binding| binding.builtin)
112            {
113                for argument in args {
114                    let value = eval(argument, ctx)?;
115                    if !matches!(value, Value::Null) {
116                        return Ok(value);
117                    }
118                }
119                return Ok(Value::Null);
120            }
121            let call_args = evaluate_call_args(args, ctx)?;
122            if let Some(binding) = binding {
123                if let Some(FunctionResolutionError::UndefinedFunction { signature }) =
124                    binding.resolution_error.as_ref()
125                {
126                    return Err(SQLError::Routine {
127                        sqlstate: "42883".into(),
128                        message: format!("function {signature} does not exist"),
129                    });
130                }
131                if binding.builtin {
132                    return eval_bound_builtin_function_call(binding, call_args, ctx);
133                }
134                let engine = ctx.engine.ok_or_else(|| {
135                    SQLError::Unsupported(
136                        "bound user function requires a logical engine session".into(),
137                    )
138                })?;
139                engine
140                    .call_bound_user_function(binding, &call_args)
141                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
142            } else {
143                eval_function_call(name, call_args, ctx)
144            }
145        }
146        Expr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
147            "window function `{name}` must be evaluated by the window-aware executor"
148        ))),
149        Expr::Case {
150            base,
151            when,
152            else_branch,
153        } => {
154            let base_value = match base {
155                Some(b) => Some(eval(b, ctx)?),
156                None => None,
157            };
158            for (cond, result) in when {
159                let matched = match &base_value {
160                    Some(bv) => values_equal(bv, &eval(cond, ctx)?),
161                    None => truthy(&eval(cond, ctx)?),
162                };
163                if matched {
164                    return eval(result, ctx);
165                }
166            }
167            match else_branch {
168                Some(e) => eval(e, ctx),
169                None => Ok(Value::Null),
170            }
171        }
172        Expr::Cast { expr, ty } => {
173            let source_ty = explicit_expr_type(expr);
174            let v = eval(expr, ctx)?;
175            cast_value_with_type_resolution(&v, source_ty, ty, ctx.engine)
176        }
177        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => {
178            Err(SQLError::Unsupported(
179                "query-valued expressions must be lowered to physical ScalarExpr/QueryPlan slots"
180                    .into(),
181            ))
182        }
183        Expr::Binary { op, lhs, rhs } => eval_binary(*op, lhs, rhs, ctx),
184        Expr::UnaryMinus(inner) => {
185            let source_ty = explicit_expr_type(inner);
186            let value = eval(inner, ctx)?;
187            negate_value(&value, source_ty)
188        }
189        Expr::Not(inner) => {
190            // SQL three-valued logic: NOT NULL -> NULL.
191            let v = eval(inner, ctx)?;
192            if matches!(v, Value::Null) {
193                return Ok(Value::Null);
194            }
195            Ok(Value::Bool(!truthy(&v)))
196        }
197        Expr::And(items) => {
198            // Kleene AND: FALSE dominates, otherwise NULL taints.
199            let mut saw_null = false;
200            for item in items {
201                let v = eval(item, ctx)?;
202                if matches!(v, Value::Null) {
203                    saw_null = true;
204                } else if !truthy(&v) {
205                    return Ok(Value::Bool(false));
206                }
207            }
208            if saw_null {
209                return Ok(Value::Null);
210            }
211            Ok(Value::Bool(true))
212        }
213        Expr::Or(items) => {
214            // Kleene OR: TRUE dominates, otherwise NULL taints.
215            let mut saw_null = false;
216            for item in items {
217                let v = eval(item, ctx)?;
218                if matches!(v, Value::Null) {
219                    saw_null = true;
220                } else if truthy(&v) {
221                    return Ok(Value::Bool(true));
222                }
223            }
224            if saw_null {
225                return Ok(Value::Null);
226            }
227            Ok(Value::Bool(false))
228        }
229        Expr::IsNull { expr, negated } => {
230            let v = eval(expr, ctx)?;
231            let is_null = matches!(v, Value::Null);
232            Ok(Value::Bool(if *negated { !is_null } else { is_null }))
233        }
234        Expr::Between { expr, low, high } => {
235            let v = eval(expr, ctx)?;
236            let lo = eval(low, ctx)?;
237            let hi = eval(high, ctx)?;
238            eval_between(&v, &lo, &hi)
239        }
240        Expr::InList {
241            expr,
242            list,
243            negated,
244        } => {
245            // Three-valued IN: found -> TRUE, a NULL comparand (or a
246            // NULL needle) downgrades a miss to NULL.
247            let v = eval(expr, ctx)?;
248            let mut saw_null = matches!(v, Value::Null);
249            for item in list {
250                let candidate = eval(item, ctx)?;
251                match values_equal_nullable(&v, &candidate) {
252                    Some(true) => return Ok(Value::Bool(!*negated)),
253                    Some(false) => {}
254                    None => saw_null = true,
255                }
256            }
257            if saw_null {
258                return Ok(Value::Null);
259            }
260            Ok(Value::Bool(*negated))
261        }
262    }
263}
264
265fn explicit_expr_type(expr: &Expr) -> Option<&str> {
266    match expr {
267        Expr::Cast { ty, .. } | Expr::TypedLiteral { ty, .. } => Some(ty),
268        Expr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => Some("integer"),
269        Expr::Literal(Value::Int(_)) => Some("bigint"),
270        Expr::Literal(Value::Bytes(_)) => Some("bytea"),
271        _ => None,
272    }
273}
274
275/// `expr BETWEEN low AND high` under three-valued logic: a definite
276/// FALSE on either bound wins over a NULL on the other.
277pub(super) fn eval_between(v: &Value, lo: &Value, hi: &Value) -> Result<Value> {
278    let ge = compare_nullable(v, lo)?.map(|ord| ord.is_ge());
279    let le = compare_nullable(v, hi)?.map(|ord| ord.is_le());
280    Ok(match (ge, le) {
281        (Some(false), _) | (_, Some(false)) => Value::Bool(false),
282        (Some(true), Some(true)) => Value::Bool(true),
283        _ => Value::Null,
284    })
285}