strixonomy_catalog/
incremental.rs1use oxigraph::model::Quad;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use strixonomy_core::{Annotation, Axiom, Diagnostic, Entity, Import, Namespace, OntologyDocument};
7
8#[derive(Debug, Clone)]
10pub(crate) struct DocumentSnapshot {
11 pub content_hash: String,
12 pub document: OntologyDocument,
13 pub entities: Vec<Entity>,
14 pub annotations: Vec<Annotation>,
15 pub axioms: Vec<Axiom>,
16 pub namespace_rows: Vec<Namespace>,
17 pub imports: Vec<Import>,
18 pub quads: Vec<Quad>,
19 pub triple_count: usize,
20 pub bridge_warning: Option<Diagnostic>,
21}
22
23impl DocumentSnapshot {
24 pub fn with_doc_id(&self, doc_id: &str) -> Self {
25 let mut snap = self.clone();
26 snap.document.id = doc_id.to_string();
27 for entity in &mut snap.entities {
28 entity.ontology_id = doc_id.to_string();
29 }
30 for ann in &mut snap.annotations {
31 ann.ontology_id = doc_id.to_string();
32 }
33 for ax in &mut snap.axioms {
34 ax.ontology_id = doc_id.to_string();
35 }
36 for ns in &mut snap.namespace_rows {
37 ns.ontology_id = doc_id.to_string();
38 }
39 for imp in &mut snap.imports {
40 imp.ontology_id = doc_id.to_string();
41 }
42 snap
43 }
44
45 pub fn with_reuse_context(&self, doc_id: &str, path: &Path, modified_time: u64) -> Self {
46 let mut snap = self.with_doc_id(doc_id);
47 snap.document.path = path.to_path_buf();
48 snap.document.modified_time = modified_time;
49 snap
50 }
51}
52
53pub(crate) fn effective_content_hash(disk_hash: &str, override_text: Option<&str>) -> String {
54 if let Some(text) = override_text {
55 content_hash_text(text)
56 } else {
57 disk_hash.to_string()
58 }
59}
60
61pub(crate) fn content_hash_text(text: &str) -> String {
62 use sha2::{Digest, Sha256};
63 let mut hasher = Sha256::new();
64 hasher.update(text.as_bytes());
65 hex::encode(hasher.finalize())
66}
67
68pub(crate) fn paths_equal(a: &Path, b: &Path) -> bool {
69 a == b || a.canonicalize().ok().zip(b.canonicalize().ok()).is_some_and(|(x, y)| x == y)
70}
71
72pub(crate) fn config_fingerprint(
74 scan_roots: &[PathBuf],
75 disk_cache: bool,
76 document_overrides: &HashMap<PathBuf, String>,
77) -> String {
78 use sha2::{Digest, Sha256};
79 let mut hasher = Sha256::new();
80 hasher.update(b"v1\0scan_roots\0");
81 for root in scan_roots {
82 hasher.update(root.to_string_lossy().as_bytes());
83 hasher.update([0]);
84 }
85 hasher.update([if disk_cache { 1 } else { 0 }]);
86 hasher.update(b"\0overrides\0");
87 let mut keys: Vec<_> = document_overrides.keys().collect();
88 keys.sort_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
89 for key in keys {
90 hasher.update(key.to_string_lossy().as_bytes());
91 hasher.update([0]);
92 }
93 hex::encode(hasher.finalize())
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum IncrementalStats {
98 FullBuild,
99 Incremental { reused: usize, reparsed: usize, removed: usize },
100}
101
102impl IncrementalStats {
103 pub fn reused_documents(&self) -> usize {
104 match self {
105 Self::FullBuild => 0,
106 Self::Incremental { reused, .. } => *reused,
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::collections::BTreeMap;
115 use std::path::PathBuf;
116 use strixonomy_core::{OntologyFormat, ParseStatus, SourceLocation};
117
118 #[test]
119 fn content_hash_is_stable() {
120 let h1 = content_hash_text("hello");
121 let h2 = content_hash_text("hello");
122 assert_eq!(h1, h2);
123 assert_ne!(h1, content_hash_text("world"));
124 }
125
126 #[test]
127 fn remap_doc_id_updates_ontology_ids() {
128 let snap = DocumentSnapshot {
129 content_hash: "abc".to_string(),
130 document: OntologyDocument {
131 id: "doc-1".to_string(),
132 path: PathBuf::from("a.ttl"),
133 format: OntologyFormat::Turtle,
134 base_iri: None,
135 version_iri: None,
136 imports: vec![],
137 namespaces: BTreeMap::new(),
138 parse_status: ParseStatus::Ok,
139 content_hash: "abc".to_string(),
140 modified_time: 0,
141 parse_message: None,
142 parse_error_location: None,
143 },
144 entities: vec![Entity {
145 iri: "http://ex#A".to_string(),
146 kind: strixonomy_core::EntityKind::Class,
147 short_name: "A".to_string(),
148 ontology_id: "doc-1".to_string(),
149 source_location: SourceLocation::default(),
150 labels: vec![],
151 comments: vec![],
152 deprecated: false,
153 obo_id: None,
154 characteristics: Default::default(),
155 }],
156 annotations: vec![],
157 axioms: vec![],
158 namespace_rows: vec![],
159 imports: vec![],
160 quads: vec![],
161 triple_count: 0,
162 bridge_warning: None,
163 };
164 let remapped = snap.with_doc_id("doc-2");
165 assert_eq!(remapped.document.id, "doc-2");
166 assert_eq!(remapped.entities[0].ontology_id, "doc-2");
167 }
168}