Provides the ability to specify the SPARQL query dataset using the API

pull/46/head
Tpt 4 years ago
parent 5e77253624
commit 496a6e1d8c
  1. 7
      CHANGELOG.md
  2. 12
      lib/src/sparql/dataset.rs
  3. 49
      lib/src/sparql/mod.rs
  4. 28
      python/src/memory_store.rs
  5. 28
      python/src/sled_store.rs
  6. 34
      python/src/store_utils.rs
  7. 8
      python/tests/test_store.py
  8. 43
      server/src/main.rs

@ -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 ## [0.1.0-rc.1] - 2020-08-01
### Added ### Added

@ -94,7 +94,7 @@ impl<S: ReadableEncodedStore> ReadableEncodedStore for DatasetView<S> {
if let Some(dataset) = &self.dataset { if let Some(dataset) = &self.dataset {
if let Some(graph_name) = graph_name { if let Some(graph_name) = graph_name {
if graph_name == EncodedTerm::DefaultGraph { if graph_name == EncodedTerm::DefaultGraph {
let iters = dataset let mut iters = dataset
.default .default
.iter() .iter()
.map(|graph_name| { .map(|graph_name| {
@ -106,6 +106,16 @@ impl<S: ReadableEncodedStore> ReadableEncodedStore for DatasetView<S> {
) )
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
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| { Box::new(map_iter(iters.into_iter().flatten()).map(|quad| {
let quad = quad?; let quad = quad?;
Ok(EncodedQuad::new( Ok(EncodedQuad::new(

@ -14,7 +14,7 @@ mod plan_builder;
mod xml_results; mod xml_results;
use crate::model::NamedNode; use crate::model::NamedNode;
use crate::sparql::algebra::QueryVariants; use crate::sparql::algebra::{DatasetSpec, QueryVariants};
use crate::sparql::dataset::DatasetView; use crate::sparql::dataset::DatasetView;
pub use crate::sparql::error::EvaluationError; pub use crate::sparql::error::EvaluationError;
use crate::sparql::eval::SimpleEvaluator; use crate::sparql::eval::SimpleEvaluator;
@ -76,7 +76,11 @@ impl<S: ReadableEncodedStore + 'static> SimplePreparedQuery<S> {
let dataset = Rc::new(DatasetView::new( let dataset = Rc::new(DatasetView::new(
store, store,
options.default_graph_as_union, options.default_graph_as_union,
&dataset, if options.dataset.is_empty() {
&dataset
} else {
&options.dataset
},
)?); )?);
let (plan, variables) = PlanBuilder::build(dataset.as_ref(), &algebra)?; let (plan, variables) = PlanBuilder::build(dataset.as_ref(), &algebra)?;
SimplePreparedQueryAction::Select { SimplePreparedQueryAction::Select {
@ -93,7 +97,11 @@ impl<S: ReadableEncodedStore + 'static> SimplePreparedQuery<S> {
let dataset = Rc::new(DatasetView::new( let dataset = Rc::new(DatasetView::new(
store, store,
options.default_graph_as_union, options.default_graph_as_union,
&dataset, if options.dataset.is_empty() {
&dataset
} else {
&options.dataset
},
)?); )?);
let (plan, _) = PlanBuilder::build(dataset.as_ref(), &algebra)?; let (plan, _) = PlanBuilder::build(dataset.as_ref(), &algebra)?;
SimplePreparedQueryAction::Ask { SimplePreparedQueryAction::Ask {
@ -110,7 +118,11 @@ impl<S: ReadableEncodedStore + 'static> SimplePreparedQuery<S> {
let dataset = Rc::new(DatasetView::new( let dataset = Rc::new(DatasetView::new(
store, store,
options.default_graph_as_union, options.default_graph_as_union,
&dataset, if options.dataset.is_empty() {
&dataset
} else {
&options.dataset
},
)?); )?);
let (plan, variables) = PlanBuilder::build(dataset.as_ref(), &algebra)?; let (plan, variables) = PlanBuilder::build(dataset.as_ref(), &algebra)?;
SimplePreparedQueryAction::Construct { SimplePreparedQueryAction::Construct {
@ -131,7 +143,11 @@ impl<S: ReadableEncodedStore + 'static> SimplePreparedQuery<S> {
let dataset = Rc::new(DatasetView::new( let dataset = Rc::new(DatasetView::new(
store, store,
options.default_graph_as_union, options.default_graph_as_union,
&dataset, if options.dataset.is_empty() {
&dataset
} else {
&options.dataset
},
)?); )?);
let (plan, _) = PlanBuilder::build(dataset.as_ref(), &algebra)?; let (plan, _) = PlanBuilder::build(dataset.as_ref(), &algebra)?;
SimplePreparedQueryAction::Describe { SimplePreparedQueryAction::Describe {
@ -167,13 +183,16 @@ impl<S: ReadableEncodedStore + 'static> SimplePreparedQuery<S> {
#[derive(Clone)] #[derive(Clone)]
pub struct QueryOptions { pub struct QueryOptions {
pub(crate) default_graph_as_union: bool, pub(crate) default_graph_as_union: bool,
pub(crate) dataset: DatasetSpec,
pub(crate) service_handler: Rc<dyn ServiceHandler<Error = EvaluationError>>, pub(crate) service_handler: Rc<dyn ServiceHandler<Error = EvaluationError>>,
} }
impl Default for QueryOptions { impl Default for QueryOptions {
#[inline]
fn default() -> Self { fn default() -> Self {
Self { Self {
default_graph_as_union: false, default_graph_as_union: false,
dataset: DatasetSpec::default(),
service_handler: Rc::new(EmptyServiceHandler), service_handler: Rc::new(EmptyServiceHandler),
} }
} }
@ -181,12 +200,30 @@ impl Default for QueryOptions {
impl QueryOptions { impl QueryOptions {
/// Consider the union of all graphs in the store as the default graph /// 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.default_graph_as_union = true;
self 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<NamedNode>) -> 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<NamedNode>) -> 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. /// 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 { pub fn with_service_handler(mut self, service_handler: impl ServiceHandler + 'static) -> Self {
self.service_handler = Rc::new(ErrorConversionServiceHandler { self.service_handler = Rc::new(ErrorConversionServiceHandler {
handler: service_handler, handler: service_handler,

@ -3,7 +3,6 @@ use crate::model::*;
use crate::store_utils::*; use crate::store_utils::*;
use oxigraph::io::{DatasetFormat, GraphFormat}; use oxigraph::io::{DatasetFormat, GraphFormat};
use oxigraph::model::*; use oxigraph::model::*;
use oxigraph::sparql::QueryOptions;
use oxigraph::MemoryStore; use oxigraph::MemoryStore;
use pyo3::basic::CompareOp; use pyo3::basic::CompareOp;
use pyo3::exceptions::{NotImplementedError, ValueError}; use pyo3::exceptions::{NotImplementedError, ValueError};
@ -108,8 +107,12 @@ impl PyMemoryStore {
/// ///
/// :param query: the query to execute /// :param query: the query to execute
/// :type query: str /// :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 /// :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. /// :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 /// :rtype: iter(QuerySolution) or iter(Triple) or bool
/// :raises SyntaxError: if the provided query is invalid /// :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.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> store.query('ASK { ?s ?p ?o }') /// >>> store.query('ASK { ?s ?p ?o }')
/// True /// True
#[text_signature = "($self, query, *, use_default_graph_as_union)"] #[text_signature = "($self, query, *, use_default_graph_as_union, default_graph_uris, named_graph_uris)"]
#[args(query, "*", use_default_graph_as_union = "false")] #[args(
query,
"*",
use_default_graph_as_union = "false",
default_graph_uris = "None",
named_graph_uris = "None"
)]
fn query( fn query(
&self, &self,
query: &str, query: &str,
use_default_graph_as_union: bool, use_default_graph_as_union: bool,
default_graph_uris: Option<Vec<PyNamedNode>>,
named_graph_uris: Option<Vec<PyNamedNode>>,
py: Python<'_>, py: Python<'_>,
) -> PyResult<PyObject> { ) -> PyResult<PyObject> {
let results = py.allow_threads(move || { let results = py.allow_threads(move || {
let mut options = QueryOptions::default(); let options = build_query_options(
if use_default_graph_as_union { use_default_graph_as_union,
options = options.with_default_graph_as_union(); default_graph_uris,
} named_graph_uris,
)?;
self.inner self.inner
.query(query, options) .query(query, options)
.map_err(map_evaluation_error) .map_err(map_evaluation_error)

@ -3,7 +3,6 @@ use crate::model::*;
use crate::store_utils::*; use crate::store_utils::*;
use oxigraph::io::{DatasetFormat, GraphFormat}; use oxigraph::io::{DatasetFormat, GraphFormat};
use oxigraph::model::*; use oxigraph::model::*;
use oxigraph::sparql::QueryOptions;
use oxigraph::SledStore; use oxigraph::SledStore;
use pyo3::exceptions::ValueError; use pyo3::exceptions::ValueError;
use pyo3::prelude::*; use pyo3::prelude::*;
@ -123,8 +122,12 @@ impl PySledStore {
/// ///
/// :param query: the query to execute /// :param query: the query to execute
/// :type query: str /// :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 /// :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. /// :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 /// :rtype: iter(QuerySolution) or iter(Triple) or bool
/// :raises SyntaxError: if the provided query is invalid /// :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.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> store.query('ASK { ?s ?p ?o }') /// >>> store.query('ASK { ?s ?p ?o }')
/// True /// True
#[text_signature = "($self, query, *, use_default_graph_as_union)"] #[text_signature = "($self, query, *, use_default_graph_as_union, default_graph_uris, named_graph_uris)"]
#[args(query, "*", use_default_graph_as_union = "false")] #[args(
query,
"*",
use_default_graph_as_union = "false",
default_graph_uris = "None",
named_graph_uris = "None"
)]
fn query( fn query(
&self, &self,
query: &str, query: &str,
use_default_graph_as_union: bool, use_default_graph_as_union: bool,
default_graph_uris: Option<Vec<PyNamedNode>>,
named_graph_uris: Option<Vec<PyNamedNode>>,
py: Python<'_>, py: Python<'_>,
) -> PyResult<PyObject> { ) -> PyResult<PyObject> {
let results = py.allow_threads(move || { let results = py.allow_threads(move || {
let mut options = QueryOptions::default(); let options = build_query_options(
if use_default_graph_as_union { use_default_graph_as_union,
options = options.with_default_graph_as_union(); default_graph_uris,
} named_graph_uris,
)?;
self.inner self.inner
.query(query, options) .query(query, options)
.map_err(map_evaluation_error) .map_err(map_evaluation_error)

@ -1,7 +1,7 @@
use crate::model::*; use crate::model::*;
use oxigraph::model::*; use oxigraph::model::*;
use oxigraph::sparql::{ use oxigraph::sparql::{
EvaluationError, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter, EvaluationError, QueryOptions, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter,
}; };
use pyo3::exceptions::{IOError, RuntimeError, SyntaxError, TypeError, ValueError}; use pyo3::exceptions::{IOError, RuntimeError, SyntaxError, TypeError, ValueError};
use pyo3::prelude::*; 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<Vec<PyNamedNode>>,
named_graph_uris: Option<Vec<PyNamedNode>>,
) -> PyResult<QueryOptions> {
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<PyObject> { pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyResult<PyObject> {
Ok(match results { Ok(match results {
QueryResults::Solutions(inner) => PyQuerySolutionIter { inner }.into_py(py), QueryResults::Solutions(inner) => PyQuerySolutionIter { inner }.into_py(py),

@ -102,6 +102,14 @@ class TestAbstractStore(unittest.TestCase, ABC):
store.add(Quad(foo, bar, baz, graph)) 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 }"))), 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))), 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): def test_load_ntriples_to_default_graph(self):
store = self.store() store = self.store()

@ -122,7 +122,7 @@ async fn handle_request(request: Request, store: RocksDbStore) -> Result<Respons
.take(MAX_SPARQL_BODY_SIZE) .take(MAX_SPARQL_BODY_SIZE)
.read_to_string(&mut buffer) .read_to_string(&mut buffer)
.await?; .await?;
evaluate_sparql_query(store, buffer, request).await? evaluate_sparql_query(store, buffer, Vec::new(), Vec::new(), request).await?
} else if content_type.essence() == "application/x-www-form-urlencoded" { } else if content_type.essence() == "application/x-www-form-urlencoded" {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
let mut request = request; let mut request = request;
@ -159,8 +159,24 @@ async fn evaluate_urlencoded_sparql_query(
encoded: Vec<u8>, encoded: Vec<u8>,
request: Request, request: Request,
) -> Result<Response> { ) -> Result<Response> {
if let Some((_, query)) = form_urlencoded::parse(&encoded).find(|(k, _)| k == "query") { let mut query = None;
evaluate_sparql_query(store, query.to_string(), request).await 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 { } else {
Ok(simple_response( Ok(simple_response(
StatusCode::BadRequest, StatusCode::BadRequest,
@ -172,6 +188,8 @@ async fn evaluate_urlencoded_sparql_query(
async fn evaluate_sparql_query( async fn evaluate_sparql_query(
store: RocksDbStore, store: RocksDbStore,
query: String, query: String,
default_graph_uris: Vec<String>,
named_graph_uris: Vec<String>,
request: Request, request: Request,
) -> Result<Response> { ) -> Result<Response> {
spawn_blocking(move || { spawn_blocking(move || {
@ -180,7 +198,24 @@ async fn evaluate_sparql_query(
e.set_status(StatusCode::BadRequest); e.set_status(StatusCode::BadRequest);
e 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)?; let results = store.query(query, options)?;
//TODO: stream //TODO: stream
if let QueryResults::Graph(_) = results { if let QueryResults::Graph(_) = results {

Loading…
Cancel
Save