Skip to content

Commit 742adeb

Browse files
committed
import_graph: rewrite imports on root; add depth/cycle tests
1 parent 9be4dec commit 742adeb

File tree

4 files changed

+365
-11
lines changed

4 files changed

+365
-11
lines changed

lib/src/api.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! This includes loading, saving, updating, and querying the environment.
33
44
use crate::config::Config;
5-
use crate::consts::{ONTOLOGY, TYPE};
5+
use crate::consts::IMPORTS;
66
use crate::doctor::{
77
ConflictingPrefixes, Doctor, DuplicateOntology, OntologyDeclaration, OntologyProblem,
88
};
@@ -12,9 +12,7 @@ use crate::transform;
1212
use crate::ToUriString;
1313
use crate::{EnvironmentStatus, FailedImport};
1414
use chrono::prelude::*;
15-
use oxigraph::model::{
16-
Dataset, Graph, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, TermRef, TripleRef,
17-
};
15+
use oxigraph::model::{Dataset, Graph, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, TripleRef};
1816
use oxigraph::store::Store;
1917
use petgraph::visit::EdgeRef;
2018
use std::io::{BufReader, Write};
@@ -1419,6 +1417,9 @@ impl OntoEnv {
14191417
recursion_depth: i32,
14201418
root: NamedNodeRef,
14211419
) -> Result<Graph> {
1420+
let imported = self.get_ontology(id)?;
1421+
let imported_imports = imported.imports.clone();
1422+
14221423
let closure = self.get_closure(id, recursion_depth)?;
14231424
let mut union = self.get_union_graph(&closure, Some(true), Some(true))?;
14241425

@@ -1431,8 +1432,32 @@ impl OntoEnv {
14311432
// Flatten dataset into a single graph, ignoring named graph labels.
14321433
let mut graph = Graph::new();
14331434
for quad in union.dataset.iter() {
1435+
// Drop owl:imports on non-root subjects to prevent retaining inner edges in cycles.
1436+
if quad.predicate == IMPORTS && quad.subject != root_nb {
1437+
continue;
1438+
}
14341439
graph.insert(TripleRef::new(quad.subject, quad.predicate, quad.object));
14351440
}
1441+
// Re-attach imports of the imported ontology and its dependencies onto the root; skip self-imports and dedup.
1442+
let closure_names: std::collections::HashSet<NamedNodeRef> =
1443+
closure.iter().map(|id| id.name()).collect();
1444+
let mut seen = std::collections::HashSet::new();
1445+
let mut add_import = |target: NamedNodeRef, dep: NamedNodeRef| {
1446+
if target == dep {
1447+
return;
1448+
}
1449+
if seen.insert(dep.to_string()) {
1450+
graph.insert(TripleRef::new(target, IMPORTS, dep));
1451+
}
1452+
};
1453+
for dep in imported_imports {
1454+
if closure_names.contains(&dep.as_ref()) {
1455+
add_import(root, dep.as_ref());
1456+
}
1457+
}
1458+
for dep_id in closure.iter().skip(1) {
1459+
add_import(root, dep_id.name());
1460+
}
14361461
Ok(graph)
14371462
}
14381463

lib/tests/test_ontoenv.rs

Lines changed: 189 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use anyhow::Result;
22
use ontoenv::api::{OntoEnv, ResolveTarget};
33
use ontoenv::config::Config;
4+
use ontoenv::consts::IMPORTS;
45
use ontoenv::ontology::OntologyLocation;
56
use ontoenv::options::{CacheMode, Overwrite, RefreshStrategy};
67
use oxigraph::model::NamedNodeRef;
8+
use oxigraph::model::NamedOrBlankNodeRef;
9+
use oxigraph::model::TermRef;
710
use std::fs;
811
use std::path::PathBuf;
912
use std::thread;
@@ -178,11 +181,20 @@ ex:b ex:p ex:o .
178181
"Merged graph missing B data"
179182
);
180183

181-
// No owl:imports should remain
182-
assert_eq!(
183-
merged.triples_for_predicate(IMPORTS).count(),
184-
0,
185-
"Merged graph should not contain owl:imports"
184+
// owl:imports should be rewritten onto the root (base) ontology
185+
let imports: Vec<_> = merged
186+
.triples_for_predicate(IMPORTS)
187+
.filter(|t| t.subject == NamedOrBlankNodeRef::NamedNode(a_id.name()))
188+
.collect();
189+
assert!(
190+
!imports.is_empty(),
191+
"Merged graph should contain rewritten imports on the root"
192+
);
193+
assert!(
194+
imports
195+
.iter()
196+
.all(|t| t.subject == NamedOrBlankNodeRef::NamedNode(a_id.name())),
197+
"All imports should be on the root ontology"
186198
);
187199

188200
// Only one owl:Ontology declaration (root) should remain
@@ -199,6 +211,178 @@ ex:b ex:p ex:o .
199211
Ok(())
200212
}
201213

