From 7fe055d2b45690159af41bb0f1b49ed58f099e25 Mon Sep 17 00:00:00 2001 From: Tpt Date: Thu, 31 Aug 2023 18:45:13 +0200 Subject: [PATCH] Exposes SPARQL results I/O in Oxigraph and improve EvaluationError --- Cargo.lock | 1 - lib/oxrdfio/README.md | 25 ++--- lib/src/io/mod.rs | 26 ++++- lib/src/lib.rs | 12 +-- lib/src/model.rs | 25 +++++ lib/src/sparql/error.rs | 159 ++++++++++++++---------------- lib/src/sparql/eval.rs | 6 +- lib/src/sparql/mod.rs | 4 +- lib/src/sparql/model.rs | 60 ++++++----- lib/src/sparql/results.rs | 47 +++++++++ lib/src/sparql/service.rs | 32 +++--- lib/src/sparql/update.rs | 46 ++++----- python/src/sparql.rs | 18 +++- server/Cargo.toml | 1 - server/src/main.rs | 2 +- testsuite/src/sparql_evaluator.rs | 5 +- 16 files changed, 280 insertions(+), 189 deletions(-) create mode 100644 lib/src/model.rs create mode 100644 lib/src/sparql/results.rs diff --git a/Cargo.lock b/Cargo.lock index fa61e235..50c7f71e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -998,7 +998,6 @@ dependencies = [ "predicates", "rand", "rayon-core", - "sparesults", "url", ] diff --git a/lib/oxrdfio/README.md b/lib/oxrdfio/README.md index 2bdd68d8..318c718c 100644 --- a/lib/oxrdfio/README.md +++ b/lib/oxrdfio/README.md @@ -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). -Usage example counting the number of people in a Turtle file: +Usage example converting a Turtle file to a N-Triples file: ```rust -use oxrdf::{NamedNodeRef, vocab::rdf}; -use oxrdfio::{RdfFormat, RdfParser}; +use oxrdfio::{RdfFormat, RdfParser, RdfSerializer}; -let file = b"@base . +let turtle_file = b"@base . @prefix schema: . a schema:Person ; schema:name \"Foo\" . a schema:Person ; schema:name \"Bar\" ."; -let schema_person = NamedNodeRef::new("http://schema.org/Person").unwrap(); -let mut count = 0; -for quad in RdfParser::from_format(RdfFormat::Turtle).parse_read(file.as_ref()) { - let quad = quad.unwrap(); - if quad.predicate == rdf::TYPE && quad.object == schema_person.into() { - count += 1; - } +let ntriples_file = b" . + \"Foo\" . + . + \"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!(2, count); +assert_eq!(writer.finish().unwrap(), ntriples_file); ``` ## License diff --git a/lib/src/io/mod.rs b/lib/src/io/mod.rs index f183157d..2e4dd2f4 100644 --- a/lib/src/io/mod.rs +++ b/lib/src/io/mod.rs @@ -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 . +//! @prefix schema: . +//! a schema:Person ; +//! schema:name \"Foo\" . +//! a schema:Person ; +//! schema:name \"Bar\" ."; +//! +//! let ntriples_file = b" . +//! \"Foo\" . +//! . +//! \"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; pub mod read; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e5a680d8..b36c4d6c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -6,17 +6,7 @@ #![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] pub mod io; +pub mod model; pub mod sparql; mod storage; 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, - }; -} diff --git a/lib/src/model.rs b/lib/src/model.rs new file mode 100644 index 00000000..ef8cbd1d --- /dev/null +++ b/lib/src/model.rs @@ -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, +}; diff --git a/lib/src/sparql/error.rs b/lib/src/sparql/error.rs index 62e5d821..4728efb7 100644 --- a/lib/src/sparql/error.rs +++ b/lib/src/sparql/error.rs @@ -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 std::convert::Infallible; -use std::error; +use std::error::Error; use std::fmt; use std::io; @@ -10,29 +13,31 @@ use std::io; #[non_exhaustive] pub enum EvaluationError { /// An error in SPARQL parsing. - Parsing(spargebra::ParseError), + Parsing(ParseError), /// An error from the storage. Storage(StorageError), /// 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). - ResultsParsing(sparesults::ParseError), - /// An error returned during store or results I/Os. - Io(io::Error), - /// An error returned during the query evaluation itself (not supported custom function...). - Query(QueryError), -} - -/// An error returned during the query evaluation itself (not supported custom function...). -#[derive(Debug)] -pub struct QueryError { - inner: QueryErrorKind, -} - -#[derive(Debug)] -enum QueryErrorKind { - Msg { msg: String }, - Other(Box), + ResultsParsing(ResultsParseError), + /// An error returned during results serialization. + ResultsSerialization(io::Error), + /// Error during `SERVICE` evaluation + Service(Box), + /// Error when `CREATE` tries to create an already existing graph + GraphAlreadyExists(NamedNode), + /// Error when `DROP` or `CLEAR` tries to remove a not existing graph + GraphDoesNotExist(NamedNode), + /// The variable storing the `SERVICE` name is unbound + UnboundService, + /// The given `SERVICE` is not supported + UnsupportedService(NamedNode), + /// The given content media type returned from an HTTP response is not supported (`SERVICE` and `LOAD`) + UnsupportedContentType(String), + /// The `SERVICE` call has not returns solutions + ServiceDoesNotReturnSolutions, + /// The results are not a RDF graph + NotAGraph, } impl fmt::Display for EvaluationError { @@ -43,64 +48,50 @@ impl fmt::Display for EvaluationError { Self::Storage(error) => error.fmt(f), Self::GraphParsing(error) => error.fmt(f), Self::ResultsParsing(error) => error.fmt(f), - Self::Io(error) => error.fmt(f), - Self::Query(error) => error.fmt(f), + Self::ResultsSerialization(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] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - 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)> { + fn source(&self) -> Option<&(dyn Error + 'static)> { match self { Self::Parsing(e) => Some(e), Self::Storage(e) => Some(e), Self::GraphParsing(e) => Some(e), Self::ResultsParsing(e) => Some(e), - Self::Io(e) => Some(e), - Self::Query(e) => Some(e), - } - } -} - -impl error::Error for QueryError { - #[inline] - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match &self.inner { - QueryErrorKind::Msg { .. } => None, - QueryErrorKind::Other(e) => Some(e.as_ref()), + Self::ResultsSerialization(e) => Some(e), + Self::Service(e) => { + let e = Box::as_ref(e); + Some(e) + } + Self::GraphAlreadyExists(_) + | Self::GraphDoesNotExist(_) + | Self::UnboundService + | Self::UnsupportedService(_) + | Self::UnsupportedContentType(_) + | Self::ServiceDoesNotReturnSolutions + | 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) -> Self { - Self::Query(QueryError { - inner: QueryErrorKind::Msg { msg: msg.into() }, - }) - } -} - impl From for EvaluationError { #[inline] fn from(error: Infallible) -> Self { @@ -108,9 +99,9 @@ impl From for EvaluationError { } } -impl From for EvaluationError { +impl From for EvaluationError { #[inline] - fn from(error: spargebra::ParseError) -> Self { + fn from(error: ParseError) -> Self { Self::Parsing(error) } } @@ -122,26 +113,16 @@ impl From for EvaluationError { } } -impl From for EvaluationError { +impl From for EvaluationError { #[inline] - fn from(error: io::Error) -> Self { - Self::Io(error) - } -} - -impl From for EvaluationError { - #[inline] - fn from(error: ParseError) -> Self { - match error { - ParseError::Syntax(error) => Self::GraphParsing(error), - ParseError::Io(error) => Self::Io(error), - } + fn from(error: RdfParseError) -> Self { + Self::GraphParsing(error) } } -impl From for EvaluationError { +impl From for EvaluationError { #[inline] - fn from(error: sparesults::ParseError) -> Self { + fn from(error: ResultsParseError) -> Self { Self::ResultsParsing(error) } } @@ -153,9 +134,19 @@ impl From for io::Error { EvaluationError::Parsing(error) => Self::new(io::ErrorKind::InvalidData, error), EvaluationError::GraphParsing(error) => error.into(), EvaluationError::ResultsParsing(error) => error.into(), - EvaluationError::Io(error) => error, + EvaluationError::ResultsSerialization(error) => error, 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), } } } diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index fb1737d6..ed9feba7 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -1077,7 +1077,7 @@ impl SimpleEvaluator { ) -> Result { let service_name = service_name .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( self.dataset.decode_named_node(&service_name)?, Query { @@ -1092,9 +1092,7 @@ impl SimpleEvaluator { )? { Ok(encode_bindings(Rc::clone(&self.dataset), variables, iter)) } else { - Err(EvaluationError::msg( - "The service call has not returned a set of solutions", - )) + Err(EvaluationError::ServiceDoesNotReturnSolutions) } } diff --git a/lib/src/sparql/mod.rs b/lib/src/sparql/mod.rs index 95249203..6d3ef593 100644 --- a/lib/src/sparql/mod.rs +++ b/lib/src/sparql/mod.rs @@ -8,13 +8,14 @@ mod error; mod eval; mod http; mod model; +pub mod results; mod service; mod update; use crate::model::{NamedNode, Term}; pub use crate::sparql::algebra::{Query, QueryDataset, Update}; 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}; pub use crate::sparql::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter}; pub use crate::sparql::service::ServiceHandler; @@ -24,7 +25,6 @@ use crate::storage::StorageReader; use json_event_parser::{JsonEvent, JsonWriter}; pub use oxrdf::{Variable, VariableNameParseError}; use oxsdatatypes::{DayTimeDuration, Float}; -pub use sparesults::QueryResultsFormat; pub use spargebra::ParseError; use sparopt::algebra::GraphPattern; use sparopt::Optimizer; diff --git a/lib/src/sparql/model.rs b/lib/src/sparql/model.rs index d076baec..71a25ea9 100644 --- a/lib/src/sparql/model.rs +++ b/lib/src/sparql/model.rs @@ -1,12 +1,12 @@ use crate::io::{RdfFormat, RdfSerializer}; use crate::model::*; use crate::sparql::error::EvaluationError; -use oxrdf::{Variable, VariableRef}; -pub use sparesults::QuerySolution; -use sparesults::{ +use crate::sparql::results::{ ParseError, QueryResultsFormat, QueryResultsParser, QueryResultsReader, QueryResultsSerializer, SolutionsReader, }; +use oxrdf::{Variable, VariableRef}; +pub use sparesults::QuerySolution; use std::io::{BufRead, Write}; use std::rc::Rc; @@ -38,7 +38,7 @@ impl QueryResults { /// ``` /// use oxigraph::store::Store; /// use oxigraph::model::*; - /// use oxigraph::sparql::QueryResultsFormat; + /// use oxigraph::sparql::results::QueryResultsFormat; /// /// let store = Store::new()?; /// let ex = NamedNodeRef::new("http://example.com")?; @@ -57,33 +57,43 @@ impl QueryResults { let serializer = QueryResultsSerializer::from_format(format); match self { Self::Boolean(value) => { - serializer.write_boolean_result(writer, value)?; + serializer + .write_boolean_result(writer, value) + .map_err(EvaluationError::ResultsSerialization)?; } Self::Solutions(solutions) => { - let mut writer = - serializer.solutions_writer(writer, solutions.variables().to_vec())?; + let mut writer = serializer + .solutions_writer(writer, solutions.variables().to_vec()) + .map_err(EvaluationError::ResultsSerialization)?; 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) => { let s = VariableRef::new_unchecked("subject"); let p = VariableRef::new_unchecked("predicate"); let o = VariableRef::new_unchecked("object"); - let mut writer = serializer.solutions_writer( - writer, - vec![s.into_owned(), p.into_owned(), o.into_owned()], - )?; + let mut writer = serializer + .solutions_writer(writer, vec![s.into_owned(), p.into_owned(), o.into_owned()]) + .map_err(EvaluationError::ResultsSerialization)?; for triple in triples { let triple = triple?; - writer.write([ - (s, &triple.subject.into()), - (p, &triple.predicate.into()), - (o, &triple.object), - ])?; + writer + .write([ + (s, &triple.subject.into()), + (p, &triple.predicate.into()), + (o, &triple.object), + ]) + .map_err(EvaluationError::ResultsSerialization)?; } - writer.finish()?; + writer + .finish() + .map_err(EvaluationError::ResultsSerialization)?; } } Ok(()) @@ -116,14 +126,16 @@ impl QueryResults { if let Self::Graph(triples) = self { let mut writer = RdfSerializer::from_format(format.into()).serialize_to_write(write); 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(()) } else { - Err(EvaluationError::msg( - "Bindings or booleans could not be formatted as an RDF graph", - )) + Err(EvaluationError::NotAGraph) } } } diff --git a/lib/src/sparql/results.rs b/lib/src/sparql/results.rs new file mode 100644 index 00000000..26fa287a --- /dev/null +++ b/lib/src/sparql/results.rs @@ -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> { +//! 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, +}; diff --git a/lib/src/sparql/service.rs b/lib/src/sparql/service.rs index 3d6576e2..ae397ee2 100644 --- a/lib/src/sparql/service.rs +++ b/lib/src/sparql/service.rs @@ -3,7 +3,7 @@ use crate::sparql::algebra::Query; use crate::sparql::error::EvaluationError; use crate::sparql::http::Client; use crate::sparql::model::QueryResults; -use crate::sparql::QueryResultsFormat; +use crate::sparql::results::QueryResultsFormat; use std::error::Error; use std::io::BufReader; use std::time::Duration; @@ -61,10 +61,8 @@ pub struct EmptyServiceHandler; impl ServiceHandler for EmptyServiceHandler { type Error = EvaluationError; - fn handle(&self, _: NamedNode, _: Query) -> Result { - Err(EvaluationError::msg( - "The SERVICE feature is not implemented", - )) + fn handle(&self, name: NamedNode, _: Query) -> Result { + Err(EvaluationError::UnsupportedService(name)) } } @@ -88,7 +86,7 @@ impl ServiceHandler for ErrorConversionServiceHandler { ) -> Result { self.handler .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, query: Query, ) -> Result { - let (content_type, body) = self.client.post( - service_name.as_str(), - query.to_string().into_bytes(), - "application/sparql-query", - "application/sparql-results+json, application/sparql-results+xml", - )?; - let format = QueryResultsFormat::from_media_type(&content_type).ok_or_else(|| { - EvaluationError::msg(format!( - "Unsupported Content-Type returned by {service_name}: {content_type}" - )) - })?; + let (content_type, body) = self + .client + .post( + service_name.as_str(), + query.to_string().into_bytes(), + "application/sparql-query", + "application/sparql-results+json, application/sparql-results+xml", + ) + .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)?) } } diff --git a/lib/src/sparql/update.rs b/lib/src/sparql/update.rs index ef4f9c54..1744c464 100644 --- a/lib/src/sparql/update.rs +++ b/lib/src/sparql/update.rs @@ -17,6 +17,7 @@ use spargebra::term::{ use spargebra::GraphUpdateOperation; use sparopt::Optimizer; use std::collections::HashMap; +use std::io; use std::rc::Rc; pub fn evaluate_update<'a, 'b: 'a>( @@ -71,9 +72,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> { } => self.eval_delete_insert( delete, insert, - using_dataset - .as_ref() - .ok_or_else(|| EvaluationError::msg("No dataset"))?, + using_dataset.as_ref().unwrap_or(&QueryDataset::new()), pattern, ), GraphUpdateOperation::Load { @@ -161,15 +160,15 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> { } fn eval_load(&mut self, from: &NamedNode, to: &GraphName) -> Result<(), EvaluationError> { - let (content_type, body) = self.client.get( - from.as_str(), - "application/n-triples, text/turtle, application/rdf+xml", - )?; - let format = RdfFormat::from_media_type(&content_type).ok_or_else(|| { - EvaluationError::msg(format!( - "Unsupported Content-Type returned by {from}: {content_type}" - )) - })?; + let (content_type, body) = self + .client + .get( + from.as_str(), + "application/n-triples, text/turtle, application/rdf+xml", + ) + .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 { GraphName::NamedNode(graph_name) => graph_name.into(), GraphName::DefaultGraph => GraphNameRef::DefaultGraph, @@ -178,11 +177,12 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> { .rename_blank_nodes() .without_named_graphs() .with_default_graph(to_graph_name); - if let Some(base_iri) = &self.base_iri { - parser = parser.with_base_iri(base_iri.as_str()).map_err(|e| { - EvaluationError::msg(format!("The LOAD IRI '{base_iri}' is invalid: {e}")) - })?; - } + parser = parser.with_base_iri(from.as_str()).map_err(|e| { + EvaluationError::Service(Box::new(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid URL: {from}: {e}"), + ))) + })?; for q in parser.parse_read(body) { 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 { Ok(()) } else { - Err(EvaluationError::msg(format!( - "The graph {graph_name} already exists" - ))) + Err(EvaluationError::GraphAlreadyExists(graph_name.clone())) } } @@ -211,9 +209,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> { } else if silent { Ok(()) } else { - Err(EvaluationError::msg(format!( - "The graph {graph} does not exists" - ))) + Err(EvaluationError::GraphDoesNotExist(graph_name.clone())) } } GraphTarget::DefaultGraph => { @@ -231,9 +227,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> { if self.transaction.remove_named_graph(graph_name.into())? || silent { Ok(()) } else { - Err(EvaluationError::msg(format!( - "The graph {graph_name} does not exists" - ))) + Err(EvaluationError::GraphDoesNotExist(graph_name.clone())) } } GraphTarget::DefaultGraph => { diff --git a/python/src/sparql.rs b/python/src/sparql.rs index 408f6f0c..7c99707b 100644 --- a/python/src/sparql.rs +++ b/python/src/sparql.rs @@ -233,9 +233,21 @@ pub fn map_evaluation_error(error: EvaluationError) -> PyErr { match error { EvaluationError::Parsing(error) => PySyntaxError::new_err(error.to_string()), EvaluationError::Storage(error) => map_storage_error(error), - EvaluationError::Io(error) => map_io_err(error), - EvaluationError::GraphParsing(error) => PySyntaxError::new_err(error.to_string()), - EvaluationError::Query(error) => PyValueError::new_err(error.to_string()), + EvaluationError::GraphParsing(error) => match error { + oxigraph::io::ParseError::Syntax(error) => PySyntaxError::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()), } } diff --git a/server/Cargo.toml b/server/Cargo.toml index 01ed8a77..350863f9 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -19,7 +19,6 @@ anyhow = "1" oxhttp = { version = "0.1", features = ["rayon"] } clap = { version = "4", features = ["derive"] } 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" url = "2" oxiri = "0.2" diff --git a/server/src/main.rs b/server/src/main.rs index e32a1fe7..817fed04 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,12 +8,12 @@ use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer}; use oxigraph::model::{ GraphName, GraphNameRef, IriParseError, NamedNode, NamedNodeRef, NamedOrBlankNode, }; +use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsSerializer}; use oxigraph::sparql::{Query, QueryOptions, QueryResults, Update}; use oxigraph::store::{BulkLoader, LoaderError, Store}; use oxiri::Iri; use rand::random; use rayon_core::ThreadPoolBuilder; -use sparesults::{QueryResultsFormat, QueryResultsSerializer}; use std::borrow::Cow; use std::cell::RefCell; use std::cmp::{max, min}; diff --git a/testsuite/src/sparql_evaluator.rs b/testsuite/src/sparql_evaluator.rs index 318da172..543f0538 100644 --- a/testsuite/src/sparql_evaluator.rs +++ b/testsuite/src/sparql_evaluator.rs @@ -6,6 +6,7 @@ use crate::vocab::*; use anyhow::{anyhow, bail, Result}; use oxigraph::model::vocab::*; use oxigraph::model::*; +use oxigraph::sparql::results::QueryResultsFormat; use oxigraph::sparql::*; use oxigraph::store::Store; use sparopt::Optimizer; @@ -339,10 +340,10 @@ impl ServiceHandler for StaticServiceHandler { self.services .get(&service_name) .ok_or_else(|| { - io::Error::new( + EvaluationError::Service(Box::new(io::Error::new( io::ErrorKind::InvalidInput, format!("Service {service_name} not found"), - ) + ))) })? .query_opt( query,