Adds a naive standalone query optimizer

This drops some left join optimizations
pull/553/head
Tpt 2 years ago committed by Thomas Tanon
parent 7c0563cb1b
commit 40b10cdabc
  1. 6
      .github/workflows/tests.yml
  2. 12
      Cargo.lock
  3. 1
      Cargo.toml
  4. 1
      lib/Cargo.toml
  5. 24
      lib/spargebra/src/term.rs
  6. 28
      lib/sparopt/Cargo.toml
  7. 33
      lib/sparopt/README.md
  8. 1712
      lib/sparopt/src/algebra.rs
  9. 5
      lib/sparopt/src/lib.rs
  10. 1022
      lib/sparopt/src/optimizer.rs
  11. 451
      lib/sparopt/src/type_inference.rs
  12. 137
      lib/src/sparql/eval.rs
  13. 1
      lib/src/sparql/mod.rs
  14. 26
      lib/src/sparql/plan.rs
  15. 1225
      lib/src/sparql/plan_builder.rs
  16. 2
      testsuite/Cargo.toml
  17. 9
      testsuite/oxigraph-tests/sparql-optimization/bgp_join_reordering_input.rq
  18. 10
      testsuite/oxigraph-tests/sparql-optimization/bgp_join_reordering_output.rq
  19. 4
      testsuite/oxigraph-tests/sparql-optimization/bind_always_false_input.rq
  20. 3
      testsuite/oxigraph-tests/sparql-optimization/bind_always_false_output.rq
  21. 4
      testsuite/oxigraph-tests/sparql-optimization/bind_always_true_input.rq
  22. 3
      testsuite/oxigraph-tests/sparql-optimization/bind_always_true_output.rq
  23. 3
      testsuite/oxigraph-tests/sparql-optimization/empty_union_input.rq
  24. 3
      testsuite/oxigraph-tests/sparql-optimization/empty_union_output.rq
  25. 5
      testsuite/oxigraph-tests/sparql-optimization/equal_to_same_term_input.rq
  26. 5
      testsuite/oxigraph-tests/sparql-optimization/equal_to_same_term_output.rq
  27. 4
      testsuite/oxigraph-tests/sparql-optimization/exists_always_false_input.rq
  28. 3
      testsuite/oxigraph-tests/sparql-optimization/exists_always_false_output.rq
  29. 4
      testsuite/oxigraph-tests/sparql-optimization/false_and_something_input.rq
  30. 3
      testsuite/oxigraph-tests/sparql-optimization/false_and_something_output.rq
  31. 4
      testsuite/oxigraph-tests/sparql-optimization/false_or_something_input.rq
  32. 4
      testsuite/oxigraph-tests/sparql-optimization/false_or_something_output.rq
  33. 4
      testsuite/oxigraph-tests/sparql-optimization/if_always_false_input.rq
  34. 4
      testsuite/oxigraph-tests/sparql-optimization/if_always_false_output.rq
  35. 4
      testsuite/oxigraph-tests/sparql-optimization/if_always_true_input.rq
  36. 4
      testsuite/oxigraph-tests/sparql-optimization/if_always_true_output.rq
  37. 132
      testsuite/oxigraph-tests/sparql-optimization/manifest.ttl
  38. 11
      testsuite/oxigraph-tests/sparql-optimization/push_filter_input.rq
  39. 18
      testsuite/oxigraph-tests/sparql-optimization/push_filter_output.rq
  40. 5
      testsuite/oxigraph-tests/sparql-optimization/push_optional_filter_input.rq
  41. 5
      testsuite/oxigraph-tests/sparql-optimization/push_optional_filter_output.rq
  42. 4
      testsuite/oxigraph-tests/sparql-optimization/something_and_false_input.rq
  43. 3
      testsuite/oxigraph-tests/sparql-optimization/something_and_false_output.rq
  44. 4
      testsuite/oxigraph-tests/sparql-optimization/something_and_true_input.rq
  45. 4
      testsuite/oxigraph-tests/sparql-optimization/something_and_true_output.rq
  46. 4
      testsuite/oxigraph-tests/sparql-optimization/something_or_false_input.rq
  47. 4
      testsuite/oxigraph-tests/sparql-optimization/something_or_false_output.rq
  48. 4
      testsuite/oxigraph-tests/sparql-optimization/something_or_true_input.rq
  49. 3
      testsuite/oxigraph-tests/sparql-optimization/something_or_true_output.rq
  50. 4
      testsuite/oxigraph-tests/sparql-optimization/true_and_something_input.rq
  51. 4
      testsuite/oxigraph-tests/sparql-optimization/true_and_something_output.rq
  52. 4
      testsuite/oxigraph-tests/sparql-optimization/true_or_something_input.rq
  53. 3
      testsuite/oxigraph-tests/sparql-optimization/true_or_something_output.rq
  54. 3
      testsuite/oxigraph-tests/sparql-optimization/unbound_bind_input.rq
  55. 3
      testsuite/oxigraph-tests/sparql-optimization/unbound_bind_output.rq
  56. 4
      testsuite/oxigraph-tests/sparql-optimization/unbound_filter_input.rq
  57. 3
      testsuite/oxigraph-tests/sparql-optimization/unbound_filter_output.rq
  58. 56
      testsuite/src/sparql_evaluator.rs
  59. 17
      testsuite/tests/oxigraph.rs
  60. 1
      testsuite/tests/sparql.rs

