Skip to main content

toasty_core/stmt/
eval.rs

1//! Client-side evaluation of constant or input-bound expressions and
2//! statements.
3//!
4//! The evaluator walks the expression tree recursively, resolving arguments
5//! via an [`Input`] implementation and producing [`Value`]s. It supports
6//! boolean logic, comparison, casting, records, lists, let-bindings, match
7//! expressions, and subqueries (VALUES only).
8//!
9//! # Examples
10//!
11//! ```
12//! use toasty_core::stmt::{Expr, Value, ConstInput};
13//!
14//! let expr = Expr::from(Value::from(42_i64));
15//! let result = expr.eval(ConstInput::new()).unwrap();
16//! assert_eq!(result, Value::from(42_i64));
17//! ```
18
19use crate::{
20    Result,
21    stmt::{
22        BinaryOp, ConstInput, Expr, ExprArg, ExprSet, Input, InputResolve, Limit, Projection,
23        Statement, Value,
24    },
25};
26use std::cmp::Ordering;
27
28enum ScopeStack<'a> {
29    Root,
30    Scope {
31        args: &'a [Value],
32        parent: &'a ScopeStack<'a>,
33    },
34}
35
36impl Statement {
37    /// Evaluates this statement using the provided [`Input`] for argument
38    /// resolution. Only `Query` statements are supported.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error for non-Query statements, or if evaluation of any
43    /// sub-expression fails.
44    pub fn eval(&self, mut input: impl Input) -> Result<Value> {
45        self.eval_ref(&ScopeStack::Root, &mut input)
46    }
47
48    /// Evaluates this statement as a constant expression (no external input).
49    pub fn eval_const(&self) -> Result<Value> {
50        self.eval(ConstInput::new())
51    }
52
53    fn eval_ref(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<Value> {
54        match self {
55            Statement::Query(query) => {
56                if query.with.is_some() {
57                    return Err(crate::Error::expression_evaluation_failed(
58                        "cannot evaluate statement with WITH clause",
59                    ));
60                }
61
62                if query.order_by.is_some() {
63                    return Err(crate::Error::expression_evaluation_failed(
64                        "cannot evaluate statement with ORDER BY clause",
65                    ));
66                }
67
68                let mut result = query.body.eval_ref(scope, input)?;
69
70                if let Some(limit) = &query.limit {
71                    limit.eval_ref(&mut result, scope, input)?;
72                }
73
74                if query.single {
75                    let Value::List(mut items) = result else {
76                        return Err(crate::Error::expression_evaluation_failed(
77                            "single-row query requires body to evaluate to a list",
78                        ));
79                    };
80                    if items.len() != 1 {
81                        return Err(crate::Error::expression_evaluation_failed(
82                            "single-row query did not return exactly one row",
83                        ));
84                    }
85                    return Ok(items.remove(0));
86                }
87
88                Ok(result)
89            }
90            _ => Err(crate::Error::expression_evaluation_failed(
91                "can only evaluate Query statements",
92            )),
93        }
94    }
95}
96
97impl Limit {
98    fn eval_ref(
99        &self,
100        value: &mut Value,
101        scope: &ScopeStack<'_>,
102        input: &mut impl Input,
103    ) -> Result<()> {
104        let Value::List(items) = value else {
105            return Err(crate::Error::expression_evaluation_failed(
106                "LIMIT requires body to evaluate to a list",
107            ));
108        };
109
110        match self {
111            Limit::Cursor(_) => {
112                return Err(crate::Error::expression_evaluation_failed(
113                    "cursor-based pagination cannot be evaluated client-side",
114                ));
115            }
116            Limit::Offset(limit_offset) => {
117                if let Some(offset_expr) = &limit_offset.offset {
118                    let skip = offset_expr.eval_ref_usize(scope, input)?;
119                    if skip >= items.len() {
120                        items.clear();
121                    } else {
122                        items.drain(..skip);
123                    }
124                }
125
126                let n = limit_offset.limit.eval_ref_usize(scope, input)?;
127                items.truncate(n);
128            }
129        }
130        Ok(())
131    }
132}
133
134impl ExprSet {
135    fn eval_ref(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<Value> {
136        let ExprSet::Values(values) = self else {
137            return Err(crate::Error::expression_evaluation_failed(
138                "can only evaluate Values expressions",
139            ));
140        };
141
142        let mut ret = vec![];
143
144        for row in &values.rows {
145            ret.push(row.eval_ref(scope, input)?);
146        }
147
148        Ok(Value::List(ret))
149    }
150}
151
152impl Expr {
153    /// Evaluates this expression using the provided [`Input`] for argument
154    /// and reference resolution.
155    pub fn eval(&self, mut input: impl Input) -> Result<Value> {
156        self.eval_ref(&ScopeStack::Root, &mut input)
157    }
158
159    /// Evaluates this expression and returns the result as a `bool`.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if the expression does not evaluate to a boolean.
164    pub fn eval_bool(&self, mut input: impl Input) -> Result<bool> {
165        self.eval_ref_bool(&ScopeStack::Root, &mut input)
166    }
167
168    /// Evaluates this expression as a constant (no external input).
169    pub fn eval_const(&self) -> Result<Value> {
170        self.eval(ConstInput::new())
171    }
172
173    fn eval_ref(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<Value> {
174        match self {
175            Expr::And(expr_and) => {
176                debug_assert!(!expr_and.operands.is_empty());
177
178                for operand in &expr_and.operands {
179                    if !operand.eval_ref_bool(scope, input)? {
180                        return Ok(false.into());
181                    }
182                }
183
184                Ok(true.into())
185            }
186            Expr::Arg(expr_arg) => {
187                let Some(expr) = scope.resolve_arg(expr_arg, &Projection::identity(), input) else {
188                    return Err(crate::Error::expression_evaluation_failed(
189                        "failed to resolve argument",
190                    ));
191                };
192                expr.eval_ref(scope, input)
193            }
194            Expr::BinaryOp(expr_binary_op) => {
195                let lhs = expr_binary_op.lhs.eval_ref(scope, input)?;
196                let rhs = expr_binary_op.rhs.eval_ref(scope, input)?;
197
198                match expr_binary_op.op {
199                    BinaryOp::Eq => Ok((lhs == rhs).into()),
200                    BinaryOp::Ne => Ok((lhs != rhs).into()),
201                    BinaryOp::Ge => Ok((cmp_ordered(&lhs, &rhs)? != Ordering::Less).into()),
202                    BinaryOp::Gt => Ok((cmp_ordered(&lhs, &rhs)? == Ordering::Greater).into()),
203                    BinaryOp::Le => Ok((cmp_ordered(&lhs, &rhs)? != Ordering::Greater).into()),
204                    BinaryOp::Lt => Ok((cmp_ordered(&lhs, &rhs)? == Ordering::Less).into()),
205                    BinaryOp::Add => lhs.checked_add(&rhs).ok_or_else(|| {
206                        crate::Error::expression_evaluation_failed(
207                            "arithmetic overflow or type mismatch in `+`",
208                        )
209                    }),
210                    BinaryOp::Sub => lhs.checked_sub(&rhs).ok_or_else(|| {
211                        crate::Error::expression_evaluation_failed(
212                            "arithmetic overflow or type mismatch in `-`",
213                        )
214                    }),
215                }
216            }
217            Expr::Cast(expr_cast) => {
218                let value = expr_cast.expr.eval_ref(scope, input)?;
219                expr_cast
220                    .ty
221                    .cast_from(&InputResolve(&*input), expr_cast.from.as_ref(), value)
222            }
223            Expr::Default => Err(crate::Error::expression_evaluation_failed(
224                "DEFAULT can only be evaluated by the database",
225            )),
226            Expr::Error(expr_error) => Err(crate::Error::expression_evaluation_failed(
227                &expr_error.message,
228            )),
229            Expr::IsNull(expr_is_null) => {
230                let value = expr_is_null.expr.eval_ref(scope, input)?;
231                Ok(value.is_null().into())
232            }
233            Expr::IsVariant(_) => Err(crate::Error::expression_evaluation_failed(
234                "IsVariant must be lowered before evaluation",
235            )),
236            Expr::Let(expr_let) => {
237                let args: Vec<_> = expr_let
238                    .bindings
239                    .iter()
240                    .map(|b| b.eval_ref(scope, input))
241                    .collect::<Result<_, _>>()?;
242                let scope = scope.scope(&args);
243                expr_let.body.eval_ref(&scope, input)
244            }
245            Expr::Not(expr_not) => {
246                let value = expr_not.expr.eval_ref_bool(scope, input)?;
247                Ok((!value).into())
248            }
249            Expr::List(exprs) => {
250                let mut ret = vec![];
251
252                for expr in &exprs.items {
253                    ret.push(expr.eval_ref(scope, input)?);
254                }
255
256                Ok(Value::List(ret))
257            }
258            Expr::Map(expr_map) => {
259                let mut base = expr_map.base.eval_ref(scope, input)?;
260
261                let Value::List(items) = &mut base else {
262                    return Err(crate::Error::expression_evaluation_failed(
263                        "Map base must evaluate to a list",
264                    ));
265                };
266
267                for item in items.iter_mut() {
268                    let args = [item.take()];
269                    let scope = scope.scope(&args);
270                    *item = expr_map.map.eval_ref(&scope, input)?;
271                }
272
273                Ok(base)
274            }
275            Expr::Project(expr_project) => match &*expr_project.base {
276                Expr::Arg(expr_arg) => {
277                    let Some(expr) = scope.resolve_arg(expr_arg, &expr_project.projection, input)
278                    else {
279                        return Err(crate::Error::expression_evaluation_failed(
280                            "failed to resolve argument",
281                        ));
282                    };
283
284                    expr.eval_ref(scope, input)
285                }
286                Expr::Reference(expr_reference) => {
287                    let Some(expr) = input.resolve_ref(expr_reference, &expr_project.projection)
288                    else {
289                        return Err(crate::Error::expression_evaluation_failed(
290                            "failed to resolve reference",
291                        ));
292                    };
293
294                    expr.eval_ref(scope, input)
295                }
296                _ => {
297                    let base = expr_project.base.eval_ref(scope, input)?;
298                    Ok(base.entry(&expr_project.projection).to_value())
299                }
300            },
301            Expr::Record(expr_record) => {
302                let mut ret = Vec::with_capacity(expr_record.len());
303
304                for expr in &expr_record.fields {
305                    ret.push(expr.eval_ref(scope, input)?);
306                }
307
308                Ok(Value::record_from_vec(ret))
309            }
310            Expr::Reference(expr_reference) => {
311                let Some(expr) = input.resolve_ref(expr_reference, &Projection::identity()) else {
312                    return Err(crate::Error::expression_evaluation_failed(
313                        "failed to resolve reference",
314                    ));
315                };
316
317                expr.eval_ref(scope, input)
318            }
319            Expr::Or(expr_or) => {
320                debug_assert!(!expr_or.operands.is_empty());
321
322                for operand in &expr_or.operands {
323                    if operand.eval_ref_bool(scope, input)? {
324                        return Ok(true.into());
325                    }
326                }
327
328                Ok(false.into())
329            }
330            Expr::Any(expr_any) => {
331                let list = expr_any.expr.eval_ref(scope, input)?;
332
333                let Value::List(items) = list else {
334                    return Err(crate::Error::expression_evaluation_failed(
335                        "Any expression must evaluate to a list",
336                    ));
337                };
338
339                for item in &items {
340                    match item {
341                        Value::Bool(true) => return Ok(true.into()),
342                        Value::Bool(false) => {}
343                        _ => {
344                            return Err(crate::Error::expression_evaluation_failed(
345                                "Any expression items must evaluate to bool",
346                            ));
347                        }
348                    }
349                }
350
351                Ok(false.into())
352            }
353            Expr::InList(expr_in_list) => {
354                let needle = expr_in_list.expr.eval_ref(scope, input)?;
355                let list = expr_in_list.list.eval_ref(scope, input)?;
356
357                let Value::List(items) = list else {
358                    return Err(crate::Error::expression_evaluation_failed(
359                        "InList right-hand side must evaluate to a list",
360                    ));
361                };
362
363                Ok(items.iter().any(|item| item == &needle).into())
364            }
365            Expr::AnyOp(e) => {
366                let lhs = e.lhs.eval_ref(scope, input)?;
367                let rhs = e.rhs.eval_ref(scope, input)?;
368                let Value::List(items) = rhs else {
369                    return Err(crate::Error::expression_evaluation_failed(
370                        "ANY right-hand side must evaluate to a list",
371                    ));
372                };
373                Ok(any_all_compare(&lhs, &items, e.op, /*all=*/ false)?.into())
374            }
375            Expr::AllOp(e) => {
376                let lhs = e.lhs.eval_ref(scope, input)?;
377                let rhs = e.rhs.eval_ref(scope, input)?;
378                let Value::List(items) = rhs else {
379                    return Err(crate::Error::expression_evaluation_failed(
380                        "ALL right-hand side must evaluate to a list",
381                    ));
382                };
383                Ok(any_all_compare(&lhs, &items, e.op, /*all=*/ true)?.into())
384            }
385            Expr::Match(expr_match) => {
386                let subject = expr_match.subject.eval_ref(scope, input)?;
387                for arm in &expr_match.arms {
388                    if subject == arm.pattern {
389                        return arm.expr.eval_ref(scope, input);
390                    }
391                }
392                expr_match.else_expr.eval_ref(scope, input)
393            }
394            Expr::Exists(expr_exists) => {
395                // Evaluate the subquery body. For Values bodies the rows are
396                // evaluated and flattened; for other bodies we evaluate the
397                // query as an expression.
398                match &expr_exists.subquery.body {
399                    ExprSet::Values(values) => {
400                        for row in &values.rows {
401                            let val = row.eval_ref(scope, input)?;
402                            match val {
403                                // An empty list means no rows — keep checking
404                                Value::List(items) if items.is_empty() => {}
405                                // Null means the row doesn't exist
406                                Value::Null => {}
407                                // Any other value means at least one row exists
408                                _ => return Ok(true.into()),
409                            }
410                        }
411                        Ok(false.into())
412                    }
413                    _ => todo!("ExprExists with non-Values body"),
414                }
415            }
416            Expr::Value(value) => Ok(value.clone()),
417            // A document path read: navigate the named wire form
418            // (`Value::Object`) by key, then cast the leaf to the extraction's
419            // declared type. This is how a driver-side in-memory check (e.g.
420            // the DynamoDB conditional-write filter probe) evaluates a lowered
421            // document path against a decoded item.
422            Expr::Func(super::ExprFunc::JsonExtract(func)) => {
423                let mut value = func.base.eval_ref(scope, input)?;
424                for key in &func.path {
425                    value = match value {
426                        Value::Object(mut object) => {
427                            match object.entries.iter().position(|(k, _)| k == key) {
428                                Some(index) => object.entries.swap_remove(index).1,
429                                None => return Ok(Value::Null),
430                            }
431                        }
432                        Value::Null => return Ok(Value::Null),
433                        other => {
434                            return Err(crate::Error::expression_evaluation_failed(format!(
435                                "document path step `{key}` into non-object value {other:?}"
436                            )));
437                        }
438                    };
439                }
440                func.ty.cast(&InputResolve(&*input), value)
441            }
442            Expr::Func(_) => Err(crate::Error::expression_evaluation_failed(
443                "database functions cannot be evaluated client-side",
444            )),
445            _ => todo!("expr={self:#?}"),
446        }
447    }
448
449    fn eval_ref_bool(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<bool> {
450        match self.eval_ref(scope, input)? {
451            Value::Bool(ret) => Ok(ret),
452            _ => Err(crate::Error::expression_evaluation_failed(
453                "expected boolean value",
454            )),
455        }
456    }
457
458    fn eval_ref_usize(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<usize> {
459        match self.eval_ref(scope, input)? {
460            Value::I64(n) if n >= 0 => Ok(n as usize),
461            _ => Err(crate::Error::expression_evaluation_failed(
462                "expected non-negative integer",
463            )),
464        }
465    }
466}
467
468impl ScopeStack<'_> {
469    fn resolve_arg(
470        &self,
471        expr_arg: &ExprArg,
472        projection: &Projection,
473        input: &mut impl Input,
474    ) -> Option<Expr> {
475        let mut nesting = expr_arg.nesting;
476        let mut scope = self;
477
478        while nesting > 0 {
479            nesting -= 1;
480
481            scope = match scope {
482                ScopeStack::Root => return None,
483                ScopeStack::Scope { parent, .. } => parent,
484            };
485        }
486
487        match scope {
488            ScopeStack::Root => input.resolve_arg(expr_arg, projection),
489            &ScopeStack::Scope { mut args, .. } => args.resolve_arg(expr_arg, projection),
490        }
491    }
492
493    fn scope<'child>(&'child self, args: &'child [Value]) -> ScopeStack<'child> {
494        ScopeStack::Scope { args, parent: self }
495    }
496}
497
498fn cmp_ordered(lhs: &Value, rhs: &Value) -> Result<Ordering> {
499    if lhs.is_null() || rhs.is_null() {
500        return Err(crate::Error::expression_evaluation_failed(
501            "ordered comparison with NULL is undefined",
502        ));
503    }
504    lhs.partial_cmp(rhs).ok_or_else(|| {
505        crate::Error::expression_evaluation_failed("ordered comparison between incompatible types")
506    })
507}
508
509fn any_all_compare(lhs: &Value, items: &[Value], op: BinaryOp, all: bool) -> Result<bool> {
510    for item in items {
511        let matches = match op {
512            BinaryOp::Eq => lhs == item,
513            BinaryOp::Ne => lhs != item,
514            BinaryOp::Ge => cmp_ordered(lhs, item)? != Ordering::Less,
515            BinaryOp::Gt => cmp_ordered(lhs, item)? == Ordering::Greater,
516            BinaryOp::Le => cmp_ordered(lhs, item)? != Ordering::Greater,
517            BinaryOp::Lt => cmp_ordered(lhs, item)? == Ordering::Less,
518            BinaryOp::Add | BinaryOp::Sub => {
519                return Err(crate::Error::expression_evaluation_failed(
520                    "ANY/ALL only supports comparison operators",
521                ));
522            }
523        };
524        if all {
525            if !matches {
526                return Ok(false);
527            }
528        } else if matches {
529            return Ok(true);
530        }
531    }
532    // ANY over empty list → false; ALL over empty list → true.
533    Ok(all)
534}