Adds basic reasoning via query rewriting

rules
Tpt 2 years ago
parent 8f94baedf6
commit 4f3f99b382
  1. 2
      lib/Cargo.toml
  2. 1
      lib/sparopt/Cargo.toml
  3. 41
      lib/sparopt/src/algebra.rs
  4. 4
      lib/sparopt/src/lib.rs
  5. 446
      lib/sparopt/src/reasoning.rs
  6. 50
      lib/src/sparql/algebra.rs
  7. 15
      lib/src/sparql/mod.rs
  8. 6
      lib/src/sparql/plan_builder.rs
  9. 1
      lib/src/sparql/update.rs
  10. 39
      testsuite/oxigraph-tests/sparql-reasoning/manifest.ttl
  11. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.rq
  12. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.rr
  13. 23
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.srj
  14. 0
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.ttl
  15. 5
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.rq
  16. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.rr
  17. 15
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.srj
  18. 2
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.ttl
  19. 5
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.rq
  20. 3
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.rr
  21. 19
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.srj
  22. 2
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.ttl
  23. 10
      testsuite/src/manifest.rs
  24. 56
      testsuite/src/sparql_evaluator.rs
  25. 8
      testsuite/src/vocab.rs
  26. 7
      testsuite/tests/oxigraph.rs

