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() != expr_is_null.negated).into())
232            }
233            Expr::IsVariant(_) => Err(crate::Error::expression_evaluation_failed(
234                "IsVariant must be lowered before evaluation",
235            )),
236            Expr::Variant(_) => Err(crate::Error::expression_evaluation_failed(
237                "Variant must be lowered before evaluation",
238            )),
239            Expr::Let(expr_let) => {
240                let args: Vec<_> = expr_let
241                    .bindings
242                    .iter()
243                    .map(|b| b.eval_ref(scope, input))
244                    .collect::<Result<_, _>>()?;
245                let scope = scope.scope(&args);
246                expr_let.body.eval_ref(&scope, input)
247            }
248            Expr::Not(expr_not) => {
249                let value = expr_not.expr.eval_ref_bool(scope, input)?;
250                Ok((!value).into())
251            }
252            Expr::List(exprs) => {
253                let mut ret = vec![];
254
255                for expr in &exprs.items {
256                    ret.push(expr.eval_ref(scope, input)?);
257                }
258
259                Ok(Value::List(ret))
260            }
261            Expr::Map(expr_map) => {
262                let mut base = expr_map.base.eval_ref(scope, input)?;
263
264                let Value::List(items) = &mut base else {
265                    return Err(crate::Error::expression_evaluation_failed(
266                        "Map base must evaluate to a list",
267                    ));
268                };
269
270                for item in items.iter_mut() {
271                    let args = [item.take()];
272                    let scope = scope.scope(&args);
273                    *item = expr_map.map.eval_ref(&scope, input)?;
274                }
275
276                Ok(base)
277            }
278            Expr::Project(expr_project) => match &*expr_project.base {
279                Expr::Arg(expr_arg) => {
280                    let Some(expr) = scope.resolve_arg(expr_arg, &expr_project.projection, input)
281                    else {
282                        return Err(crate::Error::expression_evaluation_failed(
283                            "failed to resolve argument",
284                        ));
285                    };
286
287                    expr.eval_ref(scope, input)
288                }
289                Expr::Reference(expr_reference) => {
290                    let Some(expr) = input.resolve_ref(expr_reference, &expr_project.projection)
291                    else {
292                        return Err(crate::Error::expression_evaluation_failed(
293                            "failed to resolve reference",
294                        ));
295                    };
296
297                    expr.eval_ref(scope, input)
298                }
299                _ => {
300                    let base = expr_project.base.eval_ref(scope, input)?;
301                    Ok(base.entry(&expr_project.projection).to_value())
302                }
303            },
304            Expr::Record(expr_record) => {
305                let mut ret = Vec::with_capacity(expr_record.len());
306
307                for expr in &expr_record.fields {
308                    ret.push(expr.eval_ref(scope, input)?);
309                }
310
311                Ok(Value::record_from_vec(ret))
312            }
313            Expr::Reference(expr_reference) => {
314                let Some(expr) = input.resolve_ref(expr_reference, &Projection::identity()) else {
315                    return Err(crate::Error::expression_evaluation_failed(
316                        "failed to resolve reference",
317                    ));
318                };
319
320                expr.eval_ref(scope, input)
321            }
322            Expr::Or(expr_or) => {
323                debug_assert!(!expr_or.operands.is_empty());
324
325                for operand in &expr_or.operands {
326                    if operand.eval_ref_bool(scope, input)? {
327                        return Ok(true.into());
328                    }
329                }
330
331                Ok(false.into())
332            }
333            Expr::Any(expr_any) => {
334                let list = expr_any.expr.eval_ref(scope, input)?;
335
336                let Value::List(items) = list else {
337                    return Err(crate::Error::expression_evaluation_failed(
338                        "Any expression must evaluate to a list",
339                    ));
340                };
341
342                for item in &items {
343                    match item {
344                        Value::Bool(true) => return Ok(true.into()),
345                        Value::Bool(false) => {}
346                        _ => {
347                            return Err(crate::Error::expression_evaluation_failed(
348                                "Any expression items must evaluate to bool",
349                            ));
350                        }
351                    }
352                }
353
354                Ok(false.into())
355            }
356            Expr::InList(expr_in_list) => {
357                let needle = expr_in_list.expr.eval_ref(scope, input)?;
358                let list = expr_in_list.list.eval_ref(scope, input)?;
359
360                let Value::List(items) = list else {
361                    return Err(crate::Error::expression_evaluation_failed(
362                        "InList right-hand side must evaluate to a list",
363                    ));
364                };
365
366                Ok(items.iter().any(|item| item == &needle).into())
367            }
368            Expr::AnyOp(e) => {
369                let lhs = e.lhs.eval_ref(scope, input)?;
370                let rhs = e.rhs.eval_ref(scope, input)?;
371                let Value::List(items) = rhs else {
372                    return Err(crate::Error::expression_evaluation_failed(
373                        "ANY right-hand side must evaluate to a list",
374                    ));
375                };
376                Ok(any_all_compare(&lhs, &items, e.op, /*all=*/ false)?.into())
377            }
378            Expr::AllOp(e) => {
379                let lhs = e.lhs.eval_ref(scope, input)?;
380                let rhs = e.rhs.eval_ref(scope, input)?;
381                let Value::List(items) = rhs else {
382                    return Err(crate::Error::expression_evaluation_failed(
383                        "ALL right-hand side must evaluate to a list",
384                    ));
385                };
386                Ok(any_all_compare(&lhs, &items, e.op, /*all=*/ true)?.into())
387            }
388            Expr::Match(expr_match) => {
389                let subject = expr_match.subject.eval_ref(scope, input)?;
390                for arm in &expr_match.arms {
391                    if subject == arm.pattern {
392                        return arm.expr.eval_ref(scope, input);
393                    }
394                }
395                expr_match.else_expr.eval_ref(scope, input)
396            }
397            Expr::Exists(expr_exists) => {
398                // Evaluate the subquery body. For Values bodies the rows are
399                // evaluated and flattened; for other bodies we evaluate the
400                // query as an expression.
401                match &expr_exists.subquery.body {
402                    ExprSet::Values(values) => {
403                        for row in &values.rows {
404                            let val = row.eval_ref(scope, input)?;
405                            match val {
406                                // An empty list means no rows — keep checking
407                                Value::List(items) if items.is_empty() => {}
408                                // Null means the row doesn't exist
409                                Value::Null => {}
410                                // Any other value means at least one row exists
411                                _ => return Ok(true.into()),
412                            }
413                        }
414                        Ok(false.into())
415                    }
416                    _ => todo!("ExprExists with non-Values body"),
417                }
418            }
419            Expr::Value(value) | Expr::Static(value) => Ok(value.clone()),
420            // A document path read: navigate the named wire form
421            // (`Value::Object`) by key, then cast the leaf to the extraction's
422            // declared type. This is how a driver-side in-memory check (e.g.
423            // the DynamoDB conditional-write filter probe) evaluates a lowered
424            // document path against a decoded item.
425            Expr::Func(super::ExprFunc::JsonExtract(func)) => {
426                let mut value = func.base.eval_ref(scope, input)?;
427                for key in &func.path {
428                    value = match value {
429                        Value::Object(mut object) => {
430                            match object.entries.iter().position(|(k, _)| k == key) {
431                                Some(index) => object.entries.swap_remove(index).1,
432                                None => return Ok(Value::Null),
433                            }
434                        }
435                        Value::Null => return Ok(Value::Null),
436                        other => {
437                            return Err(crate::Error::expression_evaluation_failed(format!(
438                                "document path step `{key}` into non-object value {other:?}"
439                            )));
440                        }
441                    };
442                }
443                func.ty.cast(&InputResolve(&*input), value)
444            }
445            Expr::Func(_) => Err(crate::Error::expression_evaluation_failed(
446                "database functions cannot be evaluated client-side",
447            )),
448            Expr::Incoming(_) => Err(crate::Error::expression_evaluation_failed(
449                "incoming values can only be evaluated by the database",
450            )),
451            _ => todo!("expr={self:#?}"),
452        }
453    }
454
455    fn eval_ref_bool(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<bool> {
456        match self.eval_ref(scope, input)? {
457            Value::Bool(ret) => Ok(ret),
458            _ => Err(crate::Error::expression_evaluation_failed(
459                "expected boolean value",
460            )),
461        }
462    }
463
464    fn eval_ref_usize(&self, scope: &ScopeStack<'_>, input: &mut impl Input) -> Result<usize> {
465        match self.eval_ref(scope, input)? {
466            Value::I64(n) if n >= 0 => Ok(n as usize),
467            _ => Err(crate::Error::expression_evaluation_failed(
468                "expected non-negative integer",
469            )),
470        }
471    }
472}
473
474impl ScopeStack<'_> {
475    fn resolve_arg(
476        &self,
477        expr_arg: &ExprArg,
478        projection: &Projection,
479        input: &mut impl Input,
480    ) -> Option<Expr> {
481        let mut nesting = expr_arg.nesting;
482        let mut scope = self;
483
484        while nesting > 0 {
485            nesting -= 1;
486
487            scope = match scope {
488                ScopeStack::Root => return None,
489                ScopeStack::Scope { parent, .. } => parent,
490            };
491        }
492
493        match scope {
494            ScopeStack::Root => input.resolve_arg(expr_arg, projection),
495            &ScopeStack::Scope { mut args, .. } => args.resolve_arg(expr_arg, projection),
496        }
497    }
498
499    fn scope<'child>(&'child self, args: &'child [Value]) -> ScopeStack<'child> {
500        ScopeStack::Scope { args, parent: self }
501    }
502}
503
504fn cmp_ordered(lhs: &Value, rhs: &Value) -> Result<Ordering> {
505    if lhs.is_null() || rhs.is_null() {
506        return Err(crate::Error::expression_evaluation_failed(
507            "ordered comparison with NULL is undefined",
508        ));
509    }
510    lhs.partial_cmp(rhs).ok_or_else(|| {
511        crate::Error::expression_evaluation_failed("ordered comparison between incompatible types")
512    })
513}
514
515fn any_all_compare(lhs: &Value, items: &[Value], op: BinaryOp, all: bool) -> Result<bool> {
516    for item in items {
517        let matches = match op {
518            BinaryOp::Eq => lhs == item,
519            BinaryOp::Ne => lhs != item,
520            BinaryOp::Ge => cmp_ordered(lhs, item)? != Ordering::Less,
521            BinaryOp::Gt => cmp_ordered(lhs, item)? == Ordering::Greater,
522            BinaryOp::Le => cmp_ordered(lhs, item)? != Ordering::Greater,
523            BinaryOp::Lt => cmp_ordered(lhs, item)? == Ordering::Less,
524            BinaryOp::Add | BinaryOp::Sub => {
525                return Err(crate::Error::expression_evaluation_failed(
526                    "ANY/ALL only supports comparison operators",
527                ));
528            }
529        };
530        if all {
531            if !matches {
532                return Ok(false);
533            }
534        } else if matches {
535            return Ok(true);
536        }
537    }
538    // ANY over empty list → false; ALL over empty list → true.
539    Ok(all)
540}