Exposes SPARQL results I/O in Oxigraph and improve EvaluationError

pull/617/head
Tpt 1 year ago committed by Thomas Tanon
parent 9da26c6f95
commit 7fe055d2b4
  1. 1
      Cargo.lock
  2. 25
      lib/oxrdfio/README.md
  3. 26
      lib/src/io/mod.rs
  4. 12
      lib/src/lib.rs
  5. 25
      lib/src/model.rs
  6. 159
      lib/src/sparql/error.rs
  7. 6
      lib/src/sparql/eval.rs
  8. 4
      lib/src/sparql/mod.rs
  9. 60
      lib/src/sparql/model.rs
  10. 47
      lib/src/sparql/results.rs
  11. 32
      lib/src/sparql/service.rs
  12. 46
      lib/src/sparql/update.rs
  13. 18
      python/src/sparql.rs
  14. 1
      server/Cargo.toml
  15. 2
      server/src/main.rs
  16. 5
      testsuite/src/sparql_evaluator.rs

1
Cargo.lock generated

@ -998,7 +998,6 @@ dependencies = [
"predicates", "predicates",
"rand", "rand",
"rayon-core", "rayon-core",
"sparesults",
"url", "url",
] ]

@ -21,27 +21,28 @@ Support for [SPARQL-star](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html
It is designed as a low level parser compatible with both synchronous and asynchronous I/O (behind the `async-tokio` feature). It is designed as a low level parser compatible with both synchronous and asynchronous I/O (behind the `async-tokio` feature).
Usage example counting the number of people in a Turtle file: Usage example converting a Turtle file to a N-Triples file:
```rust ```rust
use oxrdf::{NamedNodeRef, vocab::rdf}; use oxrdfio::{RdfFormat, RdfParser, RdfSerializer};
use oxrdfio::{RdfFormat, RdfParser};
let file = b"@base <http://example.com/> . let turtle_file = b"@base <http://example.com/> .
@prefix schema: <http://schema.org/> . @prefix schema: <http://schema.org/> .
<foo> a schema:Person ; <foo> a schema:Person ;
schema:name \"Foo\" . schema:name \"Foo\" .
<bar> a schema:Person ; <bar> a schema:Person ;
schema:name \"Bar\" ."; schema:name \"Bar\" .";
let schema_person = NamedNodeRef::new("http://schema.org/Person").unwrap(); let ntriples_file = b"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
let mut count = 0; <http://example.com/foo> <http://schema.org/name> \"Foo\" .
for quad in RdfParser::from_format(RdfFormat::Turtle).parse_read(file.as_ref()) { <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
let quad = quad.unwrap(); <http://example.com/bar> <http://schema.org/name> \"Bar\" .
if quad.predicate == rdf::TYPE && quad.object == schema_person.into() { ";
count += 1;
} let mut writer = RdfSerializer::from_format(RdfFormat::NTriples).serialize_to_write(Vec::new());
for quad in RdfParser::from_format(RdfFormat::Turtle).parse_read(turtle_file.as_ref()) {
writer.write_quad(&quad.unwrap()).unwrap();
} }
assert_eq!(2, count); assert_eq!(writer.finish().unwrap(), ntriples_file);
``` ```
## License ## License

@ -1,4 +1,28 @@
//! Utilities to read and write RDF graphs and datasets. //! Utilities to read and write RDF graphs and datasets using [OxRDF I/O](https://crates.io/crates/oxrdfio).
//!
//! Usage example converting a Turtle file to a N-Triples file:
//! ```
//! use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
//!
//! let turtle_file = b"@base <http://example.com/> .
//! @prefix schema: <http://schema.org/> .
//! <foo> a schema:Person ;
//! schema:name \"Foo\" .
//! <bar> a schema:Person ;
//! schema:name \"Bar\" .";
//!
//! let ntriples_file = b"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
//! <http://example.com/foo> <http://schema.org/name> \"Foo\" .
//! <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
//! <http://example.com/bar> <http://schema.org/name> \"Bar\" .
//! ";
//!
//! let mut writer = RdfSerializer::from_format(RdfFormat::NTriples).serialize_to_write(Vec::new());
//! for quad in RdfParser::from_format(RdfFormat::Turtle).parse_read(turtle_file.as_ref()) {
//! writer.write_quad(&quad.unwrap()).unwrap();
//! }
//! assert_eq!(writer.finish().unwrap(), ntriples_file);
//! ```
mod format; mod format;
pub mod read; pub mod read;

@ -6,17 +6,7 @@
#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
pub mod io; pub mod io;
pub mod model;
pub mod sparql; pub mod sparql;
mod storage; mod storage;
pub mod store; pub mod store;
pub mod model {
//! Implements data structures for [RDF 1.1 Concepts](https://www.w3.org/TR/rdf11-concepts/) using [OxRDF](https://crates.io/crates/oxrdf).
pub use oxrdf::{
dataset, graph, vocab, BlankNode, BlankNodeIdParseError, BlankNodeRef, Dataset, Graph,
GraphName, GraphNameRef, IriParseError, LanguageTagParseError, Literal, LiteralRef,
NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, QuadRef, Subject,
SubjectRef, Term, TermParseError, TermRef, Triple, TripleRef,
};
}

@ -0,0 +1,25 @@
//! Implements data structures for [RDF 1.1 Concepts](https://www.w3.org/TR/rdf11-concepts/) using [OxRDF](https://crates.io/crates/oxrdf).
//!
//! Usage example:
//!
//! ```
//! use oxigraph::model::*;
//!
//! let mut graph = Graph::default();
//!
//! // insertion
//! let ex = NamedNodeRef::new("http://example.com").unwrap();
//! let triple = TripleRef::new(ex, ex, ex);
//! graph.insert(triple);
//!
//! // simple filter
//! let results: Vec<_> = graph.triples_for_subject(ex).collect();
//! assert_eq!(vec![triple], results);
//! ```
pub use oxrdf::{
dataset, graph, vocab, BlankNode, BlankNodeIdParseError, BlankNodeRef, Dataset, Graph,
GraphName, GraphNameRef, IriParseError, LanguageTagParseError, Literal, LiteralRef, NamedNode,
NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, QuadRef, Subject, SubjectRef, Term,
TermParseError, TermRef, Triple, TripleRef,
};

@ -1,7 +1,10 @@
use crate::io::{ParseError, SyntaxError}; use crate::io::ParseError as RdfParseError;
use crate::model::NamedNode;
use crate::sparql::results::ParseError as ResultsParseError;
use crate::sparql::ParseError;
use crate::storage::StorageError; use crate::storage::StorageError;
use std::convert::Infallible; use std::convert::Infallible;
use std::error; use std::error::Error;
use std::fmt; use std::fmt;
use std::io; use std::io;
@ -10,29 +13,31 @@ use std::io;
#[non_exhaustive] #[non_exhaustive]
pub enum EvaluationError { pub enum EvaluationError {
/// An error in SPARQL parsing. /// An error in SPARQL parsing.
Parsing(spargebra::ParseError), Parsing(ParseError),
/// An error from the storage. /// An error from the storage.
Storage(StorageError), Storage(StorageError),
/// An error while parsing an external RDF file. /// An error while parsing an external RDF file.
GraphParsing(SyntaxError), GraphParsing(RdfParseError),
/// An error while parsing an external result file (likely from a federated query). /// An error while parsing an external result file (likely from a federated query).
ResultsParsing(sparesults::ParseError), ResultsParsing(ResultsParseError),
/// An error returned during store or results I/Os. /// An error returned during results serialization.
Io(io::Error), ResultsSerialization(io::Error),
/// An error returned during the query evaluation itself (not supported custom function...). /// Error during `SERVICE` evaluation
Query(QueryError), Service(Box<dyn Error + Send + Sync + 'static>),
} /// Error when `CREATE` tries to create an already existing graph
GraphAlreadyExists(NamedNode),
/// An error returned during the query evaluation itself (not supported custom function...). /// Error when `DROP` or `CLEAR` tries to remove a not existing graph
#[derive(Debug)] GraphDoesNotExist(NamedNode),
pub struct QueryError { /// The variable storing the `SERVICE` name is unbound
inner: QueryErrorKind, UnboundService,
} /// The given `SERVICE` is not supported
UnsupportedService(NamedNode),
#[derive(Debug)] /// The given content media type returned from an HTTP response is not supported (`SERVICE` and `LOAD`)
enum QueryErrorKind { UnsupportedContentType(String),
Msg { msg: String }, /// The `SERVICE` call has not returns solutions
Other(Box<dyn error::Error + Send + Sync + 'static>), ServiceDoesNotReturnSolutions,
/// The results are not a RDF graph
NotAGraph,
} }
impl fmt::Display for EvaluationError { impl fmt::Display for EvaluationError {
@ -43,64 +48,50 @@ impl fmt::Display for EvaluationError {
Self::Storage(error) => error.fmt(f), Self::Storage(error) => error.fmt(f),
Self::GraphParsing(error) => error.fmt(f), Self::GraphParsing(error) => error.fmt(f),
Self::ResultsParsing(error) => error.fmt(f), Self::ResultsParsing(error) => error.fmt(f),
Self::Io(error) => error.fmt(f), Self::ResultsSerialization(error) => error.fmt(f),
Self::Query(error) => error.fmt(f), Self::Service(error) => error.fmt(f),
Self::GraphAlreadyExists(graph) => write!(f, "The graph {graph} already exists"),
Self::GraphDoesNotExist(graph) => write!(f, "The graph {graph} does not exist"),
Self::UnboundService => write!(f, "The variable encoding the service name is unbound"),
Self::UnsupportedService(service) => {
write!(f, "The service {service} is not supported")
}
Self::UnsupportedContentType(content_type) => {
write!(f, "The content media type {content_type} is not supported")
}
Self::ServiceDoesNotReturnSolutions => write!(
f,
"The service is not returning solutions but a boolean or a graph"
),
Self::NotAGraph => write!(f, "The query results are not a RDF graph"),
} }
} }
} }
impl fmt::Display for QueryError { impl Error for EvaluationError {
#[inline] #[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.inner {
QueryErrorKind::Msg { msg } => write!(f, "{msg}"),
QueryErrorKind::Other(error) => error.fmt(f),
}
}
}
impl error::Error for EvaluationError {
#[inline]
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self { match self {
Self::Parsing(e) => Some(e), Self::Parsing(e) => Some(e),
Self::Storage(e) => Some(e), Self::Storage(e) => Some(e),
Self::GraphParsing(e) => Some(e), Self::GraphParsing(e) => Some(e),
Self::ResultsParsing(e) => Some(e), Self::ResultsParsing(e) => Some(e),
Self::Io(e) => Some(e), Self::ResultsSerialization(e) => Some(e),
Self::Query(e) => Some(e), Self::Service(e) => {
} let e = Box::as_ref(e);
} Some(e)
} }
Self::GraphAlreadyExists(_)
impl error::Error for QueryError { | Self::GraphDoesNotExist(_)
#[inline] | Self::UnboundService
fn source(&self) -> Option<&(dyn error::Error + 'static)> { | Self::UnsupportedService(_)
match &self.inner { | Self::UnsupportedContentType(_)
QueryErrorKind::Msg { .. } => None, | Self::ServiceDoesNotReturnSolutions
QueryErrorKind::Other(e) => Some(e.as_ref()), | Self::NotAGraph => None,
} }
} }
} }
impl EvaluationError {
/// Wraps another error.
#[inline]
pub(crate) fn wrap(error: impl error::Error + Send + Sync + 'static) -> Self {
Self::Query(QueryError {
inner: QueryErrorKind::Other(Box::new(error)),
})
}
/// Builds an error from a printable error message.
#[inline]
pub(crate) fn msg(msg: impl Into<String>) -> Self {
Self::Query(QueryError {
inner: QueryErrorKind::Msg { msg: msg.into() },
})
}
}
impl From<Infallible> for EvaluationError { impl From<Infallible> for EvaluationError {
#[inline] #[inline]
fn from(error: Infallible) -> Self { fn from(error: Infallible) -> Self {
@ -108,9 +99,9 @@ impl From<Infallible> for EvaluationError {
} }
} }
impl From<spargebra::ParseError> for EvaluationError { impl From<ParseError> for EvaluationError {
#[inline] #[inline]
fn from(error: spargebra::ParseError) -> Self { fn from(error: ParseError) -> Self {
Self::Parsing(error) Self::Parsing(error)
} }
} }
@ -122,26 +113,16 @@ impl From<StorageError> for EvaluationError {
} }
} }
impl From<io::Error> for EvaluationError { impl From<RdfParseError> for EvaluationError {
#[inline] #[inline]
fn from(error: io::Error) -> Self { fn from(error: RdfParseError) -> Self {
Self::Io(error) Self::GraphParsing(error)
}
}
impl From<ParseError> for EvaluationError {
#[inline]
fn from(error: ParseError) -> Self {
match error {
ParseError::Syntax(error) => Self::GraphParsing(error),
ParseError::Io(error) => Self::Io(error),
}
} }
} }
impl From<sparesults::ParseError> for EvaluationError { impl From<ResultsParseError> for EvaluationError {
#[inline] #[inline]
fn from(error: sparesults::ParseError) -> Self { fn from(error: ResultsParseError) -> Self {
Self::ResultsParsing(error) Self::ResultsParsing(error)
} }
} }
@ -153,9 +134,19 @@ impl From<EvaluationError> for io::Error {
EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error), EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error),
EvaluationError::GraphParsing(error) => error.into(), EvaluationError::GraphParsing(error) => error.into(),
EvaluationError::ResultsParsing(error) => error.into(), EvaluationError::ResultsParsing(error) => error.into(),
EvaluationError::Io(error) => error, EvaluationError::ResultsSerialization(error) => error,
EvaluationError::Storage(error) => error.into(), EvaluationError::Storage(error) => error.into(),
EvaluationError::Query(error) => Self::new(io::ErrorKind::Other, error), EvaluationError::Service(error) => match error.downcast() {
Ok(error) => *error,
Err(error) => Self::new(io::ErrorKind::Other, error),
},
EvaluationError::GraphAlreadyExists(_)
| EvaluationError::GraphDoesNotExist(_)
| EvaluationError::UnboundService
| EvaluationError::UnsupportedService(_)
| EvaluationError::UnsupportedContentType(_)
| EvaluationError::ServiceDoesNotReturnSolutions
| EvaluationError::NotAGraph => Self::new(io::ErrorKind::InvalidInput, error),
} }
} }
} }

