diff options
| author | Kevin Hoerr <kjhoerr@submelon.dev> | 2026-04-16 11:37:21 -0400 |
|---|---|---|
| committer | Kevin Hoerr <kjhoerr@submelon.dev> | 2026-04-16 11:37:21 -0400 |
| commit | 615d44041d35ee1a08ddc66423f67a9d48f53ae9 (patch) | |
| tree | 9720d6a192e0bae9c7393b8b99ff8ae401ec7f72 /src/app/store/sqlite.rs | |
| parent | 51623bc995c29937bb0a13d5b71ff908be8d1a1c (diff) | |
| download | equity-tracker-615d44041d35ee1a08ddc66423f67a9d48f53ae9.tar.gz equity-tracker-615d44041d35ee1a08ddc66423f67a9d48f53ae9.tar.bz2 equity-tracker-615d44041d35ee1a08ddc66423f67a9d48f53ae9.zip | |
Switch to distrobox as base for development
Diffstat (limited to 'src/app/store/sqlite.rs')
| -rw-r--r-- | src/app/store/sqlite.rs | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/src/app/store/sqlite.rs b/src/app/store/sqlite.rs new file mode 100644 index 0000000..4f5f8f2 --- /dev/null +++ b/src/app/store/sqlite.rs @@ -0,0 +1,199 @@ +use super::{ContextError, Store}; +use miette::Result; +use sqlite::Connection; + +pub struct SQLiteStore { + location: String, + connection: Option<Connection>, +} + +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<i64, sqlite::Error> { + 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<u8, ContextError> { + 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<u8, ContextError> { + self.update_counter(i64::from(new_count))?; + + 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()); + } + + #[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 get_counter_outside_range() { + let mut context = SQLiteStore::default(); + assert!(context.init().is_ok()); + + let new_value = -1; + if let Some(conn) = &context.connection { + let mut statement = conn.prepare("UPDATE RATAT SET COUNTER = ?").unwrap(); + statement.bind((1, new_value)).unwrap(); + statement.next().unwrap(); + } + 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); + } +} |
