Get list of finished tasks #32
10 changed files with 135 additions and 45 deletions
9
database/migrations/202208201623.sql
Normal file
9
database/migrations/202208201623.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE VIEW finished_tasks
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
row_number() OVER (ORDER BY t.finished_at DESC) AS idx
|
||||||
|
FROM tasks AS t
|
||||||
|
WHERE t.finished_at IS NOT NULL
|
||||||
|
ORDER BY t.finished_at DESC
|
||||||
|
;
|
|
@ -24,3 +24,13 @@ FROM tasks AS t
|
||||||
WHERE t.finished_at IS NULL
|
WHERE t.finished_at IS NULL
|
||||||
ORDER BY t.created_at
|
ORDER BY t.created_at
|
||||||
;
|
;
|
||||||
|
|
||||||
|
CREATE VIEW finished_tasks
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
row_number() OVER (ORDER BY t.finished_at DESC) AS idx
|
||||||
|
FROM tasks AS t
|
||||||
|
WHERE t.finished_at IS NOT NULL
|
||||||
|
ORDER BY t.finished_at DESC
|
||||||
|
;
|
||||||
|
|
8
makefile
Normal file
8
makefile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
|
||||||
|
install:
|
||||||
|
cargo install --path .
|
||||||
|
|
||||||
|
new-migration:
|
||||||
|
touch ./database/migrations/$$(date +%Y%m%d%H%M).sql
|
||||||
|
|
|
@ -4,23 +4,58 @@ use xdg::BaseDirectories;
|
||||||
fn main() {
|
fn main() {
|
||||||
let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap();
|
let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap();
|
||||||
let fs_repo = repo::fs::FsRepo::new(xdg_dirs.clone());
|
let fs_repo = repo::fs::FsRepo::new(xdg_dirs.clone());
|
||||||
let tasks = fs_repo.get_tasks().unwrap();
|
let sqlite_repo = repo::sqlite::SqliteRepo::new(xdg_dirs.clone()).unwrap();
|
||||||
|
|
||||||
let sqlite_repo = repo::sqlite::SqliteRepo::new(xdg_dirs).unwrap();
|
log::info!("active tasks");
|
||||||
for task in tasks {
|
|
||||||
log::info!("task: {}", task.name);
|
|
||||||
log::info!(" inserting...");
|
|
||||||
|
|
||||||
sqlite_repo
|
let fs_tasks = fs_repo.get_tasks(false).unwrap();
|
||||||
.insert_task(repo::InsertTaskData {
|
if !fs_tasks.is_empty() {
|
||||||
name: task.name,
|
for task in fs_tasks {
|
||||||
project: task.project,
|
log::info!("task: {}", task.name);
|
||||||
link: task.link,
|
log::info!(" inserting...");
|
||||||
dir_path: task.dir_path,
|
|
||||||
index: None,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
log::info!(" inserted");
|
sqlite_repo
|
||||||
|
.insert_task(repo::InsertTaskData {
|
||||||
|
name: task.name,
|
||||||
|
project: task.project,
|
||||||
|
link: task.link,
|
||||||
|
dir_path: task.dir_path,
|
||||||
|
index: None,
|
||||||
|
finished_at: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
log::info!(" inserted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("finished tasks");
|
||||||
|
|
||||||
|
let fs_tasks = fs_repo.get_tasks(true).unwrap();
|
||||||
|
if !fs_tasks.is_empty() {
|
||||||
|
let meta = std::fs::metadata(xdg_dirs.get_data_file(repo::fs::FINISHED_DATA_FILE)).unwrap();
|
||||||
|
let finished_at = meta
|
||||||
|
.modified()
|
||||||
|
.map(time::OffsetDateTime::from)
|
||||||
|
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||||
|
|
||||||
|
for task in fs_tasks {
|
||||||
|
log::info!("task: {}", task.name);
|
||||||
|
log::info!(" inserting...");
|
||||||
|
|
||||||
|
sqlite_repo
|
||||||
|
.insert_task(repo::InsertTaskData {
|
||||||
|
name: task.name,
|
||||||
|
project: task.project,
|
||||||
|
link: task.link,
|
||||||
|
dir_path: task.dir_path,
|
||||||
|
index: None,
|
||||||
|
finished_at: Some(finished_at),
|
||||||
|
})
|
||||||
|
// TODO: think of a better solution than idx
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
log::info!(" inserted");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ pub fn execute(repo: impl Repository, args: Args) {
|
||||||
.transpose()
|
.transpose()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
index: None,
|
index: None,
|
||||||
|
finished_at: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
|
|
|
@ -18,19 +18,26 @@ use crate::repo::Repository;
|
||||||
|
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
#[clap(short, long)]
|
||||||
|
finished: bool,
|
||||||
|
|
||||||
projects: Vec<String>,
|
projects: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(repo: impl Repository, args: Args) {
|
pub fn execute(repo: impl Repository, args: Args) {
|
||||||
let tasks = match repo.get_tasks() {
|
let tasks = match repo.get_tasks(args.finished) {
|
||||||
Ok(tasks) => tasks,
|
Ok(tasks) => tasks,
|
||||||
Err(err) => return eprintln!("Cannot read tasks: {}", err),
|
Err(err) => return eprintln!("Cannot read tasks: {}", err),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cur_task = match repo.get_current_task_opt() {
|
let cur_task = if args.finished {
|
||||||
Ok(cur_task) => cur_task,
|
None
|
||||||
Err(err) => {
|
} else {
|
||||||
return eprintln!("Cannot read current task: {}", err);
|
match repo.get_current_task_opt() {
|
||||||
|
Ok(cur_task) => cur_task,
|
||||||
|
Err(err) => {
|
||||||
|
return eprintln!("Cannot read current task: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ pub fn execute(repo: impl Repository, args: Args) {
|
||||||
project: target.project,
|
project: target.project,
|
||||||
link: target.link,
|
link: target.link,
|
||||||
dir_path: target.dir_path,
|
dir_path: target.dir_path,
|
||||||
|
finished_at: None,
|
||||||
});
|
});
|
||||||
match res {
|
match res {
|
||||||
Ok(task) => {
|
Ok(task) => {
|
||||||
|
|
|
@ -55,6 +55,7 @@ pub struct InsertTaskData {
|
||||||
pub link: Option<String>,
|
pub link: Option<String>,
|
||||||
pub dir_path: Option<PathBuf>,
|
pub dir_path: Option<PathBuf>,
|
||||||
pub index: Option<usize>,
|
pub index: Option<usize>,
|
||||||
|
pub finished_at: Option<time::OffsetDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UpdateTaskData {
|
pub struct UpdateTaskData {
|
||||||
|
@ -69,7 +70,7 @@ pub trait Repository {
|
||||||
|
|
||||||
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error>;
|
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error>;
|
||||||
|
|
||||||
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error>;
|
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error>;
|
||||||
|
|
||||||
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error>;
|
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error>;
|
||||||
|
|
||||||
|
|
|
@ -60,9 +60,9 @@ impl From<CurrentTaskInfo> for domain::CurrentTaskInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CURRENT_TASK_FILE: &str = "current.json";
|
pub const CURRENT_TASK_FILE: &str = "current.json";
|
||||||
const DATA_FILE: &str = "data.json";
|
pub const DATA_FILE: &str = "data.json";
|
||||||
const FINISHED_DATA_FILE: &str = "finished_data.json";
|
pub const FINISHED_DATA_FILE: &str = "finished_data.json";
|
||||||
|
|
||||||
pub struct FsRepo {
|
pub struct FsRepo {
|
||||||
xdg_dirs: BaseDirectories,
|
xdg_dirs: BaseDirectories,
|
||||||
|
@ -89,9 +89,14 @@ impl Repository for FsRepo {
|
||||||
Ok(Some(tasks[id - 1].clone().into()))
|
Ok(Some(tasks[id - 1].clone().into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error> {
|
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error> {
|
||||||
self.get_tasks_impl()
|
let fs_tasks = if finished {
|
||||||
.map(|tasks| tasks.into_iter().map(Task::into).collect())
|
self.get_finished_tasks_impl()?.into_iter().rev().collect()
|
||||||
|
} else {
|
||||||
|
self.get_tasks_impl()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(fs_tasks.into_iter().map(Task::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error> {
|
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error> {
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl<'r> TryFrom<&'r rusqlite::Row<'_>> for Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA_FILE: &str = "schema.sql";
|
const SCHEMA_FILE: &str = "tas.db";
|
||||||
|
|
||||||
pub struct SqliteRepo {
|
pub struct SqliteRepo {
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
|
@ -82,11 +82,16 @@ impl Repository for SqliteRepo {
|
||||||
self.get_task_opt_impl(id).map(|t| t.map(From::from))
|
self.get_task_opt_impl(id).map(|t| t.map(From::from))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error> {
|
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error> {
|
||||||
let mut stmt = self
|
let mut stmt = if finished {
|
||||||
.conn
|
self.conn
|
||||||
.prepare("SELECT * FROM active_tasks")
|
.prepare("SELECT * FROM finished_tasks")
|
||||||
.map_err(|_| Error::PrepareQuery)?;
|
.map_err(|_| Error::PrepareQuery)?
|
||||||
|
} else {
|
||||||
|
self.conn
|
||||||
|
.prepare("SELECT * FROM active_tasks")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?
|
||||||
|
};
|
||||||
|
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], |row| Task::try_from(row))
|
.query_map([], |row| Task::try_from(row))
|
||||||
|
@ -116,8 +121,8 @@ impl Repository for SqliteRepo {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare(
|
.prepare(
|
||||||
"INSERT INTO tasks (name, project, link, dir_path)
|
"INSERT INTO tasks (name, project, link, dir_path, created_at, finished_at)
|
||||||
VALUES (?1, ?2, ?3, ?4)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?5)",
|
||||||
)
|
)
|
||||||
.map_err(|_| Error::PrepareQuery)?;
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
@ -129,6 +134,7 @@ impl Repository for SqliteRepo {
|
||||||
&insert_data
|
&insert_data
|
||||||
.dir_path
|
.dir_path
|
||||||
.and_then(|p| p.into_os_string().into_string().ok()),
|
.and_then(|p| p.into_os_string().into_string().ok()),
|
||||||
|
&insert_data.finished_at,
|
||||||
))
|
))
|
||||||
.map_err(|_| Error::InsertData)?;
|
.map_err(|_| Error::InsertData)?;
|
||||||
|
|
||||||
|
@ -251,12 +257,16 @@ impl SqliteRepo {
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! run_migration {
|
macro_rules! run_migration {
|
||||||
($this:ident, $ver:ident = $version:literal) => {
|
($this:ident, $ver:ident = $version:expr) => {
|
||||||
|
run_migration!($this, $ver = $version => concat!("migrations/", $version));
|
||||||
|
};
|
||||||
|
|
||||||
|
($this:ident, $ver:ident = $version:expr => $sql_name:expr) => {
|
||||||
$this
|
$this
|
||||||
.conn
|
.conn
|
||||||
.execute_batch(&format!(
|
.execute_batch(&format!(
|
||||||
"BEGIN; {} COMMIT;",
|
"BEGIN; {} COMMIT;",
|
||||||
include_str!(concat!("../../database/migrations/", $version, ".sql"))
|
include_str!(concat!("../../database/", $sql_name, ".sql"))
|
||||||
))
|
))
|
||||||
.map_err(|_| MigrationError::Upgrade)?;
|
.map_err(|_| MigrationError::Upgrade)?;
|
||||||
|
|
||||||
|
@ -288,18 +298,21 @@ impl std::fmt::Display for MigrationError {
|
||||||
|
|
||||||
impl std::error::Error for MigrationError {}
|
impl std::error::Error for MigrationError {}
|
||||||
|
|
||||||
const LATEST_VERSION: i64 = 202208162308;
|
const LATEST_VERSION: i64 = 202208201623;
|
||||||
|
|
||||||
impl SqliteRepo {
|
impl SqliteRepo {
|
||||||
pub fn upgrade(&self) -> Result<(), MigrationError> {
|
pub fn upgrade(&self) -> Result<(), MigrationError> {
|
||||||
let mut version = self.version();
|
let mut version = self.version();
|
||||||
if version == Some(LATEST_VERSION) {
|
match version {
|
||||||
return Ok(());
|
Some(LATEST_VERSION) => return Ok(()),
|
||||||
}
|
None => {
|
||||||
|
run_migration!(self, version = LATEST_VERSION => "schema");
|
||||||
// TODO: execute full schema if version is none
|
}
|
||||||
if version.is_none() {
|
Some(v) => {
|
||||||
run_migration!(self, version = 202208162308);
|
if v == 202208162308 {
|
||||||
|
run_migration!(self, version = 202208201623);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.conn
|
self.conn
|
||||||
|
|
Loading…
Reference in a new issue