use std::path::PathBuf; use rusqlite::{Connection, OptionalExtension}; use xdg::BaseDirectories; use crate::domain; use crate::repo::{Error, Repository}; struct Task { id: i64, name: String, project: Option, link: Option, dir_path: Option, /* current: bool, finished_at: Option, */ created_at: time::OffsetDateTime, idx: i64, } impl From for domain::Task { fn from(repo: Task) -> Self { Self { name: repo.name, project: repo.project, link: repo.link, dir_path: repo.dir_path.map(PathBuf::from), created_at: repo.created_at, } } } impl<'r> TryFrom<&'r rusqlite::Row<'_>> for Task { type Error = rusqlite::Error; fn try_from(row: &'r rusqlite::Row<'_>) -> Result { Ok(Self { id: row.get("id")?, name: row.get("name")?, project: row.get("project")?, link: row.get("link")?, dir_path: row.get("dir_path")?, /* current: row.get("current")?, finished_at: row.get("finished_at")?, */ created_at: row.get("created_at")?, idx: row.get("idx")?, }) } } const SCHEMA_FILE: &str = "schema.sql"; pub struct SqliteRepo { conn: Connection, } impl SqliteRepo { pub fn new(xdg_dirs: BaseDirectories) -> Result { let file_path = xdg_dirs.get_data_file(SCHEMA_FILE); let conn = Connection::open(file_path).map_err(|_| Error::Connect)?; Ok(Self { conn }) } } impl Repository for SqliteRepo { fn get_current_task_opt(&self) -> Result, Error> { let db_task = self.get_current_task_opt_impl()?; Ok(db_task.map(|db_task| domain::CurrentTaskInfo { task_idx: db_task.idx as usize, task: db_task.into(), })) } fn get_task_opt(&self, id: domain::TaskIdx) -> Result, Error> { self.get_task_opt_impl(id).map(|t| t.map(From::from)) } fn get_tasks(&self) -> Result, Error> { let mut stmt = self .conn .prepare("SELECT * FROM active_tasks") .map_err(|_| Error::PrepareQuery)?; let rows = stmt .query_map([], |row| Task::try_from(row)) .map_err(|_| Error::QueryData)?; rows.into_iter() .map(|db_task| db_task.map(From::from).map_err(|_| Error::QueryData)) .collect::, Error>>() .map_err(|_| Error::InvalidData) } fn remove_task(&self, idx: domain::TaskIdx) -> Result { let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?; let mut stmt = self .conn .prepare("DELETE FROM tasks WHERE id = ?") .map_err(|_| Error::PrepareQuery)?; stmt.execute([&(db_task.id)]) .map_err(|_| Error::RemoveData)?; Ok(db_task.into()) } fn insert_task(&self, insert_data: super::InsertTaskData) -> Result { let mut stmt = self .conn .prepare( "INSERT INTO tasks (name, project, link, dir_path) VALUES (?1, ?2, ?3, ?4)", ) .map_err(|_| Error::PrepareQuery)?; let id = stmt .insert(( &insert_data.name, &insert_data.project, &insert_data.link, &insert_data .dir_path .and_then(|p| p.into_os_string().into_string().ok()), )) .map_err(|_| Error::InsertData)?; self.get_task_by_id_impl(id).map(From::from) } fn update_task( &self, idx: domain::TaskIdx, update_data: super::UpdateTaskData, ) -> Result { let task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?; let mut stmt = self .conn .prepare( "UPDATE tasks SET name = ?1, project = ?2, link = ?3, dir_path = ?4 WHERE id = ?5 ", ) .map_err(|_| Error::PrepareQuery)?; stmt.execute(( &update_data.name.unwrap_or(task.name), &update_data.project.unwrap_or(task.project), &update_data.link.unwrap_or(task.link), &update_data .dir_path .unwrap_or(task.dir_path.map(PathBuf::from)) .and_then(|p| p.into_os_string().into_string().ok()), &task.id, )) .map_err(|_| Error::UpdateData)?; self.get_task_opt(idx)?.ok_or(Error::NotFound) } fn start_task(&self, idx: domain::TaskIdx) -> Result { let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?; self.conn .execute( "UPDATE tasks SET current = true WHERE id = ?1", [&db_task.id], ) .map_err(|_| Error::UpdateData)?; Ok(db_task.into()) } fn stop_task(&self) -> Result { let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?; self.conn .execute( "UPDATE tasks SET current = false WHERE id = ?1", [&db_task.id], ) .map_err(|_| Error::UpdateData)?; Ok(db_task.into()) } fn finish_task(&self) -> Result { let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?; self.conn .execute( "UPDATE tasks SET current = false, finished_at = CURRENT_TIMESTAMP WHERE id = ?1", [&db_task.id], ) .map_err(|_| Error::UpdateData)?; Ok(db_task.into()) } fn get_finished_tasks(&self) -> Result { todo!() } } impl SqliteRepo { fn get_task_by_id_impl(&self, id: i64) -> Result { let mut stmt = self .conn .prepare("SELECT * FROM active_tasks WHERE id = ?") .map_err(|_| Error::PrepareQuery)?; stmt.query_row([id], |row| Task::try_from(row)) .map_err(|_| Error::QueryData) } fn get_task_opt_impl(&self, idx: domain::TaskIdx) -> Result, Error> { let mut stmt = self .conn .prepare("SELECT * FROM active_tasks WHERE idx = ?") .map_err(|_| Error::PrepareQuery)?; stmt.query_row([idx as i64], |row| Task::try_from(row)) .optional() .map_err(|_| Error::QueryData) } fn get_current_task_opt_impl(&self) -> Result, Error> { let mut stmt = self .conn .prepare("SELECT * FROM active_tasks WHERE current IS true") .map_err(|_| Error::PrepareQuery)?; let row = stmt .query_row([], |row| Task::try_from(row)) .optional() .map_err(|_| Error::QueryData)?; Ok(row) } } macro_rules! run_migration { ($this:ident, $ver:ident = $version:expr) => { run_migration!($this, $ver = $version => concat!("migrations/", $version)); }; ($this:ident, $ver:ident = $version:expr => $sql_name:expr) => { $this .conn .execute_batch(&format!( "BEGIN; {} COMMIT;", include_str!(concat!("../../database/", $sql_name, ".sql")) )) .map_err(|_| MigrationError::Upgrade)?; $ver.replace($version); }; } // TODO: move migration to a separate repository. #[derive(Debug)] pub enum MigrationError { Upgrade, DeleteInfo, InsertInfo, } impl std::fmt::Display for MigrationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MigrationError::Upgrade => f.write_str("Cannot upgrade the migration to a new version"), MigrationError::DeleteInfo => { f.write_str("Cannot delete the tas info from the database") } MigrationError::InsertInfo => { f.write_str("Cannot insert a new tas information to the database") } } } } impl std::error::Error for MigrationError {} const LATEST_VERSION: i64 = 202208201623; impl SqliteRepo { pub fn upgrade(&self) -> Result<(), MigrationError> { let mut version = self.version(); match version { Some(LATEST_VERSION) => return Ok(()), None => { run_migration!(self, version = LATEST_VERSION => "schema"); } Some(v) => { if v == 202208162308 { run_migration!(self, version = 202208201623); } } } self.conn .execute("DELETE FROM _tas_info", []) .map_err(|_| MigrationError::DeleteInfo)?; self.conn .execute( "INSERT INTO _tas_info (version) VALUES (?)", [version.unwrap()], ) .map_err(|_| MigrationError::InsertInfo)?; Ok(()) } fn version(&self) -> Option { self.conn .query_row("SELECT version FROM _tas_info", [], |r| r.get("version")) .unwrap_or_default() } }