surrealdb_expr/expr/mutation.rs
1//! Whether a stored expression can modify data anywhere in its own tree.
2//!
3//! Used at definition time to reject a `COMPUTED` field body that writes: the
4//! walk descends into subqueries, idioms, blocks and closure bodies, so a
5//! mutation anywhere inside the body itself is found.
6//!
7//! A function *call* stays opaque: the callee's body lives in the catalog,
8//! which this crate cannot see, and whether it writes may depend on which
9//! branch the arguments select. Those are left to the runtime write refusal
10//! (`Options::no_write`), which fires at the point a write is actually
11//! reached. Call *arguments* are evaluated in place and are therefore walked.
12
13use crate::expr::Expr;
14use crate::expr::visit::{Visit, Visitor};
15
16/// Stops the walk as soon as a mutating construct is reached.
17struct FoundMutation;
18
19struct MutationScanner;
20
21impl Visitor for MutationScanner {
22 type Error = FoundMutation;
23
24 /// Walk an `Expr`, failing on anything that modifies data.
25 ///
26 /// The match is **exhaustive (no `_` arm) on purpose**, matching
27 /// [`crate::expr::computed_deps`]: adding an `Expr` variant should be a
28 /// build error here so a human classifies it, rather than a new statement
29 /// kind silently passing a check whose whole job is to reject writes.
30 fn visit_expr(&mut self, expr: &Expr) -> Result<(), Self::Error> {
31 match expr {
32 // Data-modifying statements and DDL.
33 Expr::Create(_)
34 | Expr::Update(_)
35 | Expr::Upsert(_)
36 | Expr::Delete(_)
37 | Expr::Relate(_)
38 | Expr::Insert(_)
39 | Expr::Define(_)
40 | Expr::Remove(_)
41 | Expr::Rebuild(_)
42 | Expr::Alter(_) => Err(FoundMutation),
43
44 // A GQL plan carries its mutations in its stages.
45 Expr::Match(plan) => {
46 if plan.has_mutations() {
47 Err(FoundMutation)
48 } else {
49 expr.visit(self)
50 }
51 }
52
53 // Everything else delegates to the default traversal, which
54 // descends into blocks, subqueries, idiom parts, closure bodies and
55 // call arguments — so a write buried in any of them is still found.
56 Expr::Literal(_)
57 | Expr::Param(_)
58 | Expr::Idiom(_)
59 | Expr::Table(_)
60 | Expr::Mock(_)
61 | Expr::Block(_)
62 | Expr::Constant(_)
63 | Expr::Prefix {
64 ..
65 }
66 | Expr::Postfix {
67 ..
68 }
69 | Expr::Binary {
70 ..
71 }
72 | Expr::FunctionCall(_)
73 | Expr::Closure(_)
74 | Expr::Break
75 | Expr::Continue
76 | Expr::Return(_)
77 | Expr::Throw(_)
78 | Expr::IfElse(_)
79 | Expr::Select(_)
80 | Expr::Info(_)
81 | Expr::Foreach(_)
82 | Expr::Let(_)
83 | Expr::Sleep(_)
84 | Expr::Explain {
85 ..
86 } => expr.visit(self),
87 }
88 }
89}
90
91impl Expr {
92 /// Whether this expression's own tree contains a data-modifying statement,
93 /// looking through subqueries, idiom parts, blocks and closure bodies.
94 ///
95 /// A call to a user-defined function, script, module or silo is opaque:
96 /// its body lives outside this expression, and is left to the runtime
97 /// write refusal. Arguments to such a call *are* walked, since they are
98 /// evaluated at the call site.
99 pub fn contains_mutation(&self) -> bool {
100 MutationScanner.visit_expr(self).is_err()
101 }
102}