Skip to main content

surrealdb_expr/val/
field_path.rs

1//! Field path type for pure field extraction without execution.
2//!
3//! This module provides a validated subset of `Idiom` that guarantees
4//! no execution is required for field extraction. This is used in contexts
5//! like sorting where we need to extract values synchronously without
6//! database access or expression evaluation.
7
8use std::borrow::Cow;
9use std::fmt;
10
11use crate::val::{Set, Value};
12
13/// A part of a field path that can be navigated without execution.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum FieldPathPart {
16	/// Field access: `.name`
17	Field(String),
18	/// Literal integer index: `[0]`, `[1]`
19	Index(usize),
20	/// First element: `[0]`
21	First,
22	/// Last element: `[$]`
23	Last,
24	/// Graph traversal key: `->table`, `<-table`, `<->table`
25	Lookup(String),
26}
27
28/// A path for pure field extraction, with no execution required.
29///
30/// This is a validated subset of `Idiom` that only contains parts that can be
31/// extracted synchronously from a Value without database access or expression
32/// evaluation.
33///
34/// Supported patterns:
35/// - `a` - simple field
36/// - `a.b.c` - nested fields
37/// - `a[0]` - array index
38/// - `a[$]` - last element
39/// - `a[0].b.c` - mixed
40///
41/// # Examples
42///
43/// ```ignore
44/// use surrealdb::exec::FieldPath;
45///
46/// // Simple field
47/// let path = FieldPath::field("name");
48///
49/// // Convert from idiom (may fail for complex idioms)
50/// let idiom = syn::idiom("user.address.city").unwrap();
51/// let path = FieldPath::try_from(&idiom)?;
52/// ```
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct FieldPath(pub Vec<FieldPathPart>);
55
56impl FieldPath {
57	/// Create a simple single-field path.
58	pub fn field(name: impl Into<String>) -> Self {
59		FieldPath(vec![FieldPathPart::Field(name.into())])
60	}
61
62	/// Check if this is an empty path.
63	pub fn is_empty(&self) -> bool {
64		self.0.is_empty()
65	}
66
67	/// Get the number of parts in this path.
68	pub fn len(&self) -> usize {
69		self.0.len()
70	}
71
72	/// Extract the value at this path from a record.
73	/// Returns Value::None if the path doesn't exist.
74	///
75	/// Borrows into the input wherever the path is pure navigation
76	/// (object fields, array/set element access), so the common case —
77	/// a sort key like `a.b.c` — never clones the record. An owned value
78	/// is produced only for synthesised results (field projection over an
79	/// array/set, missing paths) or when descending through one.
80	pub fn extract<'a>(&self, value: &'a Value) -> Cow<'a, Value> {
81		let mut current = Cow::Borrowed(value);
82		for part in &self.0 {
83			current = match current {
84				Cow::Borrowed(v) => match step(v, part) {
85					Step::Borrowed(b) => Cow::Borrowed(b),
86					Step::Owned(o) => Cow::Owned(o),
87				},
88				// The intermediate is already owned (a synthesised
89				// array/set projection), so a borrowed step result must be
90				// cloned out of it before it drops. These intermediates are
91				// path-local, never the whole record.
92				Cow::Owned(v) => Cow::Owned(match step(&v, part) {
93					Step::Borrowed(b) => b.clone(),
94					Step::Owned(o) => o,
95				}),
96			};
97		}
98		current
99	}
100}
101
102/// Result of navigating one [`FieldPathPart`] into a value: a borrow into
103/// the input where possible, an owned value where the step synthesises one.
104enum Step<'v> {
105	Borrowed(&'v Value),
106	Owned(Value),
107}
108
109/// Navigate a single path part. Pure-navigation arms borrow; projection
110/// arms (field access over an array/set) and misses produce owned values.
111fn step<'v>(value: &'v Value, part: &FieldPathPart) -> Step<'v> {
112	match (value, part) {
113		// Field/Lookup access on object
114		(Value::Object(obj), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
115			obj.get(name).map_or(Step::Owned(Value::None), Step::Borrowed)
116		}
117		// Index access on array
118		(Value::Array(arr), FieldPathPart::Index(i)) => {
119			arr.get(*i).map_or(Step::Owned(Value::None), Step::Borrowed)
120		}
121		// Index access on set
122		(Value::Set(set), FieldPathPart::Index(i)) => {
123			set.nth(*i).map_or(Step::Owned(Value::None), Step::Borrowed)
124		}
125		// First element of array
126		(Value::Array(arr), FieldPathPart::First) => {
127			arr.first().map_or(Step::Owned(Value::None), Step::Borrowed)
128		}
129		// First element of set
130		(Value::Set(set), FieldPathPart::First) => {
131			set.first().map_or(Step::Owned(Value::None), Step::Borrowed)
132		}
133		// Last element of array
134		(Value::Array(arr), FieldPathPart::Last) => {
135			arr.last().map_or(Step::Owned(Value::None), Step::Borrowed)
136		}
137		// Last element of set
138		(Value::Set(set), FieldPathPart::Last) => {
139			set.last().map_or(Step::Owned(Value::None), Step::Borrowed)
140		}
141		// Field/Lookup access on array applies to each element
142		(Value::Array(arr), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
143			Step::Owned(Value::Array(
144				arr.iter()
145					.map(|v| match v {
146						Value::Object(obj) => obj.get(name).cloned().unwrap_or(Value::None),
147						_ => Value::None,
148					})
149					.collect::<Vec<_>>()
150					.into(),
151			))
152		}
153		// Field/Lookup access on set applies to each element
154		(Value::Set(set), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
155			Step::Owned(Value::Set(Set::from(
156				set.iter()
157					.map(|v| match v {
158						Value::Object(obj) => obj.get(name).cloned().unwrap_or(Value::None),
159						_ => Value::None,
160					})
161					.collect::<Vec<_>>(),
162			)))
163		}
164		// Any other combination returns None
165		_ => Step::Owned(Value::None),
166	}
167}
168
169impl fmt::Display for FieldPath {
170	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171		for (i, part) in self.0.iter().enumerate() {
172			match part {
173				FieldPathPart::Field(name) if i == 0 => write!(f, "{}", name)?,
174				FieldPathPart::Field(name) => write!(f, ".{}", name)?,
175				FieldPathPart::Index(idx) => write!(f, "[{}]", idx)?,
176				FieldPathPart::First => write!(f, "[0]")?,
177				FieldPathPart::Last => write!(f, "[$]")?,
178				FieldPathPart::Lookup(key) if i == 0 => write!(f, "{}", key)?,
179				FieldPathPart::Lookup(key) => write!(f, ".{}", key)?,
180			}
181		}
182		Ok(())
183	}
184}
185
186#[cfg(test)]
187mod tests {
188	use std::collections::BTreeMap;
189
190	use super::*;
191	use crate::val::Object;
192
193	/// Helper to create an Object from key-value pairs
194	fn make_obj(pairs: Vec<(&str, Value)>) -> Object {
195		let map: BTreeMap<String, Value> =
196			pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
197		Object::from(map)
198	}
199
200	#[test]
201	fn test_field_path_simple() {
202		let path = FieldPath::field("name");
203		assert_eq!(path.to_string(), "name");
204		assert_eq!(path.len(), 1);
205	}
206
207	#[test]
208	fn test_field_path_extract_simple() {
209		let path = FieldPath::field("name");
210		let obj = make_obj(vec![("name", Value::from("Alice"))]);
211		let value = Value::Object(obj);
212
213		let result = path.extract(&value);
214		assert_eq!(*result, Value::from("Alice"));
215		// Pure navigation must borrow, not clone.
216		assert!(matches!(result, Cow::Borrowed(_)));
217	}
218
219	#[test]
220	fn test_field_path_extract_nested() {
221		// Create path: user.address.city
222		let path = FieldPath(vec![
223			FieldPathPart::Field("user".into()),
224			FieldPathPart::Field("address".into()),
225			FieldPathPart::Field("city".into()),
226		]);
227
228		// Create nested object: { user: { address: { city: "Austin" } } }
229		let city_obj = make_obj(vec![("city", Value::from("Austin"))]);
230		let address_obj = make_obj(vec![("address", Value::Object(city_obj))]);
231		let user_obj = make_obj(vec![("user", Value::Object(address_obj))]);
232		let value = Value::Object(user_obj);
233
234		let result = path.extract(&value);
235		assert_eq!(*result, Value::from("Austin"));
236		// Nested object navigation stays borrowed end-to-end.
237		assert!(matches!(result, Cow::Borrowed(_)));
238	}
239
240	#[test]
241	fn test_field_path_extract_array_index() {
242		// Create path: items[0]
243		let path = FieldPath(vec![FieldPathPart::Field("items".into()), FieldPathPart::Index(0)]);
244
245		let items = Value::Array(vec![Value::from("first"), Value::from("second")].into());
246		let obj = make_obj(vec![("items", items)]);
247		let value = Value::Object(obj);
248
249		let result = path.extract(&value);
250		assert_eq!(*result, Value::from("first"));
251		assert!(matches!(result, Cow::Borrowed(_)));
252	}
253
254	#[test]
255	fn test_field_path_extract_array_last() {
256		// Create path: items[$]
257		let path = FieldPath(vec![FieldPathPart::Field("items".into()), FieldPathPart::Last]);
258
259		let items = Value::Array(vec![Value::from("first"), Value::from("second")].into());
260		let obj = make_obj(vec![("items", items)]);
261		let value = Value::Object(obj);
262
263		let result = path.extract(&value);
264		assert_eq!(*result, Value::from("second"));
265		assert!(matches!(result, Cow::Borrowed(_)));
266	}
267
268	#[test]
269	fn test_field_path_extract_missing() {
270		let path = FieldPath::field("missing");
271		let obj = make_obj(vec![("name", Value::from("Alice"))]);
272		let value = Value::Object(obj);
273
274		let result = path.extract(&value);
275		assert_eq!(*result, Value::None);
276		// A miss synthesises Value::None, so it is owned.
277		assert!(matches!(result, Cow::Owned(_)));
278	}
279
280	#[test]
281	fn test_field_path_extract_past_missing_stays_none() {
282		// Navigating further parts after a miss keeps returning None,
283		// exactly as the pre-Cow implementation did.
284		let path = FieldPath(vec![
285			FieldPathPart::Field("missing".into()),
286			FieldPathPart::Field("deeper".into()),
287			FieldPathPart::Index(3),
288		]);
289		let obj = make_obj(vec![("name", Value::from("Alice"))]);
290		let value = Value::Object(obj);
291
292		let result = path.extract(&value);
293		assert_eq!(*result, Value::None);
294		assert!(matches!(result, Cow::Owned(_)));
295	}
296
297	#[test]
298	fn test_field_path_extract_field_on_array() {
299		// Create path: users.name (should extract name from each user)
300		let path = FieldPath(vec![
301			FieldPathPart::Field("users".into()),
302			FieldPathPart::Field("name".into()),
303		]);
304
305		let user1 = Value::Object(make_obj(vec![("name", Value::from("Alice"))]));
306		let user2 = Value::Object(make_obj(vec![("name", Value::from("Bob"))]));
307		let users = Value::Array(vec![user1, user2].into());
308		let obj = make_obj(vec![("users", users)]);
309		let value = Value::Object(obj);
310
311		let result = path.extract(&value);
312		// A projection over an array synthesises a new array, so it is owned.
313		assert!(matches!(result, Cow::Owned(_)));
314		if let Value::Array(arr) = result.into_owned() {
315			assert_eq!(arr.len(), 2);
316			assert_eq!(arr[0], Value::from("Alice"));
317			assert_eq!(arr[1], Value::from("Bob"));
318		} else {
319			panic!("Expected array result");
320		}
321	}
322
323	#[test]
324	fn test_field_path_extract_through_owned_intermediate() {
325		// users.name[0] — the projection synthesises an owned array, and the
326		// subsequent index step must clone out of it correctly.
327		let path = FieldPath(vec![
328			FieldPathPart::Field("users".into()),
329			FieldPathPart::Field("name".into()),
330			FieldPathPart::Index(0),
331		]);
332
333		let user1 = Value::Object(make_obj(vec![("name", Value::from("Alice"))]));
334		let user2 = Value::Object(make_obj(vec![("name", Value::from("Bob"))]));
335		let users = Value::Array(vec![user1, user2].into());
336		let obj = make_obj(vec![("users", users)]);
337		let value = Value::Object(obj);
338
339		let result = path.extract(&value);
340		assert_eq!(*result, Value::from("Alice"));
341		assert!(matches!(result, Cow::Owned(_)));
342	}
343
344	#[test]
345	fn test_field_path_display() {
346		let path = FieldPath(vec![
347			FieldPathPart::Field("user".into()),
348			FieldPathPart::Field("address".into()),
349			FieldPathPart::Index(0),
350			FieldPathPart::Field("city".into()),
351		]);
352		assert_eq!(path.to_string(), "user.address[0].city");
353	}
354}