Skip to main content

surrealdb_expr/val/value/
diff.rs

1use crate::expr::operation::Operation;
2use crate::val::{Strand, Value};
3
4impl Value {
5	pub fn diff(&self, val: &Value) -> Vec<Operation> {
6		let mut res = Vec::new();
7		let mut path = Vec::new();
8
9		self.diff_rec(val, &mut path, &mut res);
10
11		res
12	}
13
14	fn diff_rec(&self, val: &Value, path: &mut Vec<Strand>, ops: &mut Vec<Operation>) {
15		match (self, val) {
16			(Value::Object(a), Value::Object(b)) if a != b => {
17				// Loop over old keys
18				for (key, _) in a.iter() {
19					if !b.contains_key(key) {
20						let mut path = path.clone();
21						path.push(key.clone());
22						ops.push(Operation::Remove {
23							path,
24						});
25					}
26				}
27				// Loop over new keys
28				for (key, val) in b.iter() {
29					match a.get(key) {
30						None => {
31							let mut path = path.clone();
32							path.push(key.clone());
33							ops.push(Operation::Add {
34								path,
35								value: val.clone(),
36							});
37						}
38						Some(old) => {
39							path.push(key.clone());
40							old.diff_rec(val, path, ops);
41							path.pop();
42						}
43					}
44				}
45			}
46			(Value::Array(a), Value::Array(b)) if a != b => {
47				let min_len = a.len().min(b.len());
48				for n in 0..min_len {
49					path.push(n.to_string().into());
50					a[n].diff_rec(&b[n], path, ops);
51					path.pop();
52				}
53				for n in min_len..b.len() {
54					let mut path = path.clone();
55					path.push(n.to_string().into());
56					ops.push(Operation::Add {
57						path,
58						value: b[n].clone(),
59					})
60				}
61				// Descending: the operations apply in order and a `remove`
62				// shifts every later element down, so removing the lowest index
63				// first would leave the indices still to be removed naming
64				// different elements than the ones this diff chose.
65				for n in (min_len..a.len()).rev() {
66					let mut path = path.clone();
67					path.push(n.to_string().into());
68					ops.push(Operation::Remove {
69						path,
70					})
71				}
72			}
73			(Value::String(a), Value::String(b)) if a != b => ops.push(Operation::Change {
74				path: path.clone(),
75				value: {
76					let dmp = dmp::new();
77					let pch = dmp.patch_make1(a, b);
78					let txt = dmp.patch_to_text(&pch);
79					txt.into()
80				},
81			}),
82			(a, b) if a != b => ops.push(Operation::Replace {
83				path: path.clone(),
84				value: val.clone(),
85			}),
86			(_, _) => (),
87		}
88	}
89}
90
91#[cfg(test)]
92mod tests {
93
94	use super::*;
95	use crate::syn;
96
97	macro_rules! parse_val {
98		($input:expr) => {
99			crate::val::convert_public_value_to_internal(syn::value($input).unwrap())
100		};
101	}
102
103	#[test]
104	fn diff_none() {
105		let old = parse_val!("{ test: true, text: 'text', other: { something: true } }");
106		let now = parse_val!("{ test: true, text: 'text', other: { something: true } }");
107		let res = parse_val!("[]");
108		let res = Operation::value_to_operations(res).unwrap();
109		assert_eq!(res, old.diff(&now));
110	}
111
112	#[test]
113	fn diff_add() {
114		let old = parse_val!("{ test: true }");
115		let now = parse_val!("{ test: true, other: 'test' }");
116		let res = parse_val!("[{ op: 'add', path: '/other', value: 'test' }]");
117		let res = Operation::value_to_operations(res).unwrap();
118		assert_eq!(res, old.diff(&now));
119	}
120
121	#[test]
122	fn diff_remove() {
123		let old = parse_val!("{ test: true, other: 'test' }");
124		let now = parse_val!("{ test: true }");
125		let res = parse_val!("[{ op: 'remove', path: '/other' }]");
126		let res = Operation::value_to_operations(res).unwrap();
127		assert_eq!(res, old.diff(&now));
128	}
129
130	#[test]
131	fn diff_add_array() {
132		let old = parse_val!("{ test: [1,2,3] }");
133		let now = parse_val!("{ test: [1,2,3,4] }");
134		let res = parse_val!("[{ op: 'add', path: '/test/3', value: 4 }]");
135		let res = Operation::value_to_operations(res).unwrap();
136		assert_eq!(res, old.diff(&now));
137	}
138
139	#[test]
140	fn diff_replace_embedded() {
141		let old = parse_val!("{ test: { other: 'test' } }");
142		let now = parse_val!("{ test: { other: false } }");
143		let res = parse_val!("[{ op: 'replace', path: '/test/other', value: false }]");
144		let res = Operation::value_to_operations(res).unwrap();
145		assert_eq!(res, old.diff(&now));
146	}
147
148	#[test]
149	fn diff_change_text() {
150		let old = parse_val!("{ test: { other: 'test' } }");
151		let now = parse_val!("{ test: { other: 'text' } }");
152		let res = parse_val!(
153			"[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]"
154		);
155		let res = Operation::value_to_operations(res).unwrap();
156		assert_eq!(res, old.diff(&now));
157	}
158
159	#[test]
160	fn diff_change_text_root() {
161		// Issue 7239: diffing two strings at the root must emit an empty path,
162		// which serializes to "" (RFC 6901 root pointer).
163		let old = parse_val!("'test'");
164		let now = parse_val!("'text'");
165		let ops = old.diff(&now);
166		assert_eq!(ops.len(), 1);
167		match &ops[0] {
168			Operation::Change {
169				path,
170				value: _,
171			} => {
172				assert!(path.is_empty(), "expected empty path, got {path:?}");
173			}
174			other => panic!("expected Operation::Change, got {other:?}"),
175		}
176		// Verify the JSON Patch serialization produces path "".
177		let patch = Operation::operations_to_value(ops);
178		let expected =
179			parse_val!("[{ op: 'change', path: '', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]");
180		assert_eq!(patch, expected);
181	}
182
183	#[test]
184	fn diff_replace_root() {
185		// Issue 7239: diffing two non-string scalars at the root must emit
186		// an Operation::Replace with an empty path.
187		let old = parse_val!("1");
188		let now = parse_val!("2");
189		let ops = old.diff(&now);
190		assert_eq!(ops.len(), 1);
191		match &ops[0] {
192			Operation::Replace {
193				path,
194				value: _,
195			} => {
196				assert!(path.is_empty(), "expected empty path, got {path:?}");
197			}
198			other => panic!("expected Operation::Replace, got {other:?}"),
199		}
200		let patch = Operation::operations_to_value(ops);
201		let expected = parse_val!("[{ op: 'replace', path: '', value: 2 }]");
202		assert_eq!(patch, expected);
203	}
204}