@ -41,7 +41,7 @@ json-event-parser = "0.1"
oxrdf = { version = "0.1.5", path="oxrdf", features = ["rdf-star", "oxsdatatypes"] }
oxsdatatypes = { version = "0.1.1", path="oxsdatatypes" }
spargebra = { version = "0.2.8-dev", path="spargebra", features = ["rdf-star", "sep-0002", "sep-0006"] }
sparopt = { version = "0.1.0-alpha.1", path="sparopt", features = ["rdf-star", "sep-0002", "sep-0006"] }
sparopt = { version = "0.1.0-alpha.1", path="sparopt", features = ["rdf-star", "sep-0002", "sep-0006", "rules"] }
sparesults = { version = "0.1.7", path="sparesults", features = ["rdf-star"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]

@ -18,6 +18,7 @@ default = []
rdf-star = ["oxrdf/rdf-star", "spargebra/rdf-star"]
sep-0002 = ["spargebra/sep-0002"]
sep-0006 = ["spargebra/sep-0006"]
rules = ["spargebra/rules"]
[dependencies]
oxrdf = { version = "0.1.5", path="../oxrdf" }

@ -7,12 +7,12 @@ use spargebra::algebra::{
GraphPattern as AlGraphPattern, OrderExpression as AlOrderExpression,
};
pub use spargebra::algebra::{Function, PropertyPathExpression};
#[cfg(feature = "rdf-star")]
use spargebra::term::GroundTriplePattern;
use spargebra::term::{BlankNode, TermPattern, TriplePattern};
use spargebra::term::{BlankNode, GroundSubject, TermPattern, TriplePattern};
pub use spargebra::term::{
GroundTerm, GroundTermPattern, Literal, NamedNode, NamedNodePattern, Variable,
};
#[cfg(feature = "rdf-star")]
use spargebra::term::{GroundTriple, GroundTriplePattern};
use std::collections::HashMap;
/// An [expression](https://www.w3.org/TR/sparql11-query/#expressions).
@ -191,6 +191,41 @@ impl From<Literal> for Expression {
}
}
impl From<GroundSubject> for Expression {
fn from(value: GroundSubject) -> Self {
match value {
GroundSubject::NamedNode(value) => value.into(),
#[cfg(feature = "rdf-star")]
GroundSubject::Triple(value) => (*value).into(),
}
}
}
impl From<GroundTerm> for Expression {
fn from(value: GroundTerm) -> Self {
match value {
GroundTerm::NamedNode(value) => value.into(),
GroundTerm::Literal(value) => value.into(),
#[cfg(feature = "rdf-star")]
GroundTerm::Triple(value) => (*value).into(),
}
}
}
#[cfg(feature = "rdf-star")]
impl From<GroundTriple> for Expression {
fn from(value: GroundTriple) -> Self {
Self::FunctionCall(
Function::Triple,
vec![
value.subject.into(),
value.predicate.into(),
value.object.into(),
],
)
}
}
impl From<Variable> for Expression {
fn from(value: Variable) -> Self {
Self::Variable(value)

@ -1,4 +1,8 @@
pub use crate::optimizer::Optimizer;
#[cfg(feature = "rules")]
pub use crate::reasoning::QueryRewriter;
pub mod algebra;
mod optimizer;
#[cfg(feature = "rules")]
mod reasoning;

@ -0,0 +1,446 @@
//! Reasoning via query rewriting
use crate::algebra::*;
use rand::random;
use spargebra::term::GroundTriplePattern;
use spargebra::RuleSet;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
pub struct QueryRewriter {
rules: Vec<(Vec<GroundTriplePattern>, GraphPattern)>,
}
impl QueryRewriter {
pub fn new(rule_set: RuleSet) -> Self {
Self {
rules: rule_set
.rules
.into_iter()
.map(|rule| (rule.head, (&rule.body).into()))
.collect(),
}
}
pub fn rewrite_graph_pattern(&self, pattern: &GraphPattern) -> GraphPattern {
//TODO: rewrite EXISTS
match pattern {
GraphPattern::QuadPattern {
subject,
predicate,
object,
graph_name,
} => self.rewrite_quad_pattern(subject, predicate, object, graph_name.as_ref()),
GraphPattern::Path { .. } => todo!(),
GraphPattern::Join { left, right } => GraphPattern::join(
self.rewrite_graph_pattern(left),
self.rewrite_graph_pattern(right),
),
GraphPattern::LeftJoin {
left,
right,
expression,
} => GraphPattern::left_join(
self.rewrite_graph_pattern(left),
self.rewrite_graph_pattern(right),
expression.clone(),
),
#[cfg(feature = "sep-0006")]
GraphPattern::Lateral { left, right } => GraphPattern::lateral(
self.rewrite_graph_pattern(left),
self.rewrite_graph_pattern(right),
),
GraphPattern::Filter { inner, expression } => {
GraphPattern::filter(self.rewrite_graph_pattern(inner), expression.clone())
}
GraphPattern::Union { inner } => inner
.iter()
.map(|p| self.rewrite_graph_pattern(p))
.reduce(GraphPattern::union)
.unwrap_or_else(GraphPattern::empty),
GraphPattern::Extend {
inner,
variable,
expression,
} => GraphPattern::extend(
self.rewrite_graph_pattern(inner),
variable.clone(),
expression.clone(),
),
GraphPattern::Minus { left, right } => GraphPattern::minus(
self.rewrite_graph_pattern(left),
self.rewrite_graph_pattern(right),
),
GraphPattern::Values {
variables,
bindings,
} => GraphPattern::values(variables.clone(), bindings.clone()),
GraphPattern::OrderBy { inner, expression } => {
GraphPattern::order_by(self.rewrite_graph_pattern(inner), expression.clone())
}
GraphPattern::Project { inner, variables } => {
GraphPattern::project(self.rewrite_graph_pattern(inner), variables.clone())
}
GraphPattern::Distinct { inner } => {
GraphPattern::distinct(self.rewrite_graph_pattern(inner))
}
GraphPattern::Reduced { inner } => {
GraphPattern::reduced(self.rewrite_graph_pattern(inner))
}
GraphPattern::Slice {
inner,
start,
length,
} => GraphPattern::slice(self.rewrite_graph_pattern(inner), *start, *length),
GraphPattern::Group {
inner,
variables,
aggregates,
} => GraphPattern::group(
self.rewrite_graph_pattern(inner),
variables.clone(),
aggregates.clone(),
),
GraphPattern::Service {
inner,
silent,
name,
} => GraphPattern::service(self.rewrite_graph_pattern(inner), name.clone(), *silent),
}
}
fn rewrite_quad_pattern(
&self,
subject: &GroundTermPattern,
predicate: &NamedNodePattern,
object: &GroundTermPattern,
graph_name: Option<&NamedNodePattern>,
) -> GraphPattern {
// We rewrite based on rules
let mut graph_pattern = GraphPattern::QuadPattern {
subject: subject.clone(),
predicate: predicate.clone(),
object: object.clone(),
graph_name: graph_name.cloned(),
};
for (rule_head, rule_body) in &self.rules {
for head_pattern in rule_head {
if let Some(nested) = self.apply_rule_for_quad_pattern(
subject,
predicate,
object,
graph_name,
head_pattern,
rule_body,
) {
graph_pattern = GraphPattern::union(graph_pattern, nested);
}
}
}
graph_pattern
}
/// Attempts to use a given rule to get new facts for a triple pattern
fn apply_rule_for_quad_pattern(
&self,
subject: &GroundTermPattern,
predicate: &NamedNodePattern,
object: &GroundTermPattern,
graph_name: Option<&NamedNodePattern>,
head: &GroundTriplePattern,
body: &GraphPattern,
) -> Option<GraphPattern> {
let head_unification = Self::unify_triple_pattern(
subject.clone(),
predicate.clone(),
object.clone(),
head.clone(),
)?;
// We build a nested query
// from is the parent query and to is the nested one
let mut replacements_in_rule = HashMap::new();
let mut final_binds = Vec::new();
for replacement in head_unification {
match replacement {
Replacement::ConstToVar { from, to } => match replacements_in_rule.entry(to) {
Entry::Vacant(e) => {
e.insert(TermOrVariable::Term(from));
}
Entry::Occupied(mut e) => match e.get() {
TermOrVariable::Term(c) => {
if from != *c {
return None; //Conflict
}
}
TermOrVariable::Variable(v) => {
final_binds.push((v.clone(), TermOrVariable::Term(from.clone())));
e.insert(TermOrVariable::Term(from));
}
},
},
Replacement::VarToConst { from, to } => {
final_binds.push((from, TermOrVariable::Term(to)));
}
Replacement::VarToVar { from, to } => match replacements_in_rule.entry(to) {
Entry::Vacant(e) => {
e.insert(TermOrVariable::Variable(from));
}
Entry::Occupied(e) => final_binds.push((from, e.get().clone())),
},
}
}
let mut plan = self.rewrite_rule_body(body, graph_name, &mut replacements_in_rule)?;
for (variable, value) in final_binds {
plan = GraphPattern::extend(
plan,
variable,
match value {
TermOrVariable::Term(v) => v.into(),
TermOrVariable::Variable(v) => v.into(),
},
);
}
Some(plan)
}
fn rewrite_rule_body<'a>(
&self,
pattern: &'a GraphPattern,
parent_graph_name: Option<&'a NamedNodePattern>,
replacements_in_rule: &mut HashMap<Variable, TermOrVariable>,
) -> Option<GraphPattern> {
Some(match pattern {
GraphPattern::QuadPattern {
subject,
predicate,
object,
graph_name,
} => self.rewrite_quad_pattern(
&Self::apply_replacement_on_term_pattern(subject, replacements_in_rule)?,
&Self::apply_replacement_on_named_node_pattern(predicate, replacements_in_rule)?,
&Self::apply_replacement_on_term_pattern(object, replacements_in_rule)?,
if let Some(graph_name) = graph_name {
Some(Self::apply_replacement_on_named_node_pattern(
graph_name,
replacements_in_rule,
)?)
} else {
parent_graph_name.cloned()
}
.as_ref(),
),
GraphPattern::Join { left, right } => GraphPattern::join(
self.rewrite_rule_body(left, parent_graph_name, replacements_in_rule)?,
self.rewrite_rule_body(right, parent_graph_name, replacements_in_rule)?,
),
GraphPattern::Values {
variables,
bindings,
} => {
let variable_mapping = variables
.iter()
.map(|v| {
replacements_in_rule
.entry(v.clone())
.or_insert_with(|| TermOrVariable::Variable(new_var()))
.clone()
})
.collect::<Vec<_>>();
GraphPattern::Values {
variables: variable_mapping
.iter()
.filter_map(|v| match v {
TermOrVariable::Term(_) => None,
TermOrVariable::Variable(v) => Some(v.clone()),
})
.collect(),
bindings: bindings
.iter()
.filter_map(|binding| {
let mut new_binding = Vec::with_capacity(binding.len());
for (variable, value) in variable_mapping.iter().zip(binding) {
match variable {
TermOrVariable::Variable(_) => new_binding.push(value.clone()),
TermOrVariable::Term(cst) => {
let compatible = if let Some(value) = value {
cst == value
} else {
true
};
if !compatible {
return None;
}
}
}
}
Some(new_binding)
})
.collect(),
}
}
_ => unreachable!("Not allowed by the parser yet: {pattern:?}"),
})
}
fn apply_replacement_on_named_node_pattern(
pattern: &NamedNodePattern,
replacements: &mut HashMap<Variable, TermOrVariable>,
) -> Option<NamedNodePattern> {
Some(match pattern {
NamedNodePattern::NamedNode(node) => NamedNodePattern::NamedNode(node.clone()),
NamedNodePattern::Variable(variable) => {
match replacements
.entry(variable.clone())
.or_insert_with(|| TermOrVariable::Variable(new_var()))
{
TermOrVariable::Term(c) => {
if let GroundTerm::NamedNode(node) = c {
NamedNodePattern::NamedNode(node.clone())
} else {
return None;
}
}
TermOrVariable::Variable(v) => NamedNodePattern::Variable(v.clone()),
}
}
})
}
fn apply_replacement_on_term_pattern(
pattern: &GroundTermPattern,
replacements: &mut HashMap<Variable, TermOrVariable>,
) -> Option<GroundTermPattern> {
Some(match pattern {
GroundTermPattern::NamedNode(node) => node.clone().into(),
GroundTermPattern::Literal(literal) => literal.clone().into(),
GroundTermPattern::Triple(triple) => GroundTriplePattern {
subject: Self::apply_replacement_on_term_pattern(&triple.subject, replacements)?,
predicate: Self::apply_replacement_on_named_node_pattern(
&triple.predicate,
replacements,
)?,
object: Self::apply_replacement_on_term_pattern(&triple.subject, replacements)?,
}
.into(),
GroundTermPattern::Variable(variable) => {
match replacements
.entry(variable.clone())
.or_insert_with(|| TermOrVariable::Variable(new_var()))
{
TermOrVariable::Term(c) => c.clone().into(),
TermOrVariable::Variable(v) => v.clone().into(),
}
}
})
}
fn unify_triple_pattern(
from_subject: GroundTermPattern,
from_predicate: NamedNodePattern,
from_object: GroundTermPattern,
to: GroundTriplePattern,
) -> Option<Vec<Replacement>> {
let mut mapping = Self::unify_ground_term_pattern(from_subject, to.subject)?;
mapping.extend(Self::unify_named_node_pattern(
from_predicate,
to.predicate,
)?);
mapping.extend(Self::unify_ground_term_pattern(from_object, to.object)?);
Some(mapping)
}
fn unify_named_node_pattern(
from: NamedNodePattern,
to: NamedNodePattern,
) -> Option<Vec<Replacement>> {
match from {
NamedNodePattern::NamedNode(from) => match to {
NamedNodePattern::NamedNode(to) => {
if from == to {
Some(Vec::new())
} else {
None
}
}
NamedNodePattern::Variable(to) => Some(vec![Replacement::ConstToVar {
from: from.into(),
to,
}]),
},
NamedNodePattern::Variable(from) => match to {
NamedNodePattern::NamedNode(to) => Some(vec![Replacement::VarToConst {
from,
to: to.into(),
}]),
NamedNodePattern::Variable(to) => Some(vec![Replacement::VarToVar { from, to }]),
},
}
}
fn unify_ground_term_pattern(
from: GroundTermPattern,
to: GroundTermPattern,
) -> Option<Vec<Replacement>> {
match from {
GroundTermPattern::NamedNode(from) => match to {
GroundTermPattern::NamedNode(to) => {
if from == to {
Some(Vec::new())
} else {
None
}
}
GroundTermPattern::Literal(_) | GroundTermPattern::Triple(_) => None,
GroundTermPattern::Variable(to) => Some(vec![Replacement::ConstToVar {
from: from.into(),
to,
}]),
},
GroundTermPattern::Literal(from) => match to {
GroundTermPattern::NamedNode(_) => None,
GroundTermPattern::Literal(to) => {
if from == to {
Some(Vec::new())
} else {
None
}
}
GroundTermPattern::Triple(_) => None,
GroundTermPattern::Variable(to) => Some(vec![Replacement::ConstToVar {
from: from.into(),
to,
}]),
},
GroundTermPattern::Triple(_) => unimplemented!(),
GroundTermPattern::Variable(from) => match to {
GroundTermPattern::NamedNode(to) => Some(vec![Replacement::VarToConst {
from,
to: to.into(),
}]),
GroundTermPattern::Literal(to) => Some(vec![Replacement::VarToConst {
from,
to: to.into(),
}]),
GroundTermPattern::Triple(_) => unimplemented!(),
GroundTermPattern::Variable(to) => Some(vec![Replacement::VarToVar { from, to }]),
},
}
}
}
#[derive(Clone)]
enum Replacement {
VarToConst { from: Variable, to: GroundTerm },
ConstToVar { from: GroundTerm, to: Variable },
VarToVar { from: Variable, to: Variable },
}
#[derive(Clone)]
enum TermOrVariable {
Term(GroundTerm),
Variable(Variable),
}
fn new_var() -> Variable {
Variable::new_unchecked(format!("{:x}", random::<u128>()))
}

@ -181,6 +181,56 @@ impl<'a> TryFrom<&'a String> for Update {
}
}
/// A parsed rule set.
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct RuleSet {
pub(super) inner: spargebra::RuleSet,
}
impl RuleSet {
/// Parses a rule set with an optional base IRI to resolve relative IRIs in the rule set.
pub fn parse(rule_set: &str, base_iri: Option<&str>) -> Result<Self, spargebra::ParseError> {
let rule_set = spargebra::RuleSet::parse(rule_set, base_iri)?;
Ok(rule_set.into())
}
}
impl fmt::Display for RuleSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f) //TODO: override
}
}
impl FromStr for RuleSet {
type Err = spargebra::ParseError;
fn from_str(rule_set: &str) -> Result<Self, spargebra::ParseError> {
Self::parse(rule_set, None)
}
}
impl<'a> TryFrom<&'a str> for RuleSet {
type Error = spargebra::ParseError;
fn try_from(rule_set: &str) -> Result<Self, spargebra::ParseError> {
Self::from_str(rule_set)
}
}
impl<'a> TryFrom<&'a String> for RuleSet {
type Error = spargebra::ParseError;
fn try_from(rule_set: &String) -> Result<Self, spargebra::ParseError> {
Self::from_str(rule_set)
}
}
impl From<spargebra::RuleSet> for RuleSet {
fn from(rule_set: spargebra::RuleSet) -> Self {
Self { inner: rule_set }
}
}
/// A SPARQL query [dataset specification](https://www.w3.org/TR/sparql11-query/#specifyingDataset)
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct QueryDataset {

@ -14,7 +14,7 @@ mod service;
mod update;
use crate::model::{NamedNode, Term};
pub use crate::sparql::algebra::{Query, QueryDataset, Update};
pub use crate::sparql::algebra::{Query, QueryDataset, RuleSet, Update};
use crate::sparql::dataset::DatasetView;
pub use crate::sparql::error::{EvaluationError, QueryError};
use crate::sparql::eval::{SimpleEvaluator, Timer};
@ -29,6 +29,7 @@ use json_event_parser::{JsonEvent, JsonWriter};
pub use oxrdf::{Variable, VariableNameParseError};
pub use sparesults::QueryResultsFormat;
pub use spargebra::ParseError;
use sparopt::QueryRewriter;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Duration;
@ -53,6 +54,7 @@ pub(crate) fn evaluate_query(
&pattern,
true,
&options.custom_functions,
options.query_rewriter.as_ref(),
options.without_optimizations,
)?;
let planning_duration = start_planning.elapsed();
@ -74,6 +76,7 @@ pub(crate) fn evaluate_query(
&pattern,
false,
&options.custom_functions,
options.query_rewriter.as_ref(),
options.without_optimizations,
)?;
let planning_duration = start_planning.elapsed();
@ -98,6 +101,7 @@ pub(crate) fn evaluate_query(
&pattern,
false,
&options.custom_functions,
options.query_rewriter.as_ref(),
options.without_optimizations,
)?;
let construct = PlanBuilder::build_graph_template(
@ -126,6 +130,7 @@ pub(crate) fn evaluate_query(
&pattern,
false,
&options.custom_functions,
options.query_rewriter.as_ref(),
options.without_optimizations,
)?;
let planning_duration = start_planning.elapsed();
@ -174,6 +179,7 @@ pub struct QueryOptions {
http_timeout: Option<Duration>,
http_redirection_limit: usize,
without_optimizations: bool,
query_rewriter: Option<Rc<QueryRewriter>>,
}
impl QueryOptions {
@ -267,6 +273,13 @@ impl QueryOptions {
self.without_optimizations = true;
self
}
#[inline]
#[must_use]
pub fn with_inference_rules(mut self, rules: RuleSet) -> Self {
self.query_rewriter = Some(Rc::new(QueryRewriter::new(rules.inner)));
self
}
}
/// Options for SPARQL update evaluation.

@ -9,7 +9,7 @@ use oxrdf::{BlankNode, Term, TermRef, Triple};
use regex::Regex;
use spargebra::term::{GroundSubject, GroundTriple, TermPattern, TriplePattern};
use sparopt::algebra::*;
use sparopt::Optimizer;
use sparopt::{Optimizer, QueryRewriter};
use std::collections::{BTreeSet, HashMap};
use std::mem::swap;
use std::rc::Rc;
@ -26,9 +26,13 @@ impl<'a> PlanBuilder<'a> {
pattern: &spargebra::algebra::GraphPattern,
is_cardinality_meaningful: bool,
custom_functions: &'a HashMap<NamedNode, Rc<dyn Fn(&[OxTerm]) -> Option<OxTerm>>>,
query_rewriter: Option<&Rc<QueryRewriter>>,
without_optimizations: bool,
) -> Result<(PlanNode, Vec<Variable>), EvaluationError> {
let mut pattern = GraphPattern::from(pattern);
if let Some(query_rewriter) = query_rewriter {
pattern = query_rewriter.rewrite_graph_pattern(&pattern);
}
if !without_optimizations {
pattern = Optimizer::default().optimize(pattern);
}

@ -123,6 +123,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
algebra,
false,
&self.options.query_options.custom_functions,
None,
!self.options.query_options.without_optimizations,
)?;
let evaluator = SimpleEvaluator::new(

@ -0,0 +1,39 @@
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <https://github.com/oxigraph/oxigraph/tests/sparql/manifest#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
@prefix qt: <http://www.w3.org/2001/sw/DataAccess/tests/test-query#> .
@prefix ox: <https://github.com/oxigraph/oxigraph/tests#> .
<> rdf:type mf:Manifest ;
rdfs:label "Oxigraph SPARQL reasoning tests" ;
mf:entries
(
:simple_type_inheritance
:simple_fact
:with_graph_name
) .
:simple_type_inheritance rdf:type ox:SparqlRuleEvaluationTest ;
mf:name "Simple type inheritance" ;
mf:action
[ qt:query <simple_type_inheritance.rq> ;
qt:data <simple_type_inheritance.ttl> ] ;
ox:rulesData <simple_type_inheritance.rr> ;
mf:result <simple_type_inheritance.srj> .
:simple_fact rdf:type ox:SparqlRuleEvaluationTest ;
mf:name "Simple fact" ;
mf:action
[ qt:query <simple_fact.rq> ;
qt:data <simple_fact.ttl> ] ;
ox:rulesData <simple_fact.rr> ;
mf:result <simple_fact.srj> .
:with_graph_name rdf:type ox:SparqlRuleEvaluationTest ;
mf:name "Simple type inheritance" ;
mf:action
[ qt:query <with_graph_name.rq> ;
qt:graphData <with_graph_name.ttl> ] ;
ox:rulesData <with_graph_name.rr> ;
mf:result <with_graph_name.srj> .

@ -0,0 +1,3 @@
PREFIX ex: <http://example.org/>
IF {} THEN { ex:s ex:p ex:o }

@ -0,0 +1,23 @@
{
"head": {
"vars": ["s", "p", "o"]
},
"results": {
"bindings": [
{
"s": {
"type": "uri",
"value": "http://example.org/s"
},
"p": {
"type": "uri",
"value": "http://example.org/p"
},
"o": {
"type": "uri",
"value": "http://example.org/o"
}
}
]
}
}

@ -0,0 +1,5 @@
PREFIX ex: <http://example.org/>
SELECT * WHERE {
?s a ex:Bar
}

@ -0,0 +1,3 @@
PREFIX ex: <http://example.org/>
IF { ?s a ex:Foo } THEN { ?s a ex:Bar }

@ -0,0 +1,15 @@
{
"head": {
"vars": ["s"]
},
"results": {
"bindings": [
{
"s": {
"type": "uri",
"value": "http://example.org/s1"
}
}
]
}
}

@ -0,0 +1,2 @@
PREFIX ex: <http://example.org/>
ex:s1 a ex:Foo .

@ -0,0 +1,5 @@
PREFIX ex: <http://example.org/>
SELECT * WHERE {
GRAPH ?g { ?s a ex:Bar }
}

@ -0,0 +1,3 @@
PREFIX ex: <http://example.org/>
IF { ?g a ex:Foo } THEN { ?g a ex:Bar }

@ -0,0 +1,19 @@
{
"head": {
"vars": ["s", "g"]
},
"results": {
"bindings": [
{
"s": {
"type": "uri",
"value": "http://example.org/s1"
},
"g": {
"type": "uri",
"value": "https://github.com/oxigraph/oxigraph/tests/sparql-reasoning/with_graph_name.ttl"
}
}
]
}
}

@ -0,0 +1,2 @@
PREFIX ex: <http://example.org/>
ex:s1 a ex:Foo .

@ -19,6 +19,7 @@ pub struct Test {
pub service_data: Vec<(String, String)>,
pub result: Option<String>,
pub result_graph_data: Vec<(NamedNode, String)>,
pub rules_data: Option<String>,
}
impl fmt::Display for Test {
@ -255,6 +256,14 @@ impl TestManifest {
Some(_) => bail!("invalid result"),
None => (None, Vec::new()),
};
let rules_data = if let Some(TermRef::NamedNode(n)) = self
.graph
.object_for_subject_predicate(&test_node, ox::RULES_DATA)
{
Some(n.as_str().to_owned())
} else {
None
};
return Ok(Some(Test {
id: test_node,
kind,
@ -268,6 +277,7 @@ impl TestManifest {
service_data,
result,
result_graph_data,
rules_data,
}));
}
}

@ -67,6 +67,10 @@ pub fn register_sparql_tests(evaluator: &mut TestEvaluator) {
"https://github.com/oxigraph/oxigraph/tests#NegativeTsvResultsSyntaxTest",
evaluate_negative_tsv_result_syntax_test,
);
evaluator.register(
"https://github.com/oxigraph/oxigraph/tests#SparqlRuleEvaluationTest",
evaluate_rule_evaluation_test,
);
}
fn evaluate_positive_syntax_test(test: &Test) -> Result<()> {
@ -286,6 +290,58 @@ fn evaluate_update_evaluation_test(test: &Test) -> Result<()> {
}
}
fn evaluate_rule_evaluation_test(test: &Test) -> Result<()> {
let store = Store::new()?;
if let Some(data) = &test.data {
load_dataset_to_store(data, &store)?;
}
for (name, value) in &test.graph_data {
load_graph_to_store(value, &store, name)?;
}
let query_file = test
.query
.as_deref()
.ok_or_else(|| anyhow!("No action found for test {test}"))?;
let query = Query::parse(&read_file_to_string(query_file)?, Some(query_file))
.map_err(|e| anyhow!("Failure to parse query of {test} with error: {e}"))?;
let rule_file = test
.rules_data
.as_deref()
.ok_or_else(|| anyhow!("No rules data found for test {test}"))?;
let options = QueryOptions::default().with_inference_rules(
RuleSet::parse(&read_file_to_string(rule_file)?, Some(rule_file))
.map_err(|e| anyhow!("Failure to parse rule set of {test} with error: {e}"))?,
);
let expected_results = load_sparql_query_result(test.result.as_ref().unwrap())
.map_err(|e| anyhow!("Error constructing expected graph for {test}: {e}"))?;
let with_order = if let StaticQueryResults::Solutions { ordered, .. } = &expected_results {
*ordered
} else {
false
};
for with_query_optimizer in [true, false] {
let mut options = options.clone();
if !with_query_optimizer {
options = options.without_optimizations();
}
let actual_results = store
.query_opt(query.clone(), options)
.map_err(|e| anyhow!("Failure to execute query of {test} with error: {e}"))?;
let actual_results = StaticQueryResults::from_query_results(actual_results, with_order)?;
if !are_query_results_isomorphic(&expected_results, &actual_results) {
bail!(
"Failure on {test}.\n{}\nParsed query:\n{}\nData:\n{store}\n",
results_diff(expected_results, actual_results),
Query::parse(&read_file_to_string(query_file)?, Some(query_file)).unwrap()
);
}
}
Ok(())
}
fn load_sparql_query_result(url: &str) -> Result<StaticQueryResults> {
if url.ends_with(".srx") {
StaticQueryResults::from_query_results(

@ -77,6 +77,7 @@ pub mod qt {
pub mod ut {
use oxigraph::model::NamedNodeRef;
pub const DATA: NamedNodeRef<'_> =
NamedNodeRef::new_unchecked("http://www.w3.org/2009/sparql/tests/test-update#data");
pub const GRAPH_DATA: NamedNodeRef<'_> =
@ -86,3 +87,10 @@ pub mod ut {
pub const REQUEST: NamedNodeRef<'_> =
NamedNodeRef::new_unchecked("http://www.w3.org/2009/sparql/tests/test-update#request");
}
pub mod ox {
use oxigraph::model::NamedNodeRef;
pub const RULES_DATA: NamedNodeRef<'_> =
NamedNodeRef::new_unchecked("https://github.com/oxigraph/oxigraph/tests#rulesData");
}

@ -38,3 +38,10 @@ fn oxigraph_sparql_results_testsuite() -> Result<()> {
"https://github.com/oxigraph/oxigraph/tests/sparql-results/manifest.ttl",
])
}
#[test]
fn oxigraph_sparql_reasoning_testsuite() -> Result<()> {
run_testsuite(vec![
"https://github.com/oxigraph/oxigraph/tests/sparql-reasoning/manifest.ttl",
])
}

Loading…
Cancel
Save