diff --git a/lib/src/sparql/algebra.rs b/lib/src/sparql/algebra.rs index aa0335c4..320801e7 100644 --- a/lib/src/sparql/algebra.rs +++ b/lib/src/sparql/algebra.rs @@ -1391,7 +1391,7 @@ impl fmt::Display for QueryVariants { base_iri, } => { if let Some(base_iri) = base_iri { - writeln!(f, "BASE <{}>\n", base_iri)?; + writeln!(f, "BASE <{}>", base_iri)?; } write!(f, "{}", SparqlGraphRootPattern { algebra, dataset }) } @@ -1402,7 +1402,7 @@ impl fmt::Display for QueryVariants { base_iri, } => { if let Some(base_iri) = base_iri { - writeln!(f, "BASE <{}>\n", base_iri)?; + writeln!(f, "BASE <{}>", base_iri)?; } write!(f, "CONSTRUCT {{ ")?; for triple in construct.iter() { @@ -1424,7 +1424,7 @@ impl fmt::Display for QueryVariants { base_iri, } => { if let Some(base_iri) = base_iri { - writeln!(f, "BASE <{}>\n", base_iri.as_str())?; + writeln!(f, "BASE <{}>", base_iri.as_str())?; } write!( f, @@ -1442,7 +1442,7 @@ impl fmt::Display for QueryVariants { base_iri, } => { if let Some(base_iri) = base_iri { - writeln!(f, "BASE <{}>\n", base_iri)?; + writeln!(f, "BASE <{}>", base_iri)?; } write!( f, @@ -1467,10 +1467,10 @@ pub enum GraphUpdateOperation { DeleteData { data: Vec }, /// [delete insert](https://www.w3.org/TR/sparql11-update/#def_deleteinsertoperation) DeleteInsert { - delete: Option>, - insert: Option>, - using: Rc, - algebra: Rc, + delete: Vec, + insert: Vec, + using: DatasetSpec, + algebra: GraphPattern, }, /// [load](https://www.w3.org/TR/sparql11-update/#def_loadoperation) Load { @@ -1509,14 +1509,14 @@ impl fmt::Display for GraphUpdateOperation { using, algebra, } => { - if let Some(delete) = delete { + if !delete.is_empty() { writeln!(f, "DELETE {{")?; for quad in delete { writeln!(f, "\t{}", quad)?; } writeln!(f, "}}")?; } - if let Some(insert) = insert { + if !insert.is_empty() { writeln!(f, "INSERT {{")?; for quad in insert { writeln!(f, "\t{}", quad)?; diff --git a/lib/src/sparql/dataset.rs b/lib/src/sparql/dataset.rs index 9af9e333..4caaf255 100644 --- a/lib/src/sparql/dataset.rs +++ b/lib/src/sparql/dataset.rs @@ -235,10 +235,10 @@ fn try_map_quad_pattern( graph_name: Option>>, ) -> Option> { Some(( - transpose(subject.map(|t| t.try_map_id(unwrap_store_id)))?, - transpose(predicate.map(|t| t.try_map_id(unwrap_store_id)))?, - transpose(object.map(|t| t.try_map_id(unwrap_store_id)))?, - transpose(graph_name.map(|t| t.try_map_id(unwrap_store_id)))?, + transpose(subject.map(|t| t.try_map_id(unwrap_store_id).ok()))?, + transpose(predicate.map(|t| t.try_map_id(unwrap_store_id).ok()))?, + transpose(object.map(|t| t.try_map_id(unwrap_store_id).ok()))?, + transpose(graph_name.map(|t| t.try_map_id(unwrap_store_id).ok()))?, )) } @@ -250,10 +250,10 @@ fn transpose(o: Option>) -> Option> { } } -fn unwrap_store_id(id: DatasetStrId) -> Option { +fn unwrap_store_id(id: DatasetStrId) -> Result { match id { - DatasetStrId::Store(id) => Some(id), - DatasetStrId::Temporary(_) => None, + DatasetStrId::Store(id) => Ok(id), + DatasetStrId::Temporary(_) => Err(()), } } diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index dc4342c4..7fc1b337 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -120,7 +120,7 @@ where })) } - fn eval_plan( + pub fn eval_plan( &self, node: &PlanNode, from: EncodedTuple, diff --git a/lib/src/sparql/mod.rs b/lib/src/sparql/mod.rs index b7e2c0f1..430b2be3 100644 --- a/lib/src/sparql/mod.rs +++ b/lib/src/sparql/mod.rs @@ -11,6 +11,7 @@ mod model; mod parser; mod plan; mod plan_builder; +mod update; mod xml_results; use crate::model::{GraphName, NamedNode, NamedOrBlankNode}; @@ -28,8 +29,9 @@ pub use crate::sparql::parser::ParseError; pub use crate::sparql::parser::{Query, Update}; use crate::sparql::plan::{PlanNode, TripleTemplate}; use crate::sparql::plan_builder::PlanBuilder; -use crate::store::numeric_encoder::StrEncodingAware; -use crate::store::ReadableEncodedStore; +use crate::sparql::update::SimpleUpdateEvaluator; +use crate::store::numeric_encoder::{StrContainer, StrEncodingAware}; +use crate::store::{ReadableEncodedStore, WritableEncodedStore}; use std::convert::TryInto; use std::error::Error; use std::rc::Rc; @@ -302,3 +304,20 @@ impl ServiceHandler for ErrorConversionServiceHandler { .map_err(EvaluationError::wrap) } } + +pub(crate) fn evaluate_update< + R: ReadableEncodedStore + Clone + 'static, + W: StrContainer + WritableEncodedStore, +>( + read: R, + write: &mut W, + update: &Update, +) -> Result<(), EvaluationError> { + SimpleUpdateEvaluator::new( + read, + write, + update.base_iri.clone(), + Rc::new(EmptyServiceHandler), + ) + .eval_all(&update.operations) +} diff --git a/lib/src/sparql/parser.rs b/lib/src/sparql/parser.rs index 01402022..ec33026a 100644 --- a/lib/src/sparql/parser.rs +++ b/lib/src/sparql/parser.rs @@ -100,11 +100,17 @@ impl<'a> TryFrom<&'a String> for Query { /// # Result::Ok::<_, oxigraph::sparql::ParseError>(()) /// ``` #[derive(Eq, PartialEq, Debug, Clone, Hash)] -pub struct Update(pub(crate) Vec); +pub struct Update { + pub(crate) base_iri: Option>>, + pub(crate) operations: Vec, +} impl fmt::Display for Update { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for update in &self.0 { + if let Some(base_iri) = &self.base_iri { + writeln!(f, "BASE <{}>", base_iri)?; + } + for update in &self.operations { writeln!(f, "{} ;", update)?; } Ok(()) @@ -130,13 +136,14 @@ impl Update { aggregations: Vec::default(), }; - Ok(Self( - parser::UpdateInit(&unescape_unicode_codepoints(update), &mut state).map_err(|e| { - ParseError { - inner: ParseErrorKind::Parser(e), - } - })?, - )) + let operations = parser::UpdateInit(&unescape_unicode_codepoints(update), &mut state) + .map_err(|e| ParseError { + inner: ParseErrorKind::Parser(e), + })?; + Ok(Self { + operations, + base_iri: state.base_iri, + }) } } @@ -474,20 +481,20 @@ fn copy_graph( ) .into()]); GraphUpdateOperation::DeleteInsert { - delete: None, - insert: Some(vec![QuadPattern::new( + delete: Vec::new(), + insert: vec![QuadPattern::new( Variable::new("s"), Variable::new("p"), Variable::new("o"), to.into(), - )]), - using: Rc::new(DatasetSpec::default()), - algebra: Rc::new(match &from { + )], + using: DatasetSpec::default(), + algebra: match from { NamedOrDefaultGraphTarget::NamedNode(from) => { - GraphPattern::Graph(from.clone().into(), Box::new(bgp)) + GraphPattern::Graph(from.into(), Box::new(bgp)) } NamedOrDefaultGraphTarget::DefaultGraph => bgp, - }), + }, } } @@ -1013,38 +1020,40 @@ parser! { } }).fold(GraphPattern::BGP(Vec::new()), new_join); vec![GraphUpdateOperation::DeleteInsert { - delete: Some(d), - insert: None, - using: Rc::new(DatasetSpec::default()), - algebra: Rc::new(algebra) + delete: d, + insert: Vec::new(), + using: DatasetSpec::default(), + algebra }] } //[41] rule Modify() -> Vec = with:Modify_with() _ c:Modify_clauses() _ using:(UsingClause() ** (_)) _ i("WHERE") _ algebra:GroupGraphPattern() { - let (mut delete, mut insert) = c; + let (delete, insert) = c; + let mut delete = delete.unwrap_or_else(Vec::new); + let mut insert = insert.unwrap_or_else(Vec::new); let mut algebra = algebra; if let Some(with) = with { // We inject WITH everywhere - delete = delete.map(|quads| quads.into_iter().map(|q| if q.graph_name.is_none() { + delete = delete.into_iter().map(|q| if q.graph_name.is_none() { QuadPattern::new(q.subject, q.predicate, q.object, Some(with.clone().into())) } else { q - }).collect()); - insert = insert.map(|quads| quads.into_iter().map(|q| if q.graph_name.is_none() { + }).collect(); + insert = insert.into_iter().map(|q| if q.graph_name.is_none() { QuadPattern::new(q.subject, q.predicate, q.object, Some(with.clone().into())) } else { q - }).collect()); + }).collect(); algebra = GraphPattern::Graph(with.into(), Box::new(algebra)); } vec![GraphUpdateOperation::DeleteInsert { delete, insert, - using: Rc::new(using.into_iter().fold(DatasetSpec::default(), |mut a, b| a + b)), - algebra: Rc::new(algebra) + using: using.into_iter().fold(DatasetSpec::default(), |mut a, b| a + b), + algebra }] } rule Modify_with() -> Option = i("WITH") _ i:iri() _ { diff --git a/lib/src/sparql/plan.rs b/lib/src/sparql/plan.rs index 88e47e91..2184a4ec 100644 --- a/lib/src/sparql/plan.rs +++ b/lib/src/sparql/plan.rs @@ -551,3 +551,12 @@ impl EncodedTuple { } } } + +impl IntoIterator for EncodedTuple { + type Item = Option>; + type IntoIter = std::vec::IntoIter>>; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} diff --git a/lib/src/sparql/update.rs b/lib/src/sparql/update.rs new file mode 100644 index 00000000..f6dfe19f --- /dev/null +++ b/lib/src/sparql/update.rs @@ -0,0 +1,403 @@ +use crate::sparql::algebra::{ + DatasetSpec, GraphPattern, GraphTarget, GraphUpdateOperation, NamedNodeOrVariable, QuadPattern, + TermOrVariable, +}; +use crate::sparql::dataset::{DatasetStrId, DatasetView}; +use crate::sparql::eval::SimpleEvaluator; +use crate::sparql::plan::EncodedTuple; +use crate::sparql::plan_builder::PlanBuilder; +use crate::sparql::{EvaluationError, ServiceHandler, Variable}; +use crate::store::numeric_encoder::{ + EncodedQuad, EncodedTerm, ReadEncoder, StrContainer, StrLookup, WriteEncoder, +}; +use crate::store::{ReadableEncodedStore, WritableEncodedStore}; +use oxiri::Iri; +use std::rc::Rc; + +pub(crate) struct SimpleUpdateEvaluator<'a, R, W> { + read: R, + write: &'a mut W, + base_iri: Option>>, + service_handler: Rc>, +} + +impl< + 'a, + R: ReadableEncodedStore + Clone + 'static, + W: StrContainer + WritableEncodedStore + 'a, + > SimpleUpdateEvaluator<'a, R, W> +{ + pub fn new( + read: R, + write: &'a mut W, + base_iri: Option>>, + service_handler: Rc>, + ) -> Self { + Self { + read, + write, + base_iri, + service_handler, + } + } + + pub fn eval_all(&mut self, updates: &[GraphUpdateOperation]) -> Result<(), EvaluationError> { + for update in updates { + self.eval(update)?; + } + Ok(()) + } + + fn eval(&mut self, update: &GraphUpdateOperation) -> Result<(), EvaluationError> { + match update { + GraphUpdateOperation::InsertData { data } => self.eval_insert_data(data), + GraphUpdateOperation::DeleteData { data } => self.eval_delete_data(data), + GraphUpdateOperation::DeleteInsert { + delete, + insert, + using, + algebra, + } => self.eval_delete_insert(delete, insert, using, algebra), + GraphUpdateOperation::Load { .. } => Err(EvaluationError::msg( + "SPARQL UPDATE LOAD operation is not implemented yet", + )), + GraphUpdateOperation::Clear { graph, .. } => self.eval_clear(graph), + GraphUpdateOperation::Create { .. } => Ok(()), + GraphUpdateOperation::Drop { graph, .. } => self.eval_clear(graph), + } + } + + fn eval_insert_data(&mut self, data: &[QuadPattern]) -> Result<(), EvaluationError> { + for quad in data { + if let Some(quad) = self.encode_quad_for_insertion(quad, &[], &[])? { + self.write.insert_encoded(&quad).map_err(to_eval_error)?; + } + } + Ok(()) + } + + fn eval_delete_data(&mut self, data: &[QuadPattern]) -> Result<(), EvaluationError> { + for quad in data { + if let Some(quad) = self.encode_quad_for_deletion(quad, &[], &[])? { + self.write.remove_encoded(&quad).map_err(to_eval_error)?; + } + } + Ok(()) + } + + fn eval_delete_insert( + &mut self, + delete: &[QuadPattern], + insert: &[QuadPattern], + using: &DatasetSpec, + algebra: &GraphPattern, + ) -> Result<(), EvaluationError> { + let dataset = Rc::new(DatasetView::new(self.read.clone(), false, &[], &[], using)?); + let (plan, variables) = PlanBuilder::build(dataset.as_ref(), algebra)?; + let evaluator = SimpleEvaluator::>::new( + dataset.clone(), + self.base_iri.clone(), + self.service_handler.clone(), + ); + for tuple in evaluator.eval_plan(&plan, EncodedTuple::with_capacity(variables.len())) { + // We map the tuple to only get store strings + let tuple = tuple? + .into_iter() + .map(|t| { + Ok(if let Some(t) = t { + Some( + t.try_map_id(|id| { + if let DatasetStrId::Store(s) = id { + Ok(s) + } else { + self.write + .insert_str( + &dataset + .get_str(id) + .map_err(to_eval_error)? + .ok_or_else(|| { + EvaluationError::msg( + "String not stored in the string store", + ) + }) + .map_err(to_eval_error)?, + ) + .map_err(to_eval_error) + } + }) + .map_err(to_eval_error)?, + ) + } else { + None + }) + }) + .collect::, EvaluationError>>()?; + + for quad in delete { + if let Some(quad) = self.encode_quad_for_deletion(quad, &variables, &tuple)? { + self.write.remove_encoded(&quad).map_err(to_eval_error)?; + } + } + for quad in insert { + if let Some(quad) = self.encode_quad_for_insertion(quad, &variables, &tuple)? { + self.write.insert_encoded(&quad).map_err(to_eval_error)?; + } + } + } + Ok(()) + } + + fn eval_clear(&mut self, graph: &GraphTarget) -> Result<(), EvaluationError> { + match graph { + GraphTarget::NamedNode(graph) => { + if let Some(graph) = self + .read + .get_encoded_named_node(graph.into()) + .map_err(to_eval_error)? + { + for quad in self + .read + .encoded_quads_for_pattern(None, None, None, Some(graph)) + { + self.write + .remove_encoded(&quad.map_err(to_eval_error)?) + .map_err(to_eval_error)?; + } + } else { + //we do not track created graph so it's fine + } + } + GraphTarget::DefaultGraph => { + for quad in self.read.encoded_quads_for_pattern( + None, + None, + None, + Some(EncodedTerm::DefaultGraph), + ) { + self.write + .remove_encoded(&quad.map_err(to_eval_error)?) + .map_err(to_eval_error)?; + } + } + GraphTarget::NamedGraphs => { + for quad in self.read.encoded_quads_for_pattern(None, None, None, None) { + let quad = quad.map_err(to_eval_error)?; + if !quad.graph_name.is_default_graph() { + self.write.remove_encoded(&quad).map_err(to_eval_error)?; + } + } + } + GraphTarget::AllGraphs => { + for quad in self.read.encoded_quads_for_pattern(None, None, None, None) { + self.write + .remove_encoded(&quad.map_err(to_eval_error)?) + .map_err(to_eval_error)?; + } + } + }; + Ok(()) + } + + fn encode_quad_for_insertion( + &mut self, + quad: &QuadPattern, + variables: &[Variable], + values: &[Option>], + ) -> Result>, EvaluationError> { + Ok(Some(EncodedQuad { + subject: if let Some(subject) = + self.encode_term_for_insertion(&quad.subject, variables, values, |t| { + t.is_named_node() || t.is_blank_node() + })? { + subject + } else { + return Ok(None); + }, + predicate: if let Some(predicate) = + self.encode_named_node_for_insertion(&quad.predicate, variables, values)? + { + predicate + } else { + return Ok(None); + }, + object: if let Some(object) = + self.encode_term_for_insertion(&quad.object, variables, values, |t| { + !t.is_default_graph() + })? { + object + } else { + return Ok(None); + }, + graph_name: if let Some(graph_name) = &quad.graph_name { + if let Some(graph_name) = + self.encode_named_node_for_insertion(graph_name, variables, values)? + { + graph_name + } else { + return Ok(None); + } + } else { + EncodedTerm::DefaultGraph + }, + })) + } + + fn encode_term_for_insertion( + &mut self, + term: &TermOrVariable, + variables: &[Variable], + values: &[Option>], + validate: impl FnOnce(&EncodedTerm) -> bool, + ) -> Result>, EvaluationError> { + Ok(match term { + TermOrVariable::Term(term) => { + Some(self.write.encode_term(term.into()).map_err(to_eval_error)?) + } + TermOrVariable::Variable(v) => { + if let Some(Some(term)) = variables + .iter() + .position(|v2| v == v2) + .and_then(|i| values.get(i)) + { + if validate(term) { + Some(*term) + } else { + None + } + } else { + None + } + } + }) + } + + fn encode_named_node_for_insertion( + &mut self, + term: &NamedNodeOrVariable, + variables: &[Variable], + values: &[Option>], + ) -> Result>, EvaluationError> { + Ok(match term { + NamedNodeOrVariable::NamedNode(term) => Some( + self.write + .encode_named_node(term.into()) + .map_err(to_eval_error)?, + ), + NamedNodeOrVariable::Variable(v) => { + if let Some(Some(term)) = variables + .iter() + .position(|v2| v == v2) + .and_then(|i| values.get(i)) + { + if term.is_named_node() { + Some(*term) + } else { + None + } + } else { + None + } + } + }) + } + + fn encode_quad_for_deletion( + &self, + quad: &QuadPattern, + variables: &[Variable], + values: &[Option>], + ) -> Result>, EvaluationError> { + Ok(Some(EncodedQuad { + subject: if let Some(subject) = + self.encode_term_for_deletion(&quad.subject, variables, values)? + { + subject + } else { + return Ok(None); + }, + predicate: if let Some(predicate) = + self.encode_named_node_for_deletion(&quad.predicate, variables, values)? + { + predicate + } else { + return Ok(None); + }, + object: if let Some(object) = + self.encode_term_for_deletion(&quad.object, variables, values)? + { + object + } else { + return Ok(None); + }, + graph_name: if let Some(graph_name) = &quad.graph_name { + if let Some(graph_name) = + self.encode_named_node_for_deletion(graph_name, variables, values)? + { + graph_name + } else { + return Ok(None); + } + } else { + EncodedTerm::DefaultGraph + }, + })) + } + + fn encode_term_for_deletion( + &self, + term: &TermOrVariable, + variables: &[Variable], + values: &[Option>], + ) -> Result>, EvaluationError> { + Ok(match term { + TermOrVariable::Term(term) => self + .read + .get_encoded_term(term.into()) + .map_err(to_eval_error)?, + TermOrVariable::Variable(v) => { + if let Some(Some(term)) = variables + .iter() + .position(|v2| v == v2) + .and_then(|i| values.get(i)) + { + Some(*term) + } else { + None + } + } + }) + } + + fn encode_named_node_for_deletion( + &self, + term: &NamedNodeOrVariable, + variables: &[Variable], + values: &[Option>], + ) -> Result>, EvaluationError> { + Ok(match term { + NamedNodeOrVariable::NamedNode(term) => self + .read + .get_encoded_named_node(term.into()) + .map_err(to_eval_error)?, + NamedNodeOrVariable::Variable(v) => { + if let Some(Some(term)) = variables + .iter() + .position(|v2| v == v2) + .and_then(|i| values.get(i)) + { + if term.is_named_node() { + Some(*term) + } else { + None + } + } else { + None + } + } + }) + } +} + +fn to_eval_error(e: impl Into) -> EvaluationError { + e.into() +} diff --git a/lib/src/store/memory.rs b/lib/src/store/memory.rs index f9d9eafb..b427cc01 100644 --- a/lib/src/store/memory.rs +++ b/lib/src/store/memory.rs @@ -3,7 +3,10 @@ use crate::error::{invalid_input_error, UnwrapInfallible}; use crate::io::{DatasetFormat, DatasetParser, GraphFormat, GraphParser}; use crate::model::*; -use crate::sparql::{EvaluationError, Query, QueryOptions, QueryResults, SimplePreparedQuery}; +use crate::sparql::{ + evaluate_update, EvaluationError, Query, QueryOptions, QueryResults, SimplePreparedQuery, + Update, +}; use crate::store::numeric_encoder::{ Decoder, ReadEncoder, StrContainer, StrEncodingAware, StrId, StrLookup, WriteEncoder, }; @@ -233,6 +236,38 @@ impl MemoryStore { indexes.default_spo.is_empty() && indexes.spog.is_empty() } + /// Executes a [SPARQL 1.1 update](https://www.w3.org/TR/sparql11-update/). + /// + /// The [`LOAD` operation](https://www.w3.org/TR/sparql11-update/#load) is not supported yet. + /// The store does not track the existence of empty named graphs. + /// This method has no ACID guarantees. + /// + /// Usage example: + /// ``` + /// use oxigraph::MemoryStore; + /// use oxigraph::model::*; + /// + /// let store = MemoryStore::new(); + /// + /// // insertion + /// store.update("INSERT DATA { }")?; + /// + /// // we inspect the store contents + /// let ex = NamedNodeRef::new("http://example.com").unwrap(); + /// assert!(store.contains(QuadRef::new(ex, ex, ex, None))); + /// # Result::<_,Box>::Ok(()) + /// ``` + pub fn update( + &self, + update: impl TryInto>, + ) -> Result<(), EvaluationError> { + evaluate_update( + self.clone(), + &mut &*self, + &update.try_into().map_err(|e| e.into())?, + ) + } + /// Executes an ACID transaction. /// /// The transaction is executed if the given closure returns `Ok`. diff --git a/lib/src/store/numeric_encoder.rs b/lib/src/store/numeric_encoder.rs index 1a12e5ab..dbc4340b 100644 --- a/lib/src/store/numeric_encoder.rs +++ b/lib/src/store/numeric_encoder.rs @@ -379,8 +379,11 @@ impl EncodedTerm { } } - pub fn try_map_id(self, mapping: impl Fn(I) -> Option) -> Option> { - Some(match self { + pub fn try_map_id( + self, + mut mapping: impl FnMut(I) -> Result, + ) -> Result, E> { + Ok(match self { Self::DefaultGraph { .. } => EncodedTerm::DefaultGraph, Self::NamedNode { iri_id } => EncodedTerm::NamedNode { iri_id: mapping(iri_id)?, diff --git a/lib/src/store/rocksdb.rs b/lib/src/store/rocksdb.rs index 61c1bce7..f8eb3810 100644 --- a/lib/src/store/rocksdb.rs +++ b/lib/src/store/rocksdb.rs @@ -3,7 +3,10 @@ use crate::error::invalid_data_error; use crate::io::{DatasetFormat, GraphFormat}; use crate::model::*; -use crate::sparql::{EvaluationError, Query, QueryOptions, QueryResults, SimplePreparedQuery}; +use crate::sparql::{ + evaluate_update, EvaluationError, Query, QueryOptions, QueryResults, SimplePreparedQuery, + Update, +}; use crate::store::binary_encoder::*; use crate::store::numeric_encoder::{ Decoder, ReadEncoder, StrContainer, StrEncodingAware, StrLookup, WriteEncoder, @@ -212,6 +215,26 @@ impl RocksDbStore { default && named } + /// Executes a [SPARQL 1.1 update](https://www.w3.org/TR/sparql11-update/). + /// + /// The [`LOAD` operation](https://www.w3.org/TR/sparql11-update/#load) is not supported yet. + /// The store does not track the existence of empty named graphs. + /// This method has no ACID guarantees. + /// + /// See [`MemoryStore`](../memory/struct.MemoryStore.html#method.update) for a usage example. + pub fn update( + &self, + update: impl TryInto>, + ) -> Result<(), EvaluationError> { + let mut writer = self.auto_batch_writer(); + evaluate_update( + self.clone(), + &mut writer, + &update.try_into().map_err(|e| e.into())?, + )?; + Ok(writer.apply()?) + } + /// Executes an ACID transaction. /// /// The transaction is executed if the given closure returns `Ok`. diff --git a/lib/src/store/sled.rs b/lib/src/store/sled.rs index f26d06c3..fdfb2407 100644 --- a/lib/src/store/sled.rs +++ b/lib/src/store/sled.rs @@ -3,7 +3,10 @@ use crate::error::invalid_data_error; use crate::io::{DatasetFormat, GraphFormat}; use crate::model::*; -use crate::sparql::{EvaluationError, Query, QueryOptions, QueryResults, SimplePreparedQuery}; +use crate::sparql::{ + evaluate_update, EvaluationError, Query, QueryOptions, QueryResults, SimplePreparedQuery, + Update, +}; use crate::store::binary_encoder::*; use crate::store::numeric_encoder::{ Decoder, ReadEncoder, StrContainer, StrEncodingAware, StrLookup, WriteEncoder, @@ -203,6 +206,24 @@ impl SledStore { self.gspo.is_empty() && self.dspo.is_empty() } + /// Executes a [SPARQL 1.1 update](https://www.w3.org/TR/sparql11-update/). + /// + /// The [`LOAD` operation](https://www.w3.org/TR/sparql11-update/#load) is not supported yet. + /// The store does not track the existence of empty named graphs. + /// This method has no ACID guarantees. + /// + /// See [`MemoryStore`](../memory/struct.MemoryStore.html#method.update) for a usage example. + pub fn update( + &self, + update: impl TryInto>, + ) -> Result<(), EvaluationError> { + evaluate_update( + self.clone(), + &mut &*self, + &update.try_into().map_err(|e| e.into())?, + ) + } + /// Executes an ACID transaction. /// /// The transaction is executed if the given closure returns `Ok`. diff --git a/testsuite/Cargo.toml b/testsuite/Cargo.toml index 173ca2f0..24d64fad 100644 --- a/testsuite/Cargo.toml +++ b/testsuite/Cargo.toml @@ -15,6 +15,7 @@ publish = false anyhow = "1" chrono = "0.4" oxigraph = { version = "0.1", path="../lib" } +text-diff = "0.4" [dev-dependencies] criterion = "0.3" diff --git a/testsuite/src/files.rs b/testsuite/src/files.rs index 8c31dcdb..e31d3daa 100644 --- a/testsuite/src/files.rs +++ b/testsuite/src/files.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use oxigraph::io::{DatasetFormat, GraphFormat}; -use oxigraph::model::GraphName; +use oxigraph::model::{GraphName, GraphNameRef}; use oxigraph::MemoryStore; use std::fs::File; use std::io::{BufRead, BufReader, Read}; @@ -39,7 +39,11 @@ pub fn read_file_to_string(url: &str) -> Result { Ok(buf) } -pub fn load_to_store(url: &str, store: &MemoryStore, to_graph_name: &GraphName) -> Result<()> { +pub fn load_to_store<'a>( + url: &str, + store: &MemoryStore, + to_graph_name: impl Into>, +) -> Result<()> { if url.ends_with(".nt") { store.load_graph( read_file(url)?, diff --git a/testsuite/src/manifest.rs b/testsuite/src/manifest.rs index d038ed59..9dbcb222 100644 --- a/testsuite/src/manifest.rs +++ b/testsuite/src/manifest.rs @@ -13,10 +13,12 @@ pub struct Test { pub comment: Option, pub action: Option, pub query: Option, + pub update: Option, pub data: Option, - pub graph_data: Vec, + pub graph_data: Vec<(NamedNode, String)>, pub service_data: Vec<(String, String)>, pub result: Option, + pub result_graph_data: Vec<(NamedNode, String)>, } impl fmt::Display for Test { @@ -37,7 +39,7 @@ impl fmt::Display for Test { for data in &self.data { write!(f, " with data {}", data)?; } - for data in &self.graph_data { + for (_, data) in &self.graph_data { write!(f, " and graph data {}", data)?; } for result in &self.result { @@ -85,10 +87,10 @@ impl Iterator for TestManifest { Some(Term::Literal(c)) => Some(c.value().to_string()), _ => None, }; - let (action, query, data, graph_data, service_data) = + let (action, query, update, data, graph_data, service_data) = match object_for_subject_predicate(&self.graph, &test_node, mf::ACTION) { Some(Term::NamedNode(n)) => { - (Some(n.into_string()), None, None, vec![], vec![]) + (Some(n.into_string()), None, None, None, vec![], vec![]) } Some(Term::BlankNode(n)) => { let query = @@ -96,15 +98,52 @@ impl Iterator for TestManifest { Some(Term::NamedNode(q)) => Some(q.into_string()), _ => None, }; + let update = + match object_for_subject_predicate(&self.graph, &n, ut::REQUEST) { + Some(Term::NamedNode(q)) => Some(q.into_string()), + _ => None, + }; let data = match object_for_subject_predicate(&self.graph, &n, qt::DATA) + .or_else(|| object_for_subject_predicate(&self.graph, &n, ut::DATA)) { Some(Term::NamedNode(q)) => Some(q.into_string()), _ => None, }; let graph_data = objects_for_subject_predicate(&self.graph, &n, qt::GRAPH_DATA) + .chain(objects_for_subject_predicate( + &self.graph, + &n, + ut::GRAPH_DATA, + )) .filter_map(|g| match g { - Term::NamedNode(q) => Some(q.into_string()), + Term::NamedNode(q) => Some((q.clone(), q.into_string())), + Term::BlankNode(node) => { + if let Some(Term::NamedNode(graph)) = + object_for_subject_predicate( + &self.graph, + &node, + ut::GRAPH, + ) + { + if let Some(Term::Literal(name)) = + object_for_subject_predicate( + &self.graph, + &node, + rdfs::LABEL, + ) + { + Some(( + NamedNode::new(name.value()).unwrap(), + graph.into_string(), + )) + } else { + Some((graph.clone(), graph.into_string())) + } + } else { + None + } + } _ => None, }) .collect(); @@ -133,19 +172,60 @@ impl Iterator for TestManifest { } }) .collect(); - (None, query, data, graph_data, service_data) + (None, query, update, data, graph_data, service_data) } Some(_) => return Some(Err(anyhow!("invalid action"))), None => { return Some(Err(anyhow!("action not found for test {}", test_node))); } }; - let result = match object_for_subject_predicate(&self.graph, &test_node, mf::RESULT) - { - Some(Term::NamedNode(n)) => Some(n.into_string()), - Some(_) => return Some(Err(anyhow!("invalid result"))), - None => None, - }; + let (result, result_graph_data) = + match object_for_subject_predicate(&self.graph, &test_node, mf::RESULT) { + Some(Term::NamedNode(n)) => (Some(n.into_string()), Vec::new()), + Some(Term::BlankNode(n)) => ( + if let Some(Term::NamedNode(result)) = + object_for_subject_predicate(&self.graph, &n, ut::DATA) + { + Some(result.into_string()) + } else { + None + }, + objects_for_subject_predicate(&self.graph, &n, ut::GRAPH_DATA) + .filter_map(|g| match g { + Term::NamedNode(q) => Some((q.clone(), q.into_string())), + Term::BlankNode(node) => { + if let Some(Term::NamedNode(graph)) = + object_for_subject_predicate( + &self.graph, + &node, + ut::GRAPH, + ) + { + if let Some(Term::Literal(name)) = + object_for_subject_predicate( + &self.graph, + &node, + rdfs::LABEL, + ) + { + Some(( + NamedNode::new(name.value()).unwrap(), + graph.into_string(), + )) + } else { + Some((graph.clone(), graph.into_string())) + } + } else { + None + } + } + _ => None, + }) + .collect(), + ), + Some(_) => return Some(Err(anyhow!("invalid result"))), + None => (None, Vec::new()), + }; Some(Ok(Test { id: test_node, kind, @@ -153,10 +233,12 @@ impl Iterator for TestManifest { comment, action, query, + update, data, graph_data, service_data, result, + result_graph_data, })) } Some(_) => self.next(), @@ -166,7 +248,7 @@ impl Iterator for TestManifest { let manifest = NamedOrBlankNodeRef::from(NamedNodeRef::new(url.as_str()).unwrap()); if let Err(error) = - load_to_store(&url, &self.graph, &&GraphName::DefaultGraph) + load_to_store(&url, &self.graph, GraphNameRef::DefaultGraph) { return Some(Err(error)); } diff --git a/testsuite/src/parser_evaluator.rs b/testsuite/src/parser_evaluator.rs index cca2b7b0..6018b31c 100644 --- a/testsuite/src/parser_evaluator.rs +++ b/testsuite/src/parser_evaluator.rs @@ -1,6 +1,6 @@ use crate::files::load_store; use crate::manifest::Test; -use crate::report::TestResult; +use crate::report::{store_diff, TestResult}; use anyhow::{anyhow, Result}; use chrono::Utc; @@ -59,9 +59,8 @@ fn evaluate_parser_test(test: &Test) -> Result<()> { Ok(()) } else { Err(anyhow!( - "The two files are not isomorphic. Expected:\n{}\nActual:\n{}", - expected_graph, - actual_graph + "The two files are not isomorphic. Diff:\n{}", + store_diff(&expected_graph, &actual_graph) )) } } diff --git a/testsuite/src/report.rs b/testsuite/src/report.rs index 0ce9f133..bc10f8a7 100644 --- a/testsuite/src/report.rs +++ b/testsuite/src/report.rs @@ -1,6 +1,8 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use oxigraph::model::NamedNode; +use oxigraph::MemoryStore; +use text_diff::{diff, Difference}; #[derive(Debug)] pub struct TestResult { @@ -8,3 +10,43 @@ pub struct TestResult { pub outcome: Result<()>, pub date: DateTime, } + +pub fn store_diff(expected: &MemoryStore, actual: &MemoryStore) -> String { + let (_, changeset) = diff( + &normalize_store_text(expected), + &normalize_store_text(actual), + "\n", + ); + let mut ret = String::new(); + ret.push_str("Note: missing quads in yellow and extra quads in blue\n"); + for seq in changeset { + match seq { + Difference::Same(x) => { + ret.push_str(&x); + ret.push('\n'); + } + Difference::Add(x) => { + ret.push_str("\x1B[94m"); + ret.push_str(&x); + ret.push_str("\x1B[0m"); + ret.push('\n'); + } + Difference::Rem(x) => { + ret.push_str("\x1B[93m"); + ret.push_str(&x); + ret.push_str("\x1B[0m"); + ret.push('\n'); + } + } + } + ret +} + +fn normalize_store_text(store: &MemoryStore) -> String { + let mut quads: Vec<_> = store + .quads_for_pattern(None, None, None, None) + .map(|q| q.to_string()) + .collect(); + quads.sort(); + quads.join("\n") +} diff --git a/testsuite/src/sparql_evaluator.rs b/testsuite/src/sparql_evaluator.rs index 89b0cb7f..e05ad376 100644 --- a/testsuite/src/sparql_evaluator.rs +++ b/testsuite/src/sparql_evaluator.rs @@ -1,6 +1,6 @@ use crate::files::*; use crate::manifest::*; -use crate::report::*; +use crate::report::{store_diff, TestResult}; use crate::vocab::*; use anyhow::{anyhow, Result}; use chrono::Utc; @@ -72,10 +72,10 @@ fn evaluate_sparql_test(test: &Test) -> Result<()> { { let store = MemoryStore::new(); if let Some(data) = &test.data { - load_to_store(data, &store, &GraphName::DefaultGraph)?; + load_to_store(data, &store, GraphNameRef::DefaultGraph)?; } - for graph_data in &test.graph_data { - load_to_store(&graph_data, &store, &NamedNode::new(graph_data)?.into())?; + for (name, value) in &test.graph_data { + load_to_store(value, &store, name)?; } let query_file = test .query @@ -157,6 +157,56 @@ fn evaluate_sparql_test(test: &Test) -> Result<()> { )), Err(_) => Ok(()), } + } else if test.kind + == "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#UpdateEvaluationTest" + { + let store = MemoryStore::new(); + if let Some(data) = &test.data { + load_to_store(data, &store, &GraphName::DefaultGraph)?; + } + for (name, value) in &test.graph_data { + load_to_store(value, &store, name)?; + } + + let result_store = MemoryStore::new(); + if let Some(data) = &test.result { + load_to_store(data, &result_store, &GraphName::DefaultGraph)?; + } + for (name, value) in &test.result_graph_data { + load_to_store(value, &result_store, name)?; + } + + let update_file = test + .update + .as_deref() + .ok_or_else(|| anyhow!("No action found for test {}", test))?; + match Update::parse(&read_file_to_string(update_file)?, Some(update_file)) { + Err(error) => Err(anyhow!( + "Failure to parse update of {} with error: {}", + test, + error + )), + Ok(update) => match store.update(update) { + Err(error) => Err(anyhow!( + "Failure to execute update of {} with error: {}", + test, + error + )), + Ok(()) => { + if store.is_isomorphic(&result_store) { + Ok(()) + } else { + Err(anyhow!( + "Failure on {}.\nDiff:\n{}\nParsed update:\n{}\n", + test, + store_diff(&result_store, &store), + Update::parse(&read_file_to_string(update_file)?, Some(update_file)) + .unwrap(), + )) + } + } + }, + } } else { Err(anyhow!("Unsupported test type: {}", test.kind)) } diff --git a/testsuite/src/vocab.rs b/testsuite/src/vocab.rs index 9f3a4e92..b9e9b46c 100644 --- a/testsuite/src/vocab.rs +++ b/testsuite/src/vocab.rs @@ -62,3 +62,15 @@ pub mod qt { "http://www.w3.org/2001/sw/DataAccess/tests/test-query#endpoint", ); } + +pub mod ut { + use oxigraph::model::NamedNodeRef; + pub const DATA: NamedNodeRef<'_> = + NamedNodeRef::new_unchecked("http://www.w3.org/2009/sparql/tests/test-update#data"); + pub const GRAPH_DATA: NamedNodeRef<'_> = + NamedNodeRef::new_unchecked("http://www.w3.org/2009/sparql/tests/test-update#graphData"); + pub const GRAPH: NamedNodeRef<'_> = + NamedNodeRef::new_unchecked("http://www.w3.org/2009/sparql/tests/test-update#graph"); + pub const REQUEST: NamedNodeRef<'_> = + NamedNodeRef::new_unchecked("http://www.w3.org/2009/sparql/tests/test-update#request"); +} diff --git a/testsuite/tests/sparql.rs b/testsuite/tests/sparql.rs index 5aae5982..8320d38a 100644 --- a/testsuite/tests/sparql.rs +++ b/testsuite/tests/sparql.rs @@ -124,7 +124,11 @@ fn sparql11_federation_w3c_evaluation_testsuite() -> Result<()> { #[test] fn sparql11_update_w3c_evaluation_testsuite() -> Result<()> { run_testsuite( - "http://www.w3.org/2009/sparql/docs/tests/data-sparql11/syntax-update-1/manifest.ttl", - vec![], + "http://www.w3.org/2009/sparql/docs/tests/data-sparql11/manifest-sparql11-update.ttl", + vec![ + // LOAD is not implemented yet + "http://www.w3.org/2009/sparql/docs/tests/data-sparql11/update-silent/manifest#load-into-silent", + "http://www.w3.org/2009/sparql/docs/tests/data-sparql11/update-silent/manifest#load-silent" + ] ) }