@ -1077,7 +1077,7 @@ impl SimpleEvaluator {
) -> Result<EncodedTuplesIterator, EvaluationError> { ) -> Result<EncodedTuplesIterator, EvaluationError> {
let service_name = service_name let service_name = service_name
.get_pattern_value(from) .get_pattern_value(from)
.ok_or_else(|| EvaluationError::msg("The SERVICE name is not bound"))?; .ok_or(EvaluationError::UnboundService)?;
if let QueryResults::Solutions(iter) = self.service_handler.handle( if let QueryResults::Solutions(iter) = self.service_handler.handle(
self.dataset.decode_named_node(&service_name)?, self.dataset.decode_named_node(&service_name)?,
Query { Query {
@ -1092,9 +1092,7 @@ impl SimpleEvaluator {
)? { )? {
Ok(encode_bindings(Rc::clone(&self.dataset), variables, iter)) Ok(encode_bindings(Rc::clone(&self.dataset), variables, iter))
} else { } else {
Err(EvaluationError::msg( Err(EvaluationError::ServiceDoesNotReturnSolutions)
"The service call has not returned a set of solutions",
))
} }
} }

@ -8,13 +8,14 @@ mod error;
mod eval; mod eval;
mod http; mod http;
mod model; mod model;
pub mod results;
mod service; mod service;
mod update; mod update;
use crate::model::{NamedNode, Term}; use crate::model::{NamedNode, Term};
pub use crate::sparql::algebra::{Query, QueryDataset, Update}; pub use crate::sparql::algebra::{Query, QueryDataset, Update};
use crate::sparql::dataset::DatasetView; use crate::sparql::dataset::DatasetView;
pub use crate::sparql::error::{EvaluationError, QueryError}; pub use crate::sparql::error::EvaluationError;
use crate::sparql::eval::{EvalNodeWithStats, SimpleEvaluator, Timer}; use crate::sparql::eval::{EvalNodeWithStats, SimpleEvaluator, Timer};
pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter}; pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter};
pub use crate::sparql::service::ServiceHandler; pub use crate::sparql::service::ServiceHandler;
@ -24,7 +25,6 @@ use crate::storage::StorageReader;
use json_event_parser::{JsonEvent, JsonWriter}; use json_event_parser::{JsonEvent, JsonWriter};
pub use oxrdf::{Variable, VariableNameParseError}; pub use oxrdf::{Variable, VariableNameParseError};
use oxsdatatypes::{DayTimeDuration, Float}; use oxsdatatypes::{DayTimeDuration, Float};
pub use sparesults::QueryResultsFormat;
pub use spargebra::ParseError; pub use spargebra::ParseError;
use sparopt::algebra::GraphPattern; use sparopt::algebra::GraphPattern;
use sparopt::Optimizer; use sparopt::Optimizer;

