Skip to main content

surrealdb_sql/
lookup.rs

1use std::ops::Bound;
2
3use common::fmt::{EscapeKwFreeIdent, Fmt};
4use surrealdb_types::{SqlFormat, ToSql, write_sql};
5
6use crate::order::Ordering;
7use crate::{
8	Cond, Dir, Fields, Groups, Idiom, Limit, RecordIdKeyRangeLit, Splits, Start, TableName,
9};
10
11/// A lookup is a unified way of looking up graph edges and record references.
12/// Since they both work very similarly, they also both support the same operations
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct Lookup {
15	pub kind: LookupKind,
16	pub expr: Option<Fields>,
17	pub only: bool,
18	pub what: Vec<LookupSubject>,
19	pub cond: Option<Cond>,
20	pub split: Option<Splits>,
21	pub group: Option<Groups>,
22	pub order: Option<Ordering>,
23	pub limit: Option<Limit>,
24	pub start: Option<Start>,
25	pub alias: Option<Idiom>,
26}
27
28impl ToSql for Lookup {
29	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
30		if self.what.len() <= 1
31			// When the singular lookup subject has a referencing field, it needs to be wrapped in parentheses
32			// Otherwise <~table.field will be parsed as [Lookup(<~table), Field(.field)]
33			// Whereas <~(table.field) will be parsed as [Lookup(<~table.field)]
34			//
35			// Further more `<-foo:a..` can lead to issues when the next part of the idiom starts
36			// with a `.`
37			&& self.what.iter().all(|v| {
38				if v.referencing_field().is_some() {
39					return false
40				}
41				if let LookupSubject::Range { range: RecordIdKeyRangeLit{ end: Bound::Unbounded, .. }, ..} = v {
42					return false
43				}
44				true
45			})
46			&& self.cond.is_none()
47			&& self.alias.is_none()
48			&& self.expr.is_none()
49		{
50			self.kind.fmt_sql(f, fmt);
51			if self.what.is_empty() {
52				f.push('?');
53			} else {
54				write_sql!(f, fmt, "{}", Fmt::comma_separated(self.what.iter()));
55			}
56		} else {
57			write_sql!(f, fmt, "{}(", self.kind);
58			if let Some(ref expr) = self.expr {
59				write_sql!(f, fmt, "SELECT {} FROM ", expr);
60				if self.only {
61					f.push_str("ONLY ");
62				}
63			}
64			if self.what.is_empty() {
65				f.push('?');
66			} else {
67				write_sql!(f, fmt, "{}", Fmt::comma_separated(&self.what));
68			}
69			if let Some(ref v) = self.cond {
70				write_sql!(f, fmt, " {v}");
71			}
72			if let Some(ref v) = self.split {
73				write_sql!(f, fmt, " {v}");
74			}
75			if let Some(ref v) = self.group {
76				write_sql!(f, fmt, " {v}");
77			}
78			if let Some(ref v) = self.order {
79				write_sql!(f, fmt, " {v}");
80			}
81			if let Some(ref v) = self.limit {
82				write_sql!(f, fmt, " {v}");
83			}
84			if let Some(ref v) = self.start {
85				write_sql!(f, fmt, " {v}");
86			}
87			if let Some(ref v) = self.alias {
88				write_sql!(f, fmt, " AS {v}");
89			}
90			f.push(')');
91		}
92	}
93}
94
95/// This enum instructs whether the lookup is a graph edge or a record reference
96#[derive(Clone, Debug, PartialEq, Eq)]
97#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
98pub enum LookupKind {
99	Graph(Dir),
100	Reference,
101}
102
103impl Default for LookupKind {
104	fn default() -> Self {
105		Self::Graph(Dir::Both)
106	}
107}
108
109impl ToSql for LookupKind {
110	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
111		match self {
112			Self::Graph(dir) => dir.fmt_sql(f, fmt),
113			Self::Reference => f.push_str("<~"),
114		}
115	}
116}
117
118/// This enum instructs whether we scan all edges on a table or just a specific range
119#[derive(Clone, Debug, PartialEq, Eq)]
120#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
121pub enum LookupSubject {
122	Table {
123		table: TableName,
124		referencing_field: Option<String>,
125	},
126	Range {
127		table: TableName,
128		range: RecordIdKeyRangeLit,
129		referencing_field: Option<String>,
130	},
131}
132
133impl LookupSubject {
134	pub fn referencing_field(&self) -> Option<&String> {
135		match self {
136			LookupSubject::Table {
137				referencing_field,
138				..
139			} => referencing_field.as_ref(),
140			LookupSubject::Range {
141				referencing_field,
142				..
143			} => referencing_field.as_ref(),
144		}
145	}
146}
147
148impl ToSql for LookupSubject {
149	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
150		match self {
151			Self::Table {
152				table,
153				referencing_field,
154			} => {
155				write_sql!(f, fmt, "{}", EscapeKwFreeIdent(table.as_str()));
156				if let Some(referencing_field) = referencing_field {
157					write_sql!(f, fmt, " FIELD {}", EscapeKwFreeIdent(referencing_field));
158				}
159			}
160			Self::Range {
161				table,
162				range,
163				referencing_field,
164			} => {
165				write_sql!(f, fmt, "{}:{range}", EscapeKwFreeIdent(table.as_str()));
166				if let Some(referencing_field) = referencing_field {
167					write_sql!(f, fmt, " FIELD {}", EscapeKwFreeIdent(referencing_field));
168				}
169			}
170		}
171	}
172}