@ -36,6 +36,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:
@ -76,6 +78,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
@ -119,7 +123,7 @@ jobs:
- run: rustup update
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-semver-checks || true
- run: cargo semver-checks check-release --exclude oxrocksdb-sys --exclude oxigraph_js --exclude pyoxigraph --exclude oxigraph_testsuite --exclude oxigraph_server
- run: cargo semver-checks check-release --exclude oxrocksdb-sys --exclude oxigraph_js --exclude pyoxigraph --exclude oxigraph_testsuite --exclude oxigraph_server --exclude sparopt
test_linux:
runs-on: ubuntu-latest

12
Cargo.lock generated

@ -958,6 +958,7 @@ dependencies = [
"siphasher",
"sparesults",
"spargebra",
"sparopt",
"zstd",
]
@ -998,6 +999,8 @@ dependencies = [
"anyhow",
"clap",
"oxigraph",
"spargebra",
"sparopt",
"text-diff",
"time",
]
@ -1613,6 +1616,15 @@ dependencies = [
"rand",
]
[[package]]
name = "sparopt"
version = "0.1.0-alpha.1-dev"
dependencies = [
"oxrdf",
"rand",
"spargebra",
]
[[package]]
name = "sparql-smith"
version = "0.1.0-alpha.5-dev"

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

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

@ -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 = ();
@ -799,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 = ();

@ -0,0 +1,28 @@
[package]
name = "sparopt"
version = "0.1.0-alpha.1-dev"
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 = []
rdf-star = ["oxrdf/rdf-star", "spargebra/rdf-star"]
sep-0002 = ["spargebra/sep-0002"]
sep-0006 = ["spargebra/sep-0006"]
[dependencies]
oxrdf = { version = "0.2.0-alpha.1-dev", path="../oxrdf" }
rand = "0.8"
spargebra = { version = "0.3.0-alpha.1-dev", path="../spargebra" }
[package.metadata.docs.rs]
all-features = true

