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::{BinaryOp, Expr};
12use crate::error::{Result, SQLError};
13
14use super::binary::{eval_binary, eval_comparison_truth_with_engine, truthy};
15use super::builtin::eval_bound_builtin_function_call;
16use super::call_arguments::evaluate_call_args;
17use super::call_dispatch::eval_function_call;
18use super::casting::negate_value;
19use super::context::{cast_value_with_type_resolution, EvalContext};
20
21/// Evaluate a value-producing AST expression against one row and parameter context.
22#[expect(
23    clippy::too_many_lines,
24    reason = "builtin dispatch preserves arity, NULL, and error precedence"
25)]
26pub fn eval(expr: &Expr, ctx: &EvalContext<'_>) -> Result<Value> {
27    match expr {
28        Expr::Default => Err(SQLError::Internal(
29            "DEFAULT reached scalar expression evaluation without a mutation target".into(),
30        )),
31        Expr::Literal(v) => Ok(v.clone()),
32        Expr::TypedLiteral { value, ty, .. } => {
33            Ok(super::composites::literal::evaluate_with_control(
34                value,
35                ty,
36                ctx.engine,
37                &uqa_core::memory::ProductionControl::uncontrolled(),
38            )?
39            .into_uncontrolled()
40            .expect("ordinary constant result"))
41        }
42        Expr::Param(i) => match i.checked_sub(1).and_then(|index| ctx.params.get(index)) {
43            Some(parameter) => parameter.to_value(),
44            None => Err(SQLError::MissingParam(*i)),
45        },
46        Expr::Column(name) => {
47            // Plain column refs match either an unqualified key or the
48            // suffix of a qualified `table.col` key, so the same row
49            // shape works for single-table SELECTs and JOIN tuples.
50            if ctx.row_lookup()?.column_is_ambiguous(name) {
51                return Err(SQLError::AmbiguousColumn(name.clone()));
52            }
53            Ok(ctx
54                .row_lookup()?
55                .column(name)
56                .cloned()
57                .unwrap_or(Value::Null))
58        }
59        Expr::QualifiedColumn { qualifier, column } => {
60            if ctx
61                .row_lookup()?
62                .qualified_column_is_ambiguous(qualifier, column)
63            {
64                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
65            }
66            Ok(ctx
67                .row_lookup()?
68                .qualified_column(qualifier, column)
69                .cloned()
70                .unwrap_or(Value::Null))
71        }
72        Expr::InternalColumn(column) => ctx
73            .row_lookup()?
74            .internal_column(*column)
75            .cloned()
76            .ok_or_else(|| {
77                SQLError::Internal(format!(
78                    "internal relation attribute {column:?} is unavailable"
79                ))
80            }),
81        Expr::Array(elements) => {
82            let scalar = crate::plan::ExpressionPlan::lower(Expr::Array(elements.clone())).scalar;
83            let crate::ScalarExpr::Array(items) = scalar else {
84                unreachable!("array lowering")
85            };
86            let element_oid = crate::type_resolution::scalar_array_element_oid_with_control(
87                &items,
88                &crate::RowSchema::default(),
89                ctx.params,
90                ctx.engine,
91                &uqa_core::memory::ProductionControl::uncontrolled(),
92            )?;
93            let mut out = Vec::with_capacity(elements.len());
94            for e in elements {
95                out.push(eval(e, ctx)?);
96            }
97            ArrayValue::try_new(out)
98                .map(|array| Value::Array(array.with_element_type_oid(element_oid)))
99                .ok_or_else(|| {
100                    SQLError::TypeMismatch(
101                        "multidimensional arrays must have matching dimensions".into(),
102                    )
103                })
104        }
105        Expr::CompositeRow { items, binding } => {
106            let control = uqa_core::memory::ProductionControl::uncontrolled();
107            Ok(super::composites::constructor::evaluate_with_control(
108                binding,
109                items.len(),
110                ctx.engine,
111                &control,
112                |index| {
113                    control
114                        .finish(eval(&items[index], ctx)?, None)
115                        .map_err(Into::into)
116                },
117            )?
118            .into_uncontrolled()
119            .expect("ordinary composite constructor"))
120        }
121        Expr::Row(elements) => {
122            let mut out = Vec::with_capacity(elements.len());
123            let mut fields = Vec::with_capacity(elements.len());
124            for element in elements {
125                let scalar = crate::plan::ExpressionPlan::lower(element.clone()).scalar;
126                if let Some(field) = crate::type_resolution::scalar_record_field_type_with_control(
127                    &scalar,
128                    &crate::RowSchema::default(),
129                    ctx.params,
130                    ctx.engine,
131                    &uqa_core::memory::ProductionControl::uncontrolled(),
132                )? {
133                    fields.push(field);
134                }
135                out.push(eval(element, ctx)?);
136            }
137            let row = if fields.len() == out.len() {
138                uqa_core::RowValue::typed(out, fields)?
139            } else {
140                uqa_core::RowValue::new(out)
141            };
142            Ok(Value::Row(row))
143        }
144        Expr::Star | Expr::QualifiedStar(_) => {
145            Err(SQLError::Internal("`*` cannot be evaluated".into()))
146        }
147        Expr::Func {
148            name,
149            binding,
150            args,
151            ..
152        } => {
153            if let Some(binding) = binding {
154                if let Some(error) = &binding.resolution_error {
155                    return Err(error.sql_error());
156                }
157                if let Some(clause) = crate::catalog::collations::expression::ast_clause(expr)? {
158                    // Analysis retains the selected collation for consumers; evaluating the clause only evaluates its operand.
159                    return eval(clause.argument, ctx);
160                }
161                if let Some(hook) = ctx.engine {
162                    hook.require_builtin_execute(binding)?;
163                }
164                if let Some(crate::ast::FunctionDispatch::NumericOperator(operator)) =
165                    binding.dispatch
166                {
167                    return super::numeric_operator::eval_ast_operator(
168                        operator, binding, args, ctx,
169                    );
170                }
171            }
172            if name.eq_ignore_ascii_case("coalesce")
173                && binding.as_ref().is_none_or(|binding| binding.builtin)
174            {
175                for argument in args {
176                    let value = eval(argument, ctx)?;
177                    if !matches!(value, Value::Null) {
178                        return Ok(value);
179                    }
180                }
181                return Ok(Value::Null);
182            }
183            if binding.as_ref().is_some_and(|binding| {
184                binding.builtin
185                    && binding.dispatch == Some(crate::ast::FunctionDispatch::BetweenSymmetric)
186            }) {
187                let [value, low, high] = args.as_slice() else {
188                    return Err(SQLError::TypeMismatch(
189                        "BETWEEN SYMMETRIC takes 3 args".into(),
190                    ));
191                };
192                let forward = eval_between(value, low, high, ctx, 0)?;
193                if forward == Value::Bool(true) {
194                    return Ok(forward);
195                }
196                let backward = eval_between(value, high, low, ctx, 2)?;
197                return Ok(match (forward, backward) {
198                    (_, Value::Bool(true)) => Value::Bool(true),
199                    (Value::Null, _) | (_, Value::Null) => Value::Null,
200                    _ => Value::Bool(false),
201                });
202            }
203            if let Some(access) = binding
204                .as_ref()
205                .and_then(|binding| binding.dispatch)
206                .and_then(|dispatch| {
207                    crate::ast::ArrayAccess::from_dispatch(dispatch, args.len(), |index| {
208                        matches!(args[index], Expr::Literal(Value::Null))
209                    })
210                })
211            {
212                let control = uqa_core::memory::ProductionControl::uncontrolled();
213                return Ok(
214                    super::eval_array_access_with_control(access?, &control, |index| {
215                        Ok(control.finish(eval(&args[index], ctx)?, None)?)
216                    })?
217                    .into_uncontrolled()
218                    .expect("ordinary array access"),
219                );
220            }
221            let call_args = evaluate_call_args(args, ctx)?;
222            if let Some(binding) = binding {
223                if binding.builtin {
224                    return eval_bound_builtin_function_call(binding, call_args, ctx);
225                }
226                let engine = ctx.engine.ok_or_else(|| {
227                    SQLError::Unsupported(
228                        "bound user function requires a logical engine session".into(),
229                    )
230                })?;
231                engine
232                    .call_bound_user_function(binding, &call_args)
233                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
234            } else {
235                eval_function_call(name, call_args, ctx)
236            }
237        }
238        Expr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
239            "window function `{name}` must be evaluated by the window-aware executor"
240        ))),
241        Expr::Case {
242            base,
243            when,
244            else_branch,
245        } => {
246            let base_value = match base {
247                Some(b) => Some(eval(b, ctx)?),
248                None => None,
249            };
250            for (cond, result) in when {
251                let matched = match &base_value {
252                    Some(bv) => {
253                        super::eval_value_comparison_truth_with_engine(
254                            BinaryOp::Equal,
255                            bv,
256                            &eval(cond, ctx)?,
257                            &uqa_core::memory::ProductionControl::uncontrolled(),
258                            ctx.engine,
259                            ctx.enum_comparison_state_at(0),
260                        )? == Some(true)
261                    }
262                    None => truthy(&eval(cond, ctx)?),
263                };
264                if matched {
265                    return eval(result, ctx);
266                }
267            }
268            match else_branch {
269                Some(e) => eval(e, ctx),
270                None => Ok(Value::Null),
271            }
272        }
273        Expr::Cast { expr, ty, .. } => {
274            let source_ty = explicit_expr_type(expr);
275            let v = eval(expr, ctx)?;
276            cast_value_with_type_resolution(&v, source_ty, ty, ctx.engine)
277        }
278        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => {
279            Err(SQLError::Unsupported(
280                "query-valued expressions must be lowered to physical ScalarExpr/QueryPlan slots"
281                    .into(),
282            ))
283        }
284        Expr::Binary { op, lhs, rhs } => eval_binary(*op, lhs, rhs, ctx),
285        Expr::UnaryMinus(inner) => {
286            let source_ty = explicit_expr_type(inner);
287            let value = eval(inner, ctx)?;
288            negate_value(&value, source_ty)
289        }
290        Expr::Not(inner) => {
291            // SQL three-valued logic: NOT NULL -> NULL.
292            let v = eval(inner, ctx)?;
293            if matches!(v, Value::Null) {
294                return Ok(Value::Null);
295            }
296            Ok(Value::Bool(!truthy(&v)))
297        }
298        Expr::And(items) => {
299            // Kleene AND: FALSE dominates, otherwise NULL taints.
300            let mut saw_null = false;
301            for item in items {
302                let v = eval(item, ctx)?;
303                if matches!(v, Value::Null) {
304                    saw_null = true;
305                } else if !truthy(&v) {
306                    return Ok(Value::Bool(false));
307                }
308            }
309            if saw_null {
310                return Ok(Value::Null);
311            }
312            Ok(Value::Bool(true))
313        }
314        Expr::Or(items) => {
315            // Kleene OR: TRUE dominates, otherwise NULL taints.
316            let mut saw_null = false;
317            for item in items {
318                let v = eval(item, ctx)?;
319                if matches!(v, Value::Null) {
320                    saw_null = true;
321                } else if truthy(&v) {
322                    return Ok(Value::Bool(true));
323                }
324            }
325            if saw_null {
326                return Ok(Value::Null);
327            }
328            Ok(Value::Bool(false))
329        }
330        Expr::IsNull { expr, negated } => {
331            let v = eval(expr, ctx)?;
332            Ok(Value::Bool(uqa_core::sql_null_test(Some(&v), *negated)))
333        }
334        Expr::Between { expr, low, high } => eval_between(expr, low, high, ctx, 0),
335        Expr::InList {
336            expr,
337            list,
338            negated,
339        } => {
340            // Three-valued IN: found -> TRUE, a NULL comparand (or a
341            // NULL needle) downgrades a miss to NULL.
342            let v = eval(expr, ctx)?;
343            let mut saw_null = matches!(v, Value::Null);
344            for item in list {
345                let candidate = eval(item, ctx)?;
346                match compare(BinaryOp::Equal, &v, &candidate, ctx, 0)? {
347                    Some(true) => return Ok(Value::Bool(!*negated)),
348                    Some(false) => {}
349                    None => saw_null = true,
350                }
351            }
352            if saw_null {
353                return Ok(Value::Null);
354            }
355            Ok(Value::Bool(*negated))
356        }
357    }
358}
359
360fn explicit_expr_type(expr: &Expr) -> Option<&str> {
361    match expr {
362        Expr::Cast { ty, .. }
363        | Expr::TypedLiteral { ty, .. }
364        | Expr::CompositeRow {
365            binding: crate::ast::CompositeRowBinding { ty, .. },
366            ..
367        } => Some(ty),
368        Expr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => Some("integer"),
369        Expr::Literal(Value::Int(_)) => Some("bigint"),
370        Expr::Literal(Value::Bytes(_)) => Some("bytea"),
371        _ => None,
372    }
373}
374
375/// Evaluate `PostgreSQL`'s two comparisons in order, including repeated value
376/// evaluation and the short circuit after a false lower-bound comparison.
377fn eval_between(
378    expression: &Expr,
379    low: &Expr,
380    high: &Expr,
381    context: &EvalContext<'_>,
382    slot: usize,
383) -> Result<Value> {
384    let ge = compare(
385        BinaryOp::GreaterEqual,
386        &eval(expression, context)?,
387        &eval(low, context)?,
388        context,
389        slot,
390    )?;
391    if ge == Some(false) {
392        return Ok(Value::Bool(false));
393    }
394    let le = compare(
395        BinaryOp::LessEqual,
396        &eval(expression, context)?,
397        &eval(high, context)?,
398        context,
399        slot + 1,
400    )?;
401    Ok(match (ge, le) {
402        (_, Some(false)) => Value::Bool(false),
403        (Some(true), Some(true)) => Value::Bool(true),
404        _ => Value::Null,
405    })
406}
407
408fn compare(
409    op: BinaryOp,
410    left: &Value,
411    right: &Value,
412    context: &EvalContext<'_>,
413    slot: usize,
414) -> Result<Option<bool>> {
415    eval_comparison_truth_with_engine(
416        op,
417        left,
418        right,
419        &uqa_core::memory::ProductionControl::uncontrolled(),
420        context.engine,
421        context.enum_comparison_state_at(slot),
422    )
423}