diff --git a/.travis.yml b/.travis.yml index ba5124f..50e7585 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,5 @@ script: - cargo test --features load_extension - cargo test --features trace - cargo test --features functions - - cargo test --features "backup functions load_extension trace" + - cargo test --features cache + - cargo test --features "backup cache functions load_extension trace" diff --git a/Cargo.toml b/Cargo.toml index 3555180..ddf89d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ name = "rusqlite" load_extension = ["libsqlite3-sys/load_extension"] backup = [] blob = [] +cache = [] functions = [] trace = [] diff --git a/benches/lib.rs b/benches/lib.rs new file mode 100644 index 0000000..92fddef --- /dev/null +++ b/benches/lib.rs @@ -0,0 +1,23 @@ +#![feature(test)] +extern crate test; + +extern crate rusqlite; + +use rusqlite::Connection; +use rusqlite::cache::StatementCache; +use test::Bencher; + +#[bench] +fn bench_no_cache(b: &mut Bencher) { + let db = Connection::open_in_memory().unwrap(); + let sql = "SELECT 1, 'test', 3.14 UNION SELECT 2, 'exp', 2.71"; + b.iter(|| db.prepare(sql).unwrap()); +} + +#[bench] +fn bench_cache(b: &mut Bencher) { + let db = Connection::open_in_memory().unwrap(); + let cache = StatementCache::new(&db, 15); + let sql = "SELECT 1, 'test', 3.14 UNION SELECT 2, 'exp', 2.71"; + b.iter(|| cache.get(sql).unwrap()); +} diff --git a/publish-ghp-docs.sh b/publish-ghp-docs.sh index f9eeb17..de31a13 100755 --- a/publish-ghp-docs.sh +++ b/publish-ghp-docs.sh @@ -8,7 +8,7 @@ fi cd $(git rev-parse --show-toplevel) rm -rf target/doc/ -multirust run nightly cargo doc --no-deps --features "load_extension trace" +multirust run nightly cargo doc --no-deps --features "backup cache functions load_extension trace" echo '' > target/doc/index.html ghp-import target/doc git push origin gh-pages:gh-pages diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..a0cce77 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,168 @@ +//! Prepared statements cache for faster execution. + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::ops::{Deref, DerefMut}; +use {Result, Connection, Statement}; + +/// Prepared statements LRU cache. +#[derive(Debug)] +pub struct StatementCache<'conn> { + conn: &'conn Connection, + cache: RefCell>>, // back = LRU +} + +/// Cacheable statement. +/// +/// Statement will return automatically to the cache by default. +/// If you want the statement to be discarded, you can set the `cacheable` flag to `false`. +pub struct CachedStatement<'c: 's, 's> { + stmt: Option>, + cache: &'s StatementCache<'c>, + pub cacheable: bool, +} + +impl<'c, 's> Deref for CachedStatement<'c, 's> { + type Target = Statement<'c>; + + fn deref(&self) -> &Statement<'c> { + self.stmt.as_ref().unwrap() + } +} + +impl<'c, 's> DerefMut for CachedStatement<'c, 's> { + fn deref_mut(&mut self) -> &mut Statement<'c> { + self.stmt.as_mut().unwrap() + } +} + +impl<'c, 's> Drop for CachedStatement<'c, 's> { + #[allow(unused_must_use)] + fn drop(&mut self) { + if self.cacheable { + self.cache.release(self.stmt.take().unwrap()); + } else { + self.stmt.take().unwrap().finalize(); + } + } +} + +impl<'c, 's> CachedStatement<'c, 's> { + fn new(stmt: Statement<'c>, cache: &'s StatementCache<'c>) -> CachedStatement<'c, 's> { + CachedStatement { + stmt: Some(stmt), + cache: cache, + cacheable: true, + } + } +} + +impl<'conn> StatementCache<'conn> { + /// Create a statement cache. + pub fn new(conn: &'conn Connection, capacity: usize) -> StatementCache<'conn> { + StatementCache { + conn: conn, + cache: RefCell::new(VecDeque::with_capacity(capacity)), + } + } + + /// Search the cache for a prepared-statement object that implements `sql`. + // If no such prepared-statement can be found, allocate and prepare a new one. + /// + /// # Failure + /// + /// Will return `Err` if no cached statement can be found and the underlying SQLite prepare call fails. + pub fn get<'s>(&'s self, sql: &str) -> Result> { + let mut cache = self.cache.borrow_mut(); + let stmt = match cache.iter().rposition(|entry| entry.eq(sql)) { + Some(index) => Ok(cache.swap_remove_front(index).unwrap()), // FIXME Not LRU compliant + _ => self.conn.prepare(sql), + }; + stmt.map(|stmt| CachedStatement::new(stmt, self)) + } + + /// If `discard` is true, then the statement is deleted immediately. + /// Otherwise it is added to the LRU list and may be returned + /// by a subsequent call to `get()`. + /// + /// # Failure + /// + /// Will return `Err` if `stmt` (or the already cached statement implementing the same SQL) statement is `discard`ed + /// and the underlying SQLite finalize call fails. + fn release(&self, mut stmt: Statement<'conn>) { + let mut cache = self.cache.borrow_mut(); + if cache.capacity() == cache.len() { + // is full + cache.pop_back(); // LRU dropped + } + stmt.reset_if_needed(); + stmt.clear_bindings(); + cache.push_front(stmt) + } + + /// Flush the prepared statement cache + pub fn clear(&self) { + self.cache.borrow_mut().clear(); + } + + /// Return current cache size. + pub fn len(&self) -> usize { + self.cache.borrow().len() + } + + /// Return maximum cache size. + pub fn capacity(&self) -> usize { + self.cache.borrow().capacity() + } +} + +#[cfg(test)] +mod test { + use Connection; + use super::StatementCache; + + #[test] + fn test_cache() { + let db = Connection::open_in_memory().unwrap(); + let cache = StatementCache::new(&db, 15); + assert_eq!(0, cache.len()); + assert_eq!(15, cache.capacity()); + + let sql = "PRAGMA schema_version"; + { + let mut stmt = cache.get(sql).unwrap(); + assert_eq!(0, cache.len()); + assert_eq!(0, + stmt.query(&[]).unwrap().get_expected_row().unwrap().get::(0)); + } + assert_eq!(1, cache.len()); + + { + let mut stmt = cache.get(sql).unwrap(); + assert_eq!(0, cache.len()); + assert_eq!(0, + stmt.query(&[]).unwrap().get_expected_row().unwrap().get::(0)); + } + assert_eq!(1, cache.len()); + + cache.clear(); + assert_eq!(0, cache.len()); + assert_eq!(15, cache.capacity()); + } + + #[test] + fn test_cacheable() { + let db = Connection::open_in_memory().unwrap(); + let cache = StatementCache::new(&db, 15); + + let sql = "PRAGMA schema_version"; + { + let mut stmt = cache.get(sql).unwrap(); + assert_eq!(0, cache.len()); + assert_eq!(0, + stmt.query(&[]).unwrap().get_expected_row().unwrap().get::(0)); + stmt.cacheable = false; + } + assert_eq!(0, cache.len()); + } +} diff --git a/src/lib.rs b/src/lib.rs index d4b445e..ecbdfa0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,7 @@ mod error; #[cfg(feature = "load_extension")]mod load_extension_guard; #[cfg(feature = "trace")]pub mod trace; #[cfg(feature = "backup")]pub mod backup; +#[cfg(feature = "cache")] pub mod cache; #[cfg(feature = "functions")] pub mod functions; #[cfg(feature = "blob")] pub mod blob; @@ -912,6 +913,21 @@ impl<'conn> Statement<'conn> { } } + #[cfg(feature = "cache")] + fn clear_bindings(&mut self) { + unsafe { + ffi::sqlite3_clear_bindings(self.stmt); + }; + } + + #[cfg(feature = "cache")] + fn eq(&self, sql: &str) -> bool { + unsafe { + let c_slice = CStr::from_ptr(ffi::sqlite3_sql(self.stmt)).to_bytes(); + str::from_utf8(c_slice).unwrap().eq(sql) + } + } + fn finalize_(&mut self) -> Result<()> { let r = unsafe { ffi::sqlite3_finalize(self.stmt) }; self.stmt = ptr::null_mut();