@ -0,0 +1,33 @@
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/2021-12-17.html#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,5 @@
pub use crate::optimizer::Optimizer;
pub mod algebra;
mod optimizer;
mod type_inference;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,451 @@
use crate::algebra::{Expression, GraphPattern};
use oxrdf::Variable;
use spargebra::algebra::Function;
use spargebra::term::{GroundTerm, GroundTermPattern, NamedNodePattern};
use std::collections::HashMap;
use std::ops::{BitAnd, BitOr};
pub fn infer_graph_pattern_types(
pattern: &GraphPattern,
mut types: VariableTypes,
) -> VariableTypes {
match pattern {
GraphPattern::QuadPattern {
subject,
predicate,
object,
graph_name,
} => {
add_ground_term_pattern_types(subject, &mut types, false);
if let NamedNodePattern::Variable(v) = predicate {
types.intersect_variable_with(v.clone(), VariableType::NAMED_NODE)
}
add_ground_term_pattern_types(object, &mut types, true);
if let Some(NamedNodePattern::Variable(v)) = graph_name {
types.intersect_variable_with(v.clone(), VariableType::NAMED_NODE)
}
types
}
GraphPattern::Path {
subject,
object,
graph_name,
..
} => {
add_ground_term_pattern_types(subject, &mut types, false);
add_ground_term_pattern_types(object, &mut types, true);
if let Some(NamedNodePattern::Variable(v)) = graph_name {
types.intersect_variable_with(v.clone(), VariableType::NAMED_NODE)
}
types
}
GraphPattern::Join { left, right, .. } => {
let mut output_types = infer_graph_pattern_types(left, types.clone());
output_types.intersect_with(infer_graph_pattern_types(right, types));
output_types
}
#[cfg(feature = "sep-0006")]
GraphPattern::Lateral { left, right } => {
infer_graph_pattern_types(right, infer_graph_pattern_types(left, types))
}
GraphPattern::LeftJoin { left, right, .. } => {
let mut right_types = infer_graph_pattern_types(right, types.clone()); //TODO: expression
for t in right_types.inner.values_mut() {
t.undef = true; // Right might be unset
}
let mut output_types = infer_graph_pattern_types(left, types);
output_types.intersect_with(right_types);
output_types
}
GraphPattern::Minus { left, .. } => infer_graph_pattern_types(left, types),
GraphPattern::Union { inner } => inner
.iter()
.map(|inner| infer_graph_pattern_types(inner, types.clone()))
.reduce(|mut a, b| {
a.union_with(b);
a
})
.unwrap_or_default(),
GraphPattern::Extend {
inner,
variable,
expression,
} => {
let mut types = infer_graph_pattern_types(inner, types);
types.intersect_variable_with(
variable.clone(),
infer_expression_type(expression, &types),
);
types
}
GraphPattern::Filter { inner, .. } => infer_graph_pattern_types(inner, types),
GraphPattern::Project { inner, variables } => VariableTypes {
inner: infer_graph_pattern_types(inner, types)
.inner
.into_iter()
.filter(|(v, _)| variables.contains(v))
.collect(),
},
GraphPattern::Distinct { inner }
| GraphPattern::Reduced { inner }
| GraphPattern::OrderBy { inner, .. }
| GraphPattern::Slice { inner, .. } => infer_graph_pattern_types(inner, types),
GraphPattern::Group {
inner,
variables,
aggregates,
} => {
let types = infer_graph_pattern_types(inner, types);
VariableTypes {
inner: infer_graph_pattern_types(inner, types)
.inner
.into_iter()
.filter(|(v, _)| variables.contains(v))
.chain(aggregates.iter().map(|(v, _)| (v.clone(), VariableType::ANY))) //TODO: guess from aggregate
.collect(),
}
}
GraphPattern::Values {
variables,
bindings,
} => {
for (i, v) in variables.iter().enumerate() {
let mut t = VariableType::default();
for binding in bindings {
match binding[i] {
Some(GroundTerm::NamedNode(_)) => t.named_node = true,
Some(GroundTerm::Literal(_)) => t.literal = true,
#[cfg(feature = "rdf-star")]
Some(GroundTerm::Triple(_)) => t.triple = true,
None => t.undef = true,
}
}
types.intersect_variable_with(v.clone(), t)
}
types
}
GraphPattern::Service { name, inner, .. } => {
let mut types = infer_graph_pattern_types(inner, types);
if let NamedNodePattern::Variable(v) = name {
types.intersect_variable_with(v.clone(), VariableType::NAMED_NODE)
}
types
}
}
}
fn add_ground_term_pattern_types(
pattern: &GroundTermPattern,
types: &mut VariableTypes,
is_object: bool,
) {
if let GroundTermPattern::Variable(v) = pattern {
types.intersect_variable_with(
v.clone(),
if is_object {
VariableType::TERM
} else {
VariableType::SUBJECT
},
)
}
#[cfg(feature = "rdf-star")]
if let GroundTermPattern::Triple(t) = pattern {
add_ground_term_pattern_types(&t.subject, types, false);
if let NamedNodePattern::Variable(v) = &t.predicate {
types.intersect_variable_with(v.clone(), VariableType::NAMED_NODE)
}
add_ground_term_pattern_types(&t.object, types, true);
}
}
pub fn infer_expression_type(expression: &Expression, types: &VariableTypes) -> VariableType {
match expression {
Expression::NamedNode(_) => VariableType::NAMED_NODE,
Expression::Literal(_) | Expression::Exists(_) | Expression::Bound(_) => {
VariableType::LITERAL
}
Expression::Variable(v) => types.get(v),
Expression::FunctionCall(Function::Datatype | Function::Iri, _) => {
VariableType::NAMED_NODE | VariableType::UNDEF
}
#[cfg(feature = "rdf-star")]
Expression::FunctionCall(Function::Predicate, _) => {
VariableType::NAMED_NODE | VariableType::UNDEF
}
Expression::FunctionCall(Function::BNode, _) => {
VariableType::BLANK_NODE | VariableType::UNDEF
}
Expression::Or(_)
| Expression::And(_)
| Expression::Equal(_, _)
| Expression::Greater(_, _)
| Expression::GreaterOrEqual(_, _)
| Expression::Less(_, _)
| Expression::LessOrEqual(_, _)
| Expression::Add(_, _)
| Expression::Subtract(_, _)
| Expression::Multiply(_, _)
| Expression::Divide(_, _)
| Expression::UnaryPlus(_)
| Expression::UnaryMinus(_)
| Expression::Not(_)
| Expression::FunctionCall(
Function::Str
| Function::Lang
| Function::LangMatches
| Function::Rand
| Function::Abs
| Function::Ceil
| Function::Floor
| Function::Round
| Function::Concat
| Function::SubStr
| Function::StrLen
| Function::Replace
| Function::UCase
| Function::LCase
| Function::EncodeForUri
| Function::Contains
| Function::StrStarts
| Function::StrEnds
| Function::StrBefore
| Function::StrAfter
| Function::Year
| Function::Month
| Function::Day
| Function::Hours
| Function::Minutes
| Function::Seconds
| Function::Timezone
| Function::Tz
| Function::Now
| Function::Uuid
| Function::StrUuid
| Function::Md5
| Function::Sha1
| Function::Sha256
| Function::Sha384
| Function::Sha512
| Function::StrLang
| Function::StrDt
| Function::IsIri
| Function::IsBlank
| Function::IsLiteral
| Function::IsNumeric
| Function::Regex,
_,
) => VariableType::LITERAL | VariableType::UNDEF,
#[cfg(feature = "sep-0002")]
Expression::FunctionCall(Function::Adjust, _) => {
VariableType::LITERAL | VariableType::UNDEF
}
#[cfg(feature = "rdf-star")]
Expression::FunctionCall(Function::IsTriple, _) => {
VariableType::LITERAL | VariableType::UNDEF
}
Expression::SameTerm(left, right) => {
if infer_expression_type(left, types).undef || infer_expression_type(right, types).undef
{
VariableType::LITERAL | VariableType::UNDEF
} else {
VariableType::LITERAL
}
}
Expression::If(_, then, els) => {
infer_expression_type(then, types) | infer_expression_type(els, types)
}
Expression::Coalesce(inner) => {
let mut t = VariableType::UNDEF;
for e in inner {
let new = infer_expression_type(e, types);
t = t | new;
if !new.undef {
t.undef = false;
return t;
}
}
t
}
#[cfg(feature = "rdf-star")]
Expression::FunctionCall(Function::Triple, _) => VariableType::TRIPLE | VariableType::UNDEF,
#[cfg(feature = "rdf-star")]
Expression::FunctionCall(Function::Subject, _) => {
VariableType::SUBJECT | VariableType::UNDEF
}
#[cfg(feature = "rdf-star")]
Expression::FunctionCall(Function::Object, _) => VariableType::TERM | VariableType::UNDEF,
Expression::FunctionCall(Function::Custom(_), _) => VariableType::ANY,
}
}
#[derive(Default, Clone, Debug)]
pub struct VariableTypes {
inner: HashMap<Variable, VariableType>,
}
impl VariableTypes {
pub fn get(&self, variable: &Variable) -> VariableType {
self.inner
.get(variable)
.copied()
.unwrap_or(VariableType::UNDEF)
}
pub fn iter(&self) -> impl Iterator<Item = (&Variable, &VariableType)> {
self.inner.iter()
}
pub fn intersect_with(&mut self, other: Self) {
for (v, t) in other.inner {
self.intersect_variable_with(v, t);
}
}
pub fn union_with(&mut self, other: Self) {
for (v, t) in &mut self.inner {
if other.get(v).undef {
t.undef = true; // Might be undefined
}
}
for (v, mut t) in other.inner {
self.inner
.entry(v)
.and_modify(|ex| *ex = *ex | t)
.or_insert({
t.undef = true;
t
});
}
}
fn intersect_variable_with(&mut self, variable: Variable, t: VariableType) {
let t = self.get(&variable) & t;
if t != VariableType::UNDEF {
self.inner.insert(variable, t);
}
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
pub struct VariableType {
pub undef: bool,
pub named_node: bool,
pub blank_node: bool,
pub literal: bool,
#[cfg(feature = "rdf-star")]
pub triple: bool,
}
impl VariableType {
pub const UNDEF: Self = Self {
undef: true,
named_node: false,
blank_node: false,
literal: false,
#[cfg(feature = "rdf-star")]
triple: false,
};
const NAMED_NODE: Self = Self {
undef: false,
named_node: true,
blank_node: false,
literal: false,
#[cfg(feature = "rdf-star")]
triple: false,
};
const BLANK_NODE: Self = Self {
undef: false,
named_node: false,
blank_node: true,
literal: false,
#[cfg(feature = "rdf-star")]
triple: false,
};
const LITERAL: Self = Self {
undef: false,
named_node: false,
blank_node: false,
literal: true,
#[cfg(feature = "rdf-star")]
triple: false,
};
#[cfg(feature = "rdf-star")]
const TRIPLE: Self = Self {
undef: false,
named_node: false,
blank_node: false,
literal: false,
triple: true,
};
const SUBJECT: Self = Self {
undef: false,
named_node: true,
blank_node: true,
literal: false,
#[cfg(feature = "rdf-star")]
triple: true,
};
const TERM: Self = Self {
undef: false,
named_node: true,
blank_node: true,
literal: true,
#[cfg(feature = "rdf-star")]
triple: true,
};
const ANY: Self = Self {
undef: true,
named_node: true,
blank_node: true,
literal: true,
#[cfg(feature = "rdf-star")]
triple: true,
};
}
impl BitOr for VariableType {
type Output = Self;
fn bitor(self, other: Self) -> Self {
Self {
undef: self.undef || other.undef,
named_node: self.named_node || other.named_node,
blank_node: self.blank_node || other.blank_node,
literal: self.literal || other.literal,
#[cfg(feature = "rdf-star")]
triple: self.triple || other.triple,
}
}
}
impl BitAnd for VariableType {
type Output = Self;
#[allow(clippy::nonminimal_bool)]
fn bitand(self, other: Self) -> Self {
Self {
undef: self.undef && other.undef,
named_node: self.named_node && other.named_node
|| (self.undef && other.named_node)
|| (self.named_node && other.undef),
blank_node: self.blank_node && other.blank_node
|| (self.undef && other.blank_node)
|| (self.blank_node && other.undef),
literal: self.literal && other.literal
|| (self.undef && other.literal)
|| (self.literal && other.undef),
#[cfg(feature = "rdf-star")]
triple: self.triple && other.triple
|| (self.undef && other.triple)
|| (self.triple && other.undef),
}
}
}

@ -379,21 +379,24 @@ impl SimpleEvaluator {
}
})
}
PlanNode::HashJoin { left, right } => {
let join_keys: Vec<_> = left
PlanNode::HashJoin {
probe_child,
build_child,
} => {
let join_keys: Vec<_> = probe_child
.always_bound_variables()
.intersection(&right.always_bound_variables())
.intersection(&build_child.always_bound_variables())
.copied()
.collect();
let (left, left_stats) = self.plan_evaluator(Rc::clone(left));
stat_children.push(left_stats);
let (right, right_stats) = self.plan_evaluator(Rc::clone(right));
stat_children.push(right_stats);
let (probe, probe_stats) = self.plan_evaluator(Rc::clone(probe_child));
stat_children.push(probe_stats);
let (build, build_stats) = self.plan_evaluator(Rc::clone(build_child));
stat_children.push(build_stats);
if join_keys.is_empty() {
// Cartesian product
Rc::new(move |from| {
let mut errors = Vec::default();
let right_values = right(from.clone())
let build_values = build(from.clone())
.filter_map(|result| match result {
Ok(result) => Some(result),
Err(error) => {
@ -403,8 +406,8 @@ impl SimpleEvaluator {
})
.collect::<Vec<_>>();
Box::new(CartesianProductJoinIterator {
left_iter: left(from),
right: right_values,
probe_iter: probe(from),
built: build_values,
buffered_results: errors,
})
})
@ -412,8 +415,8 @@ impl SimpleEvaluator {
// Real hash join
Rc::new(move |from| {
let mut errors = Vec::default();
let mut right_values = EncodedTupleSet::new(join_keys.clone());
right_values.extend(right(from.clone()).filter_map(
let mut built_values = EncodedTupleSet::new(join_keys.clone());
built_values.extend(build(from.clone()).filter_map(
|result| match result {
Ok(result) => Some(result),
Err(error) => {
@ -423,8 +426,8 @@ impl SimpleEvaluator {
},
));
Box::new(HashJoinIterator {
left_iter: left(from),
right: right_values,
probe_iter: probe(from),
built: built_values,
buffered_results: errors,
})
})
@ -516,33 +519,17 @@ impl SimpleEvaluator {
})
})
}
PlanNode::ForLoopLeftJoin {
left,
right,
possible_problem_vars,
} => {
PlanNode::ForLoopLeftJoin { left, right } => {
let (left, left_stats) = self.plan_evaluator(Rc::clone(left));
stat_children.push(left_stats);
let (right, right_stats) = self.plan_evaluator(Rc::clone(right));
stat_children.push(right_stats);
let possible_problem_vars = Rc::clone(possible_problem_vars);
Rc::new(move |from| {
if possible_problem_vars.is_empty() {
Box::new(ForLoopLeftJoinIterator {
right_evaluator: Rc::clone(&right),
left_iter: left(from),
current_right: Box::new(empty()),
})
} else {
Box::new(BadForLoopLeftJoinIterator {
from_tuple: from.clone(),
right_evaluator: Rc::clone(&right),
left_iter: left(from),
current_left: EncodedTuple::with_capacity(0),
current_right: Box::new(empty()),
problem_vars: Rc::clone(&possible_problem_vars),
})
}
})
}
PlanNode::Filter { child, expression } => {
@ -3887,8 +3874,8 @@ impl PathEvaluator {
}
struct CartesianProductJoinIterator {
left_iter: EncodedTuplesIterator,
right: Vec<EncodedTuple>,
probe_iter: EncodedTuplesIterator,
built: Vec<EncodedTuple>,
buffered_results: Vec<Result<EncodedTuple, EvaluationError>>,
}
@ -3900,12 +3887,12 @@ impl Iterator for CartesianProductJoinIterator {
if let Some(result) = self.buffered_results.pop() {
return Some(result);
}
let left_tuple = match self.left_iter.next()? {
Ok(left_tuple) => left_tuple,
let probe_tuple = match self.probe_iter.next()? {
Ok(probe_tuple) => probe_tuple,
Err(error) => return Some(Err(error)),
};
for right_tuple in &self.right {
if let Some(result_tuple) = left_tuple.combine_with(right_tuple) {
for built_tuple in &self.built {
if let Some(result_tuple) = probe_tuple.combine_with(built_tuple) {
self.buffered_results.push(Ok(result_tuple))
}
}
@ -3913,17 +3900,17 @@ impl Iterator for CartesianProductJoinIterator {
}
fn size_hint(&self) -> (usize, Option<usize>) {
let (min, max) = self.left_iter.size_hint();
let (min, max) = self.probe_iter.size_hint();
(
min.saturating_mul(self.right.len()),
max.map(|v| v.saturating_mul(self.right.len())),
min.saturating_mul(self.built.len()),
max.map(|v| v.saturating_mul(self.built.len())),
)
}
}
struct HashJoinIterator {
left_iter: EncodedTuplesIterator,
right: EncodedTupleSet,
probe_iter: EncodedTuplesIterator,
built: EncodedTupleSet,
buffered_results: Vec<Result<EncodedTuple, EvaluationError>>,
}
@ -3935,15 +3922,15 @@ impl Iterator for HashJoinIterator {
if let Some(result) = self.buffered_results.pop() {
return Some(result);
}
let left_tuple = match self.left_iter.next()? {
Ok(left_tuple) => left_tuple,
let probe_tuple = match self.probe_iter.next()? {
Ok(probe_tuple) => probe_tuple,
Err(error) => return Some(Err(error)),
};
self.buffered_results.extend(
self.right
.get(&left_tuple)
self.built
.get(&probe_tuple)
.iter()
.filter_map(|right_tuple| left_tuple.combine_with(right_tuple).map(Ok)),
.filter_map(|built_tuple| probe_tuple.combine_with(built_tuple).map(Ok)),
)
}
}
@ -3951,10 +3938,10 @@ impl Iterator for HashJoinIterator {
fn size_hint(&self) -> (usize, Option<usize>) {
(
0,
self.left_iter
self.probe_iter
.size_hint()
.1
.map(|v| v.saturating_mul(self.right.len())),
.map(|v| v.saturating_mul(self.built.len())),
)
}
}
@ -4034,58 +4021,6 @@ impl Iterator for ForLoopLeftJoinIterator {
}
}
struct BadForLoopLeftJoinIterator {
from_tuple: EncodedTuple,
right_evaluator: Rc<dyn Fn(EncodedTuple) -> EncodedTuplesIterator>,
left_iter: EncodedTuplesIterator,
current_left: EncodedTuple,
current_right: EncodedTuplesIterator,
problem_vars: Rc<[usize]>,
}
impl Iterator for BadForLoopLeftJoinIterator {
type Item = Result<EncodedTuple, EvaluationError>;
fn next(&mut self) -> Option<Result<EncodedTuple, EvaluationError>> {
for right_tuple in &mut self.current_right {
match right_tuple {
Ok(right_tuple) => {
if let Some(combined) = right_tuple.combine_with(&self.current_left) {
return Some(Ok(combined));
}
}
Err(error) => return Some(Err(error)),
}
}
match self.left_iter.next()? {
Ok(left_tuple) => {
let mut right_input = self.from_tuple.clone();
for (var, val) in left_tuple.iter().enumerate() {
if let Some(val) = val {
if !self.problem_vars.contains(&var) {
right_input.set(var, val);
}
}
}
self.current_right = (self.right_evaluator)(right_input);
for right_tuple in &mut self.current_right {
match right_tuple {
Ok(right_tuple) => {
if let Some(combined) = right_tuple.combine_with(&left_tuple) {
self.current_left = left_tuple;
return Some(Ok(combined));
}
}
Err(error) => return Some(Err(error)),
}
}
Some(Ok(left_tuple))
}
Err(error) => Some(Err(error)),
}
}
}
struct UnionIterator {
plans: Vec<Rc<dyn Fn(EncodedTuple) -> EncodedTuplesIterator>>,
input: EncodedTuple,

@ -105,7 +105,6 @@ pub(crate) fn evaluate_query(
&template,
variables,
&options.custom_functions,
options.without_optimizations,
);
let planning_duration = start_planning.elapsed();
let (results, explanation) = SimpleEvaluator::new(

@ -41,8 +41,8 @@ pub enum PlanNode {
},
/// Streams left and materializes right join
HashJoin {
left: Rc<Self>,
right: Rc<Self>,
probe_child: Rc<Self>,
build_child: Rc<Self>,
},
/// Right nested in left loop
ForLoopJoin {
@ -71,7 +71,6 @@ pub enum PlanNode {
ForLoopLeftJoin {
left: Rc<Self>,
right: Rc<Self>,
possible_problem_vars: Rc<[usize]>, //Variables that should not be part of the entry of the left join
},
Extend {
child: Rc<Self>,
@ -160,7 +159,10 @@ impl PlanNode {
child.lookup_used_variables(callback);
}
}
Self::HashJoin { left, right }
Self::HashJoin {
probe_child: left,
build_child: right,
}
| Self::ForLoopJoin { left, right, .. }
| Self::AntiJoin { left, right }
| Self::ForLoopLeftJoin { left, right, .. } => {
@ -296,7 +298,11 @@ impl PlanNode {
}
}
}
Self::HashJoin { left, right } | Self::ForLoopJoin { left, right, .. } => {
Self::HashJoin {
probe_child: left,
build_child: right,
}
| Self::ForLoopJoin { left, right, .. } => {
left.lookup_always_bound_variables(callback);
right.lookup_always_bound_variables(callback);
}
@ -344,16 +350,6 @@ impl PlanNode {
}
}
}
pub fn is_variable_bound(&self, variable: usize) -> bool {
let mut found = false;
self.lookup_always_bound_variables(&mut |v| {
if v == variable {
found = true;
}
});
found
}
}
#[derive(Debug, Clone)]

File diff suppressed because it is too large Load Diff

@ -16,4 +16,6 @@ anyhow = "1"
clap = { version = "4", features = ["derive"] }
time = { version = "0.3", features = ["formatting"] }
oxigraph = { path = "../lib" }
sparopt = { path = "../lib/sparopt" }
spargebra = { path = "../lib/spargebra" }
text-diff = "0.4"

@ -0,0 +1,9 @@
PREFIX ex: <http://example.com/>
SELECT ?s ?o WHERE {
?m2 ex:p2 ?o .
?s ex:p1 ?m1 , ?m2 .
?m1 ex:p2 ?o .
?s ex:p1prime ?m1 .
?s a ex:C .
}

@ -0,0 +1,10 @@
PREFIX ex: <http://example.com/>
SELECT ?s ?o WHERE {
?s a ex:C .
?s ex:p1 ?m1 .
?s ex:p1prime ?m1 .
?s ex:p1 ?m2 .
?m2 ex:p2 ?o .
?m1 ex:p2 ?o .
}

@ -0,0 +1,4 @@
SELECT ?a ?o WHERE {
?s ?p ?o .
FILTER(BOUND(?a))
}

@ -0,0 +1,4 @@
SELECT ?s WHERE {
?s ?p ?o .
FILTER(BOUND(?s))
}

@ -0,0 +1,3 @@
SELECT ?o WHERE {
{ ?s ?p ?o } UNION { VALUES () {} }
}

@ -0,0 +1,5 @@
SELECT ?s1 ?s2 ?o1 ?o2 WHERE {
?s1 ?p1 ?o1 .
?s2 ?p2 ?o2 .
FILTER(?p1 = ?p2)
}

@ -0,0 +1,5 @@
SELECT ?s1 ?s2 ?o1 ?o2 WHERE {
?s1 ?p1 ?o1 .
?s2 ?p2 ?o2 .
FILTER(sameTerm(?p2, ?p1))
}

@ -0,0 +1,4 @@
SELECT ?s WHERE {
?s ?p ?o .
FILTER(EXISTS { VALUES () {}})
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(false && ?o1 = ?o2)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(false || ?o1 = ?o2)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o2 = ?o1)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(IF(false, ?o1 = ?o2, ?o1 != ?o2))
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o2 != ?o1)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(IF(true, ?o1 = ?o2, ?o1 != ?o2))
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o2 = ?o1)
}

@ -0,0 +1,132 @@
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <https://github.com/oxigraph/oxigraph/tests/sparql-optimization/manifest#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
@prefix ox: <https://github.com/oxigraph/oxigraph/tests#> .
<> rdf:type mf:Manifest ;
rdfs:label "Oxigraph SPARQL optimization tests" ;
mf:entries
(
:unbound_filter
:unbound_bind
:something_or_true
:true_or_something
:something_or_false
:false_or_something
:something_and_true
:true_and_something
:something_and_false
:false_and_something
:equal_to_same_term
:bind_always_true
:bind_always_false
:if_always_true
:if_always_false
:exists_always_false
:push_filter
:push_optional_filter
:empty_union
:bgp_join_reordering
) .
:unbound_filter rdf:type ox:QueryOptimizationTest ;
mf:name "unbound variable in filter" ;
mf:action <unbound_filter_input.rq> ;
mf:result <unbound_filter_output.rq> .
:unbound_bind rdf:type ox:QueryOptimizationTest ;
mf:name "unbound variable in bindr" ;
mf:action <unbound_bind_input.rq> ;
mf:result <unbound_bind_output.rq> .
:something_or_true rdf:type ox:QueryOptimizationTest ;
mf:name "something || true" ;
mf:action <something_or_true_input.rq> ;
mf:result <something_or_true_output.rq> .
:true_or_something rdf:type ox:QueryOptimizationTest ;
mf:name "true || something" ;
mf:action <true_or_something_input.rq> ;
mf:result <true_or_something_output.rq> .
:something_or_false rdf:type ox:QueryOptimizationTest ;
mf:name "something || false" ;
mf:action <something_or_false_input.rq> ;
mf:result <something_or_false_output.rq> .
:false_or_something rdf:type ox:QueryOptimizationTest ;
mf:name "false || something" ;
mf:action <false_or_something_input.rq> ;
mf:result <false_or_something_output.rq> .
:something_and_true rdf:type ox:QueryOptimizationTest ;
mf:name "something && true" ;
mf:action <something_and_true_input.rq> ;
mf:result <something_and_true_output.rq> .
:true_and_something rdf:type ox:QueryOptimizationTest ;
mf:name "true && something" ;
mf:action <true_and_something_input.rq> ;
mf:result <true_and_something_output.rq> .
:something_and_false rdf:type ox:QueryOptimizationTest ;
mf:name "something && false" ;
mf:action <something_and_false_input.rq> ;
mf:result <something_and_false_output.rq> .
:false_and_something rdf:type ox:QueryOptimizationTest ;
mf:name "false && something" ;
mf:action <false_and_something_input.rq> ;
mf:result <false_and_something_output.rq> .
:equal_to_same_term a ox:QueryOptimizationTest ;
mf:name "equal to same term" ;
mf:action <equal_to_same_term_input.rq> ;
mf:result <equal_to_same_term_output.rq> .
:bind_always_true rdf:type ox:QueryOptimizationTest ;
mf:name "BIND() always true" ;
mf:action <bind_always_true_input.rq> ;
mf:result <bind_always_true_output.rq> .
:bind_always_false rdf:type ox:QueryOptimizationTest ;
mf:name "BIND() always false" ;
mf:action <bind_always_false_input.rq> ;
mf:result <bind_always_false_output.rq> .
:if_always_true rdf:type ox:QueryOptimizationTest ;
mf:name "IF() always true" ;
mf:action <if_always_true_input.rq> ;
mf:result <if_always_true_output.rq> .
:if_always_false rdf:type ox:QueryOptimizationTest ;
mf:name "IF() always false" ;
mf:action <if_always_false_input.rq> ;
mf:result <if_always_false_output.rq> .
:exists_always_false rdf:type ox:QueryOptimizationTest ;
mf:name "EXISTS {} always false" ;
mf:action <exists_always_false_input.rq> ;
mf:result <exists_always_false_output.rq> .
:push_filter rdf:type ox:QueryOptimizationTest ;
mf:name "push filter down" ;
mf:action <push_filter_input.rq> ;
mf:result <push_filter_output.rq> .
:push_optional_filter rdf:type ox:QueryOptimizationTest ;
mf:name "push OPTIONAL filter down" ;
mf:action <push_optional_filter_input.rq> ;
mf:result <push_optional_filter_output.rq> .
:empty_union rdf:type ox:QueryOptimizationTest ;
mf:name "empty UNION" ;
mf:action <empty_union_input.rq> ;
mf:result <empty_union_output.rq> .
:bgp_join_reordering rdf:type ox:QueryOptimizationTest ;
mf:name "BGP join reordering" ;
mf:action <bgp_join_reordering_input.rq> ;
mf:result <bgp_join_reordering_output.rq> .

@ -0,0 +1,11 @@
PREFIX : <http://example.com/>
SELECT ?o1 ?o2 ?o4 ?o5 WHERE {
?s :p1 ?o1 ; :p4 ?o4 ; :p5 ?o5 .
LATERAL { ?s :p2 ?o2 }
MINUS { ?s :p3 ?o3 }
FILTER(?o1 = 1)
FILTER(?o2 = 2)
FILTER(?o4 = 4)
FILTER(?o1 = ?o5)
}

@ -0,0 +1,18 @@
PREFIX : <http://example.com/>
SELECT ?o1 ?o2 ?o4 ?o5 WHERE {
{
{
{
{ ?s :p1 ?o1 FILTER(1 = ?o1) }
LATERAL { ?s :p4 ?o4 }
FILTER(?o4 = 4)
}
LATERAL { ?s :p5 ?o5 }
FILTER(?o5 = ?o1)
}
LATERAL { ?s :p2 ?o2 }
FILTER(?o2 = 2)
}
MINUS { ?s :p3 ?o3 }
}

@ -0,0 +1,5 @@
SELECT ?s ?o WHERE {
?s a ?t .
OPTIONAL { { ?s ?p ?o } UNION { ?s ?p ?o2 } FILTER(?o = 1) }
OPTIONAL { { ?s ?p ?o } UNION { ?s ?p2 ?o2 } FILTER(?o = ?t) }
}

@ -0,0 +1,5 @@
SELECT ?s ?o WHERE {
?s a ?t .
LATERAL { VALUES () {()} OPTIONAL { { ?s ?p ?o FILTER(1 = ?o) } UNION { ?s ?p ?o2 FILTER(1 = ?o) } } }
LATERAL { VALUES () {()} OPTIONAL { { ?s ?p ?o } UNION { ?s ?p2 ?o2 } FILTER(?t = ?o) } }
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o1 = ?o2 && false)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o1 = ?o2 && true)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o2 = ?o1)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o1 = ?o2 || false)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o2 = ?o1)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o1 = ?o2 || true)
}

@ -0,0 +1,3 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(true && ?o1 = ?o2)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(?o2 = ?o1)
}

@ -0,0 +1,4 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
FILTER(true || ?o1 = ?o2)
}

