aboutsummaryrefslogtreecommitdiff
path: root/src/app/store/sqlite.rs
diff options
context:
space:
mode:
authorKevin Hoerr <kjhoerr@submelon.dev>2026-04-16 11:37:21 -0400
committerKevin Hoerr <kjhoerr@submelon.dev>2026-04-16 11:37:21 -0400
commit615d44041d35ee1a08ddc66423f67a9d48f53ae9 (patch)
tree9720d6a192e0bae9c7393b8b99ff8ae401ec7f72 /src/app/store/sqlite.rs
parent51623bc995c29937bb0a13d5b71ff908be8d1a1c (diff)
downloadequity-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.rs199
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);
+ }
+}