Skip to main content

surrealdb_expr/expr/
expression.rs

1use revision::{DeserializeRevisioned, Revisioned, SerializeRevisioned};
2use surrealdb_types::{RecordId, SqlFormat, ToSql};
3
4use super::SleepStatement;
5use crate::expr::closure::ClosureExpr;
6use crate::expr::statements::info::InfoStructure;
7use crate::expr::statements::{
8	AlterStatement, CreateStatement, DefineStatement, DeleteStatement, ForeachStatement,
9	IfelseStatement, InfoStatement, InsertStatement, OutputStatement, RebuildStatement,
10	RelateStatement, RemoveStatement, SelectStatement, SetStatement, UpdateStatement,
11	UpsertStatement,
12};
13use crate::expr::{
14	BinaryOperator, Block, Constant, FunctionCall, Idiom, Literal, Mock, ObjectEntry, Param,
15	PostfixOperator, PrefixOperator, RecordIdKeyLit, RecordIdLit,
16};
17use crate::types::PublicValue;
18use crate::val::table_name_public::IntoTableName;
19use crate::val::{TableName, Value};
20
21#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Default)]
22pub enum ExplainFormat {
23	#[default]
24	Text,
25	Json,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub enum Expr {
30	Literal(Literal),
31	Param(Param),
32	Idiom(Idiom),
33	// Maybe move into Literal?
34	Table(TableName),
35	// This type can probably be removed in favour of range expressions.
36	Mock(Mock),
37	Block(Box<Block>),
38	Constant(Constant),
39	Prefix {
40		op: PrefixOperator,
41		expr: Box<Expr>,
42	},
43	Postfix {
44		expr: Box<Expr>,
45		op: PostfixOperator,
46	},
47	Binary {
48		left: Box<Expr>,
49		op: BinaryOperator,
50		right: Box<Expr>,
51	},
52	// TODO: Factor out the call from the function expression.
53	FunctionCall(Box<FunctionCall>),
54
55	Closure(Box<ClosureExpr>),
56
57	Break,
58	Continue,
59	Return(Box<OutputStatement>),
60	Throw(Box<Expr>),
61
62	IfElse(Box<IfelseStatement>),
63	Select(Box<SelectStatement>),
64	Create(Box<CreateStatement>),
65	Update(Box<UpdateStatement>),
66	Upsert(Box<UpsertStatement>),
67	Delete(Box<DeleteStatement>),
68	Relate(Box<RelateStatement>),
69	Insert(Box<InsertStatement>),
70	Define(Box<DefineStatement>),
71	Remove(Box<RemoveStatement>),
72	Rebuild(Box<RebuildStatement>),
73	Alter(Box<AlterStatement>),
74	Info(Box<InfoStatement>),
75	Foreach(Box<ForeachStatement>),
76	Let(Box<SetStatement>),
77	Sleep(Box<SleepStatement>),
78	Explain {
79		format: ExplainFormat,
80		analyze: bool,
81		statement: Box<Expr>,
82	},
83	/// An GQL `MATCH` query, lowered to its declarative binding-table plan.
84	///
85	/// Only constructed by the GQL lowering at top level. It runs exclusively
86	/// under the streaming execution planner; see the `compute` and
87	/// `From<expr::Expr> for sql::Expr` arms for the invariants it relies on.
88	// Constructed by the GQL lowering, which lands as a sibling piece of PR-A.
89	Match(Box<crate::expr::match_plan::MatchPlan>),
90}
91
92impl Expr {
93	/// Check if this expression does only reads.
94	pub fn read_only(&self) -> bool {
95		match self {
96			Expr::Param(_)
97			| Expr::Table(_)
98			| Expr::Mock(_)
99			| Expr::Constant(_)
100			| Expr::Break
101			| Expr::Continue
102			| Expr::Sleep(_) => true,
103
104			Expr::Info(info) => info.read_only(),
105
106			// Composite literals evaluate their element expressions in place.
107			Expr::Literal(l) => l.read_only(),
108
109			Expr::Idiom(x) => x.read_only(),
110			Expr::Block(block) => block.read_only(),
111			Expr::Prefix {
112				expr,
113				..
114			} => expr.read_only(),
115			Expr::Postfix {
116				expr,
117				op,
118			} => {
119				match op {
120					// Calling a closure executes a body this expression can only
121					// see when the target is a closure literal; anything else
122					// (a param, a field) must over-approximate to writable.
123					PostfixOperator::Call(args) => {
124						matches!(&**expr, Expr::Closure(_))
125							&& expr.read_only() && args.iter().all(Expr::read_only)
126					}
127					// A registered method dispatches to a builtin, so only the
128					// receiver and arguments carry user expressions. Any other
129					// name invokes a closure stored on the receiver — a runtime
130					// value whose body is invisible here — and must
131					// over-approximate to writable, mirroring `Part::Method`.
132					PostfixOperator::MethodCall(name, args) => {
133						crate::expr::method::is_builtin_method(name)
134							&& expr.read_only() && args.iter().all(Expr::read_only)
135					}
136					PostfixOperator::Range | PostfixOperator::RangeSkip => expr.read_only(),
137				}
138			}
139			Expr::Binary {
140				left,
141				right,
142				..
143			} => left.read_only() && right.read_only(),
144			Expr::FunctionCall(function) => function.read_only(),
145			Expr::Return(expr) => expr.read_only(),
146			Expr::Throw(expr) => expr.read_only(),
147			Expr::IfElse(s) => s.read_only(),
148			Expr::Select(s) => s.read_only(),
149			Expr::Let(s) => s.read_only(),
150			Expr::Foreach(s) => s.read_only(),
151			Expr::Explain {
152				statement,
153				..
154			} => statement.read_only(),
155			// A closure literal is inert as a value, but wherever it can flow
156			// it can also be invoked (call operator, closure-taking builtins),
157			// so a writing body makes the expression writable.
158			Expr::Closure(c) => c.body.read_only(),
159			// A GQL query is read-only unless it carries mutation stages; a
160			// mutation-bearing plan must run under a write transaction.
161			Expr::Match(plan) => !plan.has_mutations(),
162			Expr::Create(_)
163			| Expr::Update(_)
164			| Expr::Delete(_)
165			| Expr::Relate(_)
166			| Expr::Insert(_)
167			| Expr::Define(_)
168			| Expr::Remove(_)
169			| Expr::Rebuild(_)
170			| Expr::Upsert(_)
171			| Expr::Alter(_) => false,
172		}
173	}
174
175	pub fn from_public_value(value: PublicValue) -> Self {
176		match value {
177			surrealdb_types::Value::None => Expr::Literal(Literal::None),
178			surrealdb_types::Value::Null => Expr::Literal(Literal::Null),
179			surrealdb_types::Value::Bool(b) => Expr::Literal(Literal::Bool(b)),
180			surrealdb_types::Value::Number(n) => match n {
181				surrealdb_types::Number::Int(i) => Expr::Literal(Literal::Integer(i)),
182				surrealdb_types::Number::Float(f) => Expr::Literal(Literal::Float(f)),
183				surrealdb_types::Number::Decimal(d) => Expr::Literal(Literal::Decimal(d)),
184			},
185			surrealdb_types::Value::String(s) => Expr::Literal(Literal::String(s.into())),
186			surrealdb_types::Value::Bytes(b) => {
187				Expr::Literal(Literal::Bytes(crate::val::Bytes(b.into_inner())))
188			}
189			surrealdb_types::Value::Duration(d) => {
190				Expr::Literal(Literal::Duration(crate::val::Duration(d.into_inner())))
191			}
192			surrealdb_types::Value::Datetime(d) => {
193				Expr::Literal(Literal::Datetime(crate::val::Datetime(d.into_inner())))
194			}
195			surrealdb_types::Value::Uuid(u) => {
196				Expr::Literal(Literal::Uuid(crate::val::Uuid(u.into_inner())))
197			}
198			surrealdb_types::Value::Array(a) => {
199				Expr::Literal(Literal::Array(a.into_iter().map(Expr::from_public_value).collect()))
200			}
201			surrealdb_types::Value::Set(s) => {
202				Expr::Literal(Literal::Array(s.into_iter().map(Expr::from_public_value).collect()))
203			}
204			surrealdb_types::Value::Object(o) => Expr::Literal(Literal::Object(
205				o.into_iter()
206					.map(|(k, v)| ObjectEntry {
207						key: k.into(),
208						value: Expr::from_public_value(v),
209					})
210					.collect(),
211			)),
212			surrealdb_types::Value::Table(t) => Expr::Table(t.into_table_name()),
213			surrealdb_types::Value::RecordId(RecordId {
214				table,
215				key,
216			}) => {
217				let key_lit = match key {
218					surrealdb_types::RecordIdKey::Number(n) => RecordIdKeyLit::Number(n),
219					surrealdb_types::RecordIdKey::String(s) => RecordIdKeyLit::String(s.into()),
220					surrealdb_types::RecordIdKey::Uuid(u) => {
221						RecordIdKeyLit::Uuid(crate::val::Uuid(u.into_inner()))
222					}
223					surrealdb_types::RecordIdKey::Array(a) => {
224						RecordIdKeyLit::Array(a.into_iter().map(Expr::from_public_value).collect())
225					}
226					surrealdb_types::RecordIdKey::Object(o) => RecordIdKeyLit::Object(
227						o.into_iter()
228							.map(|(k, v)| ObjectEntry {
229								key: k.into(),
230								value: Expr::from_public_value(v),
231							})
232							.collect(),
233					),
234					_ => return Expr::Literal(Literal::None), // For unsupported key types
235				};
236				Expr::Literal(Literal::RecordId(RecordIdLit {
237					table: table.into_table_name(),
238					key: key_lit,
239				}))
240			}
241			surrealdb_types::Value::Geometry(g) => Expr::Literal(Literal::Geometry(g.into())),
242			surrealdb_types::Value::File(f) => {
243				Expr::Literal(Literal::File(crate::val::File::new(f.bucket, f.key)))
244			}
245			surrealdb_types::Value::Range(r) => Expr::from(*r),
246			surrealdb_types::Value::Regex(r) => {
247				Expr::Literal(Literal::Regex(crate::val::Regex(r.into_inner())))
248			}
249		}
250	}
251}
252
253impl From<surrealdb_types::Range> for Expr {
254	fn from(r: surrealdb_types::Range) -> Self {
255		use std::ops::Bound;
256		match r.into_inner() {
257			// Unbounded range: ..
258			(Bound::Unbounded, Bound::Unbounded) => Expr::Literal(Literal::UnboundedRange),
259			// Prefix ranges: ..end or ..=end
260			(Bound::Unbounded, Bound::Excluded(end)) => Expr::Prefix {
261				op: PrefixOperator::Range,
262				expr: Box::new(Expr::from_public_value(end)),
263			},
264			(Bound::Unbounded, Bound::Included(end)) => Expr::Prefix {
265				op: PrefixOperator::RangeInclusive,
266				expr: Box::new(Expr::from_public_value(end)),
267			},
268			// Binary ranges with inclusive start
269			(Bound::Included(start), Bound::Excluded(end)) => Expr::Binary {
270				left: Box::new(Expr::from_public_value(start)),
271				op: BinaryOperator::Range,
272				right: Box::new(Expr::from_public_value(end)),
273			},
274			(Bound::Included(start), Bound::Included(end)) => Expr::Binary {
275				left: Box::new(Expr::from_public_value(start)),
276				op: BinaryOperator::RangeInclusive,
277				right: Box::new(Expr::from_public_value(end)),
278			},
279			// Binary ranges with excluded start (skip)
280			(Bound::Excluded(start), Bound::Excluded(end)) => Expr::Binary {
281				left: Box::new(Expr::from_public_value(start)),
282				op: BinaryOperator::RangeSkip,
283				right: Box::new(Expr::from_public_value(end)),
284			},
285			(Bound::Excluded(start), Bound::Included(end)) => Expr::Binary {
286				left: Box::new(Expr::from_public_value(start)),
287				op: BinaryOperator::RangeSkipInclusive,
288				right: Box::new(Expr::from_public_value(end)),
289			},
290			// Invalid ranges with unbounded start but bounded in a way we can't represent
291			// start>.. (excluded start with no end) - not valid in SurrealQL
292			(Bound::Excluded(_), Bound::Unbounded) | (Bound::Included(_), Bound::Unbounded) => {
293				Expr::Literal(Literal::None)
294			}
295		}
296	}
297}
298
299impl Expr {
300	/// The value this expression denotes, when it denotes one without being
301	/// evaluated.
302	///
303	/// Only a literal can answer this; every other form either reads the
304	/// environment or needs the evaluator, so it answers `None`. See
305	/// [`Literal::as_static_value`] for the contract the result upholds.
306	///
307	/// Narrower than [`Self::is_static`], which is also true of expressions
308	/// that are computable without the environment but still need evaluating
309	/// (arithmetic on literals, say).
310	pub fn as_static_value(&self) -> Option<crate::val::Value> {
311		match self {
312			Expr::Literal(literal) => literal.as_static_value(),
313			_ => None,
314		}
315	}
316
317	/// Checks if a expression is 'pure' i.e. does not rely on the environment.
318	pub fn is_static(&self) -> bool {
319		match self {
320			Expr::Literal(literal) => literal.is_static(),
321			Expr::Constant(_) => true,
322			Expr::Prefix {
323				expr,
324				..
325			} => expr.is_static(),
326			Expr::Postfix {
327				expr,
328				..
329			} => expr.is_static(),
330			Expr::Binary {
331				left,
332				right,
333				..
334			} => left.is_static() && right.is_static(),
335			Expr::FunctionCall(x) => {
336				// This is not correct as functions like http::get are not 'pure' but this is
337				// replicating previous behavior.
338				//
339				// FIXME: Fix this discrepency and weird static/non-static behavior.
340				x.arguments.iter().all(|x| x.is_static())
341			}
342			Expr::Param(_)
343			| Expr::Idiom(_)
344			| Expr::Table(_)
345			| Expr::Mock(_)
346			| Expr::Block(_)
347			| Expr::Closure(_)
348			| Expr::Break
349			| Expr::Continue
350			| Expr::Return(_)
351			| Expr::Throw(_)
352			| Expr::IfElse(_)
353			| Expr::Select(_)
354			| Expr::Create(_)
355			| Expr::Update(_)
356			| Expr::Delete(_)
357			| Expr::Relate(_)
358			| Expr::Insert(_)
359			| Expr::Define(_)
360			| Expr::Remove(_)
361			| Expr::Rebuild(_)
362			| Expr::Upsert(_)
363			| Expr::Alter(_)
364			| Expr::Info(_)
365			| Expr::Foreach(_)
366			| Expr::Let(_)
367			| Expr::Sleep(_)
368			| Expr::Explain {
369				..
370			} => false,
371			// GQL MATCH reads from the datastore, so it is never static.
372			Expr::Match(_) => false,
373		}
374	}
375
376	pub fn to_idiom(&self) -> Idiom {
377		match self {
378			Expr::Idiom(i) => i.simplify(),
379			Expr::Param(i) => Idiom::field(i.clone().into_strand()),
380			Expr::FunctionCall(x) => x.receiver.to_idiom(),
381			Expr::Literal(l) => match l {
382				Literal::String(s) => Idiom::field(s.clone()),
383				Literal::Datetime(d) => Idiom::field(d.to_string()),
384				x => Idiom::field(x.to_sql()),
385			},
386			x => Idiom::field(x.to_sql()),
387		}
388	}
389
390	pub fn to_raw_string(&self) -> String {
391		match self {
392			Expr::Idiom(idiom) => idiom.to_raw_string(),
393			Expr::Table(ident) => ident.as_str().to_string(),
394			_ => self.to_sql(),
395		}
396	}
397
398	// NOTE: Changes to this function also likely require changes to
399	// crate::sql::Expr::needs_parentheses
400	/// Returns if this expression needs to be parenthesized when inside another expression.
401	#[allow(dead_code)]
402	fn needs_parentheses(&self) -> bool {
403		match self {
404			Expr::Literal(Literal::UnboundedRange | Literal::RecordId(_))
405			| Expr::Closure(_)
406			| Expr::Break
407			| Expr::Continue
408			| Expr::Throw(_)
409			| Expr::Return(_)
410			| Expr::IfElse(_)
411			| Expr::Select(_)
412			| Expr::Create(_)
413			| Expr::Update(_)
414			| Expr::Delete(_)
415			| Expr::Relate(_)
416			| Expr::Insert(_)
417			| Expr::Define(_)
418			| Expr::Remove(_)
419			| Expr::Rebuild(_)
420			| Expr::Upsert(_)
421			| Expr::Alter(_)
422			| Expr::Info(_)
423			| Expr::Foreach(_)
424			| Expr::Let(_)
425			| Expr::Sleep(_)
426			| Expr::Explain {
427				..
428			} => true,
429
430			// GQL MATCH renders as a multi-clause statement; parenthesize it
431			// when nested.
432			Expr::Match(_) => true,
433
434			Expr::Literal(_)
435			| Expr::Param(_)
436			| Expr::Idiom(_)
437			| Expr::Table(_)
438			| Expr::Mock(_)
439			| Expr::Block(_)
440			| Expr::Constant(_)
441			| Expr::Prefix {
442				..
443			}
444			| Expr::Postfix {
445				..
446			}
447			| Expr::Binary {
448				..
449			}
450			| Expr::FunctionCall(_) => false,
451		}
452	}
453}
454
455impl ToSql for Expr {
456	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
457		// `Expr::Match` cannot round-trip through `sql::Expr` (it has no SurrealQL
458		// surface). Render it directly via the dedicated `MatchPlan` renderer
459		// before the conversion would replace it with a placeholder.
460		if let Expr::Match(plan) = self {
461			plan.fmt_sql(f, fmt);
462			return;
463		}
464		let sql_expr: crate::sql::Expr = self.clone().into();
465		sql_expr.fmt_sql(f, fmt);
466	}
467}
468
469impl Expr {
470	/// Renders the canonical SurrealQL text a catalog definition stores for
471	/// this expression (e.g. a `StoredFieldDefinition.value`), matching exactly
472	/// what the old `sql::Expr`-embedding definition used to render when
473	/// nested after a clause keyword (`VALUE`, `ASSERT`, `WHEN`, ...) —
474	/// including the parenthesization `CoverStmts` applies to statement-shaped
475	/// sub-expressions (e.g. a nested `SELECT`), so the stored text is safe to
476	/// splice back in after that keyword with no further wrapping.
477	///
478	/// `Expr::Match` never reaches a stored definition (see `ToSql`'s carve-out
479	/// above), so unlike `ToSql::fmt_sql` this does not special-case it.
480	pub fn to_stored_sql(&self) -> String {
481		let sql_expr: crate::sql::Expr = self.clone().into();
482		crate::sql::CoverStmts(&sql_expr).to_sql()
483	}
484}
485
486impl InfoStructure for Expr {
487	fn structure(self) -> Value {
488		self.to_sql().into()
489	}
490}
491
492impl Revisioned for Expr {
493	fn revision() -> u16 {
494		1
495	}
496}
497
498impl SerializeRevisioned for Expr {
499	fn serialize_revisioned<W: std::io::Write>(
500		&self,
501		writer: &mut W,
502	) -> Result<(), revision::Error> {
503		// `Expr::Match` renders GQL-ish text via `to_sql()`, which the
504		// SurrealQL-only `deserialize_revisioned` path below cannot round-trip.
505		// The invariant (V2_DESIGN §2; SECURITY_GUIDE §15a) is that `Expr::Match`
506		// is only ever the top-level expr of a `PreparedGqlQuery` consumed
507		// directly by the planner — it never nests into a `sql::Ast`, the
508		// catalog, or a cached/revisioned `Expr`, so this path is unreachable by
509		// construction. Mirror the `From<expr::Expr> for sql::Expr` arm and fail
510		// loud (in debug) rather than silently emit unparseable bytes, so a
511		// future regression that nests `Expr::Match` is caught here.
512		if matches!(self, Expr::Match(_)) {
513			tracing::error!(
514				"Expr::Match reached Revisioned serialization; it must never enter a \
515				 sql::Ast, the catalog, or Revisioned serialization"
516			);
517			debug_assert!(false, "Expr::Match must not be Revisioned-serialized");
518		}
519		SerializeRevisioned::serialize_revisioned(&self.to_sql(), writer)
520	}
521}
522
523impl DeserializeRevisioned for Expr {
524	fn deserialize_revisioned<R: std::io::Read>(reader: &mut R) -> Result<Self, revision::Error> {
525		let query: String = DeserializeRevisioned::deserialize_revisioned(reader)?;
526
527		let expr = crate::syn::parse_with_settings(
528			query.as_bytes(),
529			// The wire format is engine-rendered SurrealQL text; see the
530			// constant for why decode is capability- and limit-independent.
531			crate::syn::parser::ParserSettings::STORED_TEXT,
532			// The whole of the stored text must be consumed, for the reason
533			// `syn::expr_for_definition` documents: the Pratt parser stops at
534			// the first token it cannot continue with, so trailing content
535			// would decode to a prefix and be evaluated as if that were the
536			// stored expression.
537			async |p, stk| {
538				let expr = p.parse_expr(stk).await?;
539				p.assert_finished()?;
540				Ok(expr)
541			},
542		)
543		.map_err(|err| revision::Error::Conversion(err.to_string()))?;
544		Ok(expr.into())
545	}
546}
547
548impl revision::SkipRevisioned for Expr {
549	fn skip_revisioned<R: std::io::Read>(reader: &mut R) -> Result<(), revision::Error> {
550		// Wire format is the SurrealQL source string. Skip its bytes without
551		// re-parsing into an `Expr`.
552		<String as revision::SkipRevisioned>::skip_revisioned(reader)
553	}
554}
555
556impl revision::WalkRevisioned for Expr {
557	type Walker<'r, R: revision::BorrowedReader + 'r> = revision::LeafWalker<'r, Expr, R>;
558
559	fn walk_revisioned<'r, R: revision::BorrowedReader>(
560		reader: &'r mut R,
561	) -> Result<Self::Walker<'r, R>, revision::Error> {
562		Ok(revision::LeafWalker::new(reader))
563	}
564}
565
566impl revision::LengthPrefixedBytes for Expr {}
567
568#[cfg(test)]
569mod length_prefixed_bytes_tests {
570	use revision::{SerializeRevisioned, WalkRevisioned};
571	use surrealdb_types::ToSql;
572
573	use crate::expr::Expr;
574	use crate::expr::literal::Literal;
575
576	#[test]
577	fn expr_with_bytes_matches_serialize() {
578		let expr = Expr::Literal(Literal::Integer(42));
579		let mut bytes = Vec::new();
580		expr.serialize_revisioned(&mut bytes).unwrap();
581		let wire_text = expr.to_sql();
582		let mut r = bytes.as_slice();
583		let walker = Expr::walk_revisioned(&mut r).unwrap();
584		let observed = walker.with_bytes(|raw| raw.to_vec()).unwrap();
585		assert_eq!(observed.as_slice(), wire_text.as_bytes());
586		assert!(r.is_empty());
587	}
588}