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