diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2cdcd9..898fd54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Master + +### Added +- `QueryOptions` now allows settings the query dataset graph URIs (the SPARQL protocol `default-graph-uri` and `named-graph-uri` parameters). +- `pyoxigraph` store `query` methods allows to provide the dataset graph URIs. It also provides an option to use all graph names as the default graph. +- "default graph as union option" now works with FROM NAMED. + ## [0.1.0-rc.1] - 2020-08-01 ### Added diff --git a/lib/src/sparql/dataset.rs b/lib/src/sparql/dataset.rs index 5558698c..be7a6533 100644 --- a/lib/src/sparql/dataset.rs +++ b/lib/src/sparql/dataset.rs @@ -94,7 +94,7 @@ impl ReadableEncodedStore for DatasetView { if let Some(dataset) = &self.dataset { if let Some(graph_name) = graph_name { if graph_name == EncodedTerm::DefaultGraph { - let iters = dataset + let mut iters = dataset .default .iter() .map(|graph_name| { @@ -106,6 +106,16 @@ impl ReadableEncodedStore for DatasetView { ) }) .collect::>(); + if self.default_graph_as_union { + iters.extend(dataset.named.iter().map(|graph_name| { + self.store.encoded_quads_for_pattern( + subject, + predicate, + object, + Some(*graph_name), + ) + })); + } Box::new(map_iter(iters.into_iter().flatten()).map(|quad| { let quad = quad?; Ok(EncodedQuad::new( diff --git a/lib/src/sparql/mod.rs b/lib/src/sparql/mod.rs index 014d32af..656d3d61 100644 --- a/lib/src/sparql/mod.rs +++ b/lib/src/sparql/mod.rs @@ -14,7 +14,7 @@ mod plan_builder; mod xml_results; use crate::model::NamedNode; -use crate::sparql::algebra::QueryVariants; +use crate::sparql::algebra::{DatasetSpec, QueryVariants}; use crate::sparql::dataset::DatasetView; pub use crate::sparql::error::EvaluationError; use crate::sparql::eval::SimpleEvaluator; @@ -76,7 +76,11 @@ impl SimplePreparedQuery { let dataset = Rc::new(DatasetView::new( store, options.default_graph_as_union, - &dataset, + if options.dataset.is_empty() { + &dataset + } else { + &options.dataset + }, )?); let (plan, variables) = PlanBuilder::build(dataset.as_ref(), &algebra)?; SimplePreparedQueryAction::Select { @@ -93,7 +97,11 @@ impl SimplePreparedQuery { let dataset = Rc::new(DatasetView::new( store, options.default_graph_as_union, - &dataset, + if options.dataset.is_empty() { + &dataset + } else { + &options.dataset + }, )?); let (plan, _) = PlanBuilder::build(dataset.as_ref(), &algebra)?; SimplePreparedQueryAction::Ask { @@ -110,7 +118,11 @@ impl SimplePreparedQuery { let dataset = Rc::new(DatasetView::new( store, options.default_graph_as_union, - &dataset, + if options.dataset.is_empty() { + &dataset + } else { + &options.dataset + }, )?); let (plan, variables) = PlanBuilder::build(dataset.as_ref(), &algebra)?; SimplePreparedQueryAction::Construct { @@ -131,7 +143,11 @@ impl SimplePreparedQuery { let dataset = Rc::new(DatasetView::new( store, options.default_graph_as_union, - &dataset, + if options.dataset.is_empty() { + &dataset + } else { + &options.dataset + }, )?); let (plan, _) = PlanBuilder::build(dataset.as_ref(), &algebra)?; SimplePreparedQueryAction::Describe { @@ -167,13 +183,16 @@ impl SimplePreparedQuery { #[derive(Clone)] pub struct QueryOptions { pub(crate) default_graph_as_union: bool, + pub(crate) dataset: DatasetSpec, pub(crate) service_handler: Rc>, } impl Default for QueryOptions { + #[inline] fn default() -> Self { Self { default_graph_as_union: false, + dataset: DatasetSpec::default(), service_handler: Rc::new(EmptyServiceHandler), } } @@ -181,12 +200,30 @@ impl Default for QueryOptions { impl QueryOptions { /// Consider the union of all graphs in the store as the default graph - pub const fn with_default_graph_as_union(mut self) -> Self { + #[inline] + pub fn with_default_graph_as_union(mut self) -> Self { self.default_graph_as_union = true; self } + /// Adds a named graph to the set of graphs considered by the SPARQL query as the queried dataset default graph + /// Overrides the `FROM` and `FROM NAMED` elements of the evaluated query + #[inline] + pub fn with_default_graph(mut self, default_graph_name: impl Into) -> Self { + self.dataset.default.push(default_graph_name.into()); + self + } + + /// Adds a named graph to the set of graphs considered by the SPARQL query as the queried dataset named graphs + /// Overrides the `FROM` and `FROM NAMED` elements of the evaluated query + #[inline] + pub fn with_named_graph(mut self, named_graph_name: impl Into) -> Self { + self.dataset.named.push(named_graph_name.into()); + self + } + /// Use a given [`ServiceHandler`](trait.ServiceHandler.html) to execute [SPARQL 1.1 Federated Query](https://www.w3.org/TR/sparql11-federated-query/) SERVICE calls. + #[inline] pub fn with_service_handler(mut self, service_handler: impl ServiceHandler + 'static) -> Self { self.service_handler = Rc::new(ErrorConversionServiceHandler { handler: service_handler, diff --git a/python/src/memory_store.rs b/python/src/memory_store.rs index 5e25a662..f04ce93b 100644 --- a/python/src/memory_store.rs +++ b/python/src/memory_store.rs @@ -3,7 +3,6 @@ use crate::model::*; use crate::store_utils::*; use oxigraph::io::{DatasetFormat, GraphFormat}; use oxigraph::model::*; -use oxigraph::sparql::QueryOptions; use oxigraph::MemoryStore; use pyo3::basic::CompareOp; use pyo3::exceptions::{NotImplementedError, ValueError}; @@ -108,8 +107,12 @@ impl PyMemoryStore { /// /// :param query: the query to execute /// :type query: str - /// :param use_default_graph_as_union: if the SPARQL query should look for triples in all the dataset graphs by default (i.e. without `GRAPH` operations). Disabled by default. + /// :param use_default_graph_as_union: optional, if the SPARQL query should look for triples in all the dataset graphs by default (i.e. without `GRAPH` operations). Disabled by default. /// :type use_default_graph_as_union: bool + /// :param default_graph_uris: optional, list of the named graph URIs that should be used as the query default graph. By default the store default graph is used. + /// :type default_graph_uris: list(NamedNode),None + /// :param named_graph_uris: optional, list of the named graph URIs that could be used in SPARQL `GRAPH` clause. By default all the store default graphs are available. + /// :type named_graph_uris: list(NamedNode),None /// :return: a :py:class:`bool` for ``ASK`` queries, an iterator of :py:class:`Triple` for ``CONSTRUCT`` and ``DESCRIBE`` queries and an iterator of solution bindings for ``SELECT`` queries. /// :rtype: iter(QuerySolution) or iter(Triple) or bool /// :raises SyntaxError: if the provided query is invalid @@ -134,19 +137,28 @@ impl PyMemoryStore { /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.query('ASK { ?s ?p ?o }') /// True - #[text_signature = "($self, query, *, use_default_graph_as_union)"] - #[args(query, "*", use_default_graph_as_union = "false")] + #[text_signature = "($self, query, *, use_default_graph_as_union, default_graph_uris, named_graph_uris)"] + #[args( + query, + "*", + use_default_graph_as_union = "false", + default_graph_uris = "None", + named_graph_uris = "None" + )] fn query( &self, query: &str, use_default_graph_as_union: bool, + default_graph_uris: Option>, + named_graph_uris: Option>, py: Python<'_>, ) -> PyResult { let results = py.allow_threads(move || { - let mut options = QueryOptions::default(); - if use_default_graph_as_union { - options = options.with_default_graph_as_union(); - } + let options = build_query_options( + use_default_graph_as_union, + default_graph_uris, + named_graph_uris, + )?; self.inner .query(query, options) .map_err(map_evaluation_error) diff --git a/python/src/sled_store.rs b/python/src/sled_store.rs index 7abe94c8..1cd81513 100644 --- a/python/src/sled_store.rs +++ b/python/src/sled_store.rs @@ -3,7 +3,6 @@ use crate::model::*; use crate::store_utils::*; use oxigraph::io::{DatasetFormat, GraphFormat}; use oxigraph::model::*; -use oxigraph::sparql::QueryOptions; use oxigraph::SledStore; use pyo3::exceptions::ValueError; use pyo3::prelude::*; @@ -123,8 +122,12 @@ impl PySledStore { /// /// :param query: the query to execute /// :type query: str - /// :param use_default_graph_as_union: if the SPARQL query should look for triples in all the dataset graphs by default (i.e. without `GRAPH` operations). Disabled by default. + /// :param use_default_graph_as_union: optional, if the SPARQL query should look for triples in all the dataset graphs by default (i.e. without `GRAPH` operations). Disabled by default. /// :type use_default_graph_as_union: bool + /// :param default_graph_uris: optional, list of the named graph URIs that should be used as the query default graph. By default the store default graph is used. + /// :type default_graph_uris: list(NamedNode),None + /// :param named_graph_uris: optional, list of the named graph URIs that could be used in SPARQL `GRAPH` clause. By default all the store default graphs are available. + /// :type named_graph_uris: list(NamedNode),None /// :return: a :py:class:`bool` for ``ASK`` queries, an iterator of :py:class:`Triple` for ``CONSTRUCT`` and ``DESCRIBE`` queries and an iterator of solution bindings for ``SELECT`` queries. /// :rtype: iter(QuerySolution) or iter(Triple) or bool /// :raises SyntaxError: if the provided query is invalid @@ -150,19 +153,28 @@ impl PySledStore { /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.query('ASK { ?s ?p ?o }') /// True - #[text_signature = "($self, query, *, use_default_graph_as_union)"] - #[args(query, "*", use_default_graph_as_union = "false")] + #[text_signature = "($self, query, *, use_default_graph_as_union, default_graph_uris, named_graph_uris)"] + #[args( + query, + "*", + use_default_graph_as_union = "false", + default_graph_uris = "None", + named_graph_uris = "None" + )] fn query( &self, query: &str, use_default_graph_as_union: bool, + default_graph_uris: Option>, + named_graph_uris: Option>, py: Python<'_>, ) -> PyResult { let results = py.allow_threads(move || { - let mut options = QueryOptions::default(); - if use_default_graph_as_union { - options = options.with_default_graph_as_union(); - } + let options = build_query_options( + use_default_graph_as_union, + default_graph_uris, + named_graph_uris, + )?; self.inner .query(query, options) .map_err(map_evaluation_error) diff --git a/python/src/store_utils.rs b/python/src/store_utils.rs index bf7a8cef..694bd836 100644 --- a/python/src/store_utils.rs +++ b/python/src/store_utils.rs @@ -1,7 +1,7 @@ use crate::model::*; use oxigraph::model::*; use oxigraph::sparql::{ - EvaluationError, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter, + EvaluationError, QueryOptions, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter, }; use pyo3::exceptions::{IOError, RuntimeError, SyntaxError, TypeError, ValueError}; use pyo3::prelude::*; @@ -47,6 +47,38 @@ pub fn extract_quads_pattern( )) } +pub fn build_query_options( + use_default_graph_as_union: bool, + default_graph_uris: Option>, + named_graph_uris: Option>, +) -> PyResult { + let mut options = QueryOptions::default(); + if use_default_graph_as_union { + options = options.with_default_graph_as_union(); + } + if let Some(default_graph_uris) = default_graph_uris { + if default_graph_uris.is_empty() { + return Err(ValueError::py_err( + "The list of the default graph URIs could not be empty", + )); + } + for default_graph_uri in default_graph_uris { + options = options.with_default_graph(default_graph_uri); + } + } + if let Some(named_graph_uris) = named_graph_uris { + if named_graph_uris.is_empty() { + return Err(ValueError::py_err( + "The list of the named graph URIs could not be empty", + )); + } + for named_graph_uri in named_graph_uris { + options = options.with_named_graph(named_graph_uri); + } + } + Ok(options) +} + pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyResult { Ok(match results { QueryResults::Solutions(inner) => PyQuerySolutionIter { inner }.into_py(py), diff --git a/python/tests/test_store.py b/python/tests/test_store.py index 10446edd..a8b7c8d5 100644 --- a/python/tests/test_store.py +++ b/python/tests/test_store.py @@ -102,6 +102,14 @@ class TestAbstractStore(unittest.TestCase, ABC): store.add(Quad(foo, bar, baz, graph)) self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }"))), 0) self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }", use_default_graph_as_union=True))), 1) + self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }", use_default_graph_as_union=True, named_graph_uris=[graph]))), 1) + + def test_select_query_with_default_graph(self): + store = self.store() + store.add(Quad(foo, bar, baz, graph)) + self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }"))), 0) + self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }", default_graph_uris=[graph]))), 1) + self.assertEqual(len(list(store.query("SELECT ?s WHERE { GRAPH ?g { ?s ?p ?o } }", named_graph_uris=[graph]))), 1) def test_load_ntriples_to_default_graph(self): store = self.store() diff --git a/server/src/main.rs b/server/src/main.rs index dc3c790b..21b4a180 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -122,7 +122,7 @@ async fn handle_request(request: Request, store: RocksDbStore) -> Result, request: Request, ) -> Result { - if let Some((_, query)) = form_urlencoded::parse(&encoded).find(|(k, _)| k == "query") { - evaluate_sparql_query(store, query.to_string(), request).await + let mut query = None; + let mut default_graph_uris = Vec::new(); + let mut named_graph_uris = Vec::new(); + for (k, v) in form_urlencoded::parse(&encoded) { + match k.as_ref() { + "query" => query = Some(v.into_owned()), + "default-graph-uri" => default_graph_uris.push(v.into_owned()), + "named-graph-uri" => named_graph_uris.push(v.into_owned()), + _ => { + return Ok(simple_response( + StatusCode::BadRequest, + format!("Unexpected parameter: {}", k), + )) + } + } + } + if let Some(query) = query { + evaluate_sparql_query(store, query, default_graph_uris, named_graph_uris, request).await } else { Ok(simple_response( StatusCode::BadRequest, @@ -172,6 +188,8 @@ async fn evaluate_urlencoded_sparql_query( async fn evaluate_sparql_query( store: RocksDbStore, query: String, + default_graph_uris: Vec, + named_graph_uris: Vec, request: Request, ) -> Result { spawn_blocking(move || { @@ -180,7 +198,24 @@ async fn evaluate_sparql_query( e.set_status(StatusCode::BadRequest); e })?; - let options = QueryOptions::default().with_service_handler(HttpService::default()); + + let mut options = QueryOptions::default().with_service_handler(HttpService::default()); + for default_graph_uri in default_graph_uris { + options = + options.with_default_graph(NamedNode::new(default_graph_uri).map_err(|e| { + let mut e = Error::from(e); + e.set_status(StatusCode::BadRequest); + e + })?) + } + for named_graph_uri in named_graph_uris { + options = options.with_named_graph(NamedNode::new(named_graph_uri).map_err(|e| { + let mut e = Error::from(e); + e.set_status(StatusCode::BadRequest); + e + })?) + } + let results = store.query(query, options)?; //TODO: stream if let QueryResults::Graph(_) = results {