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
pull/341/head
Tpt 2 years ago committed by Thomas Tanon
parent 3f3523963d
commit 76dd879ea6
  1. 4
      fuzz/Cargo.toml
  2. 2
      lib/Cargo.toml
  3. 1
      lib/spargebra/Cargo.toml
  4. 42
      lib/spargebra/src/algebra.rs
  5. 85
      lib/spargebra/src/parser.rs
  6. 4
      lib/sparql-smith/Cargo.toml
  7. 16
      lib/sparql-smith/src/lib.rs
  8. 98
      lib/src/sparql/eval.rs
  9. 6
      lib/src/sparql/plan.rs
  10. 21
      lib/src/sparql/plan_builder.rs
  11. 5
      testsuite/oxigraph-tests/sparql/lateral/basic_input.ttl
  12. 6
      testsuite/oxigraph-tests/sparql/lateral/filter.rq
  13. 6
      testsuite/oxigraph-tests/sparql/lateral/graph.rq
  14. 9
      testsuite/oxigraph-tests/sparql/lateral/join.rq
  15. 68
      testsuite/oxigraph-tests/sparql/lateral/manifest.ttl
  16. 6
      testsuite/oxigraph-tests/sparql/lateral/optional.rq
  17. 17
      testsuite/oxigraph-tests/sparql/lateral/simple.srx
  18. 6
      testsuite/oxigraph-tests/sparql/lateral/subselect.rq
  19. 41
      testsuite/oxigraph-tests/sparql/lateral/subselect.srx
  20. 6
      testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.rq
  21. 25
      testsuite/oxigraph-tests/sparql/lateral/subselect_aggregate.srx
  22. 6
      testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.rq
  23. 35
      testsuite/oxigraph-tests/sparql/lateral/subselect_inside_optional.srx
  24. 6
      testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.rq
  25. 46
      testsuite/oxigraph-tests/sparql/lateral/subselect_outside_optional.srx
  26. 1
      testsuite/oxigraph-tests/sparql/manifest.ttl
  27. 7
      testsuite/tests/oxigraph.rs
  28. 7
      testsuite/tests/parser.rs
  29. 7
      testsuite/tests/sparql.rs

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

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

@ -16,6 +16,7 @@ rust-version = "1.60"
[features]
default = []
rdf-star = ["oxrdf/rdf-star"]
sep-0006 = []
[dependencies]
peg = "0.8"

@ -527,6 +527,9 @@ pub enum GraphPattern {
right: Box<Self>,
expression: Option<Expression>,
},
/// Lateral join i.e. evaluate right for all result row of left
#[cfg(feature = "sep-0006")]
Lateral { left: Box<Self>, right: Box<Self> },
/// [Filter](https://www.w3.org/TR/sparql11-query/#defn_algFilter).
Filter { expr: Expression, inner: Box<Self> },
/// [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);

@ -360,6 +360,8 @@ impl<F, T: From<F>> From<FocusedTriplePattern<F>> for FocusedTripleOrPathPattern
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
enum PartialGraphPattern {
Optional(GraphPattern, Option<Expression>),
#[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<Variable>) -
}
}
/// 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<GraphName>, to: impl Into<GraphNamePattern>) -> 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<TripleOrPathPattern> = _ 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) })

