1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
(** 3-way merge of two branch tips using their Lowest Common Ancestor. Walks the
history chain to find it, diffs both sides against it, applies merge rules,
validates constraints, and persists the result. *)
type conflict =
| TupleConflict of { relation : string; hash : Conventions.Hash.t }
| SchemaConflict of string
| ConstraintConflict of string
type strategy = PreferLeft | PreferRight | RevertToAncestor
type merge_result = Clean of Database.t | Failed of conflict list
(** Module type for the subset of Manipulation.Make we need. *)
module type MANIPULATION = sig
type storage
type error
val of_string_error : string -> error
val load_database :
storage -> Conventions.Hash.t -> (Database.t option, error) result
val store_database : storage -> Database.t -> (unit, error) result
end
module StringSet = Set.Make (String)
(** Walk history to find LCA. First hash in right's ancestry that also appears
in left's ancestry set. *)
let find_lca left_db right_db =
let left_ancestors =
StringSet.of_list (left_db.Database.hash :: left_db.Database.history)
in
let right_chain = right_db.Database.hash :: right_db.Database.history in
List.find_opt (fun h -> StringSet.mem h left_ancestors) right_chain
(** Apply a single relation diff to a database, using strategy on conflicts. *)
let apply_diff db ~strategy ~left_db ~right_db ~(left_diff : Diff.relation_diff)
~(right_diff : Diff.relation_diff option) : Database.t * conflict list =
match left_diff with
| Diff.RelationAdded rel ->
(* Only left added it; right_diff must be None or same *)
(Database.add_relation db ~relation:rel, [])
| Diff.RelationRemoved name -> (Database.remove_relation db ~name, [])
| Diff.RelationModified { name; added_tuples; removed_tuples; schema_changed }
-> (
let conflicts = ref [] in
(* Determine the merged relation's Merkle tree *)
let base_rel_opt = Database.get_relation db name in
match base_rel_opt with
| None -> (db, [])
| Some base_rel ->
let base_tree =
Option.value base_rel.Relation.tree ~default:Merkle.empty
in
(* Handle schema conflict *)
let resolved_rel, schema_conflict =
if schema_changed then
match right_diff with
| Some (Diff.RelationModified { schema_changed = true; _ }) ->
(* Both changed schema — apply strategy *)
let winner =
match strategy with
| PreferLeft -> Database.get_relation left_db name
| PreferRight -> Database.get_relation right_db name
| RevertToAncestor -> Some base_rel
in
( Option.value winner ~default:base_rel,
[ SchemaConflict name ] )
| _ ->
(* Only left changed schema — take left's relation *)
let left_rel =
Option.value
(Database.get_relation left_db name)
~default:base_rel
in
(left_rel, [])
else (base_rel, [])
in
conflicts := schema_conflict @ !conflicts;
(* Apply tuple adds/removes from left *)
let merged_tree =
List.fold_left (fun t h -> Merkle.insert h t) base_tree added_tuples
in
let merged_tree =
List.fold_left
(fun t h -> Merkle.delete h t)
merged_tree removed_tuples
in
(* Apply tuple adds/removes from right if disjoint *)
let merged_tree =
match right_diff with
| Some
(Diff.RelationModified
{ added_tuples = r_add; removed_tuples = r_rem; _ }) ->
(* Detect tuple conflicts: same hash both added and removed *)
let left_add = StringSet.of_list added_tuples in
let left_rem = StringSet.of_list removed_tuples in
let right_add = StringSet.of_list r_add in
let right_rem = StringSet.of_list r_rem in
let conflict_hashes =
StringSet.inter left_add right_rem
|> StringSet.union (StringSet.inter left_rem right_add)
in
if not (StringSet.is_empty conflict_hashes) then begin
StringSet.iter
(fun h ->
conflicts :=
TupleConflict { relation = name; hash = h }
:: !conflicts)
conflict_hashes;
(* Resolve conflicting tuples per strategy *)
match strategy with
| PreferLeft ->
(* keep left's merged_tree as-is, ignore conflicting right ops *)
let safe_add = StringSet.diff right_add conflict_hashes in
let safe_rem = StringSet.diff right_rem conflict_hashes in
let t =
StringSet.fold
(fun h acc -> Merkle.insert h acc)
safe_add merged_tree
in
StringSet.fold
(fun h acc -> Merkle.delete h acc)
safe_rem t
| PreferRight ->
(* revert left's conflicting ops, apply right's *)
let t =
StringSet.fold
(fun h acc -> Merkle.delete h acc)
conflict_hashes merged_tree
in
let t =
StringSet.fold
(fun h acc -> Merkle.insert h acc)
right_add t
in
StringSet.fold
(fun h acc -> Merkle.delete h acc)
right_rem t
| RevertToAncestor ->
(* drop conflicting hashes from merged tree *)
StringSet.fold
(fun h acc -> Merkle.delete h acc)
conflict_hashes merged_tree
end
else begin
(* Disjoint — union both sides *)
let t =
List.fold_left
(fun acc h -> Merkle.insert h acc)
merged_tree r_add
in
List.fold_left (fun acc h -> Merkle.delete h acc) t r_rem
end
| _ -> merged_tree
in
let new_hash =
Hashing.hash_relation ~name:resolved_rel.Relation.name
~schema:resolved_rel.Relation.schema ~tree:merged_tree
in
let updated_rel =
{
resolved_rel with
Relation.tree = Some merged_tree;
hash = Some new_hash;
}
in
let new_db = Database.update_relation db ~relation:updated_rel in
(new_db, !conflicts))
module Make
(Storage : Physical.S)
(Manip : MANIPULATION with type storage = Storage.t) =
struct
let err s = Error (Manip.of_string_error s)
(* Load both tips, find the lowest common ancestor, compute the diffs,
lookup for right diffs by relation name, start from ancestor and
apply all the diffs, apply first the left diffs, then right-only diffs
(relations only right touched). If there are no conflicts, persist the
merged database, otherwise return Failed, unless a strategy resolved them. *)
let merge ~storage ~strategy ~left_tip ~right_tip =
match Manip.load_database storage left_tip with
| Error e -> Error e
| Ok None -> err ("Left tip not found: " ^ left_tip)
| Ok (Some left_db) -> (
match Manip.load_database storage right_tip with
| Error e -> Error e
| Ok None -> err ("Right tip not found: " ^ right_tip)
| Ok (Some right_db) -> (
match find_lca left_db right_db with
| None -> err "No common ancestor found between branches"
| Some ancestor_hash -> (
match Manip.load_database storage ancestor_hash with
| Error e -> Error e
| Ok None -> err ("Ancestor not found: " ^ ancestor_hash)
| Ok (Some ancestor_db) ->
let left_diffs =
Diff.diff ~ancestor:ancestor_db ~target:left_db
in
let right_diffs =
Diff.diff ~ancestor:ancestor_db ~target:right_db
in
let right_diff_map =
List.fold_left
(fun m d ->
let key =
match d with
| Diff.RelationAdded r -> r.Relation.name
| Diff.RelationRemoved n -> n
| Diff.RelationModified { name; _ } -> name
in
Database.RelationMap.add key d m)
Database.RelationMap.empty right_diffs
in
let merged_db, all_conflicts =
let db_after_left, left_conflicts =
List.fold_left
(fun (db, conflicts) left_d ->
let rel_name =
match left_d with
| Diff.RelationAdded r -> r.Relation.name
| Diff.RelationRemoved n -> n
| Diff.RelationModified { name; _ } -> name
in
let right_d =
Database.RelationMap.find_opt rel_name
right_diff_map
in
let new_db, cs =
apply_diff db ~strategy ~left_db ~right_db
~left_diff:left_d ~right_diff:right_d
in
(new_db, cs @ conflicts))
(ancestor_db, []) left_diffs
in
let left_names =
List.fold_left
(fun s d ->
let n =
match d with
| Diff.RelationAdded r -> r.Relation.name
| Diff.RelationRemoved n -> n
| Diff.RelationModified { name; _ } -> name
in
StringSet.add n s)
StringSet.empty left_diffs
in
let right_only =
List.filter
(fun d ->
let n =
match d with
| Diff.RelationAdded r -> r.Relation.name
| Diff.RelationRemoved n -> n
| Diff.RelationModified { name; _ } -> name
in
not (StringSet.mem n left_names))
right_diffs
in
let db_final, right_conflicts =
List.fold_left
(fun (db, conflicts) right_d ->
let new_db, cs =
apply_diff db ~strategy ~left_db ~right_db
~left_diff:right_d ~right_diff:None
in
(new_db, cs @ conflicts))
(db_after_left, left_conflicts)
right_only
in
(db_final, right_conflicts)
in
let unresolvable =
List.filter
(function ConstraintConflict _ -> true | _ -> false)
all_conflicts
in
if unresolvable <> [] then Ok (Failed unresolvable)
else begin
match Manip.store_database storage merged_db with
| Error e -> Error e
| Ok () -> Ok (Clean merged_db)
end)))
end
|