From 76dd879ea697a4bea640c1d3ccb07da1347028e4 Mon Sep 17 00:00:00 2001 From: Tpt Date: Sun, 20 Nov 2022 17:57:33 +0100 Subject: [PATCH] Implements LATERAL join Follows SPARQL 1.2 SEP 0006. It is behind a sep-0006 feature in spargebra and sparql-smith and enabled by default in oxigraph. Apache Jena tests are passing. SEP 0006: https://github.com/w3c/sparql-12/blob/main/SEP/SEP-0006/sep-0006.md --- fuzz/Cargo.toml | 4 +- lib/Cargo.toml | 2 +- lib/spargebra/Cargo.toml | 1 + lib/spargebra/src/algebra.rs | 42 ++++++-- lib/spargebra/src/parser.rs | 85 +++++++++++++++- lib/sparql-smith/Cargo.toml | 4 + lib/sparql-smith/src/lib.rs | 16 +++ lib/src/sparql/eval.rs | 98 ++++++++----------- lib/src/sparql/plan.rs | 6 -- lib/src/sparql/plan_builder.rs | 21 +++- .../sparql/lateral/basic_input.ttl | 5 + .../oxigraph-tests/sparql/lateral/filter.rq | 6 ++ .../oxigraph-tests/sparql/lateral/graph.rq | 6 ++ .../oxigraph-tests/sparql/lateral/join.rq | 9 ++ .../sparql/lateral/manifest.ttl | 68 +++++++++++++ .../oxigraph-tests/sparql/lateral/optional.rq | 6 ++ .../oxigraph-tests/sparql/lateral/simple.srx | 17 ++++ .../sparql/lateral/subselect.rq | 6 ++ .../sparql/lateral/subselect.srx | 41 ++++++++ .../sparql/lateral/subselect_aggregate.rq | 6 ++ .../sparql/lateral/subselect_aggregate.srx | 25 +++++ .../lateral/subselect_inside_optional.rq | 6 ++ .../lateral/subselect_inside_optional.srx | 35 +++++++ .../lateral/subselect_outside_optional.rq | 6 ++ .../lateral/subselect_outside_optional.srx | 46 +++++++++ testsuite/oxigraph-tests/sparql/manifest.ttl | 1 + testsuite/tests/oxigraph.rs | 7 +- testsuite/tests/parser.rs | 7 +- testsuite/tests/sparql.rs | 7 +- 29 files changed, 504 insertions(+), 85 deletions(-) create mode 100644 testsuite/oxigraph-tests/sparql/lateral/basic_input.ttl create mode 100644 testsuite/oxigraph-tests/sparql/lateral/filter.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/graph.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/join.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/manifest.ttl create mode 100644 testsuite/oxigraph-tests/sparql/lateral/optional.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/simple.srx create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect.srx create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.srx create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.srx create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.rq create mode 100644 testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.srx diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2518ec4c..7b812526 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,9 +11,9 @@ cargo-fuzz = true [dependencies] lazy_static = "1" libfuzzer-sys = "0.4" -spargebra = { path = "../lib/spargebra", features = ["rdf-star"] } +spargebra = { path = "../lib/spargebra", features = ["rdf-star", "sep-0006"] } sparesults = { path = "../lib/sparesults", features = ["rdf-star"] } -sparql-smith = { path = "../lib/sparql-smith" } +sparql-smith = { path = "../lib/sparql-smith", features = ["sep-0006"] } oxigraph = { path = "../lib" } [workspace] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e99d39f6..91a75e10 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -39,7 +39,7 @@ lazy_static = "1" sysinfo = "0.27" oxrdf = { version = "0.1.1", path="oxrdf", features = ["rdf-star", "oxsdatatypes"] } oxsdatatypes = { version = "0.1.0", path="oxsdatatypes" } -spargebra = { version = "0.2.3", path="spargebra", features = ["rdf-star"] } +spargebra = { version = "0.2.3", path="spargebra", features = ["rdf-star", "sep-0006"] } sparesults = { version = "0.1.3", path="sparesults", features = ["rdf-star"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/lib/spargebra/Cargo.toml b/lib/spargebra/Cargo.toml index 775a6269..11b4aaa7 100644 --- a/lib/spargebra/Cargo.toml +++ b/lib/spargebra/Cargo.toml @@ -16,6 +16,7 @@ rust-version = "1.60" [features] default = [] rdf-star = ["oxrdf/rdf-star"] +sep-0006 = [] [dependencies] peg = "0.8" diff --git a/lib/spargebra/src/algebra.rs b/lib/spargebra/src/algebra.rs index dd1a2f71..65e1dd31 100644 --- a/lib/spargebra/src/algebra.rs +++ b/lib/spargebra/src/algebra.rs @@ -527,6 +527,9 @@ pub enum GraphPattern { right: Box, expression: Option, }, + /// Lateral join i.e. evaluate right for all result row of left + #[cfg(feature = "sep-0006")] + Lateral { left: Box, right: Box }, /// [Filter](https://www.w3.org/TR/sparql11-query/#defn_algFilter). Filter { expr: Expression, inner: Box }, /// [Union](https://www.w3.org/TR/sparql11-query/#defn_algUnion). @@ -629,6 +632,14 @@ impl GraphPattern { } write!(f, ")") } + #[cfg(feature = "sep-0006")] + Self::Lateral { left, right } => { + write!(f, "(lateral ")?; + left.fmt_sse(f)?; + write!(f, " ")?; + right.fmt_sse(f)?; + write!(f, ")") + } Self::Filter { expr, inner } => { write!(f, "(filter ")?; expr.fmt_sse(f)?; @@ -793,17 +804,19 @@ impl fmt::Display for GraphPattern { object, } => write!(f, "{subject} {path} {object} ."), Self::Join { left, right } => { - if matches!( - right.as_ref(), + match right.as_ref() { Self::LeftJoin { .. } - | Self::Minus { .. } - | Self::Extend { .. } - | Self::Filter { .. } - ) { - // The second block might be considered as a modification of the first one. - write!(f, "{left} {{ {right} }}") - } else { - write!(f, "{left} {right}") + | Self::Minus { .. } + | Self::Extend { .. } + | Self::Filter { .. } => { + // The second block might be considered as a modification of the first one. + write!(f, "{left} {{ {right} }}") + } + #[cfg(feature = "sep-0006")] + Self::Lateral { .. } => { + write!(f, "{left} {{ {right} }}") + } + _ => write!(f, "{left} {right}"), } } Self::LeftJoin { @@ -817,6 +830,10 @@ impl fmt::Display for GraphPattern { write!(f, "{left} OPTIONAL {{ {right} }}") } } + #[cfg(feature = "sep-0006")] + Self::Lateral { left, right } => { + write!(f, "{left} LATERAL {{ {right} }}") + } Self::Filter { expr, inner } => { write!(f, "{inner} FILTER({expr})") } @@ -940,6 +957,11 @@ impl GraphPattern { left.lookup_in_scope_variables(callback); right.lookup_in_scope_variables(callback); } + #[cfg(feature = "sep-0006")] + Self::Lateral { left, right } => { + left.lookup_in_scope_variables(callback); + right.lookup_in_scope_variables(callback); + } Self::Graph { name, inner } => { if let NamedNodePattern::Variable(ref g) = name { callback(g); diff --git a/lib/spargebra/src/parser.rs b/lib/spargebra/src/parser.rs index b7c8b3bd..51318863 100644 --- a/lib/spargebra/src/parser.rs +++ b/lib/spargebra/src/parser.rs @@ -360,6 +360,8 @@ impl> From> for FocusedTripleOrPathPattern #[derive(Eq, PartialEq, Debug, Clone, Hash)] enum PartialGraphPattern { Optional(GraphPattern, Option), + #[cfg(feature = "sep-0006")] + Lateral(GraphPattern), Minus(GraphPattern), Bind(Expression, Variable), Filter(Expression), @@ -620,6 +622,67 @@ fn are_variables_bound(expression: &Expression, variables: &HashSet) - } } +/// Called on every variable defined using "AS" or "VALUES" +#[cfg(feature = "sep-0006")] +fn add_defined_variables<'a>(pattern: &'a GraphPattern, set: &mut HashSet<&'a Variable>) { + match pattern { + GraphPattern::Bgp { .. } | GraphPattern::Path { .. } => {} + GraphPattern::Join { left, right } + | GraphPattern::LeftJoin { left, right, .. } + | GraphPattern::Lateral { left, right } + | GraphPattern::Union { left, right } + | GraphPattern::Minus { left, right } => { + add_defined_variables(left, set); + add_defined_variables(right, set); + } + GraphPattern::Graph { inner, .. } => { + add_defined_variables(inner, set); + } + GraphPattern::Extend { + inner, variable, .. + } => { + set.insert(variable); + add_defined_variables(inner, set); + } + GraphPattern::Group { + variables, + aggregates, + inner, + } => { + for (v, _) in aggregates { + set.insert(v); + } + let mut inner_variables = HashSet::new(); + add_defined_variables(inner, &mut inner_variables); + for v in inner_variables { + if variables.contains(v) { + set.insert(v); + } + } + } + GraphPattern::Values { variables, .. } => { + for v in variables { + set.insert(v); + } + } + GraphPattern::Project { variables, inner } => { + let mut inner_variables = HashSet::new(); + add_defined_variables(inner, &mut inner_variables); + for v in inner_variables { + if variables.contains(v) { + set.insert(v); + } + } + } + GraphPattern::Service { inner, .. } + | GraphPattern::Filter { inner, .. } + | GraphPattern::OrderBy { inner, .. } + | GraphPattern::Distinct { inner } + | GraphPattern::Reduced { inner } + | GraphPattern::Slice { inner, .. } => add_defined_variables(inner, set), + } +} + fn copy_graph(from: impl Into, to: impl Into) -> GraphUpdateOperation { let bgp = GraphPattern::Bgp { patterns: vec![TriplePattern::new( @@ -1355,6 +1418,21 @@ parser! { PartialGraphPattern::Optional(p, f) => { g = GraphPattern::LeftJoin { left: Box::new(g), right: Box::new(p), expression: f } } + #[cfg(feature = "sep-0006")] + PartialGraphPattern::Lateral(p) => { + let mut defined_variables = HashSet::default(); + add_defined_variables(&p, &mut defined_variables); + let mut contains = false; + g.on_in_scope_variable(|v| { + if defined_variables.contains(v) { + contains = true; + } + }); + if contains { + return Err("An existing variable is overriden in the right side of LATERAL"); + } + g = GraphPattern::Lateral { left: Box::new(g), right: Box::new(p) } + } PartialGraphPattern::Minus(p) => { g = GraphPattern::Minus { left: Box::new(g), right: Box::new(p) } } @@ -1400,7 +1478,7 @@ parser! { rule TriplesBlock_inner() -> Vec = _ h:TriplesSameSubjectPath() _ { h } //[56] - rule GraphPatternNotTriples() -> PartialGraphPattern = GroupOrUnionGraphPattern() / OptionalGraphPattern() / MinusGraphPattern() / GraphGraphPattern() / ServiceGraphPattern() / Filter() / Bind() / InlineData() + rule GraphPatternNotTriples() -> PartialGraphPattern = GroupOrUnionGraphPattern() / OptionalGraphPattern() / LateralGraphPattern() / MinusGraphPattern() / GraphGraphPattern() / ServiceGraphPattern() / Filter() / Bind() / InlineData() //[57] rule OptionalGraphPattern() -> PartialGraphPattern = i("OPTIONAL") _ p:GroupGraphPattern() { @@ -1411,6 +1489,11 @@ parser! { } } + rule LateralGraphPattern() -> PartialGraphPattern = i("LATERAL") _ p:GroupGraphPattern() {? + #[cfg(feature = "sep-0006")]{Ok(PartialGraphPattern::Lateral(p))} + #[cfg(not(feature = "sep-0006"))]{Err("The LATERAL modifier is not supported")} + } + //[58] rule GraphGraphPattern() -> PartialGraphPattern = i("GRAPH") _ name:VarOrIri() _ p:GroupGraphPattern() { PartialGraphPattern::Other(GraphPattern::Graph { name, inner: Box::new(p) }) diff --git a/lib/sparql-smith/Cargo.toml b/lib/sparql-smith/Cargo.toml index 68acc44b..8bb35e6b 100644 --- a/lib/sparql-smith/Cargo.toml +++ b/lib/sparql-smith/Cargo.toml @@ -12,5 +12,9 @@ A SPARQL test cases generator """ edition = "2021" +[features] +default = [] +sep-0006 = [] + [dependencies] arbitrary = { version = "1", features = ["derive"] } diff --git a/lib/sparql-smith/src/lib.rs b/lib/sparql-smith/src/lib.rs index 1c2e57ed..6ed3a4e1 100644 --- a/lib/sparql-smith/src/lib.rs +++ b/lib/sparql-smith/src/lib.rs @@ -436,6 +436,8 @@ enum GraphPatternNotTriples { Filter(Filter), Bind(Bind), InlineData(InlineData), // TODO: ServiceGraphPattern + #[cfg(feature = "sep-0006")] + Lateral(LateralGraphPattern), } impl fmt::Display for GraphPatternNotTriples { @@ -448,6 +450,8 @@ impl fmt::Display for GraphPatternNotTriples { Self::Filter(p) => write!(f, "{p}"), Self::Bind(p) => write!(f, "{p}"), Self::InlineData(p) => write!(f, "{p}"), + #[cfg(feature = "sep-0006")] + Self::Lateral(p) => write!(f, "{p}"), } } } @@ -464,6 +468,18 @@ impl fmt::Display for OptionalGraphPattern { } } +#[derive(Debug, Arbitrary)] +struct LateralGraphPattern { + // [] LateralGraphPattern ::= 'LATERAL' GroupGraphPattern + inner: GroupGraphPattern, +} + +impl fmt::Display for LateralGraphPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " LATERAL {}", self.inner) + } +} + #[derive(Debug, Arbitrary)] struct GraphGraphPattern { // [58] GraphGraphPattern ::= 'GRAPH' VarOrIri GroupGraphPattern diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index d476b64f..48cc10aa 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -492,7 +492,8 @@ impl SimpleEvaluator { current_right: Box::new(empty()), }) } else { - Box::new(BadLeftJoinIterator { + Box::new(BadForLoopLeftJoinIterator { + from_tuple: from.clone(), right_evaluator: right.clone(), left_iter: left(from), current_left: None, @@ -635,32 +636,33 @@ impl SimpleEvaluator { let mapping = mapping.clone(); Rc::new(move |from| { let mapping = mapping.clone(); - Box::new( - child(EncodedTuple::with_capacity(mapping.len())).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) { - if let Some(existing_value) = - output_tuple.get(*output_key) - { - if existing_value != value { - return None; // Conflict - } - } else { - output_tuple.set(*output_key, value.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) { + input_tuple.set(*input_key, value.clone()); + } + } + Box::new(child(input_tuple).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) { + if let Some(existing_value) = output_tuple.get(*output_key) + { + if existing_value != value { + return None; // Conflict } + } else { + output_tuple.set(*output_key, value.clone()); } - Some(Ok(output_tuple)) } - Err(e) => Some(Err(e)), } - }, - ), - ) + Some(Ok(output_tuple)) + } + Err(e) => Some(Err(e)), + } + })) }) } PlanNode::Aggregate { @@ -2943,27 +2945,6 @@ fn put_variable_value( } } -fn unbind_variables(binding: &mut EncodedTuple, variables: &[usize]) { - for var in variables { - binding.unset(*var) - } -} - -fn combine_tuples(mut a: EncodedTuple, b: &EncodedTuple, vars: &[usize]) -> Option { - for var in vars { - if let Some(b_value) = b.get(*var) { - if let Some(a_value) = a.get(*var) { - if a_value != b_value { - return None; - } - } else { - a.set(*var, b_value.clone()); - } - } - } - Some(a) -} - pub fn are_compatible_and_not_disjointed(a: &EncodedTuple, b: &EncodedTuple) -> bool { let mut found_intersection = false; for (a_value, b_value) in a.iter().zip(b.iter()) { @@ -3702,7 +3683,8 @@ impl Iterator for ForLoopLeftJoinIterator { } } -struct BadLeftJoinIterator { +struct BadForLoopLeftJoinIterator { + from_tuple: EncodedTuple, right_evaluator: Rc EncodedTuplesIterator>, left_iter: EncodedTuplesIterator, current_left: Option, @@ -3710,18 +3692,16 @@ struct BadLeftJoinIterator { problem_vars: Rc>, } -impl Iterator for BadLeftJoinIterator { +impl Iterator for BadForLoopLeftJoinIterator { type Item = Result; fn next(&mut self) -> Option> { for right_tuple in &mut self.current_right { match right_tuple { Ok(right_tuple) => { - if let Some(combined) = combine_tuples( - right_tuple, - self.current_left.as_ref().unwrap(), - &self.problem_vars, - ) { + if let Some(combined) = + right_tuple.combine_with(self.current_left.as_ref().unwrap()) + { return Some(Ok(combined)); } } @@ -3730,15 +3710,19 @@ impl Iterator for BadLeftJoinIterator { } match self.left_iter.next()? { Ok(left_tuple) => { - let mut filtered_left = left_tuple.clone(); - unbind_variables(&mut filtered_left, &self.problem_vars); - self.current_right = (self.right_evaluator)(filtered_left); + 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) = - combine_tuples(right_tuple, &left_tuple, &self.problem_vars) - { + if let Some(combined) = right_tuple.combine_with(&left_tuple) { self.current_left = Some(left_tuple); return Some(Ok(combined)); } diff --git a/lib/src/sparql/plan.rs b/lib/src/sparql/plan.rs index 696ae517..6f6d4a06 100644 --- a/lib/src/sparql/plan.rs +++ b/lib/src/sparql/plan.rs @@ -674,12 +674,6 @@ impl EncodedTuple { self.inner[index] = Some(value); } - pub fn unset(&mut self, index: usize) { - if let Some(v) = self.inner.get_mut(index) { - *v = None; - } - } - pub fn combine_with(&self, other: &Self) -> Option { if self.inner.len() < other.inner.len() { let mut result = other.inner.clone(); diff --git a/lib/src/sparql/plan_builder.rs b/lib/src/sparql/plan_builder.rs index 46d6d0c3..5d555202 100644 --- a/lib/src/sparql/plan_builder.rs +++ b/lib/src/sparql/plan_builder.rs @@ -133,6 +133,10 @@ impl<'a> PlanBuilder<'a> { } } } + GraphPattern::Lateral { left, right } => PlanNode::ForLoopJoin { + left: Box::new(self.build_for_graph_pattern(left, variables, graph_name)?), + right: Box::new(self.build_for_graph_pattern(right, variables, graph_name)?), + }, GraphPattern::Filter { expr, inner } => self.push_filter( Box::new(self.build_for_graph_pattern(inner, variables, graph_name)?), Box::new(self.build_for_expression(expr, variables, graph_name)?), @@ -1120,8 +1124,11 @@ impl<'a> PlanBuilder<'a> { right.lookup_used_variables(&mut |v| { set.insert(v); }); + let always_already_bound = left.always_bound_variables(); expression.lookup_used_variables(&mut |v| { - set.insert(v); + if !always_already_bound.contains(&v) { + set.insert(v); + } }); } PlanNode::Extend { @@ -1138,10 +1145,14 @@ impl<'a> PlanBuilder<'a> { } PlanNode::Sort { child, .. } | PlanNode::HashDeduplicate { child } - | PlanNode::Reduced { child } - | PlanNode::Skip { child, .. } - | PlanNode::Limit { child, .. } => { - Self::add_left_join_problematic_variables(child, set) + | PlanNode::Reduced { child } => { + Self::add_left_join_problematic_variables(child, set); + } + PlanNode::Skip { child, .. } | PlanNode::Limit { child, .. } => { + // Any variable might affect arity + child.lookup_used_variables(&mut |v| { + set.insert(v); + }) } PlanNode::Service { child, silent, .. } => { if *silent { diff --git a/testsuite/oxigraph-tests/sparql/lateral/basic_input.ttl b/testsuite/oxigraph-tests/sparql/lateral/basic_input.ttl new file mode 100644 index 00000000..c3c9fb07 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/basic_input.ttl @@ -0,0 +1,5 @@ +@prefix ex: . + +ex:s1 a ex:T ; ex:p 11 , 12 , 13 . +ex:s2 a ex:T ; ex:p 21 , 22 , 23 . +ex:s3 a ex:T . \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/filter.rq b/testsuite/oxigraph-tests/sparql/lateral/filter.rq new file mode 100644 index 00000000..8c2b22c0 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/filter.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + VALUES ?s { ex:S } + LATERAL { VALUES ?o { ex:O } FILTER(BOUND(?s) && EXISTS { FILTER(BOUND(?s)) }) } +} diff --git a/testsuite/oxigraph-tests/sparql/lateral/graph.rq b/testsuite/oxigraph-tests/sparql/lateral/graph.rq new file mode 100644 index 00000000..7e96a58d --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/graph.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + VALUES ?s { ex:S } + LATERAL { GRAPH ex:G { FILTER(BOUND(?s)) . VALUES ?o { ex:O } } } +} \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/join.rq b/testsuite/oxigraph-tests/sparql/lateral/join.rq new file mode 100644 index 00000000..5a60093f --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/join.rq @@ -0,0 +1,9 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + VALUES ?s { ex:S } + LATERAL { + { VALUES ?o { ex:O } } + { FILTER(BOUND(?s) && !BOUND(?o)) } + } +} diff --git a/testsuite/oxigraph-tests/sparql/lateral/manifest.ttl b/testsuite/oxigraph-tests/sparql/lateral/manifest.ttl new file mode 100644 index 00000000..d917fbc3 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/manifest.ttl @@ -0,0 +1,68 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix mf: . +@prefix qt: . +@prefix ut: . + +<> rdf:type mf:Manifest ; + rdfs:label "Oxigraph LATERAL feature SPARQL tests" ; + mf:entries + ( + :subselect + :subselect_inside_optional + :subselect_outside_optional + :subselect_aggregate + :optional + :graph + :filter + :join + ) . + +:subselect rdf:type mf:QueryEvaluationTest ; + mf:name "Basic subselect LATERAL test" ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result . + +:subselect_inside_optional rdf:type mf:QueryEvaluationTest ; + mf:name "Basic subselect LATERAL test inside OPTIONAL" ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result . + +:subselect_outside_optional rdf:type mf:QueryEvaluationTest ; + mf:name "Basic subselect test inside LATERAL OPTIONAL" ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result . + +:subselect_aggregate rdf:type mf:QueryEvaluationTest ; + mf:name "LATERAL test with explicit aggregate" ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result . + +:optional rdf:type mf:QueryEvaluationTest ; + mf:name "LATERAL OPTIONAL test" ; + mf:action [ qt:query ] ; + mf:result . + +:graph rdf:type mf:QueryEvaluationTest ; + mf:name "LATERAL GRAPH test" ; + mf:action [ qt:query ] ; + mf:result . + +:filter rdf:type mf:QueryEvaluationTest ; + mf:name "LATERAL FILTER test" ; + mf:action [ qt:query ] ; + mf:result . + +:join rdf:type mf:QueryEvaluationTest ; + mf:name "join in LATERAL test" ; + mf:action [ qt:query ] ; + mf:result . diff --git a/testsuite/oxigraph-tests/sparql/lateral/optional.rq b/testsuite/oxigraph-tests/sparql/lateral/optional.rq new file mode 100644 index 00000000..38344613 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/optional.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + VALUES ?s { ex:S } + LATERAL { OPTIONAL { FILTER(BOUND(?s)) . VALUES ?o { ex:O } } } +} \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/simple.srx b/testsuite/oxigraph-tests/sparql/lateral/simple.srx new file mode 100644 index 00000000..e0d761e3 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/simple.srx @@ -0,0 +1,17 @@ + + + + + + + + + + http://example.org/S + + + http://example.org/O + + + + \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect.rq b/testsuite/oxigraph-tests/sparql/lateral/subselect.rq new file mode 100644 index 00000000..6d1e4163 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + ?s a ex:T. + LATERAL {SELECT ?s ?o WHERE { ?s ex:p ?o } ORDER BY ?o LIMIT 2} +} \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect.srx b/testsuite/oxigraph-tests/sparql/lateral/subselect.srx new file mode 100644 index 00000000..6c9cf41f --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect.srx @@ -0,0 +1,41 @@ + + + + + + + + + + http://example.org/s1 + + + 11 + + + + + http://example.org/s1 + + + 12 + + + + + http://example.org/s2 + + + 21 + + + + + http://example.org/s2 + + + 22 + + + + \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.rq b/testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.rq new file mode 100644 index 00000000..17c5e9ce --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?c WHERE { + ?s a ex:T. + LATERAL {SELECT ?s (MAX(?o) AS ?c) WHERE { ?s ex:p ?o } GROUP BY ?s} +} \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.srx b/testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.srx new file mode 100644 index 00000000..8523de72 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.srx @@ -0,0 +1,25 @@ + + + + + + + + + + http://example.org/s1 + + + 13 + + + + + http://example.org/s2 + + + 23 + + + + \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.rq b/testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.rq new file mode 100644 index 00000000..0beebb82 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + ?s a ex:T. + OPTIONAL { LATERAL {SELECT ?s ?o WHERE { ?s ex:p ?o } ORDER BY ?o LIMIT 2} } +} \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.srx b/testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.srx new file mode 100644 index 00000000..437ce98a --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.srx @@ -0,0 +1,35 @@ + + + + + + + + + + http://example.org/s1 + + + 11 + + + + + http://example.org/s1 + + + 12 + + + + + http://example.org/s2 + + + + + http://example.org/s3 + + + + \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.rq b/testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.rq new file mode 100644 index 00000000..8b20515f --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.rq @@ -0,0 +1,6 @@ +PREFIX ex: + +SELECT ?s ?o WHERE { + ?s a ex:T. + LATERAL { OPTIONAL {SELECT ?s ?o WHERE { ?s ex:p ?o } ORDER BY ?o LIMIT 2} } +} \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.srx b/testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.srx new file mode 100644 index 00000000..11630eb9 --- /dev/null +++ b/testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.srx @@ -0,0 +1,46 @@ + + + + + + + + + + http://example.org/s1 + + + 11 + + + + + http://example.org/s1 + + + 12 + + + + + http://example.org/s2 + + + 21 + + + + + http://example.org/s2 + + + 22 + + + + + http://example.org/s3 + + + + \ No newline at end of file diff --git a/testsuite/oxigraph-tests/sparql/manifest.ttl b/testsuite/oxigraph-tests/sparql/manifest.ttl index 19e6fbc9..48deb671 100644 --- a/testsuite/oxigraph-tests/sparql/manifest.ttl +++ b/testsuite/oxigraph-tests/sparql/manifest.ttl @@ -7,6 +7,7 @@ <> rdf:type mf:Manifest ; rdfs:label "Oxigraph SPARQL tests" ; + mf:include ( ) ; mf:entries ( :small_unicode_escape_with_multibytes_char diff --git a/testsuite/tests/oxigraph.rs b/testsuite/tests/oxigraph.rs index e1722e8e..04360a9e 100644 --- a/testsuite/tests/oxigraph.rs +++ b/testsuite/tests/oxigraph.rs @@ -16,7 +16,12 @@ fn run_testsuite(manifest_urls: Vec<&str>) -> Result<()> { } } - assert!(errors.is_empty(), "\n{}\n", errors.join("\n")); + assert!( + errors.is_empty(), + "{} failing tests:\n{}\n", + errors.len(), + errors.join("\n") + ); Ok(()) } diff --git a/testsuite/tests/parser.rs b/testsuite/tests/parser.rs index f5f500d2..5a28e37f 100644 --- a/testsuite/tests/parser.rs +++ b/testsuite/tests/parser.rs @@ -16,7 +16,12 @@ fn run_testsuite(manifest_url: &str) -> Result<()> { } } - assert!(errors.is_empty(), "\n{}\n", errors.join("\n")); + assert!( + errors.is_empty(), + "{} failing tests:\n{}\n", + errors.len(), + errors.join("\n") + ); Ok(()) } diff --git a/testsuite/tests/sparql.rs b/testsuite/tests/sparql.rs index 11ef5a69..34bbecab 100644 --- a/testsuite/tests/sparql.rs +++ b/testsuite/tests/sparql.rs @@ -18,7 +18,12 @@ fn run_testsuite(manifest_url: &str, ignored_tests: Vec<&str>) -> Result<()> { } } - assert!(errors.is_empty(), "\n{}\n", errors.join("\n")); + assert!( + errors.is_empty(), + "{} failing tests:\n{}\n", + errors.len(), + errors.join("\n") + ); Ok(()) }