use super::{ContextError, Store}; use miette::Result; use sqlite::Connection; pub struct SQLiteStore { location: String, connection: Option, } impl SQLiteStore { /// Wrap connection in a Result to make it easier to unwrap later fn get_connection(&self) -> Result<&Connection, sqlite::Error> { match &self.connection { Some(conn) => Ok(conn), None => Err(sqlite::Error { code: None, message: Some("Uninitialized".into()), }), } } fn create_table(&mut self) -> Result<(), sqlite::Error> { let conn = self.get_connection()?; let mut statement = conn.prepare("CREATE TABLE RATAT ( COUNTER SMALLINT )")?; statement.next()?; Ok(()) } fn insert_counter(&mut self, counter: i64) -> Result<(), sqlite::Error> { let conn = self.get_connection()?; let mut statement = conn.prepare("INSERT INTO RATAT ( COUNTER ) VALUES ( :counter )")?; statement.bind((":counter", counter))?; statement.next()?; Ok(()) } fn select_counter(&self) -> Result { let conn = self.get_connection()?; let mut statement = conn.prepare("SELECT COUNTER FROM RATAT")?; statement.next()?; statement.read("COUNTER") } fn update_counter(&mut self, counter: i64) -> Result<(), sqlite::Error> { let conn = self.get_connection()?; let mut statement = conn.prepare("UPDATE RATAT SET COUNTER = :counter")?; statement.bind((":counter", counter))?; statement.next()?; Ok(()) } } impl Default for SQLiteStore { fn default() -> Self { SQLiteStore { location: ":memory:".into(), connection: Option::None, } } } impl Store for SQLiteStore { /// Init SQLite for counting. Creates `RATAT` table with single row, with /// counter set to `0` fn init(&mut self) -> Result<(), ContextError> { let conn_path = self.location.clone(); let conn = sqlite::open(conn_path)?; self.connection = Option::Some(conn); self.create_table()?; self.insert_counter(0)?; Ok(()) } /// Selects counter from `RATAT` table. Since sqlite doesn't scrutinize the /// value and we have a more restricted datatype (i64 -> u8), we use /// `try_from` which may produce an error result. fn get_counter(&self) -> Result { let result = self.select_counter()?; let count = u8::try_from(result)?; Ok(count) } /// Updates row with counter value on `RATAT` table fn set_counter(&mut self, new_count: u8) -> Result { self.update_counter(i64::from(new_count))?; tracing::event!(tracing::Level::DEBUG, new_count, "Updated counter."); Ok(new_count) } } #[cfg(test)] mod tests { use super::*; // Practically, the `Store` trait is what we interface with externally, and // provides the more general `Result` interface we want to work with so // that's what we test here #[test] fn default() { let context = SQLiteStore::default(); assert_eq!(context.location, ":memory:"); assert!(context.connection.is_none()); } #[test] fn init() { let mut context = SQLiteStore::default(); let res = context.init(); assert!(res.is_ok()); assert!(context.connection.is_some()); } } #[cfg(test)] mod counter_tests { use super::*; #[test] fn get_counter() { let mut context = SQLiteStore::default(); assert!(context.init().is_ok()); let res = context.get_counter(); assert!(res.is_ok()); assert_eq!(res.unwrap(), 0); } #[test] fn get_counter_error() { let context = SQLiteStore::default(); // don't initialize let res = context.get_counter(); assert!(res.is_err()); } #[test] fn set_counter() { let mut context = SQLiteStore::default(); assert!(context.init().is_ok()); let expected = 240; let res = context.set_counter(expected); assert!(res.is_ok()); assert_eq!(res.unwrap(), expected); } #[test] fn set_counter_error() { let mut context = SQLiteStore::default(); // don't initialize let res = context.set_counter(1); assert!(res.is_err()); } #[test] fn set_and_get() { let mut context = SQLiteStore::default(); assert!(context.init().is_ok()); let count_0 = context.get_counter().unwrap(); assert_eq!(count_0, 0); let new_num = 244; let res = context.set_counter(new_num); assert!(res.is_ok()); assert_eq!(res.unwrap(), new_num); let count_1 = context.get_counter().unwrap(); assert_eq!(count_1, new_num); assert_ne!(count_0, count_1); } }