diff --git a/db/src/cache.rs b/db/src/cache.rs new file mode 100644 index 00000000..a245058d --- /dev/null +++ b/db/src/cache.rs @@ -0,0 +1,99 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +use std::cmp::Ord; +use std::collections::BTreeMap; +use std::fmt::Debug; + +use rusqlite; + +use errors::{ + Result +}; +use db::{ + TypedSQLValue, +}; +use mentat_core::{ + Entid, + TypedValue, +}; + +pub type CacheMap = BTreeMap; + +pub trait ValueProvider: Clone { + fn fetch_values<'sqlite>(&mut self, sqlite: &'sqlite rusqlite::Connection) -> Result>; +} + +pub trait Cacheable { + type Key; + type Value; + + fn cache_values<'sqlite>(&mut self, sqlite: &'sqlite rusqlite::Connection) -> Result<()>; + fn get(&self, key: &Self::Key) -> Option<&Self::Value>; +} + +#[derive(Clone)] +pub struct EagerCache where K: Ord, VP: ValueProvider { + pub cache: CacheMap, + value_provider: VP, +} + +impl EagerCache where K: Ord, VP: ValueProvider { + pub fn new(value_provider: VP) -> Self { + EagerCache { + cache: CacheMap::new(), + value_provider: value_provider, + } + } +} + +impl Cacheable for EagerCache + where K: Ord + Clone + Debug + ::std::hash::Hash, + V: Clone, + VP: ValueProvider { + type Key = K; + type Value = V; + + fn cache_values<'sqlite>(&mut self, sqlite: &'sqlite rusqlite::Connection) -> Result<()> { + // fetch results and add to cache + self.cache = self.value_provider.fetch_values(sqlite)?; + Ok(()) + } + + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.cache.get(&key) + } +} + +#[derive(Clone)] +pub struct AttributeValueProvider { + pub attribute: Entid, +} + +impl ValueProvider> for AttributeValueProvider { + fn fetch_values<'sqlite>(&mut self, sqlite: &'sqlite rusqlite::Connection) -> Result>> { + let sql = "SELECT e, v, value_type_tag FROM datoms WHERE a = ? ORDER BY e ASC"; + let mut stmt = sqlite.prepare(sql)?; + let value_iter = stmt.query_map(&[&self.attribute], |row| { + let entid: Entid = row.get(0); + let value_type_tag: i32 = row.get(2); + let value = TypedValue::from_sql_value_pair(row.get(1), value_type_tag).map(|x| x).unwrap(); + (entid, value) + }).map_err(|e| e.into()); + value_iter.map(|v| { + v.fold(CacheMap::new(), |mut map, row| { + let _ = row.map(|r| { + map.entry(r.0).or_insert(vec![]).push(r.1); + }); + map + }) + }) + } +} diff --git a/db/src/lib.rs b/db/src/lib.rs index a5927978..e1b56bb1 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -33,18 +33,19 @@ use itertools::Itertools; pub use errors::{Error, ErrorKind, ResultExt, Result}; +mod add_retract_alter_set; +pub mod cache; pub mod db; mod bootstrap; pub mod debug; -mod add_retract_alter_set; pub mod entids; pub mod errors; +pub mod internal_types; // pub because we need them for building entities programmatically. mod metadata; mod schema; -pub mod types; -pub mod internal_types; // pub because we need them for building entities programmatically. -mod upsert_resolution; mod tx; +pub mod types; +mod upsert_resolution; // Export these for reference from tests. cfg(test) should work, but doesn't. // #[cfg(test)] @@ -57,11 +58,12 @@ pub use schema::{ AttributeBuilder, AttributeValidation, }; - pub use bootstrap::{ CORE_SCHEMA_VERSION, }; +use edn::symbols; + pub use entids::{ DB_SCHEMA_CORE, }; @@ -82,8 +84,6 @@ pub use types::{ TxReport, }; -use edn::symbols; - pub fn to_namespaced_keyword(s: &str) -> Result { let splits = [':', '/']; let mut i = s.split(&splits[..]); diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 00000000..e56a7044 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,221 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +use std::collections::BTreeMap; + +use rusqlite; + +use mentat_core::{ + Entid, + TypedValue, +}; + +use mentat_db::cache::{ + AttributeValueProvider, + Cacheable, + EagerCache, + CacheMap, +}; + +use errors::{ + Result, +}; + +pub enum CacheAction { + Register, + Deregister, +} + +#[derive(Clone)] +pub struct AttributeCacher { + a_e_vs_cache: BTreeMap, AttributeValueProvider>>, // values keyed by attribute +} + +impl AttributeCacher { + + pub fn new() -> Self { + AttributeCacher { + a_e_vs_cache: BTreeMap::new(), + } + } + + pub fn register_attribute<'sqlite>(&mut self, sqlite: &'sqlite rusqlite::Connection, attribute: Entid) -> Result<()> { + let value_provider = AttributeValueProvider{ attribute: attribute }; + let mut cacher = EagerCache::new(value_provider); + cacher.cache_values(sqlite)?; + self.a_e_vs_cache.insert(attribute, cacher); + Ok(()) + } + + pub fn deregister_attribute(&mut self, attribute: &Entid) -> Option>> { + self.a_e_vs_cache.remove(&attribute).map(|m| m.cache) + } + + pub fn get(&self, attribute: &Entid) -> Option<&CacheMap>> { + self.a_e_vs_cache.get( &attribute ).map(|m| &m.cache) + } + + pub fn get_values_for_entid(&self, attribute: &Entid, entid: &Entid) -> Option<&Vec> { + self.a_e_vs_cache.get(&attribute).and_then(|c| c.get(&entid)) + } + + pub fn get_value_for_entid(&self, attribute: &Entid, entid: &Entid) -> Option<&TypedValue> { + self.get_values_for_entid(attribute, entid).and_then(|c| c.first()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; + use mentat_core::{ + HasSchema, + KnownEntid, + }; + use mentat_db::db; + use mentat_db::types::TypedValue; + + use conn::Conn; + + fn populate_db() -> (Conn, rusqlite::Connection) { + let mut sqlite = db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + let _report = conn.transact(&mut sqlite, r#"[ + { :db/ident :foo/bar + :db/valueType :db.type/long + :db/cardinality :db.cardinality/one }, + { :db/ident :foo/baz + :db/valueType :db.type/boolean + :db/cardinality :db.cardinality/one }, + { :db/ident :foo/bap + :db/valueType :db.type/string + :db/cardinality :db.cardinality/many}]"#).expect("transaction expected to succeed"); + let _report = conn.transact(&mut sqlite, r#"[ + { :foo/bar 100 + :foo/baz false + :foo/bap ["one","two","buckle my shoe"] }, + { :foo/bar 200 + :foo/baz true + :foo/bap ["three", "four", "knock at my door"] }]"#).expect("transaction expected to succeed"); + (conn, sqlite) + } + + fn assert_values_present_for_attribute(attribute_cache: &mut AttributeCacher, attribute: &KnownEntid, values: Vec>) { + let cached_values: Vec> = attribute_cache.get(&attribute.0) + .expect("Expected cached values") + .values() + .cloned() + .collect(); + assert_eq!(cached_values, values); + } + + #[test] + fn test_add_to_cache() { + let (conn, sqlite) = populate_db(); + let schema = conn.current_schema(); + let mut attribute_cache = AttributeCacher::new(); + let kw = kw!(:foo/bar); + let entid = schema.get_entid(&kw).expect("Expected entid for attribute"); + attribute_cache.register_attribute(&sqlite, entid.0.clone() ).expect("No errors on add to cache"); + assert_values_present_for_attribute(&mut attribute_cache, &entid, vec![vec![TypedValue::Long(100)], vec![TypedValue::Long(200)]]); + } + + #[test] + fn test_add_attribute_already_in_cache() { + let (conn, mut sqlite) = populate_db(); + let schema = conn.current_schema(); + + let kw = kw!(:foo/bar); + let entid = schema.get_entid(&kw).expect("Expected entid for attribute"); + let mut attribute_cache = AttributeCacher::new(); + + attribute_cache.register_attribute(&mut sqlite, entid.0.clone()).expect("No errors on add to cache"); + assert_values_present_for_attribute(&mut attribute_cache, &entid, vec![vec![TypedValue::Long(100)], vec![TypedValue::Long(200)]]); + attribute_cache.register_attribute(&mut sqlite, entid.0.clone()).expect("No errors on add to cache"); + assert_values_present_for_attribute(&mut attribute_cache, &entid, vec![vec![TypedValue::Long(100)], vec![TypedValue::Long(200)]]); + } + + #[test] + fn test_remove_from_cache() { + let (conn, mut sqlite) = populate_db(); + let schema = conn.current_schema(); + + let kwr = kw!(:foo/bar); + let entidr = schema.get_entid(&kwr).expect("Expected entid for attribute"); + let kwz = kw!(:foo/baz); + let entidz = schema.get_entid(&kwz).expect("Expected entid for attribute"); + + let mut attribute_cache = AttributeCacher::new(); + + attribute_cache.register_attribute(&mut sqlite, entidr.0.clone()).expect("No errors on add to cache"); + assert_values_present_for_attribute(&mut attribute_cache, &entidr, vec![vec![TypedValue::Long(100)], vec![TypedValue::Long(200)]]); + attribute_cache.register_attribute(&mut sqlite, entidz.0.clone()).expect("No errors on add to cache"); + assert_values_present_for_attribute(&mut attribute_cache, &entidz, vec![vec![TypedValue::Boolean(false)], vec![TypedValue::Boolean(true)]]); + + // test that we can remove an item from cache + attribute_cache.deregister_attribute(&entidz.0).expect("No errors on remove from cache"); + assert_eq!(attribute_cache.get(&entidz.0), None); + } + + #[test] + fn test_remove_attribute_not_in_cache() { + let (conn, _sqlite) = populate_db(); + let mut attribute_cache = AttributeCacher::new(); + + let schema = conn.current_schema(); + let kw = kw!(:foo/baz); + let entid = schema.get_entid(&kw).expect("Expected entid for attribute").0; + assert_eq!(None, attribute_cache.deregister_attribute(&entid)); + } + + #[test] + fn test_fetch_attribute_value_for_entid() { + let (conn, mut sqlite) = populate_db(); + let schema = conn.current_schema(); + + let entities = conn.q_once(&sqlite, r#"[:find ?e . :where [?e :foo/bar 100]]"#, None).expect("Expected query to work").into_scalar().expect("expected scalar results"); + let entid = match entities { + Some(TypedValue::Ref(entid)) => entid, + x => panic!("expected Some(Ref), got {:?}", x), + }; + + let kwr = kw!(:foo/bar); + let attr_entid = schema.get_entid(&kwr).expect("Expected entid for attribute").0; + + let mut attribute_cache = AttributeCacher::new(); + + attribute_cache.register_attribute(&mut sqlite, attr_entid.clone()).expect("No errors on add to cache"); + let val = attribute_cache.get_value_for_entid(&attr_entid, &entid).expect("Expected value"); + assert_eq!(*val, TypedValue::Long(100)); + } + + #[test] + fn test_fetch_attribute_values_for_entid() { + let (conn, mut sqlite) = populate_db(); + let schema = conn.current_schema(); + + let entities = conn.q_once(&sqlite, r#"[:find ?e . :where [?e :foo/bar 100]]"#, None).expect("Expected query to work").into_scalar().expect("expected scalar results"); + let entid = match entities { + Some(TypedValue::Ref(entid)) => entid, + x => panic!("expected Some(Ref), got {:?}", x), + }; + + let kwp = kw!(:foo/bap); + let attr_entid = schema.get_entid(&kwp).expect("Expected entid for attribute").0; + + let mut attribute_cache = AttributeCacher::new(); + + attribute_cache.register_attribute(&mut sqlite, attr_entid.clone()).expect("No errors on add to cache"); + let val = attribute_cache.get_values_for_entid(&attr_entid, &entid).expect("Expected value"); + assert_eq!(*val, vec![TypedValue::String(Rc::new("buckle my shoe".to_string())), TypedValue::String(Rc::new("one".to_string())), TypedValue::String(Rc::new("two".to_string()))]); + } +} + + diff --git a/src/conn.rs b/src/conn.rs index 86c7c347..2e45f5c3 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -10,7 +10,7 @@ #![allow(dead_code)] -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use rusqlite; use rusqlite::{ @@ -48,7 +48,20 @@ use mentat_tx::entities::TempId; use mentat_tx_parser; +use cache::{ + AttributeCacher, +}; + +pub use cache::{ + CacheAction, +}; + +use entity_builder::{ + InProgressBuilder, +}; + use errors::*; + use query::{ lookup_value_for_attribute, lookup_values_for_attribute, @@ -59,10 +72,6 @@ use query::{ QueryOutput, }; -use entity_builder::{ - InProgressBuilder, -}; - /// Connection metadata required to query from, or apply transactions to, a Mentat store. /// /// Owned data for the volatile parts (generation and partition map), and `Arc` for the infrequently @@ -102,6 +111,8 @@ pub struct Conn { // TODO: maintain cache of query plans that could be shared across threads and invalidated when // the schema changes. #315. + + attribute_cache: RwLock, } /// A convenience wrapper around a single SQLite connection and a Conn. This is suitable @@ -143,6 +154,7 @@ pub struct InProgress<'a, 'c> { generation: u64, partition_map: PartitionMap, schema: Schema, + cache: RwLockWriteGuard<'a, AttributeCacher>, } /// Represents an in-progress set of reads to the store. Just like `InProgress`, @@ -191,12 +203,14 @@ impl<'a, 'c> Queryable for InProgress<'a, 'c> { fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> where E: Into { - lookup_values_for_attribute(&*(self.transaction), &self.schema, entity, attribute) + let cc = &*self.cache; + lookup_values_for_attribute(&*(self.transaction), &self.schema, cc, entity, attribute) } fn lookup_value_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> where E: Into { - lookup_value_for_attribute(&*(self.transaction), &self.schema, entity, attribute) + let cc = &*self.cache; + lookup_value_for_attribute(&*(self.transaction), &self.schema, cc, entity, attribute) } } @@ -385,7 +399,8 @@ impl Conn { // Intentionally not public. fn new(partition_map: PartitionMap, schema: Schema) -> Conn { Conn { - metadata: Mutex::new(Metadata::new(0, partition_map, Arc::new(schema))) + metadata: Mutex::new(Metadata::new(0, partition_map, Arc::new(schema))), + attribute_cache: RwLock::new(AttributeCacher::new()) } } @@ -412,6 +427,10 @@ impl Conn { self.metadata.lock().unwrap().schema.clone() } + pub fn attribute_cache<'s>(&'s self) -> RwLockReadGuard<'s, AttributeCacher> { + self.attribute_cache.read().unwrap() + } + /// Query the Mentat store, using the given connection and the current metadata. pub fn q_once(&self, sqlite: &rusqlite::Connection, @@ -439,14 +458,16 @@ impl Conn { sqlite: &rusqlite::Connection, entity: Entid, attribute: &edn::NamespacedKeyword) -> Result> { - lookup_values_for_attribute(sqlite, &*self.current_schema(), entity, attribute) + let cc: &AttributeCacher = &*self.attribute_cache(); + lookup_values_for_attribute(sqlite, &*self.current_schema(), cc, entity, attribute) } pub fn lookup_value_for_attribute(&self, sqlite: &rusqlite::Connection, entity: Entid, attribute: &edn::NamespacedKeyword) -> Result> { - lookup_value_for_attribute(sqlite, &*self.current_schema(), entity, attribute) + let cc: &AttributeCacher = &*self.attribute_cache(); + lookup_value_for_attribute(sqlite, &*self.current_schema(), cc, entity, attribute) } /// Take a SQLite transaction. @@ -469,6 +490,7 @@ impl Conn { generation: current_generation, partition_map: current_partition_map, schema: (*current_schema).clone(), + cache: self.attribute_cache.write().unwrap(), }) } @@ -505,6 +527,34 @@ impl Conn { Ok(report) } + + // TODO: Figure out how to set max cache size and max result size and implement those on cache + // Question: Should those be only for lazy cache? The eager cache could perhaps grow infinitely + // and it becomes up to the client to manage memory usage by excising from cache when no longer + // needed + /// Adds or removes the values of a given attribute to an in memory cache + /// The attribute should be a namespaced string `:foo/bar`. + /// cache_action determines if the attribute should be added or removed from the cache. + /// CacheAction::Add is idempotent - each attribute is only added once and cannot be both lazy + /// and eager. + /// CacheAction::Remove throws an error if the attribute does not currently exist in the cache. + /// CacheType::Eager fetches all the values of the attribute and caches them on add. + /// CacheType::Lazy caches values only after they have first been fetched. + pub fn cache(&mut self, + sqlite: &mut rusqlite::Connection, + attribute: &NamespacedKeyword, + cache_action: CacheAction) -> Result<()> { + // fetch the attribute for the given name + let schema = self.current_schema(); + + let mut cache = self.attribute_cache.write().unwrap(); + let attribute_entid = schema.get_entid(&attribute).ok_or_else(|| ErrorKind::UnknownAttribute(attribute.to_string()))?; + match cache_action { + CacheAction::Register => { cache.register_attribute(sqlite, attribute_entid.0)?; }, + CacheAction::Deregister => { cache.deregister_attribute(&attribute_entid.0); }, + } + Ok(()) + } } #[cfg(test)] @@ -512,6 +562,9 @@ mod tests { use super::*; extern crate mentat_parser_utils; + + use std::time::Instant; + use mentat_core::{ TypedValue, }; @@ -708,4 +761,82 @@ mod tests { x => panic!("expected EDN parse error, got {:?}", x), } } + + #[test] + fn test_add_to_cache_failure_no_attribute() { + let mut sqlite = db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + let _report = conn.transact(&mut sqlite, r#"[ + { :db/ident :foo/bar + :db/valueType :db.type/long }, + { :db/ident :foo/baz + :db/valueType :db.type/boolean }]"#).unwrap(); + + let kw = kw!(:foo/bat); + let res = conn.cache(&mut sqlite,&kw, CacheAction::Register); + match res.unwrap_err() { + Error(ErrorKind::UnknownAttribute(msg), _) => assert_eq!(msg, ":foo/bat"), + x => panic!("expected UnknownAttribute error, got {:?}", x), + } + } + + // TODO expand tests to cover lookup_value_for_attribute comparing with and without caching + #[test] + fn test_lookup_attribute_with_caching() { + + let mut sqlite = db::new_connection("").unwrap(); + let mut conn = Conn::connect(&mut sqlite).unwrap(); + let _report = conn.transact(&mut sqlite, r#"[ + { :db/ident :foo/bar + :db/valueType :db.type/long }, + { :db/ident :foo/baz + :db/valueType :db.type/boolean }]"#).expect("transaction expected to succeed"); + + { + let mut in_progress = conn.begin_transaction(&mut sqlite).expect("transaction"); + for _ in 1..100 { + let _report = in_progress.transact(r#"[ + { :foo/bar 100 + :foo/baz false }, + { :foo/bar 200 + :foo/baz true }, + { :foo/bar 100 + :foo/baz false }, + { :foo/bar 300 + :foo/baz true }, + { :foo/bar 400 + :foo/baz false }, + { :foo/bar 500 + :foo/baz true }]"#).expect("transaction expected to succeed"); + } + in_progress.commit().expect("Committed"); + } + + let entities = conn.q_once(&sqlite, r#"[:find ?e . :where [?e :foo/bar 400]]"#, None).expect("Expected query to work").into_scalar().expect("expected rel results"); + let first = entities.expect("expected a result"); + let entid = match first { + TypedValue::Ref(entid) => entid, + x => panic!("expected Some(Ref), got {:?}", x), + }; + + let kw = kw!(:foo/bar); + let start = Instant::now(); + let uncached_val = conn.lookup_value_for_attribute(&sqlite, entid, &kw).expect("Expected value on lookup"); + let finish = Instant::now(); + let uncached_elapsed_time = finish.duration_since(start); + println!("Uncached time: {:?}", uncached_elapsed_time); + + conn.cache(&mut sqlite, &kw, CacheAction::Register).expect("expected caching to work"); + + for _ in 1..5 { + let start = Instant::now(); + let cached_val = conn.lookup_value_for_attribute(&sqlite, entid, &kw).expect("Expected value on lookup"); + let finish = Instant::now(); + let cached_elapsed_time = finish.duration_since(start); + assert_eq!(cached_val, uncached_val); + + println!("Cached time: {:?}", cached_elapsed_time); + assert!(cached_elapsed_time < uncached_elapsed_time); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 8d7edb58..bf0ce753 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,6 +73,7 @@ macro_rules! kw { }; } +pub mod cache; pub mod errors; pub mod ident; pub mod vocabulary; diff --git a/src/query.rs b/src/query.rs index 65e2e475..c804af27 100644 --- a/src/query.rs +++ b/src/query.rs @@ -69,6 +69,10 @@ use errors::{ Result, }; +use cache::{ + AttributeCacher, +}; + pub type QueryExecutionResult = Result; pub trait IntoResult { @@ -170,44 +174,59 @@ fn lookup_attribute(schema: &Schema, attribute: &NamespacedKeyword) -> Result +pub fn lookup_value<'sqlite, 'schema, 'cache, E, A> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, + cache: &'cache AttributeCacher, entity: E, attribute: A) -> Result> where E: Into, A: Into { - fetch_values(sqlite, schema, entity.into(), attribute.into(), true).into_scalar_result() + let entid = entity.into(); + let attrid = attribute.into(); + let cached = cache.get_value_for_entid(&attrid, &entid).cloned(); + if cached.is_some() { + return Ok(cached); + } + fetch_values(sqlite, schema, entid, attrid, true).into_scalar_result() } -pub fn lookup_values<'sqlite, 'schema, E, A> +pub fn lookup_values<'sqlite, 'schema, 'cache, E, A> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, + cache: &'cache AttributeCacher, entity: E, attribute: A) -> Result> where E: Into, A: Into { - fetch_values(sqlite, schema, entity.into(), attribute.into(), false).into_coll_result() + let entid = entity.into(); + let attrid = attribute.into(); + if let Some(cached) = cache.get_values_for_entid(&attrid, &entid).cloned() { + return Ok(cached); + } + fetch_values(sqlite, schema, entid, attrid, false).into_coll_result() } /// Return a single value for the provided entity and attribute. /// If the attribute is multi-valued, an arbitrary value is returned. /// If no value is present for that entity, `None` is returned. /// If `attribute` doesn't name an attribute, an error is returned. -pub fn lookup_value_for_attribute<'sqlite, 'schema, 'attribute, E> +pub fn lookup_value_for_attribute<'sqlite, 'schema, 'cache, 'attribute, E> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, + cache: &'cache AttributeCacher, entity: E, attribute: &'attribute NamespacedKeyword) -> Result> where E: Into { - lookup_value(sqlite, schema, entity.into(), lookup_attribute(schema, attribute)?) + lookup_value(sqlite, schema, cache, entity.into(), lookup_attribute(schema, attribute)?) } -pub fn lookup_values_for_attribute<'sqlite, 'schema, 'attribute, E> +pub fn lookup_values_for_attribute<'sqlite, 'schema, 'cache, 'attribute, E> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, + cache: &'cache AttributeCacher, entity: E, attribute: &'attribute NamespacedKeyword) -> Result> where E: Into { - lookup_values(sqlite, schema, entity.into(), lookup_attribute(schema, attribute)?) + lookup_values(sqlite, schema, cache, entity.into(), lookup_attribute(schema, attribute)?) } fn run_statement<'sqlite, 'stmt, 'bound>