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 mut out = Vec::with_capacity(elements.len());
83            for e in elements {
84                out.push(eval(e, ctx)?);
85            }
86            ArrayValue::try_new(out).map(Value::Array).ok_or_else(|| {
87                SQLError::TypeMismatch(
88                    "multidimensional arrays must have matching dimensions".into(),
89                )
90            })
91        }
92        Expr::CompositeRow { items, binding } => {
93            let control = uqa_core::memory::ProductionControl::uncontrolled();
94            Ok(super::composites::constructor::evaluate_with_control(
95                binding,
96                items.len(),
97                ctx.engine,
98                &control,
99                |index| {
100                    control
101                        .finish(eval(&items[index], ctx)?, None)
102                        .map_err(Into::into)
103                },
104            )?
105            .into_uncontrolled()
106            .expect("ordinary composite constructor"))
107        }
108        Expr::Row(elements) => {
109            let mut out = Vec::with_capacity(elements.len());
110            let mut fields = Vec::with_capacity(elements.len());
111            for element in elements {
112                let scalar = crate::plan::ExpressionPlan::lower(element.clone()).scalar;
113                if let Some(field) = crate::type_resolution::scalar_record_field_type_with_control(
114                    &scalar,
115                    &crate::RowSchema::default(),
116                    ctx.params,
117                    ctx.engine,
118                    &uqa_core::memory::ProductionControl::uncontrolled(),
119                )? {
120                    fields.push(field);
121                }
122                out.push(eval(element, ctx)?);
123            }
124            let row = if fields.len() == out.len() {
125                uqa_core::RowValue::typed(out, fields)?
126            } else {
127                uqa_core::RowValue::new(out)
128            };
129            Ok(Value::Row(row))
130        }
131        Expr::Star | Expr::QualifiedStar(_) => {
132            Err(SQLError::Internal("`*` cannot be evaluated".into()))
133        }
134        Expr::Func {
135            name,
136            binding,
137            args,
138            ..
139        } => {
140            if let Some(binding) = binding {
141                if let Some(error) = &binding.resolution_error {
142                    return Err(error.sql_error());
143                }
144                if let Some(hook) = ctx.engine {
145                    hook.require_builtin_execute(binding)?;
146                }
147                if let Some(crate::ast::FunctionDispatch::NumericOperator(operator)) =
148                    binding.dispatch
149                {
150                    return super::numeric_operator::eval_ast_operator(
151                        operator, binding, args, ctx,
152                    );
153                }
154            }
155            if name.eq_ignore_ascii_case("coalesce")
156                && binding.as_ref().is_none_or(|binding| binding.builtin)
157            {
158                for argument in args {
159                    let value = eval(argument, ctx)?;
160                    if !matches!(value, Value::Null) {
161                        return Ok(value);
162                    }
163                }
164                return Ok(Value::Null);
165            }
166            if binding.as_ref().is_some_and(|binding| {
167                binding.builtin
168                    && binding.dispatch == Some(crate::ast::FunctionDispatch::BetweenSymmetric)
169            }) {
170                let [value, low, high] = args.as_slice() else {
171                    return Err(SQLError::TypeMismatch(
172                        "BETWEEN SYMMETRIC takes 3 args".into(),
173                    ));
174                };
175                let forward = eval_between(value, low, high, ctx, 0)?;
176                if forward == Value::Bool(true) {
177                    return Ok(forward);
178                }
179                let backward = eval_between(value, high, low, ctx, 2)?;
180                return Ok(match (forward, backward) {
181                    (_, Value::Bool(true)) => Value::Bool(true),
182                    (Value::Null, _) | (_, Value::Null) => Value::Null,
183                    _ => Value::Bool(false),
184                });
185            }
186            let call_args = evaluate_call_args(args, ctx)?;
187            if let Some(binding) = binding {
188                if binding.builtin {
189                    return eval_bound_builtin_function_call(binding, call_args, ctx);
190                }
191                let engine = ctx.engine.ok_or_else(|| {
192                    SQLError::Unsupported(
193                        "bound user function requires a logical engine session".into(),
194                    )
195                })?;
196                engine
197                    .call_bound_user_function(binding, &call_args)
198                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
199            } else {
200                eval_function_call(name, call_args, ctx)
201            }
202        }
203        Expr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
204            "window function `{name}` must be evaluated by the window-aware executor"
205        ))),
206        Expr::Case {
207            base,
208            when,
209            else_branch,
210        } => {
211            let base_value = match base {
212                Some(b) => Some(eval(b, ctx)?),
213                None => None,
214            };
215            for (cond, result) in when {
216                let matched = match &base_value {
217                    Some(bv) => {
218                        compare(BinaryOp::Equal, bv, &eval(cond, ctx)?, ctx, 0)? == Some(true)
219                    }
220                    None => truthy(&eval(cond, ctx)?),
221                };
222                if matched {
223                    return eval(result, ctx);
224                }
225            }
226            match else_branch {
227                Some(e) => eval(e, ctx),
228                None => Ok(Value::Null),
229            }
230        }
231        Expr::Cast { expr, ty, .. } => {
232            let source_ty = explicit_expr_type(expr);
233            let v = eval(expr, ctx)?;
234            cast_value_with_type_resolution(&v, source_ty, ty, ctx.engine)
235        }
236        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => {
237            Err(SQLError::Unsupported(
238                "query-valued expressions must be lowered to physical ScalarExpr/QueryPlan slots"
239                    .into(),
240            ))
241        }
242        Expr::Binary { op, lhs, rhs } => eval_binary(*op, lhs, rhs, ctx),
243        Expr::UnaryMinus(inner) => {
244            let source_ty = explicit_expr_type(inner);
245            let value = eval(inner, ctx)?;
246            negate_value(&value, source_ty)
247        }
248        Expr::Not(inner) => {
249            // SQL three-valued logic: NOT NULL -> NULL.
250            let v = eval(inner, ctx)?;
251            if matches!(v, Value::Null) {
252                return Ok(Value::Null);
253            }
254            Ok(Value::Bool(!truthy(&v)))
255        }
256        Expr::And(items) => {
257            // Kleene AND: FALSE dominates, otherwise NULL taints.
258            let mut saw_null = false;
259            for item in items {
260                let v = eval(item, ctx)?;
261                if matches!(v, Value::Null) {
262                    saw_null = true;
263                } else if !truthy(&v) {
264                    return Ok(Value::Bool(false));
265                }
266            }
267            if saw_null {
268                return Ok(Value::Null);
269            }
270            Ok(Value::Bool(true))
271        }
272        Expr::Or(items) => {
273            // Kleene OR: TRUE dominates, otherwise NULL taints.
274            let mut saw_null = false;
275            for item in items {
276                let v = eval(item, ctx)?;
277                if matches!(v, Value::Null) {
278                    saw_null = true;
279                } else if truthy(&v) {
280                    return Ok(Value::Bool(true));
281                }
282            }
283            if saw_null {
284                return Ok(Value::Null);
285            }
286            Ok(Value::Bool(false))
287        }
288        Expr::IsNull { expr, negated } => {
289            let v = eval(expr, ctx)?;
290            Ok(Value::Bool(uqa_core::sql_null_test(Some(&v), *negated)))
291        }
292        Expr::Between { expr, low, high } => eval_between(expr, low, high, ctx, 0),
293        Expr::InList {
294            expr,
295            list,
296            negated,
297        } => {
298            // Three-valued IN: found -> TRUE, a NULL comparand (or a
299            // NULL needle) downgrades a miss to NULL.
300            let v = eval(expr, ctx)?;
301            let mut saw_null = matches!(v, Value::Null);
302            for item in list {
303                let candidate = eval(item, ctx)?;
304                match compare(BinaryOp::Equal, &v, &candidate, ctx, 0)? {
305                    Some(true) => return Ok(Value::Bool(!*negated)),
306                    Some(false) => {}
307                    None => saw_null = true,
308                }
309            }
310            if saw_null {
311                return Ok(Value::Null);
312            }
313            Ok(Value::Bool(*negated))
314        }
315    }
316}
317
318fn explicit_expr_type(expr: &Expr) -> Option<&str> {
319    match expr {
320        Expr::Cast { ty, .. }
321        | Expr::TypedLiteral { ty, .. }
322        | Expr::CompositeRow {
323            binding: crate::ast::CompositeRowBinding { ty, .. },
324            ..
325        } => Some(ty),
326        Expr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => Some("integer"),
327        Expr::Literal(Value::Int(_)) => Some("bigint"),
328        Expr::Literal(Value::Bytes(_)) => Some("bytea"),
329        _ => None,
330    }
331}
332
333/// Evaluate `PostgreSQL`'s two comparisons in order, including repeated value
334/// evaluation and the short circuit after a false lower-bound comparison.
335fn eval_between(
336    expression: &Expr,
337    low: &Expr,
338    high: &Expr,
339    context: &EvalContext<'_>,
340    slot: usize,
341) -> Result<Value> {
342    let ge = compare(
343        BinaryOp::GreaterEqual,
344        &eval(expression, context)?,
345        &eval(low, context)?,
346        context,
347        slot,
348    )?;
349    if ge == Some(false) {
350        return Ok(Value::Bool(false));
351    }
352    let le = compare(
353        BinaryOp::LessEqual,
354        &eval(expression, context)?,
355        &eval(high, context)?,
356        context,
357        slot + 1,
358    )?;
359    Ok(match (ge, le) {
360        (_, Some(false)) => Value::Bool(false),
361        (Some(true), Some(true)) => Value::Bool(true),
362        _ => Value::Null,
363    })
364}
365
366fn compare(
367    op: BinaryOp,
368    left: &Value,
369    right: &Value,
370    context: &EvalContext<'_>,
371    slot: usize,
372) -> Result<Option<bool>> {
373    eval_comparison_truth_with_engine(
374        op,
375        left,
376        right,
377        &uqa_core::memory::ProductionControl::uncontrolled(),
378        context.engine,
379        context.enum_comparison_state_at(slot),
380    )
381}