@ -1,12 +1,12 @@
use crate::io::{RdfFormat, RdfSerializer}; use crate::io::{RdfFormat, RdfSerializer};
use crate::model::*; use crate::model::*;
use crate::sparql::error::EvaluationError; use crate::sparql::error::EvaluationError;
use oxrdf::{Variable, VariableRef}; use crate::sparql::results::{
pub use sparesults::QuerySolution;
use sparesults::{
ParseError, QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer, ParseError, QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer,
SolutionsReader, SolutionsReader,
}; };
use oxrdf::{Variable, VariableRef};
pub use sparesults::QuerySolution;
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use std::rc::Rc; use std::rc::Rc;
@ -38,7 +38,7 @@ impl QueryResults {
/// ``` /// ```
/// use oxigraph::store::Store; /// use oxigraph::store::Store;
/// use oxigraph::model::*; /// use oxigraph::model::*;
/// use oxigraph::sparql::QueryResultsFormat; /// use oxigraph::sparql::results::QueryResultsFormat;
/// ///
/// let store = Store::new()?; /// let store = Store::new()?;
/// let ex = NamedNodeRef::new("http://example.com")?; /// let ex = NamedNodeRef::new("http://example.com")?;
@ -57,33 +57,43 @@ impl QueryResults {
let serializer = QueryResultsSerializer::from_format(format); let serializer = QueryResultsSerializer::from_format(format);
match self { match self {
Self::Boolean(value) => { Self::Boolean(value) => {
serializer.write_boolean_result(writer, value)?; serializer
.write_boolean_result(writer, value)
.map_err(EvaluationError::ResultsSerialization)?;
} }
Self::Solutions(solutions) => { Self::Solutions(solutions) => {
let mut writer = let mut writer = serializer
serializer.solutions_writer(writer, solutions.variables().to_vec())?; .solutions_writer(writer, solutions.variables().to_vec())
.map_err(EvaluationError::ResultsSerialization)?;
for solution in solutions { for solution in solutions {
writer.write(&solution?)?; writer
.write(&solution?)
.map_err(EvaluationError::ResultsSerialization)?;
} }
writer.finish()?; writer
.finish()
.map_err(EvaluationError::ResultsSerialization)?;
} }
Self::Graph(triples) => { Self::Graph(triples) => {
let s = VariableRef::new_unchecked("subject"); let s = VariableRef::new_unchecked("subject");
let p = VariableRef::new_unchecked("predicate"); let p = VariableRef::new_unchecked("predicate");
let o = VariableRef::new_unchecked("object"); let o = VariableRef::new_unchecked("object");
let mut writer = serializer.solutions_writer( let mut writer = serializer
writer, .solutions_writer(writer, vec![s.into_owned(), p.into_owned(), o.into_owned()])
vec![s.into_owned(), p.into_owned(), o.into_owned()], .map_err(EvaluationError::ResultsSerialization)?;
)?;
for triple in triples { for triple in triples {
let triple = triple?; let triple = triple?;
writer.write([ writer
(s, &triple.subject.into()), .write([
(p, &triple.predicate.into()), (s, &triple.subject.into()),
(o, &triple.object), (p, &triple.predicate.into()),
])?; (o, &triple.object),
])
.map_err(EvaluationError::ResultsSerialization)?;
} }
writer.finish()?; writer
.finish()
.map_err(EvaluationError::ResultsSerialization)?;
} }
} }
Ok(()) Ok(())
@ -116,14 +126,16 @@ impl QueryResults {
if let Self::Graph(triples) = self { if let Self::Graph(triples) = self {
let mut writer = RdfSerializer::from_format(format.into()).serialize_to_write(write); let mut writer = RdfSerializer::from_format(format.into()).serialize_to_write(write);
for triple in triples { for triple in triples {
writer.write_triple(&triple?)?; writer
.write_triple(&triple?)
.map_err(EvaluationError::ResultsSerialization)?;
} }
writer.finish()?; writer
.finish()
.map_err(EvaluationError::ResultsSerialization)?;
Ok(()) Ok(())
} else { } else {
Err(EvaluationError::msg( Err(EvaluationError::NotAGraph)
"Bindings or booleans could not be formatted as an RDF graph",
))
} }
} }
} }

