ggen_core/delta.rs
1//! Delta-driven projection for detecting and analyzing RDF graph changes
2//!
3//! This module provides functionality to detect and analyze changes in RDF graphs,
4//! determine which templates are affected by those changes, and support three-way
5//! merging for files that contain both generated and manual content.
6//!
7//! ## Features
8//!
9//! - **Graph Comparison**: Compare two RDF graphs and detect semantic differences
10//! - **Delta Types**: Track additions, deletions, and modifications of triples
11//! - **Impact Analysis**: Determine which templates are affected by graph changes
12//! - **Template Impact**: Calculate impact scores for template regeneration
13//! - **Change Detection**: Efficient change detection using graph hashing
14//!
15//! ## Delta Types
16//!
17//! - **Addition**: New triple added to the graph
18//! - **Deletion**: Triple removed from the graph
19//! - **Modification**: Triple's object value changed
20//!
21//! ## Examples
22//!
23//! ### Detecting Graph Changes
24//!
25//! ```rust,no_run
26//! use ggen_core::delta::{GraphDelta, ImpactAnalyzer};
27//! use ggen_core::graph::Graph;
28//!
29//! # fn main() -> ggen_utils::error::Result<()> {
30//! let old_graph = Graph::new()?;
31//! let new_graph = Graph::new()?;
32//!
33//! let delta = GraphDelta::compute(&old_graph, &new_graph)?;
34//! println!("Detected {} changes", delta.changes.len());
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ### Analyzing Template Impact
40//!
41//! ```rust,no_run
42//! use ggen_core::delta::ImpactAnalyzer;
43//! use ggen_core::graph::Graph;
44//!
45//! # fn main() -> ggen_utils::error::Result<()> {
46//! let analyzer = ImpactAnalyzer::new();
47//! let old_graph = Graph::new()?;
48//! let new_graph = Graph::new()?;
49//!
50//! let impacts = analyzer.analyze(&old_graph, &new_graph, &["template1.tmpl".into()])?;
51//! for impact in impacts {
52//! println!("Template {}: impact score {}", impact.template_path.display(), impact.score);
53//! }
54//! # Ok(())
55//! # }
56//! ```
57
58use ahash::AHasher;
59use ggen_utils::error::Result;
60use oxigraph::model::Quad;
61use serde::{Deserialize, Serialize};
62use std::collections::{BTreeMap, BTreeSet};
63use std::fmt;
64use std::hash::{Hash, Hasher};
65
66use crate::graph::Graph;
67
68/// Represents a semantic change in an RDF graph
69///
70/// A `DeltaType` describes a single change between two RDF graphs:
71/// - **Addition**: A new triple was added
72/// - **Deletion**: A triple was removed
73/// - **Modification**: A triple's object value changed
74///
75/// # Examples
76///
77/// ```rust
78/// use ggen_core::delta::DeltaType;
79///
80/// # fn main() {
81/// // Addition example
82/// let addition = DeltaType::Addition {
83/// subject: "http://example.org/subject".to_string(),
84/// predicate: "http://example.org/predicate".to_string(),
85/// object: "http://example.org/object".to_string(),
86/// };
87/// match addition {
88/// DeltaType::Addition { .. } => assert!(true),
89/// _ => panic!("Should be Addition"),
90/// }
91///
92/// // Deletion example
93/// let deletion = DeltaType::Deletion {
94/// subject: "http://example.org/subject".to_string(),
95/// predicate: "http://example.org/predicate".to_string(),
96/// object: "http://example.org/object".to_string(),
97/// };
98/// match deletion {
99/// DeltaType::Deletion { .. } => assert!(true),
100/// _ => panic!("Should be Deletion"),
101/// }
102///
103/// // Modification example
104/// let modification = DeltaType::Modification {
105/// subject: "http://example.org/subject".to_string(),
106/// predicate: "http://example.org/predicate".to_string(),
107/// old_object: "old".to_string(),
108/// new_object: "new".to_string(),
109/// };
110/// match modification {
111/// DeltaType::Modification { .. } => assert!(true),
112/// _ => panic!("Should be Modification"),
113/// }
114/// # }
115/// ```
116///
117/// ```rust,no_run
118/// use ggen_core::delta::DeltaType;
119/// use oxigraph::model::{NamedNode, Literal, Quad};
120///
121/// # fn main() -> ggen_utils::error::Result<()> {
122/// // Create an addition delta
123/// let addition = DeltaType::Addition {
124/// subject: "http://example.org/alice".to_string(),
125/// predicate: "http://example.org/name".to_string(),
126/// object: "Alice".to_string(),
127/// };
128///
129/// // Check which IRIs are affected
130/// let affected = addition.subjects();
131/// assert_eq!(affected, vec!["http://example.org/alice"]);
132/// # Ok(())
133/// # }
134/// ```
135///
136/// ```rust,no_run
137/// use ggen_core::delta::DeltaType;
138///
139/// # fn main() -> ggen_utils::error::Result<()> {
140/// // Create a modification delta
141/// let modification = DeltaType::Modification {
142/// subject: "http://example.org/alice".to_string(),
143/// predicate: "http://example.org/age".to_string(),
144/// old_object: "30".to_string(),
145/// new_object: "31".to_string(),
146/// };
147///
148/// // Check if a specific IRI is affected
149/// assert!(modification.affects_iri("http://example.org/alice"));
150/// # Ok(())
151/// # }
152/// ```
153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
154pub enum DeltaType {
155 /// A triple was added to the graph
156 Addition {
157 subject: String,
158 predicate: String,
159 object: String,
160 },
161 /// A triple was removed from the graph
162 Deletion {
163 subject: String,
164 predicate: String,
165 object: String,
166 },
167 /// A triple's object changed
168 Modification {
169 subject: String,
170 predicate: String,
171 old_object: String,
172 new_object: String,
173 },
174}
175
176impl DeltaType {
177 /// Create a delta from two quads
178 ///
179 /// Compares an old quad (from baseline graph) and a new quad (from current graph)
180 /// and returns the appropriate delta type, or `None` if there's no change.
181 ///
182 /// # Examples
183 ///
184 /// ```rust,no_run
185 /// use ggen_core::delta::DeltaType;
186 /// use oxigraph::model::{NamedNode, Literal, Quad, Subject, Term};
187 ///
188 /// # fn main() -> ggen_utils::error::Result<()> {
189 /// let old_quad = Quad::new(
190 /// Subject::NamedNode(NamedNode::new("http://example.org/alice")?),
191 /// NamedNode::new("http://example.org/name")?,
192 /// Term::Literal(Literal::new_simple_literal("Alice")),
193 /// None,
194 /// );
195 ///
196 /// let new_quad = Quad::new(
197 /// Subject::NamedNode(NamedNode::new("http://example.org/alice")?),
198 /// NamedNode::new("http://example.org/name")?,
199 /// Term::Literal(Literal::new_simple_literal("Alice Smith")),
200 /// None,
201 /// );
202 ///
203 /// // Detect modification
204 /// let delta = DeltaType::from_quads(Some(&old_quad), Some(&new_quad));
205 /// assert!(matches!(delta, Some(DeltaType::Modification { .. })));
206 /// # Ok(())
207 /// # }
208 /// ```
209 pub fn from_quads(old: Option<&Quad>, new: Option<&Quad>) -> Option<Self> {
210 match (old, new) {
211 (None, Some(new_quad)) => Some(DeltaType::Addition {
212 subject: new_quad.subject.to_string(),
213 predicate: new_quad.predicate.to_string(),
214 object: new_quad.object.to_string(),
215 }),
216 (Some(old_quad), None) => Some(DeltaType::Deletion {
217 subject: old_quad.subject.to_string(),
218 predicate: old_quad.predicate.to_string(),
219 object: old_quad.object.to_string(),
220 }),
221 (Some(old_quad), Some(new_quad)) => {
222 if old_quad.subject == new_quad.subject
223 && old_quad.predicate == new_quad.predicate
224 && old_quad.object != new_quad.object
225 {
226 Some(DeltaType::Modification {
227 subject: old_quad.subject.to_string(),
228 predicate: old_quad.predicate.to_string(),
229 old_object: old_quad.object.to_string(),
230 new_object: new_quad.object.to_string(),
231 })
232 } else {
233 None
234 }
235 }
236 (None, None) => None,
237 }
238 }
239
240 /// Get all subjects affected by this delta
241 ///
242 /// Returns a vector of subject IRIs that are affected by this change.
243 ///
244 /// # Examples
245 ///
246 /// ```rust,no_run
247 /// use ggen_core::delta::DeltaType;
248 ///
249 /// # fn main() -> ggen_utils::error::Result<()> {
250 /// let delta = DeltaType::Addition {
251 /// subject: "http://example.org/alice".to_string(),
252 /// predicate: "http://example.org/name".to_string(),
253 /// object: "Alice".to_string(),
254 /// };
255 ///
256 /// let subjects = delta.subjects();
257 /// assert_eq!(subjects, vec!["http://example.org/alice"]);
258 /// # Ok(())
259 /// # }
260 /// ```
261 pub fn subjects(&self) -> Vec<&str> {
262 match self {
263 DeltaType::Addition { subject, .. }
264 | DeltaType::Deletion { subject, .. }
265 | DeltaType::Modification { subject, .. } => vec![subject],
266 }
267 }
268
269 /// Get all predicates affected by this delta
270 pub fn predicates(&self) -> Vec<&str> {
271 match self {
272 DeltaType::Addition { predicate, .. }
273 | DeltaType::Deletion { predicate, .. }
274 | DeltaType::Modification { predicate, .. } => vec![predicate],
275 }
276 }
277
278 /// Check if this delta affects a specific IRI
279 pub fn affects_iri(&self, iri: &str) -> bool {
280 self.subjects().contains(&iri)
281 || self.predicates().contains(&iri)
282 || match self {
283 DeltaType::Addition { object, .. } | DeltaType::Deletion { object, .. } => {
284 object == iri
285 }
286 DeltaType::Modification {
287 old_object,
288 new_object,
289 ..
290 } => old_object == iri || new_object == iri,
291 }
292 }
293}
294
295impl fmt::Display for DeltaType {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 match self {
298 DeltaType::Addition {
299 subject,
300 predicate,
301 object,
302 } => write!(f, "+ {} {} {}", subject, predicate, object),
303 DeltaType::Deletion {
304 subject,
305 predicate,
306 object,
307 } => write!(f, "- {} {} {}", subject, predicate, object),
308 DeltaType::Modification {
309 subject,
310 predicate,
311 old_object,
312 new_object,
313 } => write!(
314 f,
315 "~ {} {} {} -> {}",
316 subject, predicate, old_object, new_object
317 ),
318 }
319 }
320}
321
322/// Collection of deltas representing the difference between two graphs
323///
324/// A `GraphDelta` contains all changes detected between a baseline graph and
325/// a current graph, along with metadata for tracking and verification.
326///
327/// # Examples
328///
329/// ```rust,no_run
330/// use ggen_core::delta::GraphDelta;
331/// use ggen_core::graph::Graph;
332///
333/// # fn main() -> ggen_utils::error::Result<()> {
334/// let baseline = Graph::new()?;
335/// baseline.insert_turtle(r#"
336/// @prefix ex: <http://example.org/> .
337/// ex:alice a ex:Person .
338/// "#)?;
339///
340/// let current = Graph::new()?;
341/// current.insert_turtle(r#"
342/// @prefix ex: <http://example.org/> .
343/// ex:alice a ex:Person .
344/// ex:bob a ex:Person .
345/// "#)?;
346///
347/// let delta = GraphDelta::new(&baseline, ¤t)?;
348/// println!("Detected {} changes", delta.deltas.len());
349/// # Ok(())
350/// # }
351/// ```
352#[derive(Debug, Clone, Default, Serialize, Deserialize)]
353pub struct GraphDelta {
354 /// All detected changes
355 pub deltas: Vec<DeltaType>,
356 /// Hash of the baseline graph
357 pub baseline_hash: Option<String>,
358 /// Hash of the current graph
359 pub current_hash: Option<String>,
360 /// Timestamp when delta was computed
361 pub computed_at: chrono::DateTime<chrono::Utc>,
362}
363
364impl GraphDelta {
365 /// Create a new delta by comparing two graphs
366 ///
367 /// Computes all differences between the baseline and current graphs,
368 /// including additions, deletions, and modifications.
369 ///
370 /// # Examples
371 ///
372 /// ```rust,no_run
373 /// use ggen_core::delta::GraphDelta;
374 /// use ggen_core::graph::Graph;
375 ///
376 /// # fn main() -> ggen_utils::error::Result<()> {
377 /// let baseline = Graph::new()?;
378 /// let current = Graph::new()?;
379 /// current.insert_turtle(r#"
380 /// @prefix ex: <http://example.org/> .
381 /// ex:alice ex:name "Alice" .
382 /// "#)?;
383 ///
384 /// let delta = GraphDelta::new(&baseline, ¤t)?;
385 /// assert!(!delta.deltas.is_empty());
386 /// # Ok(())
387 /// # }
388 /// ```
389 pub fn new(baseline: &Graph, current: &Graph) -> Result<Self> {
390 let mut deltas = Vec::new();
391
392 // Get all quads from both graphs
393 let baseline_quads = baseline.get_all_quads()?;
394 let current_quads = current.get_all_quads()?;
395
396 // Create lookup maps for efficient comparison
397 let baseline_map: BTreeMap<(String, String, String), Quad> = baseline_quads
398 .iter()
399 .map(|q| {
400 (
401 (
402 q.subject.to_string(),
403 q.predicate.to_string(),
404 q.object.to_string(),
405 ),
406 q.clone(),
407 )
408 })
409 .collect();
410
411 let current_map: BTreeMap<(String, String, String), Quad> = current_quads
412 .iter()
413 .map(|q| {
414 (
415 (
416 q.subject.to_string(),
417 q.predicate.to_string(),
418 q.object.to_string(),
419 ),
420 q.clone(),
421 )
422 })
423 .collect();
424
425 // Find additions and modifications
426 for ((s, p, o), current_quad) in ¤t_map {
427 match baseline_map.get(&(s.clone(), p.clone(), o.clone())) {
428 Some(baseline_quad) => {
429 // Check if objects are different (modification)
430 if baseline_quad.object != current_quad.object {
431 deltas.push(DeltaType::Modification {
432 subject: s.clone(),
433 predicate: p.clone(),
434 old_object: baseline_quad.object.to_string(),
435 new_object: current_quad.object.to_string(),
436 });
437 }
438 // If subjects/predicates match, it's not an addition
439 }
440 None => {
441 // This is an addition
442 deltas.push(DeltaType::Addition {
443 subject: s.clone(),
444 predicate: p.clone(),
445 object: o.clone(),
446 });
447 }
448 }
449 }
450
451 // Find deletions
452 for (s, p, o) in baseline_map.keys() {
453 if !current_map.contains_key(&(s.to_string(), p.clone(), o.clone())) {
454 deltas.push(DeltaType::Deletion {
455 subject: s.clone(),
456 predicate: p.clone(),
457 object: o.clone(),
458 });
459 }
460 }
461
462 Ok(Self {
463 deltas,
464 baseline_hash: baseline.compute_hash().ok(),
465 current_hash: current.compute_hash().ok(),
466 computed_at: chrono::Utc::now(),
467 })
468 }
469
470 /// Get all IRIs affected by this delta
471 ///
472 /// Returns a set of all unique IRIs (subjects, predicates, objects) that
473 /// appear in any of the changes.
474 ///
475 /// # Examples
476 ///
477 /// ```rust,no_run
478 /// use ggen_core::delta::GraphDelta;
479 /// use ggen_core::graph::Graph;
480 ///
481 /// # fn main() -> ggen_utils::error::Result<()> {
482 /// let baseline = Graph::new()?;
483 /// let current = Graph::new()?;
484 /// current.insert_turtle(r#"
485 /// @prefix ex: <http://example.org/> .
486 /// ex:alice ex:name "Alice" .
487 /// "#)?;
488 ///
489 /// let delta = GraphDelta::new(&baseline, ¤t)?;
490 /// let affected = delta.affected_iris();
491 /// assert!(affected.contains("http://example.org/alice"));
492 /// # Ok(())
493 /// # }
494 /// ```
495 pub fn affected_iris(&self) -> BTreeSet<String> {
496 let mut iris = BTreeSet::new();
497 for delta in &self.deltas {
498 iris.extend(delta.subjects().iter().map(|s| s.to_string()));
499 iris.extend(delta.predicates().iter().map(|p| p.to_string()));
500 match delta {
501 DeltaType::Addition { object, .. } | DeltaType::Deletion { object, .. } => {
502 iris.insert(object.clone());
503 }
504 DeltaType::Modification {
505 old_object,
506 new_object,
507 ..
508 } => {
509 iris.insert(old_object.clone());
510 iris.insert(new_object.clone());
511 }
512 }
513 }
514 iris
515 }
516
517 /// Check if this delta affects a specific IRI
518 ///
519 /// Returns `true` if any change in this delta affects the given IRI
520 /// (as subject, predicate, or object).
521 ///
522 /// # Examples
523 ///
524 /// ```rust,no_run
525 /// use ggen_core::delta::GraphDelta;
526 /// use ggen_core::graph::Graph;
527 ///
528 /// # fn main() -> ggen_utils::error::Result<()> {
529 /// let baseline = Graph::new()?;
530 /// let current = Graph::new()?;
531 /// current.insert_turtle(r#"
532 /// @prefix ex: <http://example.org/> .
533 /// ex:alice ex:name "Alice" .
534 /// "#)?;
535 ///
536 /// let delta = GraphDelta::new(&baseline, ¤t)?;
537 /// assert!(delta.affects_iri("http://example.org/alice"));
538 /// # Ok(())
539 /// # }
540 /// ```
541 pub fn affects_iri(&self, iri: &str) -> bool {
542 self.deltas.iter().any(|d| d.affects_iri(iri))
543 }
544
545 /// Check if this delta is empty (no changes)
546 pub fn is_empty(&self) -> bool {
547 self.deltas.is_empty()
548 }
549
550 /// Get the count of each delta type
551 pub fn counts(&self) -> BTreeMap<&str, usize> {
552 let mut counts = BTreeMap::new();
553 for delta in &self.deltas {
554 let key = match delta {
555 DeltaType::Addition { .. } => "additions",
556 DeltaType::Deletion { .. } => "deletions",
557 DeltaType::Modification { .. } => "modifications",
558 };
559 *counts.entry(key).or_insert(0) += 1;
560 }
561 counts
562 }
563
564 /// Filter deltas to only those affecting specific IRIs
565 pub fn filter_by_iris(&self, iris: &[String]) -> Self {
566 let filtered_deltas: Vec<_> = self
567 .deltas
568 .iter()
569 .filter(|d| iris.iter().any(|iri| d.affects_iri(iri)))
570 .cloned()
571 .collect();
572
573 Self {
574 deltas: filtered_deltas,
575 baseline_hash: self.baseline_hash.clone(),
576 current_hash: self.current_hash.clone(),
577 computed_at: self.computed_at,
578 }
579 }
580
581 /// Merge another delta into this one
582 pub fn merge(&mut self, other: GraphDelta) {
583 self.deltas.extend(other.deltas);
584 if self.baseline_hash.is_none() {
585 self.baseline_hash = other.baseline_hash;
586 }
587 if self.current_hash.is_none() {
588 self.current_hash = other.current_hash;
589 }
590 }
591}
592
593impl fmt::Display for GraphDelta {
594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595 writeln!(f, "GraphDelta ({} changes):", self.deltas.len())?;
596
597 let counts = self.counts();
598 for (delta_type, count) in counts {
599 writeln!(f, " {}: {}", delta_type, count)?;
600 }
601
602 if !self.deltas.is_empty() {
603 /// Maximum number of deltas to display before truncating
604 const MAX_DELTAS_DISPLAY: usize = 10;
605
606 writeln!(f)?;
607 for delta in self.deltas.iter().take(MAX_DELTAS_DISPLAY) {
608 writeln!(f, " {}", delta)?;
609 }
610
611 if self.deltas.len() > MAX_DELTAS_DISPLAY {
612 writeln!(
613 f,
614 " ... and {} more",
615 self.deltas.len() - MAX_DELTAS_DISPLAY
616 )?;
617 }
618 }
619
620 Ok(())
621 }
622}
623
624/// Template impact analysis for delta-driven regeneration
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct TemplateImpact {
627 /// Path to the template
628 pub template_path: String,
629 /// IRIs from the delta that affect this template
630 pub affected_iris: Vec<String>,
631 /// Confidence score (0.0-1.0) of how likely this template is affected
632 pub confidence: f64,
633 /// Reason for the impact assessment
634 pub reason: String,
635}
636
637impl TemplateImpact {
638 /// Create a new template impact analysis
639 pub fn new(
640 template_path: String, affected_iris: Vec<String>, confidence: f64, reason: String,
641 ) -> Self {
642 Self {
643 template_path,
644 affected_iris,
645 confidence,
646 reason,
647 }
648 }
649
650 /// Check if this impact is above a confidence threshold
651 pub fn is_confident(&self, threshold: f64) -> bool {
652 self.confidence >= threshold
653 }
654}
655
656/// Analyze which templates are affected by a graph delta
657pub struct ImpactAnalyzer {
658 /// Cache of template query patterns for performance
659 #[allow(dead_code)]
660 template_queries: BTreeMap<String, Vec<String>>,
661}
662
663impl Default for ImpactAnalyzer {
664 fn default() -> Self {
665 Self::new()
666 }
667}
668
669impl ImpactAnalyzer {
670 /// Create a new impact analyzer
671 pub fn new() -> Self {
672 Self {
673 template_queries: BTreeMap::new(),
674 }
675 }
676
677 /// Analyze template impacts for a given delta
678 pub fn analyze_impacts(
679 &mut self, delta: &GraphDelta, template_paths: &[String], graph: &Graph,
680 ) -> Result<Vec<TemplateImpact>> {
681 let mut impacts = Vec::new();
682
683 for template_path in template_paths {
684 // Get or cache template queries
685 let queries = self.get_template_queries(template_path, graph)?;
686
687 // Analyze impact based on query patterns
688 let (confidence, reason) = self.assess_impact(delta, &queries);
689
690 if confidence > 0.0 {
691 impacts.push(TemplateImpact::new(
692 template_path.clone(),
693 delta.affected_iris().into_iter().collect(),
694 confidence,
695 reason,
696 ));
697 }
698 }
699
700 // Sort by confidence (highest first)
701 impacts.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
702
703 Ok(impacts)
704 }
705
706 /// Get SPARQL queries from a template (simplified - in reality would parse template)
707 fn get_template_queries(&mut self, template_path: &str, _graph: &Graph) -> Result<Vec<String>> {
708 // Check if we have cached queries for this template
709 if let Some(queries) = self.template_queries.get(template_path) {
710 return Ok(queries.clone());
711 }
712
713 // This is a simplified implementation
714 // In practice, would need to parse template frontmatter and extract SPARQL queries
715 Ok(vec![
716 "SELECT ?s ?p ?o WHERE { ?s ?p ?o }".to_string(),
717 "SELECT ?class WHERE { ?class a rdfs:Class }".to_string(),
718 ])
719 }
720
721 /// Assess how a delta impacts a set of queries
722 fn assess_impact(&self, delta: &GraphDelta, queries: &[String]) -> (f64, String) {
723 let affected_iris = delta.affected_iris();
724
725 // Simple heuristic: check if any affected IRI appears in any query
726 let mut max_relevance = 0.0;
727 let mut reasons = Vec::new();
728
729 for query in queries {
730 let query_lower = query.to_lowercase();
731
732 for iri in &affected_iris {
733 if query_lower.contains(&iri.to_lowercase()) {
734 max_relevance = 1.0;
735 reasons.push(format!("Query directly references IRI: {}", iri));
736 break;
737 }
738 }
739 }
740
741 // If no direct matches, use pattern-based heuristics
742 if max_relevance == 0.0 {
743 // Check for schema changes (rdfs:Class, rdf:Property, etc.)
744 for iri in &affected_iris {
745 if iri.contains("rdfs:Class") || iri.contains("rdf:Property") {
746 max_relevance = 0.8;
747 reasons.push("Schema element changed".to_string());
748 }
749 }
750 }
751
752 let reason = if reasons.is_empty() {
753 "No direct impact detected".to_string()
754 } else {
755 reasons.join("; ")
756 };
757
758 (max_relevance, reason)
759 }
760}
761
762impl Graph {
763 /// Get all quads in the graph for delta computation
764 fn get_all_quads(&self) -> Result<Vec<Quad>> {
765 let pattern = self.quads_for_pattern(None, None, None, None)?;
766 Ok(pattern)
767 }
768
769 /// Compute a deterministic hash of the graph content
770 pub fn compute_hash(&self) -> Result<String> {
771 let quads = self.get_all_quads()?;
772 let mut hasher = AHasher::default();
773
774 // Create a deterministic string representation
775 let mut sorted_quads: Vec<String> = quads
776 .iter()
777 .map(|q| format!("{} {} {}", q.subject, q.predicate, q.object))
778 .collect();
779 sorted_quads.sort();
780
781 for quad_str in sorted_quads {
782 quad_str.hash(&mut hasher);
783 }
784
785 Ok(format!("{:x}", hasher.finish()))
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792 use crate::graph::Graph;
793
794 fn create_test_graph() -> Result<(Graph, Graph)> {
795 let baseline = Graph::new()?;
796 baseline.insert_turtle(
797 r#"
798 @prefix : <http://example.org/> .
799 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
800 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
801 :User a rdfs:Class .
802 :name a rdf:Property ;
803 rdfs:domain :User .
804 "#,
805 )?;
806
807 let current = Graph::new()?;
808 current.insert_turtle(
809 r#"
810 @prefix : <http://example.org/> .
811 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
812 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
813 :User a rdfs:Class .
814 :name a rdf:Property ;
815 rdfs:domain :User .
816 :email a rdf:Property ;
817 rdfs:domain :User .
818 "#,
819 )?;
820
821 Ok((baseline, current))
822 }
823
824 #[test]
825 fn test_delta_creation() {
826 let (baseline, current) = create_test_graph().unwrap();
827 let delta = GraphDelta::new(&baseline, ¤t).unwrap();
828
829 assert!(!delta.is_empty());
830 assert!(delta.affects_iri("<http://example.org/email>"));
831
832 // Should have two additions (the email property has 2 triples: type and domain)
833 let counts = delta.counts();
834 assert_eq!(counts.get("additions"), Some(&2));
835 assert_eq!(counts.get("deletions"), None); // No deletions
836 assert_eq!(counts.get("modifications"), None); // No modifications
837 }
838
839 #[test]
840 fn test_delta_affected_iris() {
841 let (baseline, current) = create_test_graph().unwrap();
842 let delta = GraphDelta::new(&baseline, ¤t).unwrap();
843
844 let affected = delta.affected_iris();
845 assert!(affected.contains("<http://example.org/email>"));
846 assert!(affected.contains("<http://example.org/User>"));
847 assert!(affected.contains("<http://www.w3.org/2000/01/rdf-schema#domain>"));
848 }
849
850 #[test]
851 fn test_delta_filtering() -> std::result::Result<(), Box<dyn std::error::Error>> {
852 let (baseline, current) = create_test_graph()?;
853 let delta = GraphDelta::new(&baseline, ¤t)?;
854
855 // Filter to only User-related changes
856 let filtered = delta.filter_by_iris(&["<http://example.org/User>".to_string()]);
857
858 // Should still contain the email addition since it affects User
859 assert!(!filtered.is_empty());
860
861 Ok(())
862 }
863
864 #[test]
865 fn test_impact_analyzer() -> std::result::Result<(), Box<dyn std::error::Error>> {
866 let (baseline, current) = create_test_graph().unwrap();
867 let delta = GraphDelta::new(&baseline, ¤t).unwrap();
868
869 let mut analyzer = ImpactAnalyzer::new();
870 // Add a mock query that should match the email property
871 analyzer.template_queries.insert(
872 "template1.tmpl".to_string(),
873 vec!["SELECT * WHERE { ?s <http://example.org/email> ?o }".to_string()],
874 );
875
876 let template_paths = vec!["template1.tmpl".to_string()];
877 let impacts = analyzer
878 .analyze_impacts(&delta, &template_paths, &baseline)
879 .unwrap();
880
881 // Should find some impacts since template queries match affected IRIs
882 assert!(!impacts.is_empty());
883
884 Ok(())
885 }
886
887 #[test]
888 fn test_graph_hash() -> std::result::Result<(), Box<dyn std::error::Error>> {
889 let (baseline, current) = create_test_graph().unwrap();
890
891 let hash1 = baseline.compute_hash().unwrap();
892 let hash2 = current.compute_hash().unwrap();
893
894 // Different graphs should have different hashes
895 assert_ne!(hash1, hash2);
896
897 // Same graph should have same hash
898 let hash3 = baseline.compute_hash().unwrap();
899 assert_eq!(hash1, hash3);
900
901 Ok(())
902 }
903
904 #[test]
905 fn test_delta_display() -> std::result::Result<(), Box<dyn std::error::Error>> {
906 let (baseline, current) = create_test_graph().unwrap();
907 let delta = GraphDelta::new(&baseline, ¤t).unwrap();
908
909 let display = format!("{}", delta);
910 assert!(display.contains("GraphDelta"));
911 assert!(display.contains("additions"));
912 assert!(display.contains("http://example.org/email"));
913
914 Ok(())
915 }
916}