@ -12,5 +12,9 @@ A SPARQL test cases generator
"""
edition = "2021"
[features]
default = []
sep-0006 = []
[dependencies]
arbitrary = { version = "1", features = ["derive"] }

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

@ -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<EncodedTuple> {
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<dyn Fn(EncodedTuple) -> EncodedTuplesIterator>,
left_iter: EncodedTuplesIterator,
current_left: Option<EncodedTuple>,
@ -3710,18 +3692,16 @@ struct BadLeftJoinIterator {
problem_vars: Rc<Vec<usize>>,
}
impl Iterator for BadLeftJoinIterator {
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) = 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));
}

@ -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<Self> {
if self.inner.len() < other.inner.len() {
let mut result = other.inner.clone();

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

@ -0,0 +1,5 @@
@prefix ex: <http://example.org/> .
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 .

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
VALUES ?s { ex:S }
LATERAL { VALUES ?o { ex:O } FILTER(BOUND(?s) && EXISTS { FILTER(BOUND(?s)) }) }
}

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
VALUES ?s { ex:S }
LATERAL { GRAPH ex:G { FILTER(BOUND(?s)) . VALUES ?o { ex:O } } }
}

@ -0,0 +1,9 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
VALUES ?s { ex:S }
LATERAL {
{ VALUES ?o { ex:O } }
{ FILTER(BOUND(?s) && !BOUND(?o)) }
}
}

@ -0,0 +1,68 @@
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <https://github.com/oxigraph/oxigraph/tests/sparql/lateral/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 ut: <http://www.w3.org/2009/sparql/tests/test-update#> .
<> 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 <subselect.rq> ;
qt:data <basic_input.ttl> ] ;
mf:result <subselect.srx> .
:subselect_inside_optional rdf:type mf:QueryEvaluationTest ;
mf:name "Basic subselect LATERAL test inside OPTIONAL" ;
mf:action
[ qt:query <subselect_inside_optional.rq> ;
qt:data <basic_input.ttl> ] ;
mf:result <subselect_inside_optional.srx> .
:subselect_outside_optional rdf:type mf:QueryEvaluationTest ;
mf:name "Basic subselect test inside LATERAL OPTIONAL" ;
mf:action
[ qt:query <subselect_outside_optional.rq> ;
qt:data <basic_input.ttl> ] ;
mf:result <subselect_outside_optional.srx> .
:subselect_aggregate rdf:type mf:QueryEvaluationTest ;
mf:name "LATERAL test with explicit aggregate" ;
mf:action
[ qt:query <subselect_aggregate.rq> ;
qt:data <basic_input.ttl> ] ;
mf:result <subselect_aggregate.srx> .
:optional rdf:type mf:QueryEvaluationTest ;
mf:name "LATERAL OPTIONAL test" ;
mf:action [ qt:query <optional.rq> ] ;
mf:result <simple.srx> .
:graph rdf:type mf:QueryEvaluationTest ;
mf:name "LATERAL GRAPH test" ;
mf:action [ qt:query <graph.rq> ] ;
mf:result <simple.srx> .
:filter rdf:type mf:QueryEvaluationTest ;
mf:name "LATERAL FILTER test" ;
mf:action [ qt:query <filter.rq> ] ;
mf:result <simple.srx> .
:join rdf:type mf:QueryEvaluationTest ;
mf:name "join in LATERAL test" ;
mf:action [ qt:query <join.rq> ] ;
mf:result <simple.srx> .

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
VALUES ?s { ex:S }
LATERAL { OPTIONAL { FILTER(BOUND(?s)) . VALUES ?o { ex:O } } }
}

@ -0,0 +1,17 @@
<?xml version="1.0"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="s"/>
<variable name="o"/>
</head>
<results>
<result>
<binding name="s">
<uri>http://example.org/S</uri>
</binding>
<binding name="o">
<uri>http://example.org/O</uri>
</binding>
</result>
</results>
</sparql>

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
?s a ex:T.
LATERAL {SELECT ?s ?o WHERE { ?s ex:p ?o } ORDER BY ?o LIMIT 2}
}

@ -0,0 +1,41 @@
<?xml version="1.0"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="s"/>
<variable name="o"/>
</head>
<results>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">11</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">12</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s2</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">21</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s2</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">22</literal>
</binding>
</result>
</results>
</sparql>

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?c WHERE {
?s a ex:T.
LATERAL {SELECT ?s (MAX(?o) AS ?c) WHERE { ?s ex:p ?o } GROUP BY ?s}
}

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="s"/>
<variable name="c"/>
</head>
<results>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="c">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">13</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s2</uri>
</binding>
<binding name="c">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">23</literal>
</binding>
</result>
</results>
</sparql>

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
?s a ex:T.
OPTIONAL { LATERAL {SELECT ?s ?o WHERE { ?s ex:p ?o } ORDER BY ?o LIMIT 2} }
}

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="s"/>
<variable name="o"/>
</head>
<results>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">11</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">12</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s2</uri>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s3</uri>
</binding>
</result>
</results>
</sparql>

@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
SELECT ?s ?o WHERE {
?s a ex:T.
LATERAL { OPTIONAL {SELECT ?s ?o WHERE { ?s ex:p ?o } ORDER BY ?o LIMIT 2} }
}

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="s"/>
<variable name="o"/>
</head>
<results>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">11</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s1</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">12</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s2</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">21</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s2</uri>
</binding>
<binding name="o">
<literal datatype="http://www.w3.org/2001/XMLSchema#integer">22</literal>
</binding>
</result>
<result>
<binding name="s">
<uri>http://example.org/s3</uri>
</binding>
</result>
</results>
</sparql>

@ -7,6 +7,7 @@
<> rdf:type mf:Manifest ;
rdfs:label "Oxigraph SPARQL tests" ;
mf:include ( <lateral/manifest.ttl> ) ;
mf:entries
(
:small_unicode_escape_with_multibytes_char

@ -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(())
}

@ -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(())
}

@ -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(())
}

Loading…
Cancel
Save