@ -0,0 +1,47 @@
//! Utilities to read and write RDF results formats using [sparesults](https://crates.io/crates/sparesults).
//!
//! It supports [SPARQL Query Results XML Format (Second Edition)](https://www.w3.org/TR/rdf-sparql-XMLres/), [SPARQL 1.1 Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) and [SPARQL 1.1 Query Results CSV and TSV Formats](https://www.w3.org/TR/sparql11-results-csv-tsv/).
//!
//! Usage example converting a JSON result file into a TSV result file:
//!
//! ```
//! use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer};
//! use std::io::Result;
//!
//! fn convert_json_to_tsv(json_file: &[u8]) -> Result<Vec<u8>> {
//! let json_parser = QueryResultsParser::from_format(QueryResultsFormat::Json);
//! let tsv_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Tsv);
//! // We start to read the JSON file and see which kind of results it is
//! match json_parser.read_results(json_file)? {
//! QueryResultsReader::Boolean(value) => {
//! // it's a boolean result, we copy it in TSV to the output buffer
//! tsv_serializer.write_boolean_result(Vec::new(), value)
//! },
//! QueryResultsReader::Solutions(solutions_reader) => {
//! // it's a set of solutions, we create a writer and we write to it while reading in streaming from the JSON file
//! let mut solutions_writer = tsv_serializer.solutions_writer(Vec::new(), solutions_reader.variables().to_vec())?;
//! for solution in solutions_reader {
//! solutions_writer.write(&solution?)?;
//! }
//! solutions_writer.finish()
//! }
//! }
//! }
//!
//! // Let's test with a boolean
//! assert_eq!(
//! convert_json_to_tsv(b"{\"boolean\":true}".as_slice()).unwrap(),
//! b"true"
//! );
//!
//! // And with a set of solutions
//! assert_eq!(
//! convert_json_to_tsv(b"{\"head\":{\"vars\":[\"foo\",\"bar\"]},\"results\":{\"bindings\":[{\"foo\":{\"type\":\"literal\",\"value\":\"test\"}}]}}".as_slice()).unwrap(),
//! b"?foo\t?bar\n\"test\"\t\n"
//! );
//! ```
pub use sparesults::{
ParseError, QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer,
SolutionsReader, SyntaxError,
};