@ -0,0 +1,3 @@
SELECT ?o1 ?o2 WHERE {
?s ?p ?o1 , ?o2 .
}

@ -0,0 +1,4 @@
SELECT ?o WHERE {
?s ?p ?o .
FILTER(?a)
}

@ -8,6 +8,7 @@ use oxigraph::model::vocab::*;
use oxigraph::model::*;
use oxigraph::sparql::*;
use oxigraph::store::Store;
use sparopt::Optimizer;
use std::collections::HashMap;
use std::fmt::Write;
use std::io::{self, Cursor};
@ -67,6 +68,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#QueryOptimizationTest",
evaluate_query_optimization_test,
);
}
fn evaluate_positive_syntax_test(test: &Test) -> Result<()> {
@ -717,3 +722,54 @@ fn load_dataset_to_store(url: &str, store: &Store) -> Result<()> {
}?;
Ok(())
}
fn evaluate_query_optimization_test(test: &Test) -> Result<()> {
let action = test
.action
.as_deref()
.ok_or_else(|| anyhow!("No action found for test {test}"))?;
let actual = (&Optimizer::optimize_graph_pattern(
(&if let spargebra::Query::Select { pattern, .. } =
spargebra::Query::parse(&read_file_to_string(action)?, Some(action))?
{
pattern
} else {
bail!("Only SELECT queries are supported in query sparql-optimization tests")
})
.into(),
))
.into();
let result = test
.result
.as_ref()
.ok_or_else(|| anyhow!("No tests result found"))?;
let expected = if let spargebra::Query::Select { pattern, .. } =
spargebra::Query::parse(&read_file_to_string(result)?, Some(result))?
{
pattern
} else {
bail!("Only SELECT queries are supported in query sparql-optimization tests")
};
if expected == actual {
Ok(())
} else {
bail!(
"Failure on {test}.\nDiff:\n{}\n",
format_diff(
&spargebra::Query::Select {
pattern: expected,
dataset: None,
base_iri: None
}
.to_sse(),
&spargebra::Query::Select {
pattern: actual,
dataset: None,
base_iri: None
}
.to_sse(),
"query"
)
)
}
}

