tas/src/repo/sqlite.rs

336 lines
9.6 KiB
Rust

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<String>,
link: Option<String>,
dir_path: Option<String>,
/*
current: bool,
finished_at: Option<time::OffsetDateTime>,
*/
created_at: time::OffsetDateTime,
idx: i64,
}
impl From<Task> 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<Self, Self::Error> {
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 = "tas.db";
pub struct SqliteRepo {
conn: Connection,
}
impl SqliteRepo {
pub fn new(xdg_dirs: BaseDirectories) -> Result<Self, Error> {
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<Option<domain::CurrentTaskInfo>, 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<Option<domain::Task>, Error> {
self.get_task_opt_impl(id).map(|t| t.map(From::from))
}
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error> {
let mut stmt = if finished {
self.conn
.prepare("SELECT * FROM finished_tasks")
.map_err(|_| Error::PrepareQuery)?
} else {
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::<Result<Vec<_>, Error>>()
.map_err(|_| Error::InvalidData)
}
fn remove_task(&self, idx: domain::TaskIdx) -> Result<domain::Task, Error> {
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<domain::Task, Error> {
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<domain::Task, Error> {
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<domain::Task, Error> {
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<domain::Task, Error> {
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<domain::Task, Error> {
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())
}
}
impl SqliteRepo {
fn get_task_by_id_impl(&self, id: i64) -> Result<Task, Error> {
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<Option<Task>, 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<Option<Task>, 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<i64> {
self.conn
.query_row("SELECT version FROM _tas_info", [], |r| r.get("version"))
.unwrap_or_default()
}
}