@ -3,7 +3,7 @@ use crate::sparql::algebra::Query;
use crate::sparql::error::EvaluationError; use crate::sparql::error::EvaluationError;
use crate::sparql::http::Client; use crate::sparql::http::Client;
use crate::sparql::model::QueryResults; use crate::sparql::model::QueryResults;
use crate::sparql::QueryResultsFormat; use crate::sparql::results::QueryResultsFormat;
use std::error::Error; use std::error::Error;
use std::io::BufReader; use std::io::BufReader;
use std::time::Duration; use std::time::Duration;
@ -61,10 +61,8 @@ pub struct EmptyServiceHandler;
impl ServiceHandler for EmptyServiceHandler { impl ServiceHandler for EmptyServiceHandler {
type Error = EvaluationError; type Error = EvaluationError;
fn handle(&self, _: NamedNode, _: Query) -> Result<QueryResults, EvaluationError> { fn handle(&self, name: NamedNode, _: Query) -> Result<QueryResults, EvaluationError> {
Err(EvaluationError::msg( Err(EvaluationError::UnsupportedService(name))
"The SERVICE feature is not implemented",
))
} }
} }
@ -88,7 +86,7 @@ impl<S: ServiceHandler> ServiceHandler for ErrorConversionServiceHandler<S> {
) -> Result<QueryResults, EvaluationError> { ) -> Result<QueryResults, EvaluationError> {
self.handler self.handler
.handle(service_name, query) .handle(service_name, query)
.map_err(EvaluationError::wrap) .map_err(|e| EvaluationError::Service(Box::new(e)))
} }
} }
@ -112,17 +110,17 @@ impl ServiceHandler for SimpleServiceHandler {
service_name: NamedNode, service_name: NamedNode,
query: Query, query: Query,
) -> Result<QueryResults, EvaluationError> { ) -> Result<QueryResults, EvaluationError> {
let (content_type, body) = self.client.post( let (content_type, body) = self
service_name.as_str(), .client
query.to_string().into_bytes(), .post(
"application/sparql-query", service_name.as_str(),
"application/sparql-results+json, application/sparql-results+xml", query.to_string().into_bytes(),
)?; "application/sparql-query",
let format = QueryResultsFormat::from_media_type(&content_type).ok_or_else(|| { "application/sparql-results+json, application/sparql-results+xml",
EvaluationError::msg(format!( )
"Unsupported Content-Type returned by {service_name}: {content_type}" .map_err(|e| EvaluationError::Service(Box::new(e)))?;
)) let format = QueryResultsFormat::from_media_type(&content_type)
})?; .ok_or_else(|| EvaluationError::UnsupportedContentType(content_type))?;
Ok(QueryResults::read(BufReader::new(body), format)?) Ok(QueryResults::read(BufReader::new(body), format)?)
} }
} }