@ -3,10 +3,10 @@ use oxigraph_testsuite::evaluator::TestEvaluator;
use oxigraph_testsuite::manifest::TestManifest;
use oxigraph_testsuite::sparql_evaluator::register_sparql_tests;
fn run_testsuite(manifest_urls: Vec<&str>) -> Result<()> {
fn run_testsuite(manifest_url: &str) -> Result<()> {
let mut evaluator = TestEvaluator::default();
register_sparql_tests(&mut evaluator);
let manifest = TestManifest::new(manifest_urls);
let manifest = TestManifest::new([manifest_url]);
let results = evaluator.evaluate(manifest)?;
let mut errors = Vec::default();
@ -27,14 +27,15 @@ fn run_testsuite(manifest_urls: Vec<&str>) -> Result<()> {
#[test]
fn oxigraph_sparql_testsuite() -> Result<()> {
run_testsuite(vec![
"https://github.com/oxigraph/oxigraph/tests/sparql/manifest.ttl",
])
run_testsuite("https://github.com/oxigraph/oxigraph/tests/sparql/manifest.ttl")
}
#[test]
fn oxigraph_sparql_results_testsuite() -> Result<()> {
run_testsuite(vec![
"https://github.com/oxigraph/oxigraph/tests/sparql-results/manifest.ttl",
])
run_testsuite("https://github.com/oxigraph/oxigraph/tests/sparql-results/manifest.ttl")
}
#[test]
fn oxigraph_optimizer_testsuite() -> Result<()> {
run_testsuite("https://github.com/oxigraph/oxigraph/tests/sparql-optimization/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