Skip to main content

surrealdb_expr/val/value/
compare.rs

1use std::cmp::Ordering;
2
3use crate::expr::part::{Next, Part};
4use crate::val::Value;
5
6impl Value {
7	pub fn compare(
8		&self,
9		other: &Self,
10		path: &[Part],
11		collate: bool,
12		numeric: bool,
13	) -> Option<Ordering> {
14		let Some(p) = path.first() else {
15			return match (collate, numeric) {
16				(true, true) => self.natural_lexical_cmp(other),
17				(true, false) => self.lexical_cmp(other),
18				(false, true) => self.natural_cmp(other),
19				_ => self.partial_cmp(other),
20			};
21		};
22
23		match (self, other) {
24			// Current path part is an object
25			(Value::Object(a), Value::Object(b)) => match p {
26				Part::Field(f) => compare_optional(a.get(f), b.get(f), path, collate, numeric),
27				_ => None,
28			},
29			// Current path part is an array
30			(Value::Array(a), Value::Array(b)) => match p {
31				Part::All => {
32					for (a, b) in a.iter().zip(b.iter()) {
33						match a.compare(b, path.next(), collate, numeric) {
34							None | Some(Ordering::Equal) => continue,
35							o => return o,
36						}
37					}
38					Some(a.len().cmp(&b.len()))
39				}
40				Part::First => compare_optional(a.first(), b.first(), path, collate, numeric),
41				Part::Last => compare_optional(a.last(), b.last(), path, collate, numeric),
42				//FIXME: It is kind of weird that a[1] works but `a[+(1)]` or `let $b = 1;
43				// a[$b]` for example doesn't as
44				x => {
45					if let Some(idx) = x.as_old_index() {
46						compare_optional(a.get(idx), b.get(idx), path, collate, numeric)
47					} else {
48						for (a, b) in a.iter().zip(b.iter()) {
49							match a.compare(b, path, collate, numeric) {
50								None | Some(Ordering::Equal) => continue,
51								o => return o,
52							}
53						}
54						Some(a.len().cmp(&b.len()))
55					}
56				}
57			},
58			(Value::Set(a), Value::Set(b)) => match p {
59				Part::All => {
60					for (a, b) in a.iter().zip(b.iter()) {
61						match a.compare(b, path.next(), collate, numeric) {
62							None | Some(Ordering::Equal) => continue,
63							o => return o,
64						}
65					}
66
67					Some(a.len().cmp(&b.len()))
68				}
69				Part::First => compare_optional(a.first(), b.first(), path, collate, numeric),
70				Part::Last => compare_optional(a.last(), b.last(), path, collate, numeric),
71				x => {
72					if let Some(idx) = x.as_old_index() {
73						compare_optional(a.nth(idx), b.nth(idx), path, collate, numeric)
74					} else {
75						for (a, b) in a.iter().zip(b.iter()) {
76							match a.compare(b, path, collate, numeric) {
77								None | Some(Ordering::Equal) => continue,
78								o => return o,
79							}
80						}
81						Some(a.len().cmp(&b.len()))
82					}
83				}
84			},
85			// Handle field access on mixed or non-object types
86			(a, b) => match p {
87				Part::Field(f) => match (a, b) {
88					// If one is an Object and the other is not, treat non-Object as missing field
89					(Value::Object(a), _) => match a.get(f) {
90						Some(a) => a.compare(&Value::None, path.next(), collate, numeric),
91						None => Some(Ordering::Equal),
92					},
93					(_, Value::Object(b)) => match b.get(f) {
94						Some(b) => Value::None.compare(b, path.next(), collate, numeric),
95						None => Some(Ordering::Equal),
96					},
97					// Both are non-object types, so both are missing the field
98					_ => Some(Ordering::Equal),
99				},
100				// For non-field path parts, continue comparing with remaining path
101				_ => a.compare(b, path.next(), collate, numeric),
102			},
103		}
104	}
105}
106
107#[inline]
108fn compare_optional(
109	a: Option<&Value>,
110	b: Option<&Value>,
111	path: &[Part],
112	collate: bool,
113	numeric: bool,
114) -> Option<Ordering> {
115	match (a, b) {
116		(Some(a), Some(b)) => a.compare(b, path.next(), collate, numeric),
117		(Some(_), None) => Some(Ordering::Greater),
118		(None, Some(_)) => Some(Ordering::Less),
119		(None, None) => Some(Ordering::Equal),
120	}
121}
122
123#[cfg(test)]
124mod tests {
125
126	use rstest::rstest;
127
128	use super::*;
129	use crate::expr::idiom::Idiom;
130	use crate::syn;
131
132	macro_rules! parse_val {
133		($input:expr) => {
134			crate::val::convert_public_value_to_internal(syn::value($input).unwrap())
135		};
136	}
137
138	#[rstest]
139	#[case::none_eq_none(Value::None, Value::None, Idiom::default(), Some(Ordering::Equal))]
140	#[case::none_eq_none(parse_val!("{ test: { other: null, something: 456 } }"), parse_val!("{ test: { other: null, something: 123 } }"), syn::idiom("test.something").unwrap().into(), Some(Ordering::Greater))]
141	fn test_compare(
142		#[case] a: Value,
143		#[case] b: Value,
144		#[case] path: Idiom,
145		#[case] expected: Option<Ordering>,
146	) {
147		let res = a.compare(&b, &path, false, false);
148		assert_eq!(res, expected);
149	}
150
151	#[test]
152	fn compare_none() {
153		let idi: Idiom = Default::default();
154		let one = parse_val!("{ test: { other: null, something: 456 } }");
155		let two = parse_val!("{ test: { other: null, something: 123 } }");
156		let res = one.compare(&two, &idi, false, false);
157		assert_eq!(res, Some(Ordering::Greater));
158	}
159
160	#[test]
161	fn compare_basic() {
162		let idi: Idiom = syn::idiom("test.something").unwrap().into();
163		let one = parse_val!("{ test: { other: null, something: 456 } }");
164		let two = parse_val!("{ test: { other: null, something: 123 } }");
165		let res = one.compare(&two, &idi, false, false);
166		assert_eq!(res, Some(Ordering::Greater));
167	}
168
169	#[test]
170	fn compare_basic_missing_left() {
171		let idi: Idiom = syn::idiom("test.something").unwrap().into();
172		let one = parse_val!("{ test: { other: null } }");
173		let two = parse_val!("{ test: { other: null, something: 123 } }");
174		let res = one.compare(&two, &idi, false, false);
175		assert_eq!(res, Some(Ordering::Less));
176	}
177
178	#[test]
179	fn compare_basic_missing_right() {
180		let idi: Idiom = syn::idiom("test.something").unwrap().into();
181		let one = parse_val!("{ test: { other: null, something: 456 } }");
182		let two = parse_val!("{ test: { other: null } }");
183		let res = one.compare(&two, &idi, false, false);
184		assert_eq!(res, Some(Ordering::Greater));
185	}
186
187	#[test]
188	fn compare_array() {
189		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
190		let one = parse_val!("{ test: { other: null, something: [4, 5, 6] } }");
191		let two = parse_val!("{ test: { other: null, something: [1, 2, 3] } }");
192		let res = one.compare(&two, &idi, false, false);
193		assert_eq!(res, Some(Ordering::Greater));
194	}
195
196	#[test]
197	fn compare_array_longer_left() {
198		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
199		let one = parse_val!("{ test: { other: null, something: [1, 2, 3, 4, 5, 6] } }");
200		let two = parse_val!("{ test: { other: null, something: [1, 2, 3] } }");
201		let res = one.compare(&two, &idi, false, false);
202		assert_eq!(res, Some(Ordering::Greater));
203	}
204
205	#[test]
206	fn compare_array_longer_right() {
207		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
208		let one = parse_val!("{ test: { other: null, something: [1, 2, 3] } }");
209		let two = parse_val!("{ test: { other: null, something: [1, 2, 3, 4, 5, 6] } }");
210		let res = one.compare(&two, &idi, false, false);
211		assert_eq!(res, Some(Ordering::Less));
212	}
213
214	#[test]
215	fn compare_array_missing_left() {
216		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
217		let one = parse_val!("{ test: { other: null, something: null } }");
218		let two = parse_val!("{ test: { other: null, something: [1, 2, 3] } }");
219		let res = one.compare(&two, &idi, false, false);
220		assert_eq!(res, Some(Ordering::Less));
221	}
222
223	#[test]
224	fn compare_array_missing_right() {
225		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
226		let one = parse_val!("{ test: { other: null, something: [4, 5, 6] } }");
227		let two = parse_val!("{ test: { other: null, something: null } }");
228		let res = one.compare(&two, &idi, false, false);
229		assert_eq!(res, Some(Ordering::Greater));
230	}
231
232	#[test]
233	fn compare_array_missing_value_left() {
234		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
235		let one = parse_val!("{ test: { other: null, something: [1, null, 3] } }");
236		let two = parse_val!("{ test: { other: null, something: [1, 2, 3] } }");
237		let res = one.compare(&two, &idi, false, false);
238		assert_eq!(res, Some(Ordering::Less));
239	}
240
241	#[test]
242	fn compare_array_missing_value_right() {
243		let idi: Idiom = syn::idiom("test.something.*").unwrap().into();
244		let one = parse_val!("{ test: { other: null, something: [1, 2, 3] } }");
245		let two = parse_val!("{ test: { other: null, something: [1, null, 3] } }");
246		let res = one.compare(&two, &idi, false, false);
247		assert_eq!(res, Some(Ordering::Greater));
248	}
249
250	#[test]
251	fn compare_last() {
252		let idi: Idiom = syn::idiom("test[$]").unwrap().into();
253		let one = parse_val!("{ test: [1,5] }");
254		let two = parse_val!("{ test: [2,4] }");
255		let res = one.compare(&two, &idi, false, false);
256		assert_eq!(res, Some(Ordering::Greater))
257	}
258
259	#[test]
260	fn compare_field_on_record_id() {
261		// Both are RecordIds, trying to access a field should be Equal (both missing field)
262		let idi: Idiom = syn::idiom("city.name").unwrap().into();
263		let one = parse_val!("{ city: city:1 }");
264		let two = parse_val!("{ city: city:2 }");
265		let res = one.compare(&two, &idi, false, false);
266		assert_eq!(res, Some(Ordering::Equal));
267	}
268
269	#[test]
270	fn compare_field_on_record_id_different_order() {
271		// Even with different record IDs, field access should treat them as equal (both missing)
272		let idi: Idiom = syn::idiom("city.name").unwrap().into();
273		let one = parse_val!("{ city: city:100 }");
274		let two = parse_val!("{ city: city:1 }");
275		let res = one.compare(&two, &idi, false, false);
276		assert_eq!(res, Some(Ordering::Equal));
277	}
278
279	#[test]
280	fn compare_field_object_vs_record_id() {
281		// Object with field vs RecordId without field
282		let idi: Idiom = syn::idiom("city.name").unwrap().into();
283		let one = parse_val!("{ city: { name: 'San Francisco' } }");
284		let two = parse_val!("{ city: city:1 }");
285		let res = one.compare(&two, &idi, false, false);
286		// Object has field, RecordId doesn't, so Object > RecordId (Greater)
287		assert_eq!(res, Some(Ordering::Greater));
288	}
289
290	#[test]
291	fn compare_field_record_id_vs_object() {
292		// RecordId without field vs Object with field
293		let idi: Idiom = syn::idiom("city.name").unwrap().into();
294		let one = parse_val!("{ city: city:1 }");
295		let two = parse_val!("{ city: { name: 'San Francisco' } }");
296		let res = one.compare(&two, &idi, false, false);
297		// RecordId doesn't have field, Object does, so RecordId < Object (Less)
298		assert_eq!(res, Some(Ordering::Less));
299	}
300
301	#[test]
302	fn compare_field_object_missing_vs_record_id() {
303		// Object without the specific field vs RecordId
304		let idi: Idiom = syn::idiom("city.name").unwrap().into();
305		let one = parse_val!("{ city: { country: 'USA' } }");
306		let two = parse_val!("{ city: city:1 }");
307		let res = one.compare(&two, &idi, false, false);
308		// Both missing field, should be Equal
309		assert_eq!(res, Some(Ordering::Equal));
310	}
311
312	#[test]
313	fn compare_field_on_string() {
314		// Both are Strings, trying to access a field should be Equal (both missing field)
315		let idi: Idiom = syn::idiom("value.nested").unwrap().into();
316		let one = parse_val!("{ value: 'hello' }");
317		let two = parse_val!("{ value: 'world' }");
318		let res = one.compare(&two, &idi, false, false);
319		assert_eq!(res, Some(Ordering::Equal));
320	}
321
322	#[test]
323	fn compare_field_on_number() {
324		// Both are Numbers, trying to access a field should be Equal (both missing field)
325		let idi: Idiom = syn::idiom("value.nested").unwrap().into();
326		let one = parse_val!("{ value: 42 }");
327		let two = parse_val!("{ value: 100 }");
328		let res = one.compare(&two, &idi, false, false);
329		assert_eq!(res, Some(Ordering::Equal));
330	}
331
332	#[test]
333	fn compare_field_mixed_types() {
334		// String vs Number, both can't access field, should be Equal
335		let idi: Idiom = syn::idiom("value.nested").unwrap().into();
336		let one = parse_val!("{ value: 'hello' }");
337		let two = parse_val!("{ value: 42 }");
338		let res = one.compare(&two, &idi, false, false);
339		assert_eq!(res, Some(Ordering::Equal));
340	}
341
342	#[test]
343	fn compare_nested_field_on_record_id() {
344		// Deeply nested field access on RecordId
345		let idi: Idiom = syn::idiom("user.city.name").unwrap().into();
346		let one = parse_val!("{ user: user:1 }");
347		let two = parse_val!("{ user: user:2 }");
348		let res = one.compare(&two, &idi, false, false);
349		assert_eq!(res, Some(Ordering::Equal));
350	}
351}