1use crate::{PatchOperation, RdfPatch};
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::info;
8
9pub struct ConflictResolver {
10 strategy: ConflictStrategy,
11 priority_rules: Vec<PriorityRule>,
12 merge_policies: HashMap<String, MergePolicy>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum ConflictStrategy {
17 FirstWins,
18 LastWins,
19 Merge,
20 Manual,
21 Priority,
22 Temporal,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PriorityRule {
27 pub operation_type: String,
28 pub priority: i32,
29 pub source_pattern: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum MergePolicy {
34 Union,
35 Intersection,
36 CustomLogic(String),
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ConflictReport {
41 pub conflicts_found: usize,
42 pub conflicts_resolved: usize,
43 pub resolution_strategy: ConflictStrategy,
44 pub detailed_conflicts: Vec<DetailedConflict>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DetailedConflict {
49 pub conflict_type: String,
50 pub operation1: PatchOperation,
51 pub operation2: PatchOperation,
52 pub resolution: ConflictResolution,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub enum ConflictResolution {
57 KeepFirst,
58 KeepSecond,
59 KeepBoth,
60 Merged(PatchOperation),
61 RequiresManualReview,
62}
63
64impl ConflictResolver {
65 pub fn new(strategy: ConflictStrategy) -> Self {
66 Self {
67 strategy,
68 priority_rules: Vec::new(),
69 merge_policies: HashMap::new(),
70 }
71 }
72
73 pub fn with_priority_rule(mut self, rule: PriorityRule) -> Self {
74 self.priority_rules.push(rule);
75 self
76 }
77
78 pub fn with_merge_policy(mut self, operation_type: String, policy: MergePolicy) -> Self {
79 self.merge_policies.insert(operation_type, policy);
80 self
81 }
82
83 pub fn resolve_conflicts(
85 &self,
86 patch1: &RdfPatch,
87 patch2: &RdfPatch,
88 ) -> Result<(RdfPatch, ConflictReport)> {
89 let mut merged_patch = RdfPatch::new();
90 merged_patch.id = format!("merged-{}-{}", patch1.id, patch2.id);
91
92 let mut conflicts = Vec::new();
93
94 let mut merged_ops: Vec<PatchOperation> = patch1.operations.clone();
97 let mut resolved_indices: std::collections::HashSet<usize> =
100 std::collections::HashSet::new();
101
102 for op2 in &patch2.operations {
103 let conflicting = patch1.operations.iter().enumerate().find(|(idx, op1)| {
105 !resolved_indices.contains(idx) && self.operations_conflict(op1, op2)
106 });
107
108 if let Some((idx, op1)) = conflicting {
109 resolved_indices.insert(idx);
110 let resolution = self.resolve_operation_conflict(op1, op2)?;
111
112 match &resolution {
114 ConflictResolution::KeepFirst => {
115 }
117 ConflictResolution::KeepSecond => {
118 if let Some(slot) = merged_ops.get_mut(idx) {
120 *slot = op2.clone();
121 }
122 }
123 ConflictResolution::KeepBoth => {
124 merged_ops.push(op2.clone());
125 }
126 ConflictResolution::Merged(merged_op) => {
127 if let Some(slot) = merged_ops.get_mut(idx) {
128 *slot = merged_op.clone();
129 }
130 }
131 ConflictResolution::RequiresManualReview => {
132 merged_ops.push(PatchOperation::Header {
133 key: "conflict".to_string(),
134 value: format!("Manual review required for {:?} vs {:?}", op1, op2),
135 });
136 }
137 }
138
139 conflicts.push(DetailedConflict {
140 conflict_type: "operation_overlap".to_string(),
141 operation1: op1.clone(),
142 operation2: op2.clone(),
143 resolution,
144 });
145 } else if !merged_ops.contains(op2) {
146 merged_ops.push(op2.clone());
148 }
149 }
150
151 for operation in merged_ops {
152 merged_patch.add_operation(operation);
153 }
154
155 let report = ConflictReport {
156 conflicts_found: conflicts.len(),
157 conflicts_resolved: conflicts
158 .iter()
159 .filter(|c| !matches!(c.resolution, ConflictResolution::RequiresManualReview))
160 .count(),
161 resolution_strategy: self.strategy.clone(),
162 detailed_conflicts: conflicts,
163 };
164
165 info!(
166 "Conflict resolution completed: {}/{} conflicts resolved",
167 report.conflicts_resolved, report.conflicts_found
168 );
169 Ok((merged_patch, report))
170 }
171
172 fn operations_conflict(&self, op1: &PatchOperation, op2: &PatchOperation) -> bool {
180 if let (Some((add1, s1, p1, o1)), Some((add2, s2, p2, o2))) =
181 (Self::triple_parts(op1), Self::triple_parts(op2))
182 {
183 if s1 == s2 && p1 == p2 {
184 return o1 != o2 || add1 != add2;
188 }
189 return false;
190 }
191
192 if let (Some((add1, g1)), Some((add2, g2))) =
193 (Self::graph_parts(op1), Self::graph_parts(op2))
194 {
195 return g1 == g2 && add1 != add2;
197 }
198
199 false
200 }
201
202 fn triple_parts(operation: &PatchOperation) -> Option<(bool, &str, &str, &str)> {
204 match operation {
205 PatchOperation::Add {
206 subject,
207 predicate,
208 object,
209 } => Some((true, subject, predicate, object)),
210 PatchOperation::Delete {
211 subject,
212 predicate,
213 object,
214 } => Some((false, subject, predicate, object)),
215 _ => None,
216 }
217 }
218
219 fn graph_parts(operation: &PatchOperation) -> Option<(bool, &str)> {
221 match operation {
222 PatchOperation::AddGraph { graph } => Some((true, graph)),
223 PatchOperation::DeleteGraph { graph } => Some((false, graph)),
224 _ => None,
225 }
226 }
227
228 fn resolve_operation_conflict(
229 &self,
230 op1: &PatchOperation,
231 op2: &PatchOperation,
232 ) -> Result<ConflictResolution> {
233 match self.strategy {
234 ConflictStrategy::FirstWins => Ok(ConflictResolution::KeepFirst),
235 ConflictStrategy::LastWins => Ok(ConflictResolution::KeepSecond),
236 ConflictStrategy::Merge => {
237 self.attempt_merge(op1, op2)
239 }
240 ConflictStrategy::Priority => {
241 self.resolve_by_priority(op1, op2)
243 }
244 ConflictStrategy::Temporal => {
245 Ok(ConflictResolution::KeepSecond) }
248 ConflictStrategy::Manual => Ok(ConflictResolution::RequiresManualReview),
249 }
250 }
251
252 fn attempt_merge(
253 &self,
254 op1: &PatchOperation,
255 op2: &PatchOperation,
256 ) -> Result<ConflictResolution> {
257 match (op1, op2) {
258 (
259 PatchOperation::Add {
260 subject: s1,
261 predicate: p1,
262 object: o1,
263 },
264 PatchOperation::Add {
265 subject: s2,
266 predicate: p2,
267 object: o2,
268 },
269 ) => {
270 if s1 == s2 && p1 == p2 && o1 == o2 {
271 Ok(ConflictResolution::KeepFirst)
273 } else {
274 Ok(ConflictResolution::KeepBoth)
277 }
278 }
279 (PatchOperation::Add { .. }, PatchOperation::Delete { .. })
282 | (PatchOperation::Delete { .. }, PatchOperation::Add { .. }) => {
283 Ok(ConflictResolution::KeepFirst)
284 }
285 _ => Ok(ConflictResolution::RequiresManualReview),
286 }
287 }
288
289 fn resolve_by_priority(
290 &self,
291 op1: &PatchOperation,
292 op2: &PatchOperation,
293 ) -> Result<ConflictResolution> {
294 let priority1 = self.get_operation_priority(op1);
295 let priority2 = self.get_operation_priority(op2);
296
297 if priority1 > priority2 {
298 Ok(ConflictResolution::KeepFirst)
299 } else if priority2 > priority1 {
300 Ok(ConflictResolution::KeepSecond)
301 } else {
302 Ok(ConflictResolution::RequiresManualReview)
303 }
304 }
305
306 fn get_operation_priority(&self, operation: &PatchOperation) -> i32 {
307 let op_type = match operation {
308 PatchOperation::Add { .. } => "add",
309 PatchOperation::Delete { .. } => "delete",
310 PatchOperation::AddGraph { .. } => "add_graph",
311 PatchOperation::DeleteGraph { .. } => "delete_graph",
312 _ => "other",
313 };
314
315 for rule in &self.priority_rules {
316 if rule.operation_type == op_type {
317 return rule.priority;
318 }
319 }
320
321 0 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 fn add(subject: &str, object: &str) -> PatchOperation {
330 PatchOperation::Add {
331 subject: subject.to_string(),
332 predicate: "http://example.org/p".to_string(),
333 object: object.to_string(),
334 }
335 }
336
337 fn delete(subject: &str, object: &str) -> PatchOperation {
338 PatchOperation::Delete {
339 subject: subject.to_string(),
340 predicate: "http://example.org/p".to_string(),
341 object: object.to_string(),
342 }
343 }
344
345 #[test]
346 fn regression_add_vs_delete_conflict_detected() {
347 let mut p1 = RdfPatch::new();
348 p1.add_operation(add("http://example.org/s", "http://example.org/o"));
349
350 let mut p2 = RdfPatch::new();
351 p2.add_operation(delete("http://example.org/s", "http://example.org/o"));
352
353 let resolver = ConflictResolver::new(ConflictStrategy::LastWins);
354 let (merged, report) = resolver.resolve_conflicts(&p1, &p2).unwrap();
355
356 assert_eq!(report.conflicts_found, 1);
359 assert_eq!(report.conflicts_resolved, 1);
360
361 assert_eq!(merged.operations.len(), 1);
363 assert!(matches!(
364 merged.operations[0],
365 PatchOperation::Delete { .. }
366 ));
367 }
368
369 #[test]
370 fn regression_non_conflicting_ops_concatenate() {
371 let mut p1 = RdfPatch::new();
372 p1.add_operation(add("http://example.org/s1", "http://example.org/o"));
373
374 let mut p2 = RdfPatch::new();
375 p2.add_operation(add("http://example.org/s2", "http://example.org/o"));
376
377 let resolver = ConflictResolver::new(ConflictStrategy::LastWins);
378 let (merged, report) = resolver.resolve_conflicts(&p1, &p2).unwrap();
379
380 assert_eq!(report.conflicts_found, 0);
381 assert_eq!(merged.operations.len(), 2);
382 }
383}