214+
#[test]
215+
fn import_graph_handles_cycles() -> Result<()> {
216+
use ontoenv::consts::{IMPORTS, ONTOLOGY, TYPE};
217+
218+
let dir = TempDir::new("ontoenv-import-cycle")?;
219+
220+
let a_path = dir.path().join("A.ttl");
221+
let b_path = dir.path().join("B.ttl");
222+
let a_iri = format!("file://{}", a_path.display());
223+
let b_iri = format!("file://{}", b_path.display());
224+
225+
fs::write(
226+
&a_path,
227+
format!(
228+
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix ex: <http://example.com/A#> .\n<{a}> a owl:Ontology ; owl:imports <{b}> .\nex:A a owl:Class .\n",
229+
a = a_iri,
230+
b = b_iri
231+
),
232+
)?;
233+
fs::write(
234+
&b_path,
235+
format!(
236+
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix ex: <http://example.com/B#> .\n<{b}> a owl:Ontology ; owl:imports <{a}> .\nex:B a owl:Class .\n",
237+
a = a_iri,
238+
b = b_iri
239+
),
240+
)?;
241+
242+
let cfg = default_config(&dir);
243+
let mut env = OntoEnv::init(cfg, false)?;
244+
env.add(
245+
OntologyLocation::File(a_path),
246+
Overwrite::Allow,
247+
RefreshStrategy::UseCache,
248+
)?;
249+
env.add(
250+
OntologyLocation::File(b_path),
251+
Overwrite::Allow,
252+
RefreshStrategy::UseCache,
253+
)?;
254+
255+
let a_id = env
256+
.resolve(ResolveTarget::Location(OntologyLocation::from_str(&a_iri)?))
257+
.unwrap();
258+
let merged = env.import_graph(&a_id, -1)?;
259+
260+
// Single root ontology
261+
let ontology_decls = merged
262+
.triples_for_object(ONTOLOGY)
263+
.filter(|t| t.predicate == TYPE)
264+
.count();
265+
assert_eq!(ontology_decls, 1);
266+
267+
// Imports rewritten onto root with no self-loop
268+
let imports: Vec<_> = merged
269+
.triples_for_predicate(IMPORTS)
270+
.filter(|t| t.subject == NamedOrBlankNodeRef::NamedNode(a_id.name()))
271+
.collect();
272+
assert_eq!(imports.len(), 1);
273+
if let TermRef::NamedNode(obj) = imports[0].object {
274+
assert_eq!(obj.as_str(), b_iri);
275+
} else {
276+
panic!("Import object was not a NamedNode");
277+
}
278+
279+
// No imports hanging off B
280+
assert_eq!(
281+
merged
282+
.triples_for_predicate(IMPORTS)
283+
.filter(|t| {
284+
t.subject == NamedOrBlankNodeRef::NamedNode(NamedNodeRef::new_unchecked(&b_iri))
285+
})
286+
.count(),
287+
0
288+
);
289+
290+
// Data from both ontologies present
291+
assert!(merged
292+
.iter()
293+
.any(|t| format!("{:?}", t.subject).contains("#A")));
294+
assert!(merged
295+
.iter()
296+
.any(|t| format!("{:?}", t.subject).contains("#B")));
297+
298+
teardown(dir);
299+
Ok(())
300+
}
301+
302+
#[test]
303+
fn import_graph_respects_recursion_depth() -> Result<()> {
304+
let dir = TempDir::new("ontoenv-import-depth")?;
305+
306+
let a_path = dir.path().join("A.ttl");
307+
let b_path = dir.path().join("B.ttl");
308+
let c_path = dir.path().join("C.ttl");
309+
310+
let a_iri = format!("file://{}", a_path.display());
311+
let b_iri = format!("file://{}", b_path.display());
312+
let c_iri = format!("file://{}", c_path.display());
313+
314+
fs::write(
315+
&a_path,
316+
format!(
317+
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n<{a}> a owl:Ontology ; owl:imports <{b}> .",
318+
a = a_iri, b = b_iri
319+
),
320+
)?;
321+
fs::write(
322+
&b_path,
323+
format!(
324+
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n<{b}> a owl:Ontology ; owl:imports <{c}> .",
325+
b = b_iri, c = c_iri
326+
),
327+
)?;
328+
fs::write(
329+
&c_path,
330+
format!(
331+
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n<{c}> a owl:Ontology .",
332+
c = c_iri
333+
),
334+
)?;
335+
336+
let cfg = default_config(&dir);
337+
let mut env = OntoEnv::init(cfg, false)?;
338+
env.add(
339+
OntologyLocation::File(a_path),
340+
Overwrite::Allow,
341+
RefreshStrategy::UseCache,
342+
)?;
343+
env.add(
344+
OntologyLocation::File(b_path),
345+
Overwrite::Allow,
346+
RefreshStrategy::UseCache,
347+
)?;
348+
env.add(
349+
OntologyLocation::File(c_path),
350+
Overwrite::Allow,
351+
RefreshStrategy::UseCache,
352+
)?;
353+
354+
let a_id = env
355+
.resolve(ResolveTarget::Location(OntologyLocation::from_str(&a_iri)?))
356+
.unwrap();
357+
358+
// depth 0: only A (no imports attached)
359+
let g0 = env.import_graph(&a_id, 0)?;
360+
let imports0 = g0
361+
.triples_for_predicate(IMPORTS)
362+
.filter(|t| t.subject == NamedOrBlankNodeRef::NamedNode(a_id.name()))
363+
.count();
364+
assert_eq!(imports0, 0, "depth 0 should not carry imports on root");
365+
366+
// depth 1: A imports B
367+
let g1 = env.import_graph(&a_id, 1)?;
368+
let imports_b: Vec<_> = g1
369+
.triples_for_predicate(IMPORTS)
370+
.filter(|t| t.subject == NamedOrBlankNodeRef::NamedNode(a_id.name()))
371+
.collect();
372+
assert_eq!(imports_b.len(), 1);
373+
374+
// depth -1: full closure, includes C
375+
let gfull = env.import_graph(&a_id, -1)?;
376+
let imports_full: Vec<_> = gfull
377+
.triples_for_predicate(IMPORTS)
378+
.filter(|t| t.subject == NamedOrBlankNodeRef::NamedNode(a_id.name()))
379+
.collect();
380+
assert_eq!(imports_full.len(), 2);
381+
382+
teardown(dir);
383+
Ok(())
384+
}
385+
202386
#[cfg(unix)]
203387
mod unix_permission_tests {
204388
use super::*;

python/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,25 @@ impl OntoEnv {
521521
};
522522
let root_node = root_node_owned.as_ref();
523523

524+
// Remove owl:imports in the destination graph only for ontologies that will be rewritten.
525+
let imports_uri = uriref_constructor.call1((IMPORTS.as_str(),))?;
526+
let closure_set: std::collections::HashSet<String> =
527+
closure.iter().map(|c| c.to_uri_string()).collect();
528+
let triples_to_remove_imports = destination_graph.call_method(
529+
"triples",
530+
((py.None(), imports_uri, py.None()),),
531+
None,
532+
)?;
533+
for triple in triples_to_remove_imports.try_iter()? {
534+
let t = triple?;
535+
let obj: Bound<'_, PyAny> = t.get_item(2)?;
536+
if let Ok(s) = obj.str() {
537+
if closure_set.contains(s.to_str()?) {
538+
destination_graph.getattr("remove")?.call1((t,))?;
539+
}
540+
}
541+
}
542+
524543
// Remove any ontology declarations in the destination that are not the chosen root.
525544
let triples_to_remove = destination_graph.call_method(
526545
"triples",
@@ -559,6 +578,19 @@ impl OntoEnv {
559578
)?;
560579
destination_graph.getattr("add")?.call1((t,))?;
561580
}
581+
// Re-attach imports from the original closure onto the root in the destination graph.
582+
for dep in closure.iter().skip(1) {
583+
let dep_uri = dep.to_uri_string();
584+
let t = PyTuple::new(
585+
py,
586+
&[
587+
uriref_constructor.call1((root_node.as_str(),))?,
588+
uriref_constructor.call1((IMPORTS.as_str(),))?,
589+
uriref_constructor.call1((dep_uri.as_str(),))?,
590+
],
591+
)?;
592+
destination_graph.getattr("add")?.call1((t,))?;
593+
}
562594
Ok(())
563595
}
564596

0 commit comments

Comments
 (0)