1use std::collections::HashSet;
2
3use oxigraph::model::*;
4use oxigraph::sparql::{QueryResults, SparqlEvaluator};
5
6use crate::graph::{RDF, iri, objects, subject, subjects};
7use crate::path::rdf_list;
8use crate::{Error, Result};
9
10#[cfg(test)]
11#[path = "inference_tests.rs"]
12mod tests;
13
14mod axioms;
15
16const OWL: &str = "http://www.w3.org/2002/07/owl#";
17
18const PREFIXES: &str = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
19PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
20PREFIX owl: <http://www.w3.org/2002/07/owl#>
21PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>";
22
23const RDFS_RULES: &[(&str, &str)] = &[
24 ("?p a rdf:Property", "?s ?p ?o"),
25 ("?s a ?c", "?p rdfs:domain ?c . ?s ?p ?o"),
26 ("?o a ?c", "?p rdfs:range ?c . ?s ?p ?o"),
27 (
28 "?p rdfs:subPropertyOf ?r",
29 "?p rdfs:subPropertyOf ?q . ?q rdfs:subPropertyOf ?r",
30 ),
31 ("?p rdfs:subPropertyOf ?p", "?p a rdf:Property"),
32 ("?s ?q ?o", "?p rdfs:subPropertyOf ?q . ?s ?p ?o"),
33 ("?c rdfs:subClassOf rdfs:Resource, ?c", "?c a rdfs:Class"),
34 ("?s a ?d", "?c rdfs:subClassOf ?d . ?s a ?c"),
35 (
36 "?a rdfs:subClassOf ?c",
37 "?a rdfs:subClassOf ?b . ?b rdfs:subClassOf ?c",
38 ),
39 (
40 "?p rdfs:subPropertyOf rdfs:member",
41 "?p a rdfs:ContainerMembershipProperty",
42 ),
43 ("?d rdfs:subClassOf rdfs:Literal", "?d a rdfs:Datatype"),
44];
45
46const OWL_RULES: &[(&str, &str)] = &[
49 (
50 "?s owl:sameAs ?s . ?p owl:sameAs ?p . ?o owl:sameAs ?o",
51 "?s ?p ?o",
52 ),
53 ("?y owl:sameAs ?x", "?x owl:sameAs ?y"),
54 ("?x owl:sameAs ?z", "?x owl:sameAs ?y . ?y owl:sameAs ?z"),
55 ("?y ?p ?o", "?x owl:sameAs ?y . ?x ?p ?o"),
56 ("?s ?q ?o", "?p owl:sameAs ?q . ?s ?p ?o"),
57 ("?s ?p ?y", "?x owl:sameAs ?y . ?s ?p ?x"),
58 ("?s a ?c", "?p rdfs:domain ?c . ?s ?p ?o"),
59 ("?o a ?c", "?p rdfs:range ?c . ?s ?p ?o"),
60 (
61 "?y owl:sameAs ?z",
62 "?p a owl:FunctionalProperty . ?x ?p ?y, ?z",
63 ),
64 (
65 "?x owl:sameAs ?y",
66 "?p a owl:InverseFunctionalProperty . ?x ?p ?z . ?y ?p ?z",
67 ),
68 ("?y ?p ?x", "?p a owl:SymmetricProperty . ?x ?p ?y"),
69 (
70 "?x ?p ?z",
71 "?p a owl:TransitiveProperty . ?x ?p ?y . ?y ?p ?z",
72 ),
73 ("?x ?q ?y", "?p rdfs:subPropertyOf ?q . ?x ?p ?y"),
74 (
75 "?p rdfs:subPropertyOf ?q . ?q rdfs:subPropertyOf ?p",
76 "?p owl:equivalentProperty ?q FILTER(!sameTerm(?p, ?q))",
77 ),
78 ("?y ?q ?x", "?p owl:inverseOf ?q . ?x ?p ?y"),
79 ("?y ?p ?x", "?p owl:inverseOf ?q . ?x ?q ?y"),
80 ("?x a ?d", "?c rdfs:subClassOf ?d . ?x a ?c"),
81 (
82 "?c rdfs:subClassOf ?d . ?d rdfs:subClassOf ?c",
83 "?c owl:equivalentClass ?d FILTER(!sameTerm(?c, ?d))",
84 ),
85 (
86 "?x a ?member",
87 "?c owl:intersectionOf ?list . ?list rdf:rest*/rdf:first ?member . ?x a ?c",
88 ),
89 (
90 "?x a ?c",
91 "?c owl:intersectionOf ?list . ?x a ?any FILTER NOT EXISTS { ?list rdf:rest*/rdf:first ?member FILTER NOT EXISTS { ?x a ?member } }",
92 ),
93 (
94 "?x a ?c",
95 "?c owl:unionOf ?list . ?list rdf:rest*/rdf:first ?member . ?x a ?member",
96 ),
97 (
98 "?x a ?c",
99 "?c owl:oneOf ?list . ?list rdf:rest*/rdf:first ?x",
100 ),
101 (
102 "?x a ?r",
103 "?r owl:onProperty ?p ; owl:someValuesFrom ?c . ?x ?p ?y . ?y a ?c",
104 ),
105 (
106 "?x a ?r",
107 "?r owl:onProperty ?p ; owl:someValuesFrom owl:Thing . ?x ?p ?y",
108 ),
109 (
110 "?y a ?c",
111 "?r owl:onProperty ?p ; owl:allValuesFrom ?c . ?x a ?r ; ?p ?y",
112 ),
113 (
114 "?x ?p ?v",
115 "?r owl:onProperty ?p ; owl:hasValue ?v . ?x a ?r",
116 ),
117 (
118 "?x a ?r",
119 "?r owl:onProperty ?p ; owl:hasValue ?v . ?x ?p ?v",
120 ),
121 (
122 "?y owl:sameAs ?z",
123 "?r owl:onProperty ?p ; owl:maxCardinality ?n . FILTER(?n = 1) ?x a ?r ; ?p ?y, ?z",
124 ),
125 (
126 "?y owl:sameAs ?z",
127 "?r owl:onProperty ?p ; owl:maxQualifiedCardinality ?n ; owl:onClass ?c . FILTER(?n = 1) ?x a ?r ; ?p ?y, ?z . ?y a ?c . ?z a ?c",
128 ),
129 (
130 "?y owl:sameAs ?z",
131 "?r owl:onProperty ?p ; owl:maxQualifiedCardinality ?n ; owl:onClass owl:Thing . FILTER(?n = 1) ?x a ?r ; ?p ?y, ?z",
132 ),
133 (
134 "?c rdfs:subClassOf ?c, owl:Thing ; owl:equivalentClass ?c . owl:Nothing rdfs:subClassOf ?c",
135 "?c a owl:Class",
136 ),
137 (
140 "?a rdfs:subClassOf ?c",
141 "?a rdfs:subClassOf ?b . ?b rdfs:subClassOf ?c FILTER(!sameTerm(?a, ?c))",
142 ),
143 (
144 "?a owl:equivalentClass ?b",
145 "?a rdfs:subClassOf ?b . ?b rdfs:subClassOf ?a",
146 ),
147 (
148 "?p rdfs:subPropertyOf ?p ; owl:equivalentProperty ?p",
149 "{ ?p a owl:ObjectProperty } UNION { ?p a owl:DatatypeProperty } UNION { ?p a rdf:Property }",
150 ),
151 (
152 "?a rdfs:subPropertyOf ?c",
153 "?a rdfs:subPropertyOf ?b . ?b rdfs:subPropertyOf ?c FILTER(!sameTerm(?a, ?c))",
154 ),
155 (
156 "?a owl:equivalentProperty ?b",
157 "?a rdfs:subPropertyOf ?b . ?b rdfs:subPropertyOf ?a FILTER(!sameTerm(?a, ?b))",
158 ),
159 (
160 "?p rdfs:domain ?d",
161 "?p rdfs:domain ?c . ?c rdfs:subClassOf ?d",
162 ),
163 (
164 "?p rdfs:domain ?c",
165 "?p rdfs:subPropertyOf ?q . ?q rdfs:domain ?c",
166 ),
167 (
168 "?p rdfs:range ?d",
169 "?p rdfs:range ?c . ?c rdfs:subClassOf ?d",
170 ),
171 (
172 "?p rdfs:range ?c",
173 "?p rdfs:subPropertyOf ?q . ?q rdfs:range ?c",
174 ),
175 (
176 "?r rdfs:subClassOf ?s",
177 "?r owl:hasValue ?v ; owl:onProperty ?p . ?s owl:hasValue ?v ; owl:onProperty ?q . ?p rdfs:subPropertyOf ?q",
178 ),
179 (
180 "?r rdfs:subClassOf ?s",
181 "?r owl:someValuesFrom ?c ; owl:onProperty ?p . ?s owl:someValuesFrom ?d ; owl:onProperty ?p . ?c rdfs:subClassOf ?d",
182 ),
183 (
184 "?r rdfs:subClassOf ?s",
185 "?r owl:someValuesFrom ?c ; owl:onProperty ?p . ?s owl:someValuesFrom ?c ; owl:onProperty ?q . ?p rdfs:subPropertyOf ?q",
186 ),
187 (
188 "?r rdfs:subClassOf ?s",
189 "?r owl:allValuesFrom ?c ; owl:onProperty ?p . ?s owl:allValuesFrom ?d ; owl:onProperty ?p . ?c rdfs:subClassOf ?d",
190 ),
191 (
192 "?r rdfs:subClassOf ?s",
193 "?r owl:allValuesFrom ?c ; owl:onProperty ?p . ?s owl:allValuesFrom ?c ; owl:onProperty ?q . ?q rdfs:subPropertyOf ?p",
194 ),
195 (
196 "?c rdfs:subClassOf ?member",
197 "?c owl:intersectionOf ?list . ?list rdf:rest*/rdf:first ?member",
198 ),
199 (
200 "?member rdfs:subClassOf ?c",
201 "?c owl:unionOf ?list . ?list rdf:rest*/rdf:first ?member",
202 ),
203];
204
205pub fn expand(data: &mut Dataset, mode: &str) -> Result<()> {
206 if data.iter().any(|q| !q.graph_name.is_default_graph()) {
207 let before = crate::graph::with_union(data);
208 let mut working = before.clone();
209 expand_inner(&mut working, mode)?;
210 crate::graph::commit_additions(data, &before, &working);
211 return Ok(());
212 }
213 expand_inner(data, mode)
214}
215
216pub fn expand_into(data: &mut Dataset, mode: &str, destination: GraphName) -> Result<()> {
219 let before = crate::graph::with_union(data);
220 let mut working = before.clone();
221 expand_inner(&mut working, mode)?;
222 data.extend(
223 working
224 .iter()
225 .filter(|q| !before.contains(*q))
226 .map(|q| Quad::new(q.subject, q.predicate, q.object, destination.clone())),
227 );
228 Ok(())
229}
230
231fn expand_inner(data: &mut Dataset, mode: &str) -> Result<()> {
232 match mode {
233 "none" | "" => return Ok(()),
234 "rdfs" | "owlrl" | "both" => {}
235 _ => return Err(Error::Unsupported(format!("inference: {mode}"))),
236 }
237 let mut rules = Vec::new();
238 if mode != "owlrl" {
239 rules.extend_from_slice(RDFS_RULES);
240 }
241 if mode != "rdfs" {
242 rules.extend_from_slice(OWL_RULES);
243 axioms::owl(data, mode == "both");
244 }
245 let mut initial_resources = if mode != "owlrl" {
246 axioms::resources(data)
247 } else {
248 Vec::new()
249 };
250 let queries = rules
251 .iter()
252 .map(|(template, pattern)| {
253 SparqlEvaluator::new()
254 .parse_query(&format!(
255 "{PREFIXES}\nCONSTRUCT {{ {template} }} WHERE {{ {pattern} }}"
256 ))
257 .map_err(|e| Error::Runtime(format!("Invalid inference rule: {e}")))
258 })
259 .collect::<Result<Vec<_>>>()?;
260 loop {
261 let mut additions = std::mem::take(&mut initial_resources);
262 for query in &queries {
263 let QueryResults::Graph(triples) =
264 query.clone().on_queryable_dataset(&*data).execute()?
265 else {
266 unreachable!("Inference rules are CONSTRUCT queries")
267 };
268 for triple in triples {
269 let triple = triple?;
270 additions.push(Quad::new(
271 triple.subject,
272 triple.predicate,
273 triple.object,
274 GraphName::DefaultGraph,
275 ));
276 }
277 }
278 if mode != "rdfs" {
279 for quad in data
281 .iter()
282 .filter(|q| q.predicate == iri(OWL, "propertyChainAxiom").as_ref())
283 {
284 let Term::NamedNode(property) = Term::from(quad.subject.into_owned()) else {
285 return Err(Error::Invalid(
286 "Property chain predicate must be an IRI".into(),
287 ));
288 };
289 let chain = rdf_list(data, &quad.object.into_owned())?;
290 if chain.is_empty() {
291 continue;
292 }
293 let starts: Vec<Term> =
294 data.iter().map(|q| q.subject.into_owned().into()).collect();
295 for start in starts {
296 let mut current = vec![start.clone()];
297 for step in &chain {
298 let Term::NamedNode(step) = step else {
299 return Err(Error::Invalid(
300 "Property chain members must be IRIs".into(),
301 ));
302 };
303 current = current
304 .iter()
305 .map(|n| objects(data, n, step))
306 .collect::<Result<Vec<_>>>()?
307 .into_iter()
308 .flatten()
309 .collect();
310 }
311 for end in current {
312 additions.push(Quad::new(
313 crate::graph::subject(&start)?,
314 property.clone(),
315 end,
316 GraphName::DefaultGraph,
317 ));
318 }
319 }
320 }
321 for quad in data
322 .iter()
323 .filter(|q| q.predicate == iri(OWL, "hasKey").as_ref())
324 {
325 let keys = rdf_list(data, &quad.object.into_owned())?;
326 if keys.is_empty() {
328 continue;
329 }
330 let keys = keys
331 .into_iter()
332 .map(|key| match key {
333 Term::NamedNode(key) => Ok(key),
334 _ => Err(Error::Invalid("Key properties must be IRIs".into())),
335 })
336 .collect::<Result<Vec<_>>>()?;
337 let class = quad.subject.into_owned().into();
338 let members: HashSet<_> = subjects(data, &iri(RDF, "type"), Some(&class))?
339 .into_iter()
340 .collect();
341 for member in &members {
342 let mut candidates: Option<HashSet<Term>> = None;
343 for key in &keys {
344 let mut matches = HashSet::new();
348 for value in objects(data, member, key)? {
349 matches.extend(
350 subjects(data, key, Some(&value))?
351 .into_iter()
352 .filter(|n| n != member && members.contains(n)),
353 );
354 }
355 if let Some(candidates) = &mut candidates {
356 candidates.retain(|n| matches.contains(n));
357 } else {
358 candidates = Some(matches);
359 }
360 if candidates.as_ref().is_some_and(HashSet::is_empty) {
361 break;
362 }
363 }
364 for other in candidates.into_iter().flatten() {
365 additions.push(Quad::new(
366 subject(member)?,
367 iri(OWL, "sameAs"),
368 other,
369 GraphName::DefaultGraph,
370 ));
371 }
372 }
373 }
374 }
375 let before = data.len();
376 data.extend(additions);
377 if data.len() == before {
378 return Ok(());
379 }
380 }
381}