Adds a SPARQL update parser

pull/51/head
Tpt 4 years ago
parent 67721246dd
commit 867600dba4
  1. 262
      lib/src/sparql/algebra.rs
  2. 2
      lib/src/sparql/mod.rs
  3. 261
      lib/src/sparql/parser.rs
  4. 34
      testsuite/src/sparql_evaluator.rs
  5. 8
      testsuite/tests/sparql.rs

@ -162,7 +162,45 @@ impl TriplePattern {
impl fmt::Display for TriplePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.subject, self.predicate, self.object)
write!(f, "{} {} {} .", self.subject, self.predicate, self.object)
}
}
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct QuadPattern {
pub subject: TermOrVariable,
pub predicate: NamedNodeOrVariable,
pub object: TermOrVariable,
pub graph_name: Option<NamedNodeOrVariable>,
}
impl QuadPattern {
pub fn new(
subject: impl Into<TermOrVariable>,
predicate: impl Into<NamedNodeOrVariable>,
object: impl Into<TermOrVariable>,
graph_name: Option<NamedNodeOrVariable>,
) -> Self {
Self {
subject: subject.into(),
predicate: predicate.into(),
object: object.into(),
graph_name,
}
}
}
impl fmt::Display for QuadPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(graph_name) = &self.graph_name {
write!(
f,
"GRAPH {} {{ {} {} {} }}",
graph_name, self.subject, self.predicate, self.object
)
} else {
write!(f, "{} {} {} .", self.subject, self.predicate, self.object)
}
}
}
@ -273,7 +311,7 @@ impl<'a> fmt::Display for SparqlPathPattern<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {}",
"{} {} {} .",
self.0.subject,
SparqlPropertyPath(&self.0.path),
self.0.object
@ -678,7 +716,7 @@ impl fmt::Display for GraphPattern {
p.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(" . ")
.join(" ")
),
GraphPattern::Join(a, b) => write!(f, "Join({}, {})", a, b),
GraphPattern::LeftJoin(a, b, e) => {
@ -843,7 +881,7 @@ impl<'a> fmt::Display for SparqlGraphPattern<'a> {
match self.0 {
GraphPattern::BGP(p) => {
for pattern in p {
write!(f, "{} .", SparqlTripleOrPathPattern(pattern))?
write!(f, "{}", SparqlTripleOrPathPattern(pattern))?
}
Ok(())
}
@ -1353,7 +1391,7 @@ impl fmt::Display for QueryVariants {
base_iri,
} => {
if let Some(base_iri) = base_iri {
writeln!(f, "BASE <{}>", base_iri)?;
writeln!(f, "BASE <{}>\n", base_iri)?;
}
write!(f, "{}", SparqlGraphRootPattern { algebra, dataset })
}
@ -1364,16 +1402,15 @@ impl fmt::Display for QueryVariants {
base_iri,
} => {
if let Some(base_iri) = base_iri {
writeln!(f, "BASE <{}>", base_iri)?;
writeln!(f, "BASE <{}>\n", base_iri)?;
}
write!(f, "CONSTRUCT {{ ")?;
for triple in construct.iter() {
write!(f, "{} ", triple)?;
}
write!(
f,
"CONSTRUCT {{ {} }} {} WHERE {{ {} }}",
construct
.iter()
.map(|t| t.to_string())
.collect::<Vec<String>>()
.join(" . "),
"}} {} WHERE {{ {} }}",
dataset,
SparqlGraphRootPattern {
algebra,
@ -1387,7 +1424,7 @@ impl fmt::Display for QueryVariants {
base_iri,
} => {
if let Some(base_iri) = base_iri {
writeln!(f, "BASE <{}>", base_iri.as_str())?;
writeln!(f, "BASE <{}>\n", base_iri.as_str())?;
}
write!(
f,
@ -1405,7 +1442,7 @@ impl fmt::Display for QueryVariants {
base_iri,
} => {
if let Some(base_iri) = base_iri {
writeln!(f, "BASE <{}>", base_iri)?;
writeln!(f, "BASE <{}>\n", base_iri)?;
}
write!(
f,
@ -1420,3 +1457,200 @@ impl fmt::Display for QueryVariants {
}
}
}
/// The [graph update operations](https://www.w3.org/TR/sparql11-update/#formalModelGraphUpdate)
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub enum GraphUpdateOperation {
/// [insert data](https://www.w3.org/TR/sparql11-update/#def_insertdataoperation)
OpInsertData { data: Vec<QuadPattern> },
/// [delete data](https://www.w3.org/TR/sparql11-update/#def_deletedataoperation)
OpDeleteData { data: Vec<QuadPattern> },
/// [delete insert](https://www.w3.org/TR/sparql11-update/#def_deleteinsertoperation)
OpDeleteInsert {
with: Option<NamedNode>,
delete: Option<Vec<QuadPattern>>,
insert: Option<Vec<QuadPattern>>,
using: Rc<DatasetSpec>,
algebra: Rc<GraphPattern>,
},
/// [load](https://www.w3.org/TR/sparql11-update/#def_loadoperation)
OpLoad {
silent: bool,
from: NamedNode,
to: Option<NamedNode>,
},
/// [clear](https://www.w3.org/TR/sparql11-update/#def_clearoperation)
OpClear { silent: bool, graph: GraphTarget },
/// [create](https://www.w3.org/TR/sparql11-update/#def_createoperation)
OpCreate { silent: bool, graph: NamedNode },
/// [drop](https://www.w3.org/TR/sparql11-update/#def_dropoperation)
OpDrop { silent: bool, graph: GraphTarget },
}
impl fmt::Display for GraphUpdateOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GraphUpdateOperation::OpInsertData { data } => {
writeln!(f, "INSERT DATA {{")?;
for quad in data {
writeln!(f, "\t{}", quad)?;
}
write!(f, "}}")
}
GraphUpdateOperation::OpDeleteData { data } => {
writeln!(f, "DELETE DATA {{")?;
for quad in data {
writeln!(f, "\t{}", quad)?;
}
write!(f, "}}")
}
GraphUpdateOperation::OpDeleteInsert {
with,
delete,
insert,
using,
algebra,
} => {
if let Some(with) = with {
writeln!(f, "WITH {}", with)?;
}
if let Some(delete) = delete {
writeln!(f, "DELETE {{")?;
for quad in delete {
writeln!(f, "\t{}", quad)?;
}
writeln!(f, "}}")?;
}
if let Some(insert) = insert {
writeln!(f, "INSERT {{")?;
for quad in insert {
writeln!(f, "\t{}", quad)?;
}
writeln!(f, "}}")?;
}
for g in &using.default {
writeln!(f, "USING {}", g)?;
}
for g in &using.named {
writeln!(f, "USING NAMED {}", g)?;
}
write!(
f,
"WHERE {{ {} }}",
SparqlGraphRootPattern {
algebra,
dataset: &DatasetSpec::default()
}
)
}
GraphUpdateOperation::OpLoad { silent, from, to } => {
write!(f, "LOAD ")?;
if *silent {
write!(f, "SILENT ")?;
}
write!(f, "{}", from)?;
if let Some(to) = to {
write!(f, " INTO GRAPH {}", to)?;
}
Ok(())
}
GraphUpdateOperation::OpClear { silent, graph } => {
write!(f, "CLEAR ")?;
if *silent {
write!(f, "SILENT ")?;
}
write!(f, "{}", graph)
}
GraphUpdateOperation::OpCreate { silent, graph } => {
write!(f, "CREATE ")?;
if *silent {
write!(f, "SILENT ")?;
}
write!(f, "GRAPH {}", graph)
}
GraphUpdateOperation::OpDrop { silent, graph } => {
write!(f, "DROP ")?;
if *silent {
write!(f, "SILENT ")?;
}
write!(f, "{}", graph)
}
}
}
}
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub enum GraphTarget {
NamedNode(NamedNode),
DefaultGraph,
NamedGraphs,
AllGraphs,
}
impl fmt::Display for GraphTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NamedNode(node) => write!(f, "GRAPH {}", node),
Self::DefaultGraph => write!(f, "DEFAULT"),
Self::NamedGraphs => write!(f, "NAMED"),
Self::AllGraphs => write!(f, "ALL"),
}
}
}
impl From<NamedNode> for GraphTarget {
fn from(node: NamedNode) -> Self {
Self::NamedNode(node)
}
}
impl From<NamedNodeRef<'_>> for GraphTarget {
fn from(node: NamedNodeRef<'_>) -> Self {
Self::NamedNode(node.into())
}
}
impl From<NamedOrDefaultGraphTarget> for GraphTarget {
fn from(graph: NamedOrDefaultGraphTarget) -> Self {
match graph {
NamedOrDefaultGraphTarget::NamedNode(node) => Self::NamedNode(node),
NamedOrDefaultGraphTarget::DefaultGraph => Self::DefaultGraph,
}
}
}
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub enum NamedOrDefaultGraphTarget {
NamedNode(NamedNode),
DefaultGraph,
}
impl fmt::Display for NamedOrDefaultGraphTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NamedNode(node) => write!(f, "GRAPH {}", node),
Self::DefaultGraph => write!(f, "DEFAULT"),
}
}
}
impl From<NamedNode> for NamedOrDefaultGraphTarget {
fn from(node: NamedNode) -> Self {
Self::NamedNode(node)
}
}
impl From<NamedNodeRef<'_>> for NamedOrDefaultGraphTarget {
fn from(node: NamedNodeRef<'_>) -> Self {
Self::NamedNode(node.into())
}
}
impl From<NamedOrDefaultGraphTarget> for Option<NamedNodeOrVariable> {
fn from(graph: NamedOrDefaultGraphTarget) -> Self {
match graph {
NamedOrDefaultGraphTarget::NamedNode(node) => Some(node.into()),
NamedOrDefaultGraphTarget::DefaultGraph => None,
}
}
}

