Skip to main content

reddb_server/application/
merge_json.rs

1//! 3-way JSON merge used by `VCS.merge`, `cherry_pick`, and `revert`.
2//!
3//! Given `base` (LCA), `ours` (current HEAD content), and `theirs` (commit
4//! being merged in), produces either a cleanly merged value or a list of
5//! conflicting paths. Pure data logic, no storage dependencies — unit
6//! testable in isolation.
7//!
8//! Semantics per node:
9//! - equal on both sides             → take it
10//! - one side equals base            → take the other
11//! - both objects                    → recurse per key union
12//! - both arrays                     → length-aware element merge
13//! - primitives diverge both ways    → conflict
14//! - type mismatch across sides      → conflict
15
16use crate::json::{Map, Value};
17
18/// Dot-path into a JSON value, with `[idx]` for array indices.
19/// Example: `user.roles[2].name`. Used to report conflict locations.
20pub type JsonPath = String;
21
22#[derive(Debug, Clone)]
23pub struct MergeConflict {
24    pub path: JsonPath,
25    pub base: Value,
26    pub ours: Value,
27    pub theirs: Value,
28}
29
30#[derive(Debug, Clone)]
31pub struct MergeResult {
32    /// Best-effort merged value. Conflicting positions fall back to
33    /// `ours` so callers can still persist a coherent document.
34    pub merged: Value,
35    pub conflicts: Vec<MergeConflict>,
36}
37
38impl MergeResult {
39    pub fn is_clean(&self) -> bool {
40        self.conflicts.is_empty()
41    }
42}
43
44/// Run a 3-way merge. Returns merged value + conflict list.
45pub fn three_way_merge(base: &Value, ours: &Value, theirs: &Value) -> MergeResult {
46    let mut conflicts = Vec::new();
47    let merged = merge_at(base, ours, theirs, "", &mut conflicts);
48    MergeResult { merged, conflicts }
49}
50
51fn merge_at(
52    base: &Value,
53    ours: &Value,
54    theirs: &Value,
55    path: &str,
56    conflicts: &mut Vec<MergeConflict>,
57) -> Value {
58    if ours == theirs {
59        return ours.clone();
60    }
61    if ours == base {
62        return theirs.clone();
63    }
64    if theirs == base {
65        return ours.clone();
66    }
67
68    match (ours, theirs) {
69        (Value::Object(o), Value::Object(t)) => {
70            let empty = Map::new();
71            let b = match base {
72                Value::Object(m) => m,
73                _ => &empty,
74            };
75            merge_objects(b, o, t, path, conflicts)
76        }
77        (Value::Array(o), Value::Array(t)) => {
78            let empty: Vec<Value> = Vec::new();
79            let b: &Vec<Value> = match base {
80                Value::Array(a) => a,
81                _ => &empty,
82            };
83            merge_arrays(b, o, t, path, conflicts)
84        }
85        _ => {
86            conflicts.push(MergeConflict {
87                path: path.to_string(),
88                base: base.clone(),
89                ours: ours.clone(),
90                theirs: theirs.clone(),
91            });
92            ours.clone()
93        }
94    }
95}
96
97fn merge_objects(
98    base: &Map<String, Value>,
99    ours: &Map<String, Value>,
100    theirs: &Map<String, Value>,
101    path: &str,
102    conflicts: &mut Vec<MergeConflict>,
103) -> Value {
104    let mut out = Map::new();
105    let null = Value::Null;
106
107    let mut keys: Vec<&str> = Vec::new();
108    for k in ours.keys() {
109        keys.push(k.as_str());
110    }
111    for k in theirs.keys() {
112        if !ours.contains_key(k) {
113            keys.push(k.as_str());
114        }
115    }
116    for k in base.keys() {
117        if !ours.contains_key(k) && !theirs.contains_key(k) {
118            keys.push(k.as_str());
119        }
120    }
121
122    for k in keys {
123        let b = base.get(k).unwrap_or(&null);
124        let o = ours.get(k).unwrap_or(&null);
125        let t = theirs.get(k).unwrap_or(&null);
126
127        let child_path = if path.is_empty() {
128            k.to_string()
129        } else {
130            format!("{path}.{k}")
131        };
132
133        // Both sides deleted (absent + base had it).
134        let ours_deleted = !ours.contains_key(k) && base.contains_key(k);
135        let theirs_deleted = !theirs.contains_key(k) && base.contains_key(k);
136
137        if ours_deleted && theirs_deleted {
138            continue; // both removed — honour the removal
139        }
140        if ours_deleted && t == b {
141            continue; // we removed, they left untouched
142        }
143        if theirs_deleted && o == b {
144            continue; // they removed, we left untouched
145        }
146        if ours_deleted && t != b {
147            // we removed, they modified — conflict
148            conflicts.push(MergeConflict {
149                path: child_path,
150                base: b.clone(),
151                ours: Value::Null,
152                theirs: t.clone(),
153            });
154            out.insert(k.to_string(), t.clone());
155            continue;
156        }
157        if theirs_deleted && o != b {
158            // they removed, we modified — conflict, keep ours
159            conflicts.push(MergeConflict {
160                path: child_path,
161                base: b.clone(),
162                ours: o.clone(),
163                theirs: Value::Null,
164            });
165            out.insert(k.to_string(), o.clone());
166            continue;
167        }
168
169        let merged = merge_at(b, o, t, &child_path, conflicts);
170        out.insert(k.to_string(), merged);
171    }
172
173    Value::Object(out)
174}
175
176/// Array merge: length-aware. If lengths match, element-wise recursive.
177/// If they differ, the side that extended the array from base wins;
178/// if both extended differently, whole-array conflict.
179fn merge_arrays(
180    base: &[Value],
181    ours: &[Value],
182    theirs: &[Value],
183    path: &str,
184    conflicts: &mut Vec<MergeConflict>,
185) -> Value {
186    // Fast path: exactly one side changed length vs base.
187    let ours_same_len = ours.len() == base.len();
188    let theirs_same_len = theirs.len() == base.len();
189
190    if ours_same_len && theirs_same_len {
191        // Both kept the length — element-wise 3-way.
192        let mut out = Vec::with_capacity(ours.len());
193        for i in 0..ours.len() {
194            let child = format!("{path}[{i}]");
195            out.push(merge_at(&base[i], &ours[i], &theirs[i], &child, conflicts));
196        }
197        return Value::Array(out);
198    }
199
200    if theirs_same_len && !ours_same_len {
201        // Only we changed length — take ours.
202        return Value::Array(ours.to_vec());
203    }
204    if ours_same_len && !theirs_same_len {
205        // Only they changed length — take theirs.
206        return Value::Array(theirs.to_vec());
207    }
208
209    // Both changed length differently — whole-array conflict, keep ours.
210    conflicts.push(MergeConflict {
211        path: path.to_string(),
212        base: Value::Array(base.to_vec()),
213        ours: Value::Array(ours.to_vec()),
214        theirs: Value::Array(theirs.to_vec()),
215    });
216    Value::Array(ours.to_vec())
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::json::json;
223
224    #[test]
225    fn identical_trivial() {
226        let v = json!({"a": 1});
227        let r = three_way_merge(&v, &v, &v);
228        assert!(r.is_clean());
229        assert_eq!(r.merged, v);
230    }
231
232    #[test]
233    fn one_sided_change_ours() {
234        let base = json!({"a": 1});
235        let ours = json!({"a": 2});
236        let theirs = json!({"a": 1});
237        let r = three_way_merge(&base, &ours, &theirs);
238        assert!(r.is_clean());
239        assert_eq!(r.merged, ours);
240    }
241
242    #[test]
243    fn one_sided_change_theirs() {
244        let base = json!({"a": 1});
245        let ours = json!({"a": 1});
246        let theirs = json!({"a": 9});
247        let r = three_way_merge(&base, &ours, &theirs);
248        assert!(r.is_clean());
249        assert_eq!(r.merged, theirs);
250    }
251
252    #[test]
253    fn disjoint_object_edits_merge_clean() {
254        let base = json!({"a": 1, "b": 2});
255        let ours = json!({"a": 10, "b": 2});
256        let theirs = json!({"a": 1, "b": 20});
257        let r = three_way_merge(&base, &ours, &theirs);
258        assert!(r.is_clean());
259        assert_eq!(r.merged, json!({"a": 10, "b": 20}));
260    }
261
262    #[test]
263    fn conflicting_object_value() {
264        let base = json!({"a": 1});
265        let ours = json!({"a": 2});
266        let theirs = json!({"a": 3});
267        let r = three_way_merge(&base, &ours, &theirs);
268        assert_eq!(r.conflicts.len(), 1);
269        assert_eq!(r.conflicts[0].path, "a");
270    }
271
272    #[test]
273    fn nested_disjoint_edits() {
274        let base = json!({"user": json!({"name": "a", "age": 30})});
275        let ours = json!({"user": json!({"name": "b", "age": 30})});
276        let theirs = json!({"user": json!({"name": "a", "age": 31})});
277        let r = three_way_merge(&base, &ours, &theirs);
278        assert!(r.is_clean());
279        assert_eq!(r.merged, json!({"user": json!({"name": "b", "age": 31})}));
280    }
281
282    #[test]
283    fn both_added_same_key_conflict() {
284        let base = json!({});
285        let ours = json!({"x": 1});
286        let theirs = json!({"x": 2});
287        let r = three_way_merge(&base, &ours, &theirs);
288        assert_eq!(r.conflicts.len(), 1);
289    }
290
291    #[test]
292    fn both_added_same_key_equal() {
293        let base = json!({});
294        let ours = json!({"x": 1});
295        let theirs = json!({"x": 1});
296        let r = three_way_merge(&base, &ours, &theirs);
297        assert!(r.is_clean());
298    }
299
300    #[test]
301    fn array_elementwise_disjoint() {
302        let base = json!([1, 2, 3]);
303        let ours = json!([10, 2, 3]);
304        let theirs = json!([1, 2, 30]);
305        let r = three_way_merge(&base, &ours, &theirs);
306        assert!(r.is_clean());
307        assert_eq!(r.merged, json!([10, 2, 30]));
308    }
309
310    #[test]
311    fn array_elementwise_conflict() {
312        let base = json!([1]);
313        let ours = json!([2]);
314        let theirs = json!([3]);
315        let r = three_way_merge(&base, &ours, &theirs);
316        assert_eq!(r.conflicts.len(), 1);
317        assert_eq!(r.conflicts[0].path, "[0]");
318    }
319
320    #[test]
321    fn array_one_sided_length_change() {
322        let base = json!([1, 2]);
323        let ours = json!([1, 2, 3]);
324        let theirs = json!([1, 2]);
325        let r = three_way_merge(&base, &ours, &theirs);
326        assert!(r.is_clean());
327        assert_eq!(r.merged, ours);
328    }
329
330    #[test]
331    fn array_both_extended_conflict() {
332        let base = json!([1]);
333        let ours = json!([1, 2]);
334        let theirs = json!([1, 3]);
335        let r = three_way_merge(&base, &ours, &theirs);
336        assert_eq!(r.conflicts.len(), 1);
337    }
338
339    #[test]
340    fn deletion_both_sides_clean() {
341        let base = json!({"a": 1, "b": 2});
342        let ours = json!({"a": 1});
343        let theirs = json!({"a": 1});
344        let r = three_way_merge(&base, &ours, &theirs);
345        assert!(r.is_clean());
346        assert_eq!(r.merged, json!({"a": 1}));
347    }
348
349    #[test]
350    fn deletion_vs_modification_conflict() {
351        let base = json!({"a": 1, "b": 2});
352        let ours = json!({"a": 1});
353        let theirs = json!({"a": 1, "b": 99});
354        let r = three_way_merge(&base, &ours, &theirs);
355        assert_eq!(r.conflicts.len(), 1);
356        assert_eq!(r.conflicts[0].path, "b");
357    }
358
359    #[test]
360    fn type_mismatch_conflict() {
361        let base = json!(null);
362        let ours = json!({"a": 1});
363        let theirs = json!([1, 2]);
364        let r = three_way_merge(&base, &ours, &theirs);
365        assert_eq!(r.conflicts.len(), 1);
366    }
367}