Skip to main content

oxirs_stream/patch/
conflict.rs

1//! Conflict resolution for patches
2
3use crate::{PatchOperation, RdfPatch};
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, 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    /// Resolve conflicts between two patches
84    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        let mut operation_map = BTreeMap::new();
94
95        // Index operations from both patches
96        for (idx, op) in patch1.operations.iter().enumerate() {
97            let key = self.operation_key(op);
98            operation_map.insert(format!("p1-{idx}-{key}"), (op, "patch1"));
99        }
100
101        for (idx, op) in patch2.operations.iter().enumerate() {
102            let key = self.operation_key(op);
103            let conflict_key = format!("p2-{idx}-{key}");
104
105            // Check for conflicts
106            if let Some(existing) = operation_map
107                .iter()
108                .find(|(k, _)| self.operations_conflict(op, k.split('-').nth(2).unwrap_or("")))
109            {
110                let conflict = DetailedConflict {
111                    conflict_type: "operation_overlap".to_string(),
112                    operation1: existing.1 .0.clone(),
113                    operation2: op.clone(),
114                    resolution: self.resolve_operation_conflict(existing.1 .0, op)?,
115                };
116                conflicts.push(conflict);
117            } else {
118                operation_map.insert(conflict_key, (op, "patch2"));
119            }
120        }
121
122        // Apply resolution strategy
123        for (_, (operation, _source)) in operation_map {
124            merged_patch.add_operation(operation.clone());
125        }
126
127        // Apply conflict resolutions
128        for conflict in &conflicts {
129            match &conflict.resolution {
130                ConflictResolution::KeepFirst => {
131                    // Already in merged patch
132                }
133                ConflictResolution::KeepSecond => {
134                    // Replace with second operation
135                    merged_patch.add_operation(conflict.operation2.clone());
136                }
137                ConflictResolution::KeepBoth => {
138                    merged_patch.add_operation(conflict.operation1.clone());
139                    merged_patch.add_operation(conflict.operation2.clone());
140                }
141                ConflictResolution::Merged(merged_op) => {
142                    merged_patch.add_operation(merged_op.clone());
143                }
144                ConflictResolution::RequiresManualReview => {
145                    // Add as comment or metadata
146                    merged_patch.add_operation(PatchOperation::Header {
147                        key: "conflict".to_string(),
148                        value: format!("Manual review required: {:?}", conflict.conflict_type),
149                    });
150                }
151            }
152        }
153
154        let report = ConflictReport {
155            conflicts_found: conflicts.len(),
156            conflicts_resolved: conflicts
157                .iter()
158                .filter(|c| !matches!(c.resolution, ConflictResolution::RequiresManualReview))
159                .count(),
160            resolution_strategy: self.strategy.clone(),
161            detailed_conflicts: conflicts,
162        };
163
164        info!(
165            "Conflict resolution completed: {}/{} conflicts resolved",
166            report.conflicts_resolved, report.conflicts_found
167        );
168        Ok((merged_patch, report))
169    }
170
171    fn operation_key(&self, operation: &PatchOperation) -> String {
172        match operation {
173            PatchOperation::Add {
174                subject,
175                predicate,
176                object,
177            } => {
178                format!("add-{subject}-{predicate}-{object}")
179            }
180            PatchOperation::Delete {
181                subject,
182                predicate,
183                object,
184            } => {
185                format!("delete-{subject}-{predicate}-{object}")
186            }
187            PatchOperation::AddGraph { graph } => {
188                format!("add-graph-{graph}")
189            }
190            PatchOperation::DeleteGraph { graph } => {
191                format!("delete-graph-{graph}")
192            }
193            _ => format!("{operation:?}"),
194        }
195    }
196
197    fn operations_conflict(&self, _op1: &PatchOperation, _op2_key: &str) -> bool {
198        // Simplified conflict detection - in practice this would be more sophisticated
199        false
200    }
201
202    fn resolve_operation_conflict(
203        &self,
204        op1: &PatchOperation,
205        op2: &PatchOperation,
206    ) -> Result<ConflictResolution> {
207        match self.strategy {
208            ConflictStrategy::FirstWins => Ok(ConflictResolution::KeepFirst),
209            ConflictStrategy::LastWins => Ok(ConflictResolution::KeepSecond),
210            ConflictStrategy::Merge => {
211                // Attempt to merge operations
212                self.attempt_merge(op1, op2)
213            }
214            ConflictStrategy::Priority => {
215                // Use priority rules
216                self.resolve_by_priority(op1, op2)
217            }
218            ConflictStrategy::Temporal => {
219                // Use timestamps if available
220                Ok(ConflictResolution::KeepSecond) // Default to later operation
221            }
222            ConflictStrategy::Manual => Ok(ConflictResolution::RequiresManualReview),
223        }
224    }
225
226    fn attempt_merge(
227        &self,
228        op1: &PatchOperation,
229        op2: &PatchOperation,
230    ) -> Result<ConflictResolution> {
231        match (op1, op2) {
232            (
233                PatchOperation::Add {
234                    subject: s1,
235                    predicate: p1,
236                    object: _o1,
237                },
238                PatchOperation::Add {
239                    subject: s2,
240                    predicate: p2,
241                    object: _o2,
242                },
243            ) => {
244                if s1 == s2 && p1 == p2 {
245                    // Different objects for same subject/predicate - keep both
246                    Ok(ConflictResolution::KeepBoth)
247                } else {
248                    Ok(ConflictResolution::KeepBoth)
249                }
250            }
251            _ => Ok(ConflictResolution::RequiresManualReview),
252        }
253    }
254
255    fn resolve_by_priority(
256        &self,
257        op1: &PatchOperation,
258        op2: &PatchOperation,
259    ) -> Result<ConflictResolution> {
260        let priority1 = self.get_operation_priority(op1);
261        let priority2 = self.get_operation_priority(op2);
262
263        if priority1 > priority2 {
264            Ok(ConflictResolution::KeepFirst)
265        } else if priority2 > priority1 {
266            Ok(ConflictResolution::KeepSecond)
267        } else {
268            Ok(ConflictResolution::RequiresManualReview)
269        }
270    }
271
272    fn get_operation_priority(&self, operation: &PatchOperation) -> i32 {
273        let op_type = match operation {
274            PatchOperation::Add { .. } => "add",
275            PatchOperation::Delete { .. } => "delete",
276            PatchOperation::AddGraph { .. } => "add_graph",
277            PatchOperation::DeleteGraph { .. } => "delete_graph",
278            _ => "other",
279        };
280
281        for rule in &self.priority_rules {
282            if rule.operation_type == op_type {
283                return rule.priority;
284            }
285        }
286
287        0 // Default priority
288    }
289}