@ -25,7 +25,7 @@ pub use crate::sparql::model::QuerySolutionIter;
pub use crate::sparql::model::QueryTripleIter;
pub use crate::sparql::model::Variable;
pub use crate::sparql::parser::ParseError;
pub use crate::sparql::parser::Query;
pub use crate::sparql::parser::{Query, Update};
use crate::sparql::plan::{PlanNode, TripleTemplate};
use crate::sparql::plan_builder::PlanBuilder;
use crate::store::numeric_encoder::StrEncodingAware;

@ -88,6 +88,82 @@ impl<'a> TryFrom<&'a String> for Query {
}
}
/// A parsed [SPARQL update](https://www.w3.org/TR/sparql11-update/)
///
/// ```
/// use oxigraph::sparql::Update;
///
/// let update_str = "CLEAR ALL ;";
/// let update = Update::parse(update_str, None)?;
///
/// assert_eq!(update.to_string().trim(), update_str);
/// # Result::Ok::<_, oxigraph::sparql::ParseError>(())
/// ```
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct Update(pub(crate) Vec<GraphUpdateOperation>);
impl fmt::Display for Update {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for update in &self.0 {
writeln!(f, "{} ;", update)?;
}
Ok(())
}
}
impl Update {
/// Parses a SPARQL update with an optional base IRI to resolve relative IRIs in the query
pub fn parse(update: &str, base_iri: Option<&str>) -> Result<Self, ParseError> {
let mut state = ParserState {
base_iri: if let Some(base_iri) = base_iri {
Some(Rc::new(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(),
aggregations: Vec::default(),
};
Ok(Self(
parser::UpdateInit(&unescape_unicode_codepoints(update), &mut state).map_err(|e| {
ParseError {
inner: ParseErrorKind::Parser(e),
}
})?,
))
}
}
impl FromStr for Update {
type Err = ParseError;
fn from_str(update: &str) -> Result<Self, ParseError> {
Self::parse(update, None)
}
}
impl<'a> TryFrom<&'a str> for Update {
type Error = ParseError;
fn try_from(update: &str) -> Result<Self, ParseError> {
Self::from_str(update)
}
}
impl<'a> TryFrom<&'a String> for Update {
type Error = ParseError;
fn try_from(update: &String) -> Result<Self, ParseError> {
Self::from_str(update)
}
}
/// Error returned during SPARQL parsing.
#[derive(Debug)]
pub struct ParseError {
@ -259,6 +335,10 @@ fn new_join(l: GraphPattern, r: GraphPattern) -> GraphPattern {
pl.extend_from_slice(&pr);
GraphPattern::BGP(pl)
}
(GraphPattern::Graph(g1, l), GraphPattern::Graph(g2, r)) if g1 == g2 => {
// We merge identical graphs
GraphPattern::Graph(g1, Box::new(new_join(*l, *r)))
}
(l, r) => GraphPattern::Join(Box::new(l), Box::new(r)),
}
}
@ -383,6 +463,35 @@ fn build_select(
m
}
fn copy_graph(
from: NamedOrDefaultGraphTarget,
to: impl Into<Option<NamedNodeOrVariable>>,
) -> GraphUpdateOperation {
let bgp = GraphPattern::BGP(vec![TriplePattern::new(
Variable::new("s"),
Variable::new("p"),
Variable::new("o"),
)
.into()]);
GraphUpdateOperation::OpDeleteInsert {
with: None,
delete: None,
insert: Some(vec![QuadPattern::new(
Variable::new("s"),
Variable::new("p"),
Variable::new("o"),
to.into(),
)]),
using: Rc::new(DatasetSpec::default()),
algebra: Rc::new(match &from {
NamedOrDefaultGraphTarget::NamedNode(from) => {
GraphPattern::Graph(from.clone().into(), Box::new(bgp))
}
NamedOrDefaultGraphTarget::DefaultGraph => bgp,
}),
}
}
enum Either<L, R> {
Left(L),
Right(R),
@ -637,6 +746,9 @@ parser! {
q
}
//[3]
pub rule UpdateInit() -> Vec<GraphUpdateOperation> = Update()
//[4]
rule Prologue() = (BaseDecl() _ / PrefixDecl() _)* {}
@ -749,6 +861,7 @@ parser! {
d.into_iter().fold(DatasetSpec::default(), |mut a, b| a + b)
}
rule DatasetClauses_item() -> DatasetSpec = d:DatasetClause() _ { d }
//[14]
rule DefaultGraphClause() -> DatasetSpec = s:SourceSelector() {
DatasetSpec::new_with_default(s)
@ -830,6 +943,154 @@ parser! {
i("VALUES") _ p:DataBlock() { Some(p) } /
{ None }
//[29]
rule Update() -> Vec<GraphUpdateOperation> = _ Prologue() _ u:(Update1() ** (_ ";" _)) _ ( ";" _)? { u.into_iter().flatten().collect() }
//[30]
rule Update1() -> Vec<GraphUpdateOperation> = Load() / Clear() / Drop() / Add() / Move() / Copy() / Create() / InsertData() / DeleteData() / DeleteWhere() / Modify()
rule Update1_silent() -> bool = i("SILENT") { true } / { false }
//[31]
rule Load() -> Vec<GraphUpdateOperation> = i("LOAD") _ silent:Update1_silent() _ from:iri() _ to:Load_to()? {
vec![GraphUpdateOperation::OpLoad { silent, from, to }]
}
rule Load_to() -> NamedNode = i("INTO") _ g: GraphRef() { g }
//[32]
rule Clear() -> Vec<GraphUpdateOperation> = i("CLEAR") _ silent:Update1_silent() _ graph:GraphRefAll() {
vec![GraphUpdateOperation::OpClear { silent, graph }]
}
//[33]
rule Drop() -> Vec<GraphUpdateOperation> = i("DROP") _ silent:Update1_silent() _ graph:GraphRefAll() {
vec![GraphUpdateOperation::OpDrop { silent, graph }]
}
//[34]
rule Create() -> Vec<GraphUpdateOperation> = i("CREATE") _ silent:Update1_silent() _ graph:GraphRef() {
vec![GraphUpdateOperation::OpCreate { silent, graph }]
}
//[35]
rule Add() -> Vec<GraphUpdateOperation> = i("ADD") _ silent:Update1_silent() _ from:GraphOrDefault() _ i("TO") _ to:GraphOrDefault() {
// Rewriting defined by https://www.w3.org/TR/sparql11-update/#add
let bgp = GraphPattern::BGP(vec![TriplePattern::new(Variable::new("s"), Variable::new("p"), Variable::new("o")).into()]);
vec![copy_graph(from, to)]
}
//[36]
rule Move() -> Vec<GraphUpdateOperation> = i("MOVE") _ silent:Update1_silent() _ from:GraphOrDefault() _ i("TO") _ to:GraphOrDefault() {
// Rewriting defined by https://www.w3.org/TR/sparql11-update/#move
let bgp = GraphPattern::BGP(vec![TriplePattern::new(Variable::new("s"), Variable::new("p"), Variable::new("o")).into()]);
vec![GraphUpdateOperation::OpDrop { silent, graph: to.clone().into() }, copy_graph(from.clone(), to), GraphUpdateOperation::OpDrop { silent, graph: from.into() }]
}
//[37]
rule Copy() -> Vec<GraphUpdateOperation> = i("COPY") _ silent:Update1_silent() _ from:GraphOrDefault() _ i("TO") _ to:GraphOrDefault() {
// Rewriting defined by https://www.w3.org/TR/sparql11-update/#copy
let bgp = GraphPattern::BGP(vec![TriplePattern::new(Variable::new("s"), Variable::new("p"), Variable::new("o")).into()]);
vec![GraphUpdateOperation::OpDrop { silent, graph: to.clone().into() }, copy_graph(from, to)]
}
//[38]
rule InsertData() -> Vec<GraphUpdateOperation> = i("INSERT") _ i("DATA") _ data:QuadData() {
vec![GraphUpdateOperation::OpInsertData { data }]
}
//[39]
rule DeleteData() -> Vec<GraphUpdateOperation> = i("DELETE") _ i("DATA") _ data:QuadData() {
vec![GraphUpdateOperation::OpDeleteData { data }]
}
//[40]
rule DeleteWhere() -> Vec<GraphUpdateOperation> = i("DELETE") _ i("WHERE") _ d:QuadData() {
let algebra = d.iter().map(|q| {
let bgp = GraphPattern::BGP(vec![TriplePattern::new(q.subject.clone(), q.predicate.clone(), q.object.clone()).into()]);
if let Some(graph_name) = &q.graph_name {
GraphPattern::Graph(graph_name.clone(), Box::new(bgp))
} else {
bgp
}
}).fold(GraphPattern::BGP(Vec::new()), new_join);
vec![GraphUpdateOperation::OpDeleteInsert {
with: None,
delete: Some(d),
insert: None,
using: Rc::new(DatasetSpec::default()),
algebra: Rc::new(algebra)
}]
}
//[41]
rule Modify() -> Vec<GraphUpdateOperation> = with:Modify_with() _ c:Modify_clauses() _ using:(UsingClause() ** (_)) _ i("WHERE") _ algebra:GroupGraphPattern() {
let (delete, insert) = c;
vec![GraphUpdateOperation::OpDeleteInsert {
with,
delete,
insert,
using: Rc::new(using.into_iter().fold(DatasetSpec::default(), |mut a, b| a + b)),
algebra: Rc::new(algebra)
}]
}
rule Modify_with() -> Option<NamedNode> = i("WITH") _ i:iri() _ {
Some(i)
} / { None }
rule Modify_clauses() -> (Option<Vec<QuadPattern>>, Option<Vec<QuadPattern>>) = d:DeleteClause() _ i:InsertClause()? {
(Some(d), i)
} / i:InsertClause() {
(None, Some(i))
}
//[42]
rule DeleteClause() -> Vec<QuadPattern> = i("DELETE") _ q:QuadPattern() { q }
//[43]
rule InsertClause() -> Vec<QuadPattern> = i("INSERT") _ q:QuadPattern() { q }
//[44]
rule UsingClause() -> DatasetSpec = i("USING") _ d:(UsingClause_default() / UsingClause_named()) { d }
rule UsingClause_default() -> DatasetSpec = i:iri() {
DatasetSpec::new_with_default(i)
}
rule UsingClause_named() -> DatasetSpec = i("NAMED") _ i:iri() {
DatasetSpec::new_with_named(i)
}
//[45]
rule GraphOrDefault() -> NamedOrDefaultGraphTarget = i("GRAPH") _ g:iri() {
NamedOrDefaultGraphTarget::NamedNode(g)
} / i("DEFAULT") { NamedOrDefaultGraphTarget::DefaultGraph }
//[46]
rule GraphRef() -> NamedNode = i("GRAPH") _ g:iri() { g }
//[47]
rule GraphRefAll() -> GraphTarget = i: GraphRef() { i.into() }
/ i("DEFAULT") { GraphTarget::DefaultGraph }
/ i("NAMED") { GraphTarget::NamedGraphs }
/ i("ALL") { GraphTarget::AllGraphs }
//[48]
rule QuadPattern() -> Vec<QuadPattern> = "{" _ q:Quads() _ "}" { q }
//[49]
rule QuadData() -> Vec<QuadPattern> = "{" _ q:Quads() _ "}" { q }
//[50]
rule Quads() -> Vec<QuadPattern> = q:(Quads_TriplesTemplate() / Quads_QuadsNotTriples()) ** (_) {
q.into_iter().flatten().collect()
}
rule Quads_TriplesTemplate() -> Vec<QuadPattern> = t:TriplesTemplate() {
t.into_iter().map(|t| QuadPattern::new(t.subject, t.predicate, t.object, None)).collect()
} //TODO: return iter?
rule Quads_QuadsNotTriples() -> Vec<QuadPattern> = q:QuadsNotTriples() _ "."? { q }
//[51]
rule QuadsNotTriples() -> Vec<QuadPattern> = i("GRAPH") _ g:VarOrIri() _ "{" _ t:TriplesTemplate()? _ "}" {
t.unwrap_or_else(Vec::new).into_iter().map(|t| QuadPattern::new(t.subject, t.predicate, t.object, Some(g.clone()))).collect()
}
//[52]
rule TriplesTemplate() -> Vec<TriplePattern> = h:TriplesSameSubject() _ t:TriplesTemplate_tail()? {
let mut triples = h;

@ -123,6 +123,40 @@ fn evaluate_sparql_test(test: &Test) -> Result<()> {
}
},
}
} else if test.kind
== "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#PositiveUpdateSyntaxTest11"
{
let update_file = test
.action
.as_deref()
.ok_or_else(|| anyhow!("No action found for test {}", test))?;
match Update::parse(&read_file_to_string(&update_file)?, Some(&update_file)) {
Err(error) => Err(anyhow!("Not able to parse {} with error: {}", test, error)),
Ok(update) => match Update::parse(&update.to_string(), None) {
Ok(_) => Ok(()),
Err(error) => Err(anyhow!(
"Failure to deserialize \"{}\" of {} with error: {}",
update.to_string(),
test,
error
)),
},
}
} else if test.kind
== "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#NegativeUpdateSyntaxTest11"
{
let update_file = test
.action
.as_deref()
.ok_or_else(|| anyhow!("No action found for test {}", test))?;
match Query::parse(&read_file_to_string(update_file)?, Some(update_file)) {
Ok(result) => Err(anyhow!(
"Oxigraph parses even if it should not {}. The output tree is: {}",
test,
result
)),
Err(_) => Ok(()),
}
} else {
Err(anyhow!("Unsupported test type: {}", test.kind))
}

@ -120,3 +120,11 @@ fn sparql11_federation_w3c_evaluation_testsuite() -> Result<()> {
],
)
}
#[test]
fn sparql11_update_w3c_evaluation_testsuite() -> Result<()> {
run_testsuite(
"http://www.w3.org/2009/sparql/docs/tests/data-sparql11/syntax-update-1/manifest.ttl",
vec![],
)
}

Loading…
Cancel
Save