Skip to main content

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}