diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f80c2379..3b31513e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -14,6 +14,7 @@ edition = "2018" [dependencies] lazy_static = "1" rocksdb = { version = "0.14", optional = true } +sled = { version = "0.31", optional = true } quick-xml = "0.18" rand = "0.7" md-5 = "0.8" @@ -43,6 +44,10 @@ anyhow = "1" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3" +[[bench]] +name = "store" +harness = false + [[bench]] name = "sparql_query" harness = false diff --git a/lib/benches/store.rs b/lib/benches/store.rs new file mode 100644 index 00000000..49990d2c --- /dev/null +++ b/lib/benches/store.rs @@ -0,0 +1,98 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use oxigraph::model::*; +use oxigraph::*; +use rand::random; +use std::env::temp_dir; +use std::fs::remove_dir_all; + +criterion_group!( + store_load, + memory_load_bench, + sled_load_bench, + rocksdb_load_bench +); + +criterion_main!(store_load); + +fn memory_load_bench(c: &mut Criterion) { + let mut group = c.benchmark_group("memory"); + group.nresamples(10); + group.sample_size(10); + for size in [100, 1_000, 10_000].iter() { + group.throughput(Throughput::Elements(*size as u64)); + let quads = create_quads(*size); + group.bench_function(BenchmarkId::from_parameter(size), |b| { + b.iter(|| { + let store = MemoryStore::new(); + for quad in &quads { + store.insert(quad).unwrap(); + } + }); + }); + } + group.finish(); +} + +fn sled_load_bench(c: &mut Criterion) { + let mut group = c.benchmark_group("sled"); + group.nresamples(10); + group.sample_size(10); + for size in [100, 1_000, 10_000].iter() { + group.throughput(Throughput::Elements(*size as u64)); + let quads = create_quads(*size); + group.bench_function(BenchmarkId::from_parameter(size), |b| { + b.iter(|| { + let store = SledStore::new().unwrap(); + for quad in &quads { + store.insert(quad).unwrap(); + } + }); + }); + } + group.finish(); +} + +fn rocksdb_load_bench(c: &mut Criterion) { + let mut group = c.benchmark_group("rocksdb"); + group.nresamples(10); + group.sample_size(10); + let temp_dir = temp_dir(); + for size in [100, 1_000, 10_000].iter() { + group.throughput(Throughput::Elements(*size as u64)); + let quads = create_quads(*size); + group.bench_function(BenchmarkId::from_parameter(size), |b| { + b.iter(|| { + let mut dir = temp_dir.clone(); + dir.push(random::().to_string()); + let store = RocksDbStore::open(&dir).unwrap(); + for quad in &quads { + store.insert(quad).unwrap(); + } + remove_dir_all(&dir).unwrap(); + }); + }); + } + group.finish(); +} + +fn create_quads(size: u64) -> Vec { + (0..size) + .map(|_| { + Quad::new( + NamedNode::new_unchecked(format!( + "http://example.com/id/{}", + random::() % size + )), + NamedNode::new_unchecked(format!( + "http://example.com/id/{}", + random::() % size + )), + NamedNode::new_unchecked(format!( + "http://example.com/id/{}", + random::() % size + )), + None, + ) + }) + .collect() +} diff --git a/lib/src/error.rs b/lib/src/error.rs index c49421c1..cd6aae79 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -132,3 +132,10 @@ impl From for Error { Self::wrap(error) } } + +#[cfg(feature = "sled")] +impl From for Error { + fn from(error: sled::Error) -> Self { + Self::wrap(error) + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 86a49e5a..b7093e13 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,9 +2,15 @@ //! //! Its goal is to provide a compliant, safe and fast graph database. //! -//! It currently provides two `Store` implementation providing [SPARQL 1.1 query](https://www.w3.org/TR/sparql11-query/) capability: +//! It currently provides three `Store` implementation providing [SPARQL 1.1 query](https://www.w3.org/TR/sparql11-query/) capability: //! * `MemoryStore`: a simple in memory implementation. //! * `RocksDbStore`: a file system implementation based on the [RocksDB](https://rocksdb.org/) key-value store. +//! It requires the `"rocksdb"` feature to be activated. +//! It also requires the clang](https://clang.llvm.org/) compiler to be installed. +//! * `Sled`: an other file system implementation based on the [Sled](https://sled.rs/) key-value store. +//! It requires the `"sled"` feature to be activated. +//! Sled is much faster to build than RockDB and does not require a C++ compiler. +//! However Sled is still in heavy developpment, less tested and data load seems much slower than RocksDB. //! //! Usage example with the `MemoryStore`: //! @@ -116,6 +122,8 @@ pub use crate::store::MemoryTransaction; pub use crate::store::RocksDbStore; #[cfg(feature = "rocksdb")] pub use crate::store::RocksDbTransaction; +#[cfg(feature = "sled")] +pub use crate::store::SledStore; pub use crate::syntax::DatasetSyntax; pub use crate::syntax::FileSyntax; pub use crate::syntax::GraphSyntax; diff --git a/lib/src/store/mod.rs b/lib/src/store/mod.rs index 40c9ef5e..a75bac7b 100644 --- a/lib/src/store/mod.rs +++ b/lib/src/store/mod.rs @@ -4,6 +4,8 @@ mod memory; pub(crate) mod numeric_encoder; #[cfg(feature = "rocksdb")] mod rocksdb; +#[cfg(feature = "sled")] +mod sled; use crate::sparql::GraphPattern; pub use crate::store::memory::MemoryStore; @@ -12,6 +14,8 @@ pub use crate::store::memory::MemoryTransaction; pub use crate::store::rocksdb::RocksDbStore; #[cfg(feature = "rocksdb")] pub use crate::store::rocksdb::RocksDbTransaction; +#[cfg(feature = "sled")] +pub use crate::store::sled::SledStore; use crate::model::*; use crate::store::numeric_encoder::*; diff --git a/lib/src/store/sled.rs b/lib/src/store/sled.rs new file mode 100644 index 00000000..cc6eb37a --- /dev/null +++ b/lib/src/store/sled.rs @@ -0,0 +1,637 @@ +use crate::model::*; +use crate::sparql::{GraphPattern, PreparedQuery, QueryOptions, SimplePreparedQuery}; +use crate::store::numeric_encoder::*; +use crate::store::{load_dataset, load_graph, ReadableEncodedStore, WritableEncodedStore}; +use crate::{DatasetSyntax, GraphSyntax, Result}; +use sled::{Config, Iter, Tree}; +use std::io::BufRead; +use std::path::Path; +use std::str; + +/// Store based on the [Sled](https://sled.rs/) key-value database. +/// It encodes a [RDF dataset](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset) and allows to query and update it using SPARQL. +/// +/// To use it, the `"sled"` feature needs to be activated. +/// +/// Warning: quad insertions and deletions are not (yet) atomic. +/// +/// Usage example: +/// ``` +/// use oxigraph::model::*; +/// use oxigraph::{Result, SledStore}; +/// use oxigraph::sparql::{PreparedQuery, QueryOptions, QueryResult}; +/// # use std::fs::remove_dir_all; +/// +/// # { +/// let store = SledStore::open("example.db")?; +/// +/// // insertion +/// let ex = NamedNode::parse("http://example.com")?; +/// let quad = Quad::new(ex.clone(), ex.clone(), ex.clone(), None); +/// store.insert(&quad)?; +/// +/// // quad filter +/// let results: Result> = store.quads_for_pattern(None, None, None, None).collect(); +/// assert_eq!(vec![quad], results?); +/// +/// // SPARQL query +/// let prepared_query = store.prepare_query("SELECT ?s WHERE { ?s ?p ?o }", QueryOptions::default())?; +/// let results = prepared_query.exec()?; +/// if let QueryResult::Bindings(results) = results { +/// assert_eq!(results.into_values_iter().next().unwrap()?[0], Some(ex.into())); +/// } +/// # +/// # } +/// # remove_dir_all("example.db")?; +/// # Result::Ok(()) +/// ``` +#[derive(Clone)] +pub struct SledStore { + id2str: Tree, + spog: Tree, + posg: Tree, + ospg: Tree, + gspo: Tree, + gpos: Tree, + gosp: Tree, +} + +//TODO: indexes for the default graph and indexes for the named graphs (no more Optional and space saving) + +impl SledStore { + /// Opens a temporary `SledStore` that will be deleted after drop. + pub fn new() -> Result { + Self::do_open(Config::new().temporary(true)) + } + + /// Opens a `SledStore` + pub fn open(path: impl AsRef) -> Result { + Self::do_open(Config::new().path(path)) + } + + fn do_open(config: Config) -> Result { + let db = config.open()?; + let new = Self { + id2str: db.open_tree("id2str")?, + spog: db.open_tree("spog")?, + posg: db.open_tree("posg")?, + ospg: db.open_tree("ospg")?, + gspo: db.open_tree("gspo")?, + gpos: db.open_tree("gpos")?, + gosp: db.open_tree("gosp")?, + }; + (&new).set_first_strings()?; + Ok(new) + } + + /// Prepares a [SPARQL 1.1 query](https://www.w3.org/TR/sparql11-query/) and returns an object that could be used to execute it. + /// + /// See `MemoryStore` for a usage example. + pub fn prepare_query<'a>( + &'a self, + query: &str, + options: QueryOptions<'_>, + ) -> Result { + SimplePreparedQuery::new((*self).clone(), query, options) + } + + /// This is similar to `prepare_query`, but useful if a SPARQL query has already been parsed, which is the case when building `ServiceHandler`s for federated queries with `SERVICE` clauses. For examples, look in the tests. + pub fn prepare_query_from_pattern<'a>( + &'a self, + graph_pattern: &GraphPattern, + options: QueryOptions<'_>, + ) -> Result { + SimplePreparedQuery::new_from_pattern((*self).clone(), graph_pattern, options) + } + + /// Retrieves quads with a filter on each quad component + /// + /// See `MemoryStore` for a usage example. + #[allow(clippy::option_option)] + pub fn quads_for_pattern( + &self, + subject: Option<&NamedOrBlankNode>, + predicate: Option<&NamedNode>, + object: Option<&Term>, + graph_name: Option>, + ) -> impl Iterator> { + let subject = subject.map(|s| s.into()); + let predicate = predicate.map(|p| p.into()); + let object = object.map(|o| o.into()); + let graph_name = graph_name.map(|g| g.map_or(ENCODED_DEFAULT_GRAPH, |g| g.into())); + let this = self.clone(); + self.encoded_quads_for_pattern_inner(subject, predicate, object, graph_name) + .map(move |quad| this.decode_quad(&quad?)) + } + + /// Checks if this store contains a given quad + pub fn contains(&self, quad: &Quad) -> Result { + let quad = quad.into(); + self.contains_encoded(&quad) + } + + /// Loads a graph file (i.e. triples) into the store + /// + /// Warning: This functions saves the triples in batch. If the parsing fails in the middle of the file, + /// only a part of it may be written. Use a (memory greedy) transaction if you do not want that. + /// + /// See `MemoryStore` for a usage example. + pub fn load_graph( + &self, + reader: impl BufRead, + syntax: GraphSyntax, + to_graph_name: Option<&NamedOrBlankNode>, + base_iri: Option<&str>, + ) -> Result<()> { + let mut store = self; + load_graph(&mut store, reader, syntax, to_graph_name, base_iri) + } + + /// Loads a dataset file (i.e. quads) into the store. + /// + /// Warning: This functions saves the quads in batch. If the parsing fails in the middle of the file, + /// only a part of it may be written. Use a (memory greedy) transaction if you do not want that. + /// + /// See `MemoryStore` for a usage example. + pub fn load_dataset( + &self, + reader: impl BufRead, + syntax: DatasetSyntax, + base_iri: Option<&str>, + ) -> Result<()> { + let mut store = self; + load_dataset(&mut store, reader, syntax, base_iri) + } + + /// Adds a quad to this store. + pub fn insert(&self, quad: &Quad) -> Result<()> { + let mut store = self; + let quad = store.encode_quad(quad)?; + store.insert_encoded(&quad) + } + + /// Removes a quad from this store. + pub fn remove(&self, quad: &Quad) -> Result<()> { + let mut store = self; + let quad = quad.into(); + store.remove_encoded(&quad) + } + + fn contains_encoded(&self, quad: &EncodedQuad) -> Result { + let mut buffer = Vec::with_capacity(4 * WRITTEN_TERM_MAX_SIZE); + write_spog_quad(&mut buffer, quad); + Ok(self.spog.contains_key(buffer)?) + } + + fn encoded_quads_for_pattern_inner( + &self, + subject: Option, + predicate: Option, + object: Option, + graph_name: Option, + ) -> DecodingQuadIterator { + match subject { + Some(subject) => match predicate { + Some(predicate) => match object { + Some(object) => match graph_name { + Some(graph_name) => self + .spog_quads(encode_term_quad(subject, predicate, object, graph_name)), + None => self.quads_for_subject_predicate_object(subject, predicate, object), + }, + None => match graph_name { + Some(graph_name) => { + self.quads_for_subject_predicate_graph(subject, predicate, graph_name) + } + None => self.quads_for_subject_predicate(subject, predicate), + }, + }, + None => match object { + Some(object) => match graph_name { + Some(graph_name) => { + self.quads_for_subject_object_graph(subject, object, graph_name) + } + None => self.quads_for_subject_object(subject, object), + }, + None => match graph_name { + Some(graph_name) => self.quads_for_subject_graph(subject, graph_name), + None => self.quads_for_subject(subject), + }, + }, + }, + None => match predicate { + Some(predicate) => match object { + Some(object) => match graph_name { + Some(graph_name) => { + self.quads_for_predicate_object_graph(predicate, object, graph_name) + } + None => self.quads_for_predicate_object(predicate, object), + }, + None => match graph_name { + Some(graph_name) => self.quads_for_predicate_graph(predicate, graph_name), + None => self.quads_for_predicate(predicate), + }, + }, + None => match object { + Some(object) => match graph_name { + Some(graph_name) => self.quads_for_object_graph(object, graph_name), + None => self.quads_for_object(object), + }, + None => match graph_name { + Some(graph_name) => self.quads_for_graph(graph_name), + None => self.quads(), + }, + }, + }, + } + } + + fn quads(&self) -> DecodingQuadIterator { + self.spog_quads(Vec::default()) + } + + fn quads_for_subject(&self, subject: EncodedTerm) -> DecodingQuadIterator { + self.spog_quads(encode_term(subject)) + } + + fn quads_for_subject_predicate( + &self, + subject: EncodedTerm, + predicate: EncodedTerm, + ) -> DecodingQuadIterator { + self.spog_quads(encode_term_pair(subject, predicate)) + } + + fn quads_for_subject_predicate_object( + &self, + subject: EncodedTerm, + predicate: EncodedTerm, + object: EncodedTerm, + ) -> DecodingQuadIterator { + self.spog_quads(encode_term_triple(subject, predicate, object)) + } + + fn quads_for_subject_object( + &self, + subject: EncodedTerm, + object: EncodedTerm, + ) -> DecodingQuadIterator { + self.ospg_quads(encode_term_pair(object, subject)) + } + + fn quads_for_predicate(&self, predicate: EncodedTerm) -> DecodingQuadIterator { + self.posg_quads(encode_term(predicate)) + } + + fn quads_for_predicate_object( + &self, + predicate: EncodedTerm, + object: EncodedTerm, + ) -> DecodingQuadIterator { + self.posg_quads(encode_term_pair(predicate, object)) + } + + fn quads_for_object(&self, object: EncodedTerm) -> DecodingQuadIterator { + self.ospg_quads(encode_term(object)) + } + + fn quads_for_graph(&self, graph_name: EncodedTerm) -> DecodingQuadIterator { + self.gspo_quads(encode_term(graph_name)) + } + + fn quads_for_subject_graph( + &self, + subject: EncodedTerm, + graph_name: EncodedTerm, + ) -> DecodingQuadIterator { + self.gspo_quads(encode_term_pair(graph_name, subject)) + } + + fn quads_for_subject_predicate_graph( + &self, + subject: EncodedTerm, + predicate: EncodedTerm, + graph_name: EncodedTerm, + ) -> DecodingQuadIterator { + self.gspo_quads(encode_term_triple(graph_name, subject, predicate)) + } + + fn quads_for_subject_object_graph( + &self, + subject: EncodedTerm, + object: EncodedTerm, + graph_name: EncodedTerm, + ) -> DecodingQuadIterator { + self.gosp_quads(encode_term_triple(graph_name, object, subject)) + } + + fn quads_for_predicate_graph( + &self, + predicate: EncodedTerm, + graph_name: EncodedTerm, + ) -> DecodingQuadIterator { + self.gpos_quads(encode_term_pair(graph_name, predicate)) + } + + fn quads_for_predicate_object_graph( + &self, + predicate: EncodedTerm, + object: EncodedTerm, + graph_name: EncodedTerm, + ) -> DecodingQuadIterator { + self.gpos_quads(encode_term_triple(graph_name, predicate, object)) + } + + fn quads_for_object_graph( + &self, + object: EncodedTerm, + graph_name: EncodedTerm, + ) -> DecodingQuadIterator { + self.gosp_quads(encode_term_pair(graph_name, object)) + } + + fn spog_quads(&self, prefix: Vec) -> DecodingQuadIterator { + self.inner_quads(&self.spog, prefix, QuadEncoding::SPOG) + } + + fn posg_quads(&self, prefix: Vec) -> DecodingQuadIterator { + self.inner_quads(&self.posg, prefix, QuadEncoding::POSG) + } + + fn ospg_quads(&self, prefix: Vec) -> DecodingQuadIterator { + self.inner_quads(&self.ospg, prefix, QuadEncoding::OSPG) + } + + fn gspo_quads(&self, prefix: Vec) -> DecodingQuadIterator { + self.inner_quads(&self.gspo, prefix, QuadEncoding::GSPO) + } + + fn gpos_quads(&self, prefix: Vec) -> DecodingQuadIterator { + self.inner_quads(&self.gpos, prefix, QuadEncoding::GPOS) + } + + fn gosp_quads(&self, prefix: Vec) -> DecodingQuadIterator { + self.inner_quads(&self.gosp, prefix, QuadEncoding::GOSP) + } + + fn inner_quads( + &self, + tree: &Tree, + prefix: Vec, + order: QuadEncoding, + ) -> DecodingQuadIterator { + DecodingQuadIterator { + iter: tree.scan_prefix(prefix), + order, + } + } +} + +impl StrLookup for SledStore { + fn get_str(&self, id: StrHash) -> Result> { + Ok(self + .id2str + .get(id.to_be_bytes())? + .map(|v| String::from_utf8(v.to_vec())) + .transpose()?) + } +} + +impl ReadableEncodedStore for SledStore { + fn encoded_quads_for_pattern<'a>( + &'a self, + subject: Option, + predicate: Option, + object: Option, + graph_name: Option, + ) -> Box> + 'a> { + Box::new(self.encoded_quads_for_pattern_inner(subject, predicate, object, graph_name)) + } +} + +impl<'a> StrContainer for &'a SledStore { + fn insert_str(&mut self, key: StrHash, value: &str) -> Result<()> { + self.id2str.insert(key.to_be_bytes(), value)?; + Ok(()) + } +} + +impl<'a> WritableEncodedStore for &'a SledStore { + fn insert_encoded(&mut self, quad: &EncodedQuad) -> Result<()> { + //TODO: atomicity + let mut buffer = Vec::with_capacity(4 * WRITTEN_TERM_MAX_SIZE); + + write_spog_quad(&mut buffer, quad); + self.spog.insert(&buffer, &[])?; + buffer.clear(); + + write_posg_quad(&mut buffer, quad); + self.posg.insert(&buffer, &[])?; + buffer.clear(); + + write_ospg_quad(&mut buffer, quad); + self.ospg.insert(&buffer, &[])?; + buffer.clear(); + + write_gspo_quad(&mut buffer, quad); + self.gspo.insert(&buffer, &[])?; + buffer.clear(); + + write_gpos_quad(&mut buffer, quad); + self.gpos.insert(&buffer, &[])?; + buffer.clear(); + + write_gosp_quad(&mut buffer, quad); + self.gosp.insert(&buffer, &[])?; + buffer.clear(); + + Ok(()) + } + + fn remove_encoded(&mut self, quad: &EncodedQuad) -> Result<()> { + //TODO: atomicity + let mut buffer = Vec::with_capacity(4 * WRITTEN_TERM_MAX_SIZE); + + write_spog_quad(&mut buffer, quad); + self.spog.remove(&buffer)?; + buffer.clear(); + + write_posg_quad(&mut buffer, quad); + self.posg.remove(&buffer)?; + buffer.clear(); + + write_ospg_quad(&mut buffer, quad); + self.ospg.remove(&buffer)?; + buffer.clear(); + + write_gspo_quad(&mut buffer, quad); + self.gspo.remove(&buffer)?; + buffer.clear(); + + write_gpos_quad(&mut buffer, quad); + self.gpos.remove(&buffer)?; + buffer.clear(); + + write_gosp_quad(&mut buffer, quad); + self.gosp.remove(&buffer)?; + buffer.clear(); + + Ok(()) + } +} + +fn encode_term(t: EncodedTerm) -> Vec { + let mut vec = Vec::with_capacity(WRITTEN_TERM_MAX_SIZE); + write_term(&mut vec, t); + vec +} + +fn encode_term_pair(t1: EncodedTerm, t2: EncodedTerm) -> Vec { + let mut vec = Vec::with_capacity(2 * WRITTEN_TERM_MAX_SIZE); + write_term(&mut vec, t1); + write_term(&mut vec, t2); + vec +} + +fn encode_term_triple(t1: EncodedTerm, t2: EncodedTerm, t3: EncodedTerm) -> Vec { + let mut vec = Vec::with_capacity(3 * WRITTEN_TERM_MAX_SIZE); + write_term(&mut vec, t1); + write_term(&mut vec, t2); + write_term(&mut vec, t3); + vec +} + +fn encode_term_quad(t1: EncodedTerm, t2: EncodedTerm, t3: EncodedTerm, t4: EncodedTerm) -> Vec { + let mut vec = Vec::with_capacity(4 * WRITTEN_TERM_MAX_SIZE); + write_term(&mut vec, t1); + write_term(&mut vec, t2); + write_term(&mut vec, t3); + write_term(&mut vec, t4); + vec +} + +struct DecodingQuadIterator { + iter: Iter, + order: QuadEncoding, +} + +impl Iterator for DecodingQuadIterator { + type Item = Result; + + fn next(&mut self) -> Option> { + Some(match self.iter.next()? { + Ok((encoded, _)) => self.order.decode(&encoded), + Err(error) => Err(error.into()), + }) + } +} + +#[test] +fn store() -> Result<()> { + use crate::model::*; + use crate::*; + + let main_s = NamedOrBlankNode::from(BlankNode::default()); + let main_p = NamedNode::parse("http://example.com")?; + let main_o = Term::from(Literal::from(1)); + + let main_quad = Quad::new(main_s.clone(), main_p.clone(), main_o.clone(), None); + let all_o = vec![ + Quad::new(main_s.clone(), main_p.clone(), Literal::from(0), None), + Quad::new(main_s.clone(), main_p.clone(), main_o.clone(), None), + Quad::new(main_s.clone(), main_p.clone(), Literal::from(2), None), + ]; + + let store = SledStore::new()?; + store.insert(&main_quad)?; + for t in &all_o { + store.insert(t)?; + } + + let target = vec![main_quad]; + assert_eq!( + store + .quads_for_pattern(None, None, None, None) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), None, None, None) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), Some(&main_p), None, None) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), Some(&main_p), Some(&main_o), None) + .collect::>>()?, + target + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), Some(&main_p), Some(&main_o), Some(None)) + .collect::>>()?, + target + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), Some(&main_p), None, Some(None)) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), None, Some(&main_o), None) + .collect::>>()?, + target + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), None, Some(&main_o), Some(None)) + .collect::>>()?, + target + ); + assert_eq!( + store + .quads_for_pattern(Some(&main_s), None, None, Some(None)) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(None, Some(&main_p), None, None) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(None, Some(&main_p), Some(&main_o), None) + .collect::>>()?, + target + ); + assert_eq!( + store + .quads_for_pattern(None, None, Some(&main_o), None) + .collect::>>()?, + target + ); + assert_eq!( + store + .quads_for_pattern(None, None, None, Some(None)) + .collect::>>()?, + all_o + ); + assert_eq!( + store + .quads_for_pattern(None, Some(&main_p), Some(&main_o), Some(None)) + .collect::>>()?, + target + ); + + Ok(()) +}