Compare commits

...

11 Commits
main ... rules

  1. 4
      .github/workflows/tests.yml
  2. 12
      Cargo.lock
  3. 1
      Cargo.toml
  4. 45
      fuzz/fuzz_targets/sparql_eval.rs
  5. 3
      lib/Cargo.toml
  6. 3
      lib/spargebra/Cargo.toml
  7. 3
      lib/spargebra/src/lib.rs
  8. 37
      lib/spargebra/src/parser.rs
  9. 120
      lib/spargebra/src/rule.rs
  10. 29
      lib/spargebra/src/term.rs
  11. 30
      lib/sparopt/Cargo.toml
  12. 32
      lib/sparopt/README.md
  13. 1904
      lib/sparopt/src/algebra.rs
  14. 8
      lib/sparopt/src/lib.rs
  15. 195
      lib/sparopt/src/optimizer.rs
  16. 1057
      lib/sparopt/src/reasoning.rs
  17. 50
      lib/src/sparql/algebra.rs
  18. 363
      lib/src/sparql/eval.rs
  19. 15
      lib/src/sparql/mod.rs
  20. 244
      lib/src/sparql/plan.rs
  21. 1151
      lib/src/sparql/plan_builder.rs
  22. 1
      lib/src/sparql/update.rs
  23. 48
      testsuite/oxigraph-tests/sparql-reasoning/manifest.ttl
  24. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.rq
  25. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.rr
  26. 23
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.srj
  27. 0
      testsuite/oxigraph-tests/sparql-reasoning/simple_fact.ttl
  28. 5
      testsuite/oxigraph-tests/sparql-reasoning/simple_recursion.rq
  29. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_recursion.rr
  30. 69
      testsuite/oxigraph-tests/sparql-reasoning/simple_recursion.srj
  31. 4
      testsuite/oxigraph-tests/sparql-reasoning/simple_recursion.ttl
  32. 5
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.rq
  33. 3
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.rr
  34. 15
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.srj
  35. 2
      testsuite/oxigraph-tests/sparql-reasoning/simple_type_inheritance.ttl
  36. 5
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.rq
  37. 3
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.rr
  38. 19
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.srj
  39. 2
      testsuite/oxigraph-tests/sparql-reasoning/with_graph_name.ttl
  40. 10
      testsuite/src/manifest.rs
  41. 80
      testsuite/src/sparql_evaluator.rs
  42. 8
      testsuite/src/vocab.rs
  43. 7
      testsuite/tests/oxigraph.rs
  44. 1
      testsuite/tests/sparql.rs

@ -35,6 +35,8 @@ jobs:
working-directory: ./lib/sparesults
- run: cargo clippy
working-directory: ./lib/spargebra
- run: cargo clippy
working-directory: ./lib/sparopt
- run: cargo clippy --all-targets --all-features
clippy_wasm_js:
@ -75,6 +77,8 @@ jobs:
working-directory: ./lib/sparesults
- run: cargo clippy -- -D warnings -D clippy::all
working-directory: ./lib/spargebra
- run: cargo clippy -- -D warnings -D clippy::all
working-directory: ./lib/sparopt
- run: cargo clippy --all-targets -- -D warnings -D clippy::all
working-directory: ./server

12
Cargo.lock generated

