Skip to main content

surrealdb_sql/
read_only.rs

1//! Whether a statement can run inside a read-only transaction.
2//!
3//! The predicate is consulted *before* a statement executes, to choose the
4//! transaction mode it runs under. It therefore has to answer from the syntax
5//! alone, with no catalog, no bound parameters and no runtime values.
6//!
7//! # The approximation is one-sided
8//!
9//! `read_only` **over-approximates**: it may call a statement writable that
10//! turns out only to read, and that costs nothing but a write transaction. It
11//! must never call a writing statement read-only — a mutation that reaches a
12//! read-only transaction fails partway through, after the statement has
13//! already started producing results.
14//!
15//! Every construct whose effect is invisible from the syntax therefore answers
16//! `false`. A closure invoked through a value the classifier cannot see into
17//! (`$fn(...)`, `value.unregistered(...)`), a custom or scripted function, a
18//! module or silo call, and the `eval::*` / `api::invoke` builtins that
19//! evaluate arbitrary nested queries are all writable as far as this walk is
20//! concerned.
21//!
22//! # Keeping this in step with the expression layer
23//!
24//! `surrealdb-expr` carries the same predicate over its own tree, and the two
25//! enums evolve separately. The equivalence test in
26//! `surrealdb-core`'s `dbs::read_only_test` asserts that for every fixture the
27//! surface AST and the expression it converts to answer identically, which is
28//! what catches a variant added to one side and not the other.
29
30use std::ops::Bound;
31
32use crate::ast::TopLevelExpr;
33use crate::block::Block;
34use crate::closure::Closure;
35use crate::expression::Expr;
36use crate::fetch::{Fetch, Fetchs};
37use crate::field::{Field, Fields, Selector};
38use crate::function::{Function, FunctionCall};
39use crate::group::{Group, Groups};
40use crate::idiom::Idiom;
41use crate::limit::Limit;
42use crate::literal::Literal;
43use crate::lookup::{Lookup, LookupSubject};
44use crate::method::is_builtin_method;
45use crate::operator::PostfixOperator;
46use crate::order::{OrderList, Ordering};
47use crate::part::{DestructurePart, Part, RecurseInstruction};
48use crate::record_id::{RecordIdKeyLit, RecordIdKeyRangeLit, RecordIdLit};
49use crate::split::{Split, Splits};
50use crate::start::Start;
51use crate::statements::{
52	ForeachStatement, IfelseStatement, InfoStatement, OutputStatement, SelectStatement,
53	SetStatement,
54};
55
56impl TopLevelExpr {
57	/// Whether this statement can run inside a read-only transaction.
58	///
59	/// See the [module docs](self) for the one-sided contract this upholds.
60	pub fn read_only(&self) -> bool {
61		match self {
62			TopLevelExpr::Begin
63			| TopLevelExpr::Cancel
64			| TopLevelExpr::Commit
65			| TopLevelExpr::Show(_) => true,
66			TopLevelExpr::Kill(_)
67			| TopLevelExpr::Live(_)
68			| TopLevelExpr::Option(_)
69			| TopLevelExpr::Use(_)
70			| TopLevelExpr::Access(_) => false,
71			TopLevelExpr::Expr(expr) => expr.read_only(),
72		}
73	}
74}
75
76impl Expr {
77	/// Whether evaluating this expression does only reads.
78	///
79	/// See the [module docs](self) for the one-sided contract this upholds.
80	pub fn read_only(&self) -> bool {
81		match self {
82			Expr::Param(_)
83			| Expr::Table(_)
84			| Expr::Mock(_)
85			| Expr::Constant(_)
86			| Expr::Break
87			| Expr::Continue
88			| Expr::Sleep(_) => true,
89
90			Expr::Info(info) => info.read_only(),
91
92			// Composite literals evaluate their element expressions in place.
93			Expr::Literal(l) => l.read_only(),
94
95			Expr::Idiom(x) => x.read_only(),
96			Expr::Block(block) => block.read_only(),
97			Expr::Prefix {
98				expr,
99				..
100			} => expr.read_only(),
101			Expr::Postfix {
102				expr,
103				op,
104			} => {
105				match op {
106					// Calling a closure executes a body this expression can only
107					// see when the target is a closure literal; anything else
108					// (a param, a field) must over-approximate to writable.
109					PostfixOperator::Call(args) => {
110						matches!(&**expr, Expr::Closure(_))
111							&& expr.read_only() && args.iter().all(Expr::read_only)
112					}
113					// A registered method dispatches to a builtin, so only the
114					// receiver and arguments carry user expressions. Any other
115					// name invokes a closure stored on the receiver — a runtime
116					// value whose body is invisible here — and must
117					// over-approximate to writable, mirroring `Part::Method`.
118					PostfixOperator::MethodCall(name, args) => {
119						is_builtin_method(name)
120							&& expr.read_only() && args.iter().all(Expr::read_only)
121					}
122					PostfixOperator::Range | PostfixOperator::RangeSkip => expr.read_only(),
123				}
124			}
125			Expr::Binary {
126				left,
127				right,
128				..
129			} => left.read_only() && right.read_only(),
130			Expr::FunctionCall(function) => function.read_only(),
131			Expr::Return(stmt) => stmt.read_only(),
132			Expr::Throw(expr) => expr.read_only(),
133			Expr::IfElse(s) => s.read_only(),
134			Expr::Select(s) => s.read_only(),
135			Expr::Let(s) => s.read_only(),
136			Expr::Foreach(s) => s.read_only(),
137			Expr::Explain {
138				statement,
139				..
140			} => statement.read_only(),
141			// A closure literal is inert as a value, but wherever it can flow
142			// it can also be invoked (call operator, closure-taking builtins),
143			// so a writing body makes the expression writable.
144			Expr::Closure(c) => c.read_only(),
145			Expr::Create(_)
146			| Expr::Update(_)
147			| Expr::Delete(_)
148			| Expr::Relate(_)
149			| Expr::Insert(_)
150			| Expr::Define(_)
151			| Expr::Remove(_)
152			| Expr::Rebuild(_)
153			| Expr::Upsert(_)
154			| Expr::Alter(_) => false,
155		}
156	}
157}
158
159impl Closure {
160	fn read_only(&self) -> bool {
161		self.body.read_only()
162	}
163}
164
165impl Block {
166	pub fn read_only(&self) -> bool {
167		self.0.iter().all(|x| x.read_only())
168	}
169}
170
171impl Literal {
172	pub fn read_only(&self) -> bool {
173		match self {
174			Literal::None
175			| Literal::Null
176			| Literal::UnboundedRange
177			| Literal::Bool(_)
178			| Literal::Float(_)
179			| Literal::Integer(_)
180			| Literal::Decimal(_)
181			| Literal::String(_)
182			| Literal::Bytes(_)
183			| Literal::Regex(_)
184			| Literal::Duration(_)
185			| Literal::Datetime(_)
186			| Literal::Uuid(_)
187			| Literal::File(_)
188			| Literal::Geometry(_) => true,
189			Literal::RecordId(record_id_lit) => record_id_lit.read_only(),
190			Literal::Array(exprs) => exprs.iter().all(|x| x.read_only()),
191			Literal::Set(exprs) => exprs.iter().all(|x| x.read_only()),
192			Literal::Object(items) => items.iter().all(|x| x.value.read_only()),
193		}
194	}
195}
196
197impl RecordIdLit {
198	pub fn read_only(&self) -> bool {
199		self.key.read_only()
200	}
201}
202
203impl RecordIdKeyLit {
204	pub fn read_only(&self) -> bool {
205		match self {
206			RecordIdKeyLit::Number(_)
207			| RecordIdKeyLit::String(_)
208			| RecordIdKeyLit::Uuid(_)
209			| RecordIdKeyLit::Generate(_) => true,
210			RecordIdKeyLit::Range(record_id_key_range_lit) => record_id_key_range_lit.read_only(),
211			RecordIdKeyLit::Array(exprs) => exprs.iter().all(|x| x.read_only()),
212			RecordIdKeyLit::Object(items) => items.iter().all(|x| x.value.read_only()),
213		}
214	}
215}
216
217impl RecordIdKeyRangeLit {
218	pub fn read_only(&self) -> bool {
219		let bound_read_only = |bound: &Bound<RecordIdKeyLit>| match bound {
220			Bound::Included(x) | Bound::Excluded(x) => x.read_only(),
221			Bound::Unbounded => true,
222		};
223		bound_read_only(&self.start) && bound_read_only(&self.end)
224	}
225}
226
227impl Idiom {
228	pub fn read_only(&self) -> bool {
229		self.0.iter().all(|v| v.read_only())
230	}
231}
232
233impl Part {
234	pub fn read_only(&self) -> bool {
235		match self {
236			Part::Start(v) => v.read_only(),
237			Part::Where(v) => v.read_only(),
238			Part::Value(v) => v.read_only(),
239			// A builtin method writes nothing beyond what its arguments
240			// carry, but an unregistered name invokes a closure stored on
241			// the receiver — a runtime value whose body is invisible here —
242			// so it must over-approximate to writable.
243			Part::Method(name, v) => is_builtin_method(name) && v.iter().all(Expr::read_only),
244			Part::Graph(v) => v.read_only(),
245			Part::Destructure(v) => v.iter().all(DestructurePart::read_only),
246			Part::Recurse(_, alias, instruction) => {
247				alias.as_ref().map(|x| x.read_only()).unwrap_or(true)
248					&& instruction.as_ref().map(|x| x.read_only()).unwrap_or(true)
249			}
250			Part::All
251			| Part::Flatten
252			| Part::Last
253			| Part::First
254			| Part::Field(_)
255			| Part::Optional
256			| Part::Doc
257			| Part::RepeatRecurse => true,
258		}
259	}
260}
261
262impl DestructurePart {
263	pub fn read_only(&self) -> bool {
264		match self {
265			DestructurePart::All(_) | DestructurePart::Field(_) => true,
266			DestructurePart::Aliased(_, v) => v.read_only(),
267			DestructurePart::Destructure(_, v) => v.iter().all(DestructurePart::read_only),
268		}
269	}
270}
271
272impl RecurseInstruction {
273	pub fn read_only(&self) -> bool {
274		match self {
275			RecurseInstruction::Path {
276				..
277			}
278			| RecurseInstruction::Collect {
279				..
280			} => true,
281			RecurseInstruction::Shortest {
282				expects,
283				..
284			} => expects.read_only(),
285		}
286	}
287}
288
289impl Lookup {
290	pub fn read_only(&self) -> bool {
291		self.what.iter().all(LookupSubject::read_only)
292			&& self.expr.as_ref().map(|x| x.read_only()).unwrap_or(true)
293			&& self.cond.as_ref().map(|x| x.0.read_only()).unwrap_or(true)
294			&& self.split.as_ref().map(|x| x.read_only()).unwrap_or(true)
295			&& self.group.as_ref().map(|x| x.read_only()).unwrap_or(true)
296			&& self.order.as_ref().map(|x| x.read_only()).unwrap_or(true)
297			&& self.limit.as_ref().map(|x| x.read_only()).unwrap_or(true)
298			&& self.start.as_ref().map(|x| x.read_only()).unwrap_or(true)
299			&& self.alias.as_ref().map(|x| x.read_only()).unwrap_or(true)
300	}
301}
302
303impl LookupSubject {
304	/// A range subject's bounds are record-id keys, which carry arbitrary
305	/// expressions in their array and object forms.
306	pub fn read_only(&self) -> bool {
307		match self {
308			LookupSubject::Table {
309				..
310			} => true,
311			LookupSubject::Range {
312				range,
313				..
314			} => range.read_only(),
315		}
316	}
317}
318
319/// `INFO` reads catalog metadata, but its subject and `VERSION` clause are
320/// expressions the statement evaluates first, so a write reaches the
321/// transaction through them.
322impl InfoStatement {
323	pub fn read_only(&self) -> bool {
324		match self {
325			InfoStatement::Root(_, version)
326			| InfoStatement::Ns(_, version)
327			| InfoStatement::Db(_, version) => version.as_ref().is_none_or(|version| version.read_only()),
328			InfoStatement::Tb(table, _, version) => {
329				table.read_only() && version.as_ref().is_none_or(|version| version.read_only())
330			}
331			InfoStatement::User(user, _, _) => user.read_only(),
332			InfoStatement::Index(index, table, _) => index.read_only() && table.read_only(),
333		}
334	}
335}
336
337impl Function {
338	pub fn read_only(&self) -> bool {
339		match self {
340			Self::Custom(_)
341			| Self::Script(_)
342			| Self::Module(_, _)
343			| Self::Silo {
344				..
345			} => false,
346			Self::Normal(f) => !crate::method::is_writer_builtin(f),
347			Self::Model(_) => true,
348		}
349	}
350}
351
352impl FunctionCall {
353	pub fn read_only(&self) -> bool {
354		self.receiver.read_only() && self.arguments.iter().all(|x| x.read_only())
355	}
356}
357
358impl Fields {
359	pub fn read_only(&self) -> bool {
360		match self {
361			Fields::Value(field) => field.read_only(),
362			Fields::Select(fields) => fields.iter().all(|x| x.read_only()),
363		}
364	}
365}
366
367impl Field {
368	pub fn read_only(&self) -> bool {
369		match self {
370			Field::All => true,
371			Field::Single(x) => x.read_only(),
372		}
373	}
374}
375
376impl Selector {
377	pub fn read_only(&self) -> bool {
378		self.expr.read_only()
379	}
380}
381
382impl Fetchs {
383	pub fn read_only(&self) -> bool {
384		self.0.iter().all(|x| x.read_only())
385	}
386}
387
388impl Fetch {
389	pub fn read_only(&self) -> bool {
390		self.0.read_only()
391	}
392}
393
394impl Groups {
395	pub fn read_only(&self) -> bool {
396		self.0.iter().all(|x| x.read_only())
397	}
398}
399
400impl Group {
401	pub fn read_only(&self) -> bool {
402		self.0.read_only()
403	}
404}
405
406impl Splits {
407	pub fn read_only(&self) -> bool {
408		self.0.iter().all(|x| x.read_only())
409	}
410}
411
412impl Split {
413	pub fn read_only(&self) -> bool {
414		self.0.read_only()
415	}
416}
417
418impl Ordering {
419	pub fn read_only(&self) -> bool {
420		match self {
421			Ordering::Random => true,
422			Ordering::Order(list) => list.read_only(),
423		}
424	}
425}
426
427impl OrderList {
428	pub fn read_only(&self) -> bool {
429		self.0.iter().all(|x| x.value.read_only())
430	}
431}
432
433impl Limit {
434	pub fn read_only(&self) -> bool {
435		self.0.read_only()
436	}
437}
438
439impl Start {
440	pub fn read_only(&self) -> bool {
441		self.0.read_only()
442	}
443}
444
445impl SelectStatement {
446	pub fn read_only(&self) -> bool {
447		!self.for_update
448			&& self.fields.read_only()
449			&& self.omit.iter().all(|v| v.read_only())
450			&& self.what.iter().all(|v| v.read_only())
451			&& self.cond.as_ref().map(|x| x.0.read_only()).unwrap_or(true)
452			&& self.split.as_ref().map(|x| x.read_only()).unwrap_or(true)
453			&& self.group.as_ref().map(|x| x.read_only()).unwrap_or(true)
454			&& self.order.as_ref().map(|x| x.read_only()).unwrap_or(true)
455			&& self.limit.as_ref().map(|x| x.read_only()).unwrap_or(true)
456			&& self.start.as_ref().map(|x| x.read_only()).unwrap_or(true)
457			&& self.fetch.as_ref().map(|x| x.read_only()).unwrap_or(true)
458			&& self.version.read_only()
459			&& self.timeout.read_only()
460	}
461}
462
463impl SetStatement {
464	pub fn read_only(&self) -> bool {
465		self.what.read_only()
466	}
467}
468
469impl OutputStatement {
470	pub fn read_only(&self) -> bool {
471		self.what.read_only() && self.fetch.as_ref().map(|x| x.read_only()).unwrap_or(true)
472	}
473}
474
475impl IfelseStatement {
476	pub fn read_only(&self) -> bool {
477		self.exprs.iter().all(|x| x.0.read_only() && x.1.read_only())
478			&& self.close.as_ref().map(|x| x.read_only()).unwrap_or(true)
479	}
480}
481
482impl ForeachStatement {
483	pub fn read_only(&self) -> bool {
484		self.range.read_only() && self.block.read_only()
485	}
486}