@ -17,6 +17,7 @@ use spargebra::term::{
use spargebra::GraphUpdateOperation; use spargebra::GraphUpdateOperation;
use sparopt::Optimizer; use sparopt::Optimizer;
use std::collections::HashMap; use std::collections::HashMap;
use std::io;
use std::rc::Rc; use std::rc::Rc;
pub fn evaluate_update<'a, 'b: 'a>( pub fn evaluate_update<'a, 'b: 'a>(
@ -71,9 +72,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
} => self.eval_delete_insert( } => self.eval_delete_insert(
delete, delete,
insert, insert,
using_dataset using_dataset.as_ref().unwrap_or(&QueryDataset::new()),
.as_ref()
.ok_or_else(|| EvaluationError::msg("No dataset"))?,
pattern, pattern,
), ),
GraphUpdateOperation::Load { GraphUpdateOperation::Load {
@ -161,15 +160,15 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
} }
fn eval_load(&mut self, from: &NamedNode, to: &GraphName) -> Result<(), EvaluationError> { fn eval_load(&mut self, from: &NamedNode, to: &GraphName) -> Result<(), EvaluationError> {
let (content_type, body) = self.client.get( let (content_type, body) = self
from.as_str(), .client
"application/n-triples, text/turtle, application/rdf+xml", .get(
)?; from.as_str(),
let format = RdfFormat::from_media_type(&content_type).ok_or_else(|| { "application/n-triples, text/turtle, application/rdf+xml",
EvaluationError::msg(format!( )
"Unsupported Content-Type returned by {from}: {content_type}" .map_err(|e| EvaluationError::Service(Box::new(e)))?;
)) let format = RdfFormat::from_media_type(&content_type)
})?; .ok_or_else(|| EvaluationError::UnsupportedContentType(content_type))?;
let to_graph_name = match to { let to_graph_name = match to {
GraphName::NamedNode(graph_name) => graph_name.into(), GraphName::NamedNode(graph_name) => graph_name.into(),
GraphName::DefaultGraph => GraphNameRef::DefaultGraph, GraphName::DefaultGraph => GraphNameRef::DefaultGraph,
@ -178,11 +177,12 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
.rename_blank_nodes() .rename_blank_nodes()
.without_named_graphs() .without_named_graphs()
.with_default_graph(to_graph_name); .with_default_graph(to_graph_name);
if let Some(base_iri) = &self.base_iri { parser = parser.with_base_iri(from.as_str()).map_err(|e| {
parser = parser.with_base_iri(base_iri.as_str()).map_err(|e| { EvaluationError::Service(Box::new(io::Error::new(
EvaluationError::msg(format!("The LOAD IRI '{base_iri}' is invalid: {e}")) io::ErrorKind::InvalidInput,
})?; format!("Invalid URL: {from}: {e}"),
} )))
})?;
for q in parser.parse_read(body) { for q in parser.parse_read(body) {
self.transaction.insert(q?.as_ref())?; self.transaction.insert(q?.as_ref())?;
} }
@ -193,9 +193,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
if self.transaction.insert_named_graph(graph_name.into())? || silent { if self.transaction.insert_named_graph(graph_name.into())? || silent {
Ok(()) Ok(())
} else { } else {
Err(EvaluationError::msg(format!( Err(EvaluationError::GraphAlreadyExists(graph_name.clone()))
"The graph {graph_name} already exists"
)))
} }
} }
@ -211,9 +209,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
} else if silent { } else if silent {
Ok(()) Ok(())
} else { } else {
Err(EvaluationError::msg(format!( Err(EvaluationError::GraphDoesNotExist(graph_name.clone()))
"The graph {graph} does not exists"
)))
} }
} }
GraphTarget::DefaultGraph => { GraphTarget::DefaultGraph => {
@ -231,9 +227,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
if self.transaction.remove_named_graph(graph_name.into())? || silent { if self.transaction.remove_named_graph(graph_name.into())? || silent {
Ok(()) Ok(())
} else { } else {
Err(EvaluationError::msg(format!( Err(EvaluationError::GraphDoesNotExist(graph_name.clone()))
"The graph {graph_name} does not exists"
)))
} }
} }
GraphTarget::DefaultGraph => { GraphTarget::DefaultGraph => {

@ -233,9 +233,21 @@ pub fn map_evaluation_error(error: EvaluationError) -> PyErr {
match error { match error {
EvaluationError::Parsing(error) => PySyntaxError::new_err(error.to_string()), EvaluationError::Parsing(error) => PySyntaxError::new_err(error.to_string()),
EvaluationError::Storage(error) => map_storage_error(error), EvaluationError::Storage(error) => map_storage_error(error),
EvaluationError::Io(error) => map_io_err(error), EvaluationError::GraphParsing(error) => match error {
EvaluationError::GraphParsing(error) => PySyntaxError::new_err(error.to_string()), oxigraph::io::ParseError::Syntax(error) => PySyntaxError::new_err(error.to_string()),
EvaluationError::Query(error) => PyValueError::new_err(error.to_string()), oxigraph::io::ParseError::Io(error) => map_io_err(error),
},
EvaluationError::ResultsParsing(error) => match error {
oxigraph::sparql::results::ParseError::Syntax(error) => {
PySyntaxError::new_err(error.to_string())
}
oxigraph::sparql::results::ParseError::Io(error) => map_io_err(error),
},
EvaluationError::ResultsSerialization(error) => map_io_err(error),
EvaluationError::Service(error) => match error.downcast() {
Ok(error) => map_io_err(*error),
Err(error) => PyRuntimeError::new_err(error.to_string()),
},
_ => PyRuntimeError::new_err(error.to_string()), _ => PyRuntimeError::new_err(error.to_string()),
} }
} }

@ -19,7 +19,6 @@ anyhow = "1"
oxhttp = { version = "0.1", features = ["rayon"] } oxhttp = { version = "0.1", features = ["rayon"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
oxigraph = { version = "0.4.0-alpha.1-dev", path = "../lib", features = ["http_client"] } oxigraph = { version = "0.4.0-alpha.1-dev", path = "../lib", features = ["http_client"] }
sparesults = { version = "0.2.0-alpha.1-dev", path = "../lib/sparesults", features = ["rdf-star"] }
rand = "0.8" rand = "0.8"
url = "2" url = "2"
oxiri = "0.2" oxiri = "0.2"

@ -8,12 +8,12 @@ use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::{ use oxigraph::model::{
GraphName, GraphNameRef, IriParseError, NamedNode, NamedNodeRef, NamedOrBlankNode, GraphName, GraphNameRef, IriParseError, NamedNode, NamedNodeRef, NamedOrBlankNode,
}; };
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsSerializer};
use oxigraph::sparql::{Query, QueryOptions, QueryResults, Update}; use oxigraph::sparql::{Query, QueryOptions, QueryResults, Update};
use oxigraph::store::{BulkLoader, LoaderError, Store}; use oxigraph::store::{BulkLoader, LoaderError, Store};
use oxiri::Iri; use oxiri::Iri;
use rand::random; use rand::random;
use rayon_core::ThreadPoolBuilder; use rayon_core::ThreadPoolBuilder;
use sparesults::{QueryResultsFormat, QueryResultsSerializer};
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp::{max, min}; use std::cmp::{max, min};

@ -6,6 +6,7 @@ use crate::vocab::*;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use oxigraph::model::vocab::*; use oxigraph::model::vocab::*;
use oxigraph::model::*; use oxigraph::model::*;
use oxigraph::sparql::results::QueryResultsFormat;
use oxigraph::sparql::*; use oxigraph::sparql::*;
use oxigraph::store::Store; use oxigraph::store::Store;
use sparopt::Optimizer; use sparopt::Optimizer;
@ -339,10 +340,10 @@ impl ServiceHandler for StaticServiceHandler {
self.services self.services
.get(&service_name) .get(&service_name)
.ok_or_else(|| { .ok_or_else(|| {
io::Error::new( EvaluationError::Service(Box::new(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
format!("Service {service_name} not found"), format!("Service {service_name} not found"),
) )))
})? })?
.query_opt( .query_opt(
query, query,

Loading…
Cancel
Save