@ -954,6 +954,7 @@ dependencies = [
"siphasher",
"sparesults",
"spargebra",
"sparopt",
"zstd",
]
@ -1638,7 +1639,7 @@ dependencies = [
[[package]]
name = "spargebra"
version = "0.2.7"
version = "0.2.8-dev"
dependencies = [
"oxilangtag",
"oxiri",
@ -1647,6 +1648,15 @@ dependencies = [
"rand",
]
[[package]]
name = "sparopt"
version = "0.1.0-alpha.1"
dependencies = [
"oxrdf",
"rand",
"spargebra",
]
[[package]]
name = "sparql-smith"
version = "0.1.0-alpha.3"

@ -6,6 +6,7 @@ members = [
"lib/oxsdatatypes",
"lib/spargebra",
"lib/sparesults",
"lib/sparopt",
"lib/sparql-smith",
"oxrocksdb-sys",
"python",

@ -3,7 +3,7 @@
use lazy_static::lazy_static;
use libfuzzer_sys::fuzz_target;
use oxigraph::io::DatasetFormat;
use oxigraph::sparql::{Query, QueryOptions, QueryResults, QuerySolutionIter};
use oxigraph::sparql::{Query, QueryOptions, QueryResults, QuerySolutionIter, RuleSet};
use oxigraph::store::Store;
lazy_static! {
@ -26,24 +26,39 @@ fuzz_target!(|data: sparql_smith::Query| {
let options = QueryOptions::default();
let with_opt = STORE.query_opt(query.clone(), options.clone()).unwrap();
let without_opt = STORE
.query_opt(query, options.without_optimizations())
.query_opt(query.clone(), options.clone().without_optimizations())
.unwrap();
match (with_opt, without_opt) {
(QueryResults::Solutions(with_opt), QueryResults::Solutions(without_opt)) => {
assert_eq!(
query_solutions_key(with_opt, query_str.contains(" REDUCED ")),
query_solutions_key(without_opt, query_str.contains(" REDUCED "))
)
}
(QueryResults::Graph(_), QueryResults::Graph(_)) => unimplemented!(),
(QueryResults::Boolean(with_opt), QueryResults::Boolean(without_opt)) => {
assert_eq!(with_opt, without_opt)
}
_ => panic!("Different query result types"),
}
compare_results(with_opt, without_opt, &query_str);
let with_opt_and_reasoning = STORE
.query_opt(
query.clone(),
options
.clone()
.with_inference_rules(RuleSet::default())
.clone(),
)
.unwrap();
let with_opt = STORE.query_opt(query.clone(), options).unwrap();
compare_results(with_opt, with_opt_and_reasoning, &query_str);
}
});
fn compare_results(a: QueryResults, b: QueryResults, query_str: &str) {
match (a, b) {
(QueryResults::Solutions(a), QueryResults::Solutions(b)) => {
assert_eq!(
query_solutions_key(a, query_str.contains(" REDUCED ")),
query_solutions_key(b, query_str.contains(" REDUCED "))
)
}
(QueryResults::Graph(_), QueryResults::Graph(_)) => unimplemented!(),
(QueryResults::Boolean(a), QueryResults::Boolean(b)) => {
assert_eq!(a, b)
}
_ => panic!("Different query result types"),
}
}
fn query_solutions_key(iter: QuerySolutionIter, is_reduced: bool) -> String {
// TODO: ordering
let mut b = iter

@ -40,7 +40,8 @@ lazy_static = "1"
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.7", path="spargebra", features = ["rdf-star", "sep-0002", "sep-0006"] }
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", "rules"] }
sparesults = { version = "0.1.7", path="sparesults", features = ["rdf-star"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]

@ -1,6 +1,6 @@
[package]
name = "spargebra"
version = "0.2.7"
version = "0.2.8-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
@ -18,6 +18,7 @@ default = []
rdf-star = ["oxrdf/rdf-star"]
sep-0002 = []
sep-0006 = []
rules = []
[dependencies]
peg = "0.8"

@ -8,9 +8,12 @@
pub mod algebra;
mod parser;
mod query;
mod rule;
pub mod term;
mod update;
pub use parser::ParseError;
pub use query::*;
#[cfg(feature = "rules")]
pub use rule::*;
pub use update::*;

@ -1,5 +1,6 @@
use crate::algebra::*;
use crate::query::*;
use crate::rule::*;
use crate::term::*;
use crate::update::*;
use oxilangtag::LanguageTag;
@ -54,7 +55,7 @@ pub fn parse_update(update: &str, base_iri: Option<&str>) -> Result<Update, Pars
};
let operations =
parser::UpdateInit(&unescape_unicode_codepoints(update), &mut state).map_err(|e| {
parser::UpdateUnit(&unescape_unicode_codepoints(update), &mut state).map_err(|e| {
ParseError {
inner: ParseErrorKind::Parser(e),
}
@ -65,6 +66,27 @@ pub fn parse_update(update: &str, base_iri: Option<&str>) -> Result<Update, Pars
})
}
/// Parses a set of if/then rules with an optional base IRI to resolve relative IRIs in the rule.
pub fn parse_rule_set(rules: &str, base_iri: Option<&str>) -> Result<RuleSet, ParseError> {
let mut state = ParserState {
base_iri: if let Some(base_iri) = base_iri {
Some(Iri::parse(base_iri.to_owned()).map_err(|e| ParseError {
inner: ParseErrorKind::InvalidBaseIri(e),
})?)
} else {
None
},
namespaces: HashMap::default(),
used_bnodes: HashSet::default(),
currently_used_bnodes: HashSet::default(),
aggregates: Vec::new(),
};
parser::RuleSetUnit(&unescape_unicode_codepoints(rules), &mut state).map_err(|e| ParseError {
inner: ParseErrorKind::Parser(e),
})
}
/// Error returned during SPARQL parsing.
#[derive(Debug)]
pub struct ParseError {
@ -966,7 +988,7 @@ parser! {
}
//[3]
pub rule UpdateInit() -> Vec<GraphUpdateOperation> = Update()
pub rule UpdateUnit() -> Vec<GraphUpdateOperation> = Update()
//[4]
rule Prologue() = (BaseDecl() _ / PrefixDecl() _)* {}
@ -2445,5 +2467,16 @@ parser! {
Err(literal)
}
}
pub rule RuleSetUnit() -> RuleSet = RuleSet()
rule RuleSet() -> RuleSet = _ Prologue() _ rules:(Rule() ** (_ ";" _)) _ ( ";" _)? { RuleSet { rules } }
rule Rule() -> Rule = i("IF") _ body:ConstructTemplate() _ i("THEN") _ head:ConstructTemplate() {?
Ok(Rule {
body,
head: head.into_iter().map(GroundTriplePattern::try_from).collect::<Result<_, ()>>().map_err(|_| "Blank nodes are not allowed in rules head")?
})
}
}
}

@ -0,0 +1,120 @@
#![cfg_attr(not(feature = "rules"), allow(dead_code))]
use crate::parser::{parse_rule_set, ParseError};
use crate::term::*;
use std::fmt;
use std::str::FromStr;
/// A parsed if/then rule set.
#[derive(Eq, PartialEq, Debug, Clone, Hash, Default)]
pub struct RuleSet {
pub rules: Vec<Rule>,
}
impl RuleSet {
/// Parses a set of rules with an optional base IRI to resolve relative IRIs in the rules.
/// Note that this base IRI will not be used during execution.
pub fn parse(rules: &str, base_iri: Option<&str>) -> Result<Self, ParseError> {
parse_rule_set(rules, base_iri)
}
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
pub fn to_sse(&self) -> String {
let mut buffer = String::new();
self.fmt_sse(&mut buffer).unwrap();
buffer
}
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
fn fmt_sse(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "(")?;
for (i, r) in self.rules.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
r.fmt_sse(f)?;
}
write!(f, ") ")
}
}
impl fmt::Display for RuleSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for r in &self.rules {
writeln!(f, "{r} ;")?;
}
Ok(())
}
}
impl FromStr for RuleSet {
type Err = ParseError;
fn from_str(rules: &str) -> Result<Self, ParseError> {
Self::parse(rules, None)
}
}
impl<'a> TryFrom<&'a str> for RuleSet {
type Error = ParseError;
fn try_from(rules: &str) -> Result<Self, ParseError> {
Self::from_str(rules)
}
}
impl<'a> TryFrom<&'a String> for RuleSet {
type Error = ParseError;
fn try_from(rules: &String) -> Result<Self, ParseError> {
Self::from_str(rules)
}
}
/// A parsed if/then rule.
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct Rule {
/// The construction template.
pub head: Vec<GroundTriplePattern>,
/// The rule body graph pattern.
pub body: Vec<TriplePattern>,
}
impl Rule {
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
pub fn to_sse(&self) -> String {
let mut buffer = String::new();
self.fmt_sse(&mut buffer).unwrap();
buffer
}
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
fn fmt_sse(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "(rule (")?;
for (i, t) in self.head.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
t.fmt_sse(f)?;
}
write!(f, ") (bgp")?;
for pattern in &self.body {
write!(f, " ")?;
pattern.fmt_sse(f)?;
}
write!(f, "))")
}
}
impl fmt::Display for Rule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IF {{ ")?;
for triple in &self.body {
write!(f, "{triple} . ")?;
}
write!(f, "}} THEN {{ ")?;
for triple in &self.head {
write!(f, "{triple} . ")?;
}
write!(f, "}}")
}
}

@ -541,6 +541,19 @@ impl From<NamedNodePattern> for TermPattern {
}
}
impl From<GroundTermPattern> for TermPattern {
#[inline]
fn from(element: GroundTermPattern) -> Self {
match element {
GroundTermPattern::NamedNode(node) => node.into(),
GroundTermPattern::Literal(literal) => literal.into(),
#[cfg(feature = "rdf-star")]
GroundTermPattern::Triple(t) => TriplePattern::from(*t).into(),
GroundTermPattern::Variable(variable) => variable.into(),
}
}
}
impl TryFrom<TermPattern> for Subject {
type Error = ();
@ -577,6 +590,7 @@ pub enum GroundTermPattern {
NamedNode(NamedNode),
Literal(Literal),
Variable(Variable),
#[cfg(feature = "rdf-star")]
Triple(Box<GroundTriplePattern>),
}
@ -587,6 +601,7 @@ impl GroundTermPattern {
Self::NamedNode(term) => write!(f, "{term}"),
Self::Literal(term) => write!(f, "{term}"),
Self::Variable(var) => write!(f, "{var}"),
#[cfg(feature = "rdf-star")]
Self::Triple(triple) => triple.fmt_sse(f),
}
}
@ -599,6 +614,7 @@ impl fmt::Display for GroundTermPattern {
Self::NamedNode(term) => term.fmt(f),
Self::Literal(term) => term.fmt(f),
Self::Variable(var) => var.fmt(f),
#[cfg(feature = "rdf-star")]
Self::Triple(triple) => write!(f, "<<{triple}>>"),
}
}
@ -618,6 +634,7 @@ impl From<Literal> for GroundTermPattern {
}
}
#[cfg(feature = "rdf-star")]
impl From<GroundTriplePattern> for GroundTermPattern {
#[inline]
fn from(triple: GroundTriplePattern) -> Self {
@ -795,6 +812,17 @@ impl From<Triple> for TriplePattern {
}
}
impl From<GroundTriplePattern> for TriplePattern {
#[inline]
fn from(triple: GroundTriplePattern) -> Self {
Self {
subject: triple.subject.into(),
predicate: triple.predicate,
object: triple.object.into(),
}
}
}
impl TryFrom<TriplePattern> for Triple {
type Error = ();
@ -818,6 +846,7 @@ pub struct GroundTriplePattern {
impl GroundTriplePattern {
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
#[allow(dead_code)]
pub(crate) fn fmt_sse(&self, f: &mut impl Write) -> fmt::Result {
write!(f, "(triple ")?;
self.subject.fmt_sse(f)?;

@ -0,0 +1,30 @@
[package]
name = "sparopt"
version = "0.1.0-alpha.1"
authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
keywords = ["SPARQL"]
repository = "https://github.com/oxigraph/oxigraph/tree/main/lib/sparopt"
homepage = "https://oxigraph.org/"
description = """
A SPARQL optimizer
"""
edition = "2021"
rust-version = "1.60"
[features]
default = []
fixed-point = []
rdf-star = ["oxrdf/rdf-star", "spargebra/rdf-star"]
sep-0002 = ["spargebra/sep-0002"]
sep-0006 = ["spargebra/sep-0006"]
rules = ["spargebra/rules", "fixed-point"]
[dependencies]
oxrdf = { version = "0.1.5", path="../oxrdf" }
rand = "0.8"
spargebra = { version = "0.2.8-dev", path="../spargebra" }
[package.metadata.docs.rs]
all-features = true

@ -0,0 +1,32 @@
sparopt
=========
[![Latest Version](https://img.shields.io/crates/v/sparopt.svg)](https://crates.io/crates/sparopt)
[![Released API docs](https://docs.rs/sparopt/badge.svg)](https://docs.rs/sparopt)
[![Crates.io downloads](https://img.shields.io/crates/d/sparopt)](https://crates.io/crates/sparopt)
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions)
[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
sparopt is a work in progress [SPARQL Query](https://www.w3.org/TR/sparql11-query/) optimizer.
It relies on the output of [spargebra](https://crates.io/crates/spargebra).
Support for [SPARQL-star](https://w3c.github.io/rdf-star/cg-spec/#sparql-star) is also available behind the `rdf-star` feature.
This crate is intended to be a building piece for SPARQL implementations in Rust like [Oxigraph](https://oxigraph.org).
## License
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or
`<http://www.apache.org/licenses/LICENSE-2.0>`)
* MIT license ([LICENSE-MIT](../LICENSE-MIT) or
`<http://opensource.org/licenses/MIT>`)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

File diff suppressed because it is too large Load Diff

@ -0,0 +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,195 @@
use crate::algebra::{Expression, FixedPointGraphPattern, GraphPattern};
#[derive(Default)]
pub struct Optimizer {}
impl Optimizer {
pub fn optimize(&mut self, pattern: GraphPattern) -> GraphPattern {
Self::normalize_pattern(pattern)
}
fn normalize_pattern(pattern: GraphPattern) -> GraphPattern {
match pattern {
GraphPattern::QuadPattern {
subject,
predicate,
object,
graph_name,
} => GraphPattern::QuadPattern {
subject,
predicate,
object,
graph_name,
},
GraphPattern::Path {
subject,
path,
object,
graph_name,
} => GraphPattern::Path {
subject,
path,
object,
graph_name,
},
GraphPattern::Join { left, right } => GraphPattern::join(
Self::normalize_pattern(*left),
Self::normalize_pattern(*right),
),
GraphPattern::LeftJoin {
left,
right,
expression,
} => GraphPattern::left_join(
Self::normalize_pattern(*left),
Self::normalize_pattern(*right),
Self::normalize_expression(expression),
),
#[cfg(feature = "sep-0006")]
GraphPattern::Lateral { left, right } => GraphPattern::lateral(
Self::normalize_pattern(*left),
Self::normalize_pattern(*right),
),
GraphPattern::Filter { inner, expression } => GraphPattern::filter(
Self::normalize_pattern(*inner),
Self::normalize_expression(expression),
),
GraphPattern::Union { inner } => inner
.into_iter()
.map(Self::normalize_pattern)
.reduce(GraphPattern::union)
.unwrap_or_else(GraphPattern::empty),
GraphPattern::Extend {
inner,
variable,
expression,
} => GraphPattern::extend(Self::normalize_pattern(*inner), variable, expression),
GraphPattern::Minus { left, right } => GraphPattern::minus(
Self::normalize_pattern(*left),
Self::normalize_pattern(*right),
),
GraphPattern::Values {
variables,
bindings,
} => GraphPattern::values(variables, bindings),
GraphPattern::OrderBy { inner, expression } => {
GraphPattern::order_by(Self::normalize_pattern(*inner), expression)
}
GraphPattern::Project { inner, variables } => {
GraphPattern::project(Self::normalize_pattern(*inner), variables)
}
GraphPattern::Distinct { inner } => {
GraphPattern::distinct(Self::normalize_pattern(*inner))
}
GraphPattern::Reduced { inner } => {
GraphPattern::reduced(Self::normalize_pattern(*inner))
}
GraphPattern::Slice {
inner,
start,
length,
} => GraphPattern::slice(Self::normalize_pattern(*inner), start, length),
GraphPattern::Group {
inner,
variables,
aggregates,
} => GraphPattern::group(Self::normalize_pattern(*inner), variables, aggregates),
GraphPattern::Service {
name,
inner,
silent,
} => GraphPattern::service(Self::normalize_pattern(*inner), name, silent),
#[cfg(feature = "fixed-point")]
GraphPattern::FixedPoint {
id,
variables,
constant,
recursive,
} => {
GraphPattern::fixed_point(
id,
FixedPointGraphPattern::union(*constant, *recursive),
variables,
)
//TODO: recursive normalization
}
}
}
fn normalize_expression(expression: Expression) -> Expression {
match expression {
Expression::NamedNode(node) => node.into(),
Expression::Literal(literal) => literal.into(),
Expression::Variable(variable) => variable.into(),
Expression::Or(left, right) => Expression::or(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::And(left, right) => Expression::and(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Equal(left, right) => Expression::equal(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::SameTerm(left, right) => Expression::same_term(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Greater(left, right) => Expression::greater(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::GreaterOrEqual(left, right) => Expression::greater_or_equal(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Less(left, right) => Expression::less(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::LessOrEqual(left, right) => Expression::less_or_equal(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Add(left, right) => Expression::add(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Subtract(left, right) => Expression::subtract(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Multiply(left, right) => Expression::multiply(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::Divide(left, right) => Expression::divide(
Self::normalize_expression(*left),
Self::normalize_expression(*right),
),
Expression::UnaryPlus(inner) => {
Expression::unary_plus(Self::normalize_expression(*inner))
}
Expression::UnaryMinus(inner) => {
Expression::unary_minus(Self::normalize_expression(*inner))
}
Expression::Not(inner) => Expression::not(Self::normalize_expression(*inner)),
Expression::Exists(inner) => Expression::exists(Self::normalize_pattern(*inner)),
Expression::Bound(variable) => Expression::Bound(variable),
Expression::If(cond, then, els) => Expression::if_cond(
Self::normalize_expression(*cond),
Self::normalize_expression(*then),
Self::normalize_expression(*els),
),
Expression::Coalesce(inners) => {
Expression::coalesce(inners.into_iter().map(Self::normalize_expression).collect())
}
Expression::FunctionCall(name, args) => Expression::call(
name,
args.into_iter().map(Self::normalize_expression).collect(),
),
}
}
}

File diff suppressed because it is too large Load Diff

@ -181,6 +181,56 @@ impl<'a> TryFrom<&'a String> for Update {
}
}
/// A parsed rule set.
#[derive(Eq, PartialEq, Debug, Clone, Hash, Default)]
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 {

@ -36,6 +36,8 @@ const REGEX_SIZE_LIMIT: usize = 1_000_000;
type EncodedTuplesIterator = Box<dyn Iterator<Item = Result<EncodedTuple, EvaluationError>>>;
type CustomFunctionRegistry = HashMap<NamedNode, Rc<dyn Fn(&[Term]) -> Option<Term>>>;
type FixedPointEvaluationFn =
Rc<dyn Fn(EncodedTuple, HashMap<usize, Rc<Vec<EncodedTuple>>>) -> EncodedTuplesIterator>;
#[derive(Clone)]
pub struct SimpleEvaluator {
@ -144,6 +146,7 @@ impl SimpleEvaluator {
Rc<PlanNodeWithStats>,
) {
let mut stat_children = Vec::new();
let mut fixed_point_stat_children = Vec::new();
let mut evaluator: Rc<dyn Fn(EncodedTuple) -> EncodedTuplesIterator> = match node.as_ref() {
PlanNode::StaticBindings { encoded_tuples, .. } => {
let tuples = encoded_tuples.clone();
@ -817,10 +820,26 @@ impl SimpleEvaluator {
)
})
}
PlanNode::FixedPoint {
id,
constant,
recursive,
} => {
let (constant, constant_stats) = self.fixed_point_plan_evaluator(constant.clone());
fixed_point_stat_children.push(constant_stats);
let (recursive, recursive_stats) =
self.fixed_point_plan_evaluator(recursive.clone());
fixed_point_stat_children.push(recursive_stats);
let id = *id;
Rc::new(move |from| {
evaluate_fixed_point(id, &constant, &recursive, &from, &HashMap::new())
})
}
};
let stats = Rc::new(PlanNodeWithStats {
node,
children: stat_children,
fixed_point_children: fixed_point_stat_children,
exec_count: Cell::new(0),
exec_duration: Cell::new(std::time::Duration::from_secs(0)),
});
@ -841,6 +860,258 @@ impl SimpleEvaluator {
(evaluator, stats)
}
pub fn fixed_point_plan_evaluator(
&self,
node: Rc<FixedPointPlanNode>,
) -> (FixedPointEvaluationFn, Rc<FixedPointPlanNodeWithStats>) {
let mut stat_children = Vec::new();
let mut evaluator: Rc<
dyn Fn(EncodedTuple, HashMap<usize, Rc<Vec<EncodedTuple>>>) -> EncodedTuplesIterator,
> = match node.as_ref() {
FixedPointPlanNode::StaticBindings { encoded_tuples, .. } => {
let tuples = encoded_tuples.clone();
Rc::new(move |from, _| {
Box::new(
tuples
.iter()
.filter_map(move |t| Some(Ok(t.combine_with(&from)?)))
.collect::<Vec<_>>()
.into_iter(),
)
})
}
FixedPointPlanNode::QuadPattern {
subject,
predicate,
object,
graph_name,
} => {
let subject = TupleSelector::from(subject);
let predicate = TupleSelector::from(predicate);
let object = TupleSelector::from(object);
let graph_name = TupleSelector::from(graph_name);
let dataset = self.dataset.clone();
Rc::new(move |from, _| {
let iter = dataset.encoded_quads_for_pattern(
get_pattern_value(&subject, &from).as_ref(),
get_pattern_value(&predicate, &from).as_ref(),
get_pattern_value(&object, &from).as_ref(),
get_pattern_value(&graph_name, &from).as_ref(),
);
let subject = subject.clone();
let predicate = predicate.clone();
let object = object.clone();
let graph_name = graph_name.clone();
Box::new(iter.filter_map(move |quad| match quad {
Ok(quad) => {
let mut new_tuple = from.clone();
put_pattern_value(&subject, quad.subject, &mut new_tuple)?;
put_pattern_value(&predicate, quad.predicate, &mut new_tuple)?;
put_pattern_value(&object, quad.object, &mut new_tuple)?;
put_pattern_value(&graph_name, quad.graph_name, &mut new_tuple)?;
Some(Ok(new_tuple))
}
Err(error) => Some(Err(error)),
}))
})
}
FixedPointPlanNode::HashJoin { left, right } => {
let join_keys: Vec<_> = left
.used_variables()
.intersection(&right.used_variables())
.copied()
.collect();
let (left, left_stats) = self.fixed_point_plan_evaluator(left.clone());
stat_children.push(left_stats);
let (right, right_stats) = self.fixed_point_plan_evaluator(right.clone());
stat_children.push(right_stats);
if join_keys.is_empty() {
// Cartesian product
Rc::new(move |from, fixed_point_values| {
let mut errors = Vec::default();
let right_values = right(from.clone(), fixed_point_values.clone())
.filter_map(|result| match result {
Ok(result) => Some(result),
Err(error) => {
errors.push(Err(error));
None
}
})
.collect::<Vec<_>>();
Box::new(CartesianProductJoinIterator {
left_iter: left(from, fixed_point_values),
right: right_values,
buffered_results: errors,
})
})
} else {
// Real hash join
Rc::new(move |from, fixed_point_values| {
let mut errors = Vec::default();
let mut right_values = EncodedTupleSet::new(join_keys.clone());
right_values.extend(
right(from.clone(), fixed_point_values.clone()).filter_map(|result| {
match result {
Ok(result) => Some(result),
Err(error) => {
errors.push(Err(error));
None
}
}
}),
);
Box::new(HashJoinIterator {
left_iter: left(from, fixed_point_values),
right: right_values,
buffered_results: errors,
})
})
}
}
FixedPointPlanNode::Filter { child, expression } => {
let (child, child_stats) = self.fixed_point_plan_evaluator(child.clone());
stat_children.push(child_stats);
let expression = self.expression_evaluator(expression);
Rc::new(move |from, fixed_point_values| {
let expression = expression.clone();
Box::new(child(from, fixed_point_values).filter(move |tuple| {
match tuple {
Ok(tuple) => expression(tuple)
.and_then(|term| to_bool(&term))
.unwrap_or(false),
Err(_) => true,
}
}))
})
}
FixedPointPlanNode::Union { children } => {
let children: Vec<_> = children
.iter()
.map(|child| {
let (child, child_stats) = self.fixed_point_plan_evaluator(child.clone());
stat_children.push(child_stats);
child
})
.collect();
Rc::new(move |from, fixed_point_values| {
Box::new(FixedPointUnionIterator {
plans: children.clone(),
input: (from, fixed_point_values),
current_iterator: Box::new(empty()),
current_plan: 0,
})
})
}
FixedPointPlanNode::Extend {
child,
variable,
expression,
} => {
let (child, child_stats) = self.fixed_point_plan_evaluator(child.clone());
stat_children.push(child_stats);
let position = variable.encoded;
let expression = self.expression_evaluator(expression);
Rc::new(move |from, fixed_point_values| {
let expression = expression.clone();
Box::new(child(from, fixed_point_values).map(move |tuple| {
let mut tuple = tuple?;
if let Some(value) = expression(&tuple) {
tuple.set(position, value);
}
Ok(tuple)
}))
})
}
FixedPointPlanNode::Project { child, mapping } => {
let (child, child_stats) = self.fixed_point_plan_evaluator(child.clone());
stat_children.push(child_stats);
let mapping = mapping.clone();
Rc::new(move |from, fixed_point_values| {
let mapping = mapping.clone();
let mut input_tuple = EncodedTuple::with_capacity(mapping.len());
for (input_key, output_key) in mapping.iter() {
if let Some(value) = from.get(output_key.encoded) {
input_tuple.set(input_key.encoded, value.clone());
}
}
Box::new(
child(input_tuple, fixed_point_values).filter_map(move |tuple| {
match tuple {
Ok(tuple) => {
let mut output_tuple = from.clone();
for (input_key, output_key) in mapping.iter() {
if let Some(value) = tuple.get(input_key.encoded) {
if let Some(existing_value) =
output_tuple.get(output_key.encoded)
{
if existing_value != value {
return None; // Conflict
}
} else {
output_tuple.set(output_key.encoded, value.clone());
}
}
}
Some(Ok(output_tuple))
}
Err(e) => Some(Err(e)),
}
}),
)
})
}
FixedPointPlanNode::FixedPoint {
id,
constant,
recursive,
} => {
let (constant, constant_stats) = self.fixed_point_plan_evaluator(constant.clone());
stat_children.push(constant_stats);
let (recursive, recursive_stats) =
self.fixed_point_plan_evaluator(recursive.clone());
stat_children.push(recursive_stats);
let id = *id;
Rc::new(move |from, fixed_point_values| {
evaluate_fixed_point(id, &constant, &recursive, &from, &fixed_point_values)
})
}
FixedPointPlanNode::FixedPointEntry { id, .. } => {
let id = *id;
Rc::new(move |from, fixed_point_values| {
Box::new(
fixed_point_values[&id]
.as_ref()
.clone()
.into_iter()
.filter_map(move |t| t.combine_with(&from))
.map(Ok),
)
})
}
};
let stats = Rc::new(FixedPointPlanNodeWithStats {
node,
children: stat_children,
exec_count: Cell::new(0),
exec_duration: Cell::new(std::time::Duration::from_secs(0)),
});
if self.run_stats {
let stats = stats.clone();
evaluator = Rc::new(move |tuple, fixed_point_values| {
let start = Timer::now();
let inner = evaluator(tuple, fixed_point_values);
stats
.exec_duration
.set(stats.exec_duration.get() + start.elapsed());
Box::new(FixedPointStatsIterator {
inner,
stats: stats.clone(),
})
})
}
(evaluator, stats)
}
fn evaluate_service(
&self,
service_name: &PatternValue,
@ -3883,6 +4154,52 @@ impl PathEvaluator {
}
}
fn evaluate_fixed_point(
id: usize,
constant: &FixedPointEvaluationFn,
recursive: &FixedPointEvaluationFn,
from: &EncodedTuple,
fixed_point_values: &HashMap<usize, Rc<Vec<EncodedTuple>>>,
) -> EncodedTuplesIterator {
// Naive algorithm. We should at least be semi-naive
let mut errors = Vec::new();
let mut all_results = constant(from.clone(), HashMap::new())
.filter_map(|result| match result {
Ok(result) => Some(result),
Err(error) => {
errors.push(error);
None
}
})
.collect::<HashSet<_>>();
let mut new_set = all_results.iter().cloned().collect::<Vec<_>>();
while !new_set.is_empty() {
let mut fixed_point_values = fixed_point_values.clone();
fixed_point_values.insert(id, Rc::new(all_results.iter().cloned().collect()));
new_set = recursive(from.clone(), fixed_point_values)
.filter_map(|result| match result {
Ok(result) => {
if all_results.insert(result.clone()) {
Some(result)
} else {
None
}
}
Err(error) => {
errors.push(error);
None
}
})
.collect();
}
Box::new(
errors
.into_iter()
.map(Err)
.chain(all_results.into_iter().map(Ok)),
)
}
struct CartesianProductJoinIterator {
left_iter: EncodedTuplesIterator,
right: Vec<EncodedTuple>,
@ -4109,6 +4426,31 @@ impl Iterator for UnionIterator {
}
}
struct FixedPointUnionIterator {
plans: Vec<FixedPointEvaluationFn>,
input: (EncodedTuple, HashMap<usize, Rc<Vec<EncodedTuple>>>),
current_iterator: EncodedTuplesIterator,
current_plan: usize,
}
impl Iterator for FixedPointUnionIterator {
type Item = Result<EncodedTuple, EvaluationError>;
fn next(&mut self) -> Option<Result<EncodedTuple, EvaluationError>> {
loop {
if let Some(tuple) = self.current_iterator.next() {
return Some(tuple);
}
if self.current_plan >= self.plans.len() {
return None;
}
self.current_iterator =
self.plans[self.current_plan](self.input.0.clone(), self.input.1.clone());
self.current_plan += 1;
}
}
}
struct ConsecutiveDeduplication {
inner: EncodedTuplesIterator,
current: Option<EncodedTuple>,
@ -4802,6 +5144,27 @@ impl Iterator for StatsIterator {
}
}
struct FixedPointStatsIterator {
inner: EncodedTuplesIterator,
stats: Rc<FixedPointPlanNodeWithStats>,
}
impl Iterator for FixedPointStatsIterator {
type Item = Result<EncodedTuple, EvaluationError>;
fn next(&mut self) -> Option<Result<EncodedTuple, EvaluationError>> {
let start = Timer::now();
let result = self.inner.next();
self.stats
.exec_duration
.set(self.stats.exec_duration.get() + start.elapsed());
if matches!(result, Some(Ok(_))) {
self.stats.exec_count.set(self.stats.exec_count.get() + 1);
}
result
}
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
pub struct Timer {
timestamp_ms: f64,

@ -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.

@ -107,6 +107,11 @@ pub enum PlanNode {
key_variables: Rc<Vec<PlanVariable>>,
aggregates: Rc<Vec<(PlanAggregation, PlanVariable)>>,
},
FixedPoint {
id: usize,
constant: Rc<FixedPointPlanNode>,
recursive: Rc<FixedPointPlanNode>,
},
}
impl PlanNode {
@ -218,6 +223,14 @@ impl PlanNode {
callback(var.encoded);
}
}
Self::FixedPoint {
constant,
recursive,
..
} => {
constant.lookup_used_variables(callback);
recursive.lookup_used_variables(callback);
}
}
}
@ -336,7 +349,7 @@ impl PlanNode {
}
}
}
Self::Aggregate { .. } => {
Self::Aggregate { .. } | Self::FixedPoint { .. } => {
//TODO
}
}
@ -353,6 +366,130 @@ impl PlanNode {
}
}
#[derive(Debug)]
pub enum FixedPointPlanNode {
StaticBindings {
encoded_tuples: Vec<EncodedTuple>,
variables: Vec<PlanVariable>,
plain_bindings: Vec<Vec<Option<GroundTerm>>>,
},
QuadPattern {
subject: PatternValue,
predicate: PatternValue,
object: PatternValue,
graph_name: PatternValue,
},
/// Streams left and materializes right join
HashJoin {
left: Rc<Self>,
right: Rc<Self>,
},
Filter {
child: Rc<Self>,
expression: PlanExpression,
},
Union {
children: Vec<Rc<Self>>,
},
Extend {
child: Rc<Self>,
variable: PlanVariable,
expression: PlanExpression,
},
Project {
child: Rc<Self>,
mapping: Rc<Vec<(PlanVariable, PlanVariable)>>, // pairs of (variable key in child, variable key in output)
},
FixedPoint {
id: usize,
constant: Rc<Self>,
recursive: Rc<Self>,
},
FixedPointEntry {
id: usize,
variables: Vec<PlanVariable>,
},
}
impl FixedPointPlanNode {
/// Returns variables that might be bound in the result set
pub fn used_variables(&self) -> BTreeSet<usize> {
let mut set = BTreeSet::default();
self.lookup_used_variables(&mut |v| {
set.insert(v);
});
set
}
pub fn lookup_used_variables(&self, callback: &mut impl FnMut(usize)) {
match self {
Self::StaticBindings { encoded_tuples, .. } => {
for tuple in encoded_tuples {
for (key, value) in tuple.iter().enumerate() {
if value.is_some() {
callback(key);
}
}
}
}
Self::QuadPattern {
subject,
predicate,
object,
graph_name,
} => {
subject.lookup_variables(callback);
predicate.lookup_variables(callback);
object.lookup_variables(callback);
graph_name.lookup_variables(callback);
}
Self::Filter { child, expression } => {
expression.lookup_used_variables(callback);
child.lookup_used_variables(callback);
}
Self::Union { children } => {
for child in children.iter() {
child.lookup_used_variables(callback);
}
}
Self::HashJoin { left, right } => {
left.lookup_used_variables(callback);
right.lookup_used_variables(callback);
}
Self::Extend {
child,
variable,
expression,
} => {
callback(variable.encoded);
expression.lookup_used_variables(callback);
child.lookup_used_variables(callback);
}
Self::Project { mapping, child } => {
let child_bound = child.used_variables();
for (child_i, output_i) in mapping.iter() {
if child_bound.contains(&child_i.encoded) {
callback(output_i.encoded);
}
}
}
Self::FixedPoint {
constant,
recursive,
..
} => {
constant.lookup_used_variables(callback);
recursive.lookup_used_variables(callback);
}
Self::FixedPointEntry { variables, .. } => {
for variable in variables {
callback(variable.encoded);
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PlanTerm<T> {
pub encoded: EncodedTerm,
@ -1005,7 +1142,8 @@ impl IntoIterator for EncodedTuple {
pub struct PlanNodeWithStats {
pub node: Rc<PlanNode>,
pub children: Vec<Rc<PlanNodeWithStats>>,
pub children: Vec<Rc<Self>>,
pub fixed_point_children: Vec<Rc<FixedPointPlanNodeWithStats>>,
pub exec_count: Cell<usize>,
pub exec_duration: Cell<Duration>,
}
@ -1032,6 +1170,9 @@ impl PlanNodeWithStats {
for child in &self.children {
child.json_node(writer, with_stats)?;
}
for child in &self.fixed_point_children {
child.json_node(writer, with_stats)?;
}
writer.write_event(JsonEvent::EndArray)?;
writer.write_event(JsonEvent::EndObject)
}
@ -1123,11 +1264,110 @@ impl PlanNodeWithStats {
)
}
PlanNode::Union { .. } => "Union".to_owned(),
PlanNode::FixedPoint { id, .. } => format!("FixedPoint{id}"),
}
}
}
impl fmt::Debug for PlanNodeWithStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut obj = f.debug_struct("Node");
obj.field("name", &self.node_label());
if self.exec_duration.get() > Duration::default() {
obj.field("number of results", &self.exec_count.get());
obj.field("duration in seconds", &self.exec_duration.get());
}
if !self.children.is_empty() {
obj.field("children", &self.children);
}
if !self.fixed_point_children.is_empty() {
obj.field("children", &self.fixed_point_children);
}
obj.finish()
}
}
pub struct FixedPointPlanNodeWithStats {
pub node: Rc<FixedPointPlanNode>,
pub children: Vec<Rc<Self>>,
pub exec_count: Cell<usize>,
pub exec_duration: Cell<Duration>,
}
impl FixedPointPlanNodeWithStats {
pub fn json_node(
&self,
writer: &mut JsonWriter<impl io::Write>,
with_stats: bool,
) -> io::Result<()> {
writer.write_event(JsonEvent::StartObject)?;
writer.write_event(JsonEvent::ObjectKey("name"))?;
writer.write_event(JsonEvent::String(&self.node_label()))?;
if with_stats {
writer.write_event(JsonEvent::ObjectKey("number of results"))?;
writer.write_event(JsonEvent::Number(&self.exec_count.get().to_string()))?;
writer.write_event(JsonEvent::ObjectKey("duration in seconds"))?;
writer.write_event(JsonEvent::Number(
&self.exec_duration.get().as_secs_f32().to_string(),
))?;
}
writer.write_event(JsonEvent::ObjectKey("children"))?;
writer.write_event(JsonEvent::StartArray)?;
for child in &self.children {
child.json_node(writer, with_stats)?;
}
writer.write_event(JsonEvent::EndArray)?;
writer.write_event(JsonEvent::EndObject)
}
fn node_label(&self) -> String {
match self.node.as_ref() {
FixedPointPlanNode::Extend {
expression,
variable,
..
} => format!("Extend({expression} -> {variable})"),
FixedPointPlanNode::Filter { expression, .. } => format!("Filter({expression})"),
FixedPointPlanNode::HashJoin { .. } => "HashJoin".to_owned(),
FixedPointPlanNode::Project { mapping, .. } => {
format!(
"Project({})",
mapping
.iter()
.map(|(f, t)| if f.plain == t.plain {
f.to_string()
} else {
format!("{f} -> {t}")
})
.collect::<Vec<_>>()
.join(", ")
)
}
FixedPointPlanNode::QuadPattern {
subject,
predicate,
object,
graph_name,
} => format!("QuadPattern({subject} {predicate} {object} {graph_name})"),
FixedPointPlanNode::StaticBindings { variables, .. } => {
format!(
"StaticBindings({})",
variables
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
FixedPointPlanNode::Union { .. } => "Union".to_owned(),
FixedPointPlanNode::FixedPoint { id, .. } => format!("FixedPoint{id}"),
FixedPointPlanNode::FixedPointEntry { id, .. } => format!("FixedPointEntry{id}"),
}
}
}
impl fmt::Debug for FixedPointPlanNodeWithStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut obj = f.debug_struct("Node");
obj.field("name", &self.node_label());

File diff suppressed because it is too large Load Diff

@ -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,48 @@
@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_recursion
) .
: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> .
:simple_recursion rdf:type ox:SparqlRuleEvaluationTest ;
mf:name "Simple recursion" ;
mf:action
[ qt:query <simple_recursion.rq> ;
qt:data <simple_recursion.ttl> ] ;
ox:rulesData <simple_recursion.rr> ;
mf:result <simple_recursion.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 DISTINCT * WHERE {
?s ex:includedIn ?o
}

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

@ -0,0 +1,69 @@
{
"head": {
"vars": ["s", "o"]
},
"results": {
"bindings": [
{
"s": {
"type": "uri",
"value": "http://example.org/Bar"
},
"o": {
"type": "uri",
"value": "http://example.org/Foo"
}
},
{
"s": {
"type": "uri",
"value": "http://example.org/Baz"
},
"o": {
"type": "uri",
"value": "http://example.org/Foo"
}
},
{
"s": {
"type": "uri",
"value": "http://example.org/Baz"
},
"o": {
"type": "uri",
"value": "http://example.org/Bar"
}
},
{
"s": {
"type": "uri",
"value": "http://example.org/Baa"
},
"o": {
"type": "uri",
"value": "http://example.org/Foo"
}
},
{
"s": {
"type": "uri",
"value": "http://example.org/Baa"
},
"o": {
"type": "uri",
"value": "http://example.org/Bar"
}
},
{
"s": {
"type": "uri",
"value": "http://example.org/Baa"
},
"o": {
"type": "uri",
"value": "http://example.org/Baz"
}
}
]
}
}

@ -0,0 +1,4 @@
PREFIX ex: <http://example.org/>
ex:Bar ex:includedIn ex:Foo .
ex:Baz ex:includedIn ex:Bar .
ex:Baa ex:includedIn ex:Baz .

@ -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<()> {
@ -148,6 +152,12 @@ fn result_syntax_check(test: &Test, format: QueryResultsFormat) -> Result<()> {
}
fn evaluate_evaluation_test(test: &Test) -> Result<()> {
enum Mode {
Base,
WithoutOptimizer,
WithInferenceRules,
}
let store = Store::new()?;
if let Some(data) = &test.data {
load_dataset_to_store(data, &store)?;
@ -194,11 +204,12 @@ fn evaluate_evaluation_test(test: &Test) -> Result<()> {
false
};
for with_query_optimizer in [true, false] {
let mut options = options.clone();
if !with_query_optimizer {
options = options.without_optimizations();
}
for mode in [Mode::Base, Mode::WithoutOptimizer, Mode::WithInferenceRules] {
let options = match mode {
Mode::Base => options.clone(),
Mode::WithoutOptimizer => options.clone().without_optimizations(),
Mode::WithInferenceRules => options.clone().with_inference_rules(RuleSet::default()),
};
let actual_results = store
.query_opt(query.clone(), options)
.map_err(|e| anyhow!("Failure to execute query of {test} with error: {e}"))?;
@ -206,7 +217,12 @@ fn evaluate_evaluation_test(test: &Test) -> Result<()> {
if !are_query_results_isomorphic(&expected_results, &actual_results) {
bail!(
"Failure on {test}.\n{}\nParsed query:\n{}\nData:\n{store}\n",
"Failure{} on {test}.\n{}\nParsed query:\n{}\nData:\n{store}\n",
match mode {
Mode::Base => "",
Mode::WithoutOptimizer => " without optimizer",
Mode::WithInferenceRules => " with inference rules",
},
results_diff(expected_results, actual_results),
Query::parse(&read_file_to_string(query_file)?, Some(query_file)).unwrap()
);
@ -286,6 +302,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",
])
}

@ -67,6 +67,7 @@ fn sparql10_w3c_query_evaluation_testsuite() -> Result<()> {
// We choose to simplify first the nested group patterns in OPTIONAL
"http://www.w3.org/2001/sw/DataAccess/tests/data-r2/optional-filter/manifest#dawg-optional-filter-005-not-simplified",
// This test relies on naive iteration on the input file
"http://www.w3.org/2001/sw/DataAccess/tests/data-r2/reduced/manifest#reduced-1",
"http://www.w3.org/2001/sw/DataAccess/tests/data-r2/reduced/manifest#reduced-2"
])
}

Loading…
Cancel
Save