Update database schema

Also bump major version to match schema version
This commit is contained in:
2020-11-09 17:44:28 +01:00
parent a91266b705
commit a85d43b35d
12 changed files with 247 additions and 41 deletions

2
Cargo.lock generated
View File

@ -1584,7 +1584,7 @@ checksum = "7345c971d1ef21ffdbd103a75990a15eb03604fc8b8852ca8cb418ee1a099028"
[[package]]
name = "stl"
version = "0.2.0"
version = "2.0.0"
dependencies = [
"bincode",
"chrono",

View File

@ -1,7 +1,7 @@
[package]
name = "stl"
description = "studielogg aka scuffed toggl"
version = "0.2.0"
version = "2.0.0"
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
license = "MPL-2.0"
edition = "2018"

View File

@ -0,0 +1,36 @@
pub mod v1_to_v2;
use crate::database::unversioned::global::schema_version;
use duplicate::duplicate;
use sled::Db;
pub fn migrate(db: &mut Db, from: schema_version::V, to: schema_version::V) -> Result<(), MigrationError> {
for current in from..to {
let next = current + 1;
println!("Will migrate from {} to {}", current, next);
match (current, next) {
(1, 2) => v1_to_v2::migrate(db)?,
_ => panic!("No valid migration from version {} to version {}", current, next),
}
}
Ok(())
}
#[derive(Debug)]
pub enum MigrationError {
Serde(bincode::Error),
Sled(sled::Error),
}
#[duplicate(
Variant Error;
[ Sled ] [ sled::Error ];
[ Serde ] [ bincode::Error ];
)]
impl From<Error> for MigrationError {
fn from(e: Error) -> MigrationError {
MigrationError::Variant(e)
}
}

View File

@ -0,0 +1,84 @@
use crate::database::v1;
use crate::database::v2;
use super::MigrationError;
use bincode::{serialize, deserialize};
use sled::Db;
use chrono::{DateTime, Utc};
/// Migrate from database version 1 to version 2
pub fn migrate(db: &mut Db) -> Result<(), MigrationError> {
{ // Migrate sessions
// Open old & new trees
let v1_past_sessions = db.open_tree(v1::trees::past_sessions::NAME)?;
let v2_sessions = db.open_tree(v2::trees::sessions::NAME)?;
// Iterate over old tree
for r in v1_past_sessions.iter() {
let (k, v) = r?;
// Deserialize old value
let v1::trees::past_sessions::V {
category,
started,
ended
} = deserialize(&v)?;
// Convert to new value
let v2_value = v2::trees::sessions::V {
category,
started: DateTime::<Utc>::from_utc(started, Utc).into(),
ended: DateTime::<Utc>::from_utc(ended, Utc).into(),
deleted: false,
};
// Insert into new tree
v2_sessions.insert(k, serialize(&v2_value)?)?;
}
// Remove old tree
v1_past_sessions.clear()?;
}
{ // Migrate categories
// Open the old tree, and a TMP tree since the old tree name hasn't changed
let categories = db.open_tree(v1::trees::categories::NAME)?;
let v2_categories_tmp = db.open_tree("TEMP")?;
// Iterate over old tree
for r in categories.iter() {
let (k, v) = r?;
// Deserialize old value
let v1::trees::categories::V {
name,
color,
started,
} = deserialize(&v)?;
// Convert to new value
let v2_value = v2::trees::categories::V {
name,
description: None,
color,
started: started.map(|ndt| DateTime::<Utc>::from_utc(ndt, Utc).into()),
parent: None,
deleted: false,
};
// Insert into temporary tree
v2_categories_tmp.insert(k, serialize(&v2_value)?)?;
}
// Copy data from temp-tree back into old tree
categories.clear()?;
for r in v2_categories_tmp.iter() {
let (k, v) = r?;
categories.insert(k ,v)?;
}
v2_categories_tmp.clear()?;
}
Ok(())
}

View File

@ -1,5 +1,8 @@
pub mod unversioned;
pub mod v1;
pub mod v2;
pub const SCHEMA_VERSION: unversioned::global::schema_version::V = 1;
pub use self::v1 as latest;
pub mod migrations;
pub const SCHEMA_VERSION: unversioned::global::schema_version::V = 2;
pub use v2 as latest;

View File

@ -1,3 +1,5 @@
#![allow(dead_code)] // old schema, not used anymore
pub(self) use chrono::NaiveDateTime;
pub(self) use serde_derive::{Deserialize, Serialize};
pub(self) use uuid::Uuid;
@ -18,7 +20,7 @@ pub mod trees {
/// The name of the category
pub name: String,
/// The color of the button in the rendered view
/// The HTML color of the category in the rendered view
pub color: String,
/// If the session is not active, this will be None

64
src/database/v2.rs Normal file
View File

@ -0,0 +1,64 @@
pub(self) use chrono::{DateTime, Local};
pub(self) use serde_derive::{Deserialize, Serialize};
pub(self) use uuid::Uuid;
/// Stuff in the default namespace
pub mod global {}
pub mod trees {
pub mod categories {
use super::super::*;
pub const NAME: &str = "CATEGORIES";
pub type K = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V {
/// The name of the category
pub name: String,
/// The description of the category
pub description: Option<String>,
/// The HTML color of the category in the rendered view
pub color: String,
/// If the session is not active, this will be None
pub started: Option<DateTime<Local>>,
/// The parent category of this category
/// If none, the category has no parent
pub parent: Option<K>,
// FIXME: this field is currently not used
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
pub deleted: bool,
}
}
pub mod sessions {
use super::super::*;
pub const NAME: &str = "SESSIONS";
pub type K = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V {
/// The UUID of the category to which this session belongs
pub category: trees::categories::K,
/// The time when this session was started
pub started: DateTime<Local>,
/// The time when this session was ended
pub ended: DateTime<Local>,
// FIXME: this field is currently not used
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
pub deleted: bool,
}
}
}

View File

@ -1,10 +1,10 @@
use chrono::NaiveDateTime;
use chrono::{DateTime, Local};
use handlebars::handlebars_helper;
use rocket_contrib::templates::Engines;
pub fn register_helpers(engines: &mut Engines) {
handlebars_helper!(pretty_datetime: |dt: str| {
let dt: NaiveDateTime = dt.parse().unwrap();
let dt: DateTime<Local> = dt.parse().unwrap();
format!("{}", dt.format("%Y-%m-%d %H:%M"))
});
engines

View File

@ -5,7 +5,9 @@ mod routes;
mod status_json;
use bincode::{deserialize, serialize};
use database::{unversioned::global::schema_version, SCHEMA_VERSION};
use crate::database::SCHEMA_VERSION;
use crate::database::unversioned::global::schema_version;
use crate::database::migrations::migrate;
use dotenv::dotenv;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template;
@ -16,7 +18,7 @@ fn main() -> io::Result<()> {
let db_path = env::var("DB_PATH").expect("DB_PATH not set");
let sled = sled::open(db_path)?;
let mut sled = sled::open(db_path)?;
match sled.insert(
serialize(schema_version::K).unwrap(),
serialize(&SCHEMA_VERSION).unwrap(),
@ -27,6 +29,9 @@ fn main() -> io::Result<()> {
"Schema version: {}, previously: {}",
SCHEMA_VERSION, prev_schema_version
);
migrate(&mut sled, prev_schema_version, SCHEMA_VERSION)
.expect("Migration failed")
}
None => {
println!("Schema version: {}, previously: None", SCHEMA_VERSION);

View File

@ -1,9 +1,8 @@
use crate::database::latest::trees::{categories, past_sessions};
use crate::database::latest::trees::{categories, sessions};
use crate::routes::pages;
use crate::status_json::StatusJson;
use bincode::{deserialize, serialize};
use chrono::NaiveDateTime;
use chrono::{Duration, Utc};
use chrono::{NaiveDateTime, TimeZone, Duration, Local};
use rocket::http::Status;
use rocket::request::{Form, FromForm};
use rocket::response::Redirect;
@ -29,8 +28,11 @@ pub fn create_category(
serialize(&uuid::Uuid::new_v4())?,
serialize(&categories::V {
name: category.name,
description: None,
color: category.color,
started: None,
parent: None,
deleted: false,
})?,
)?;
@ -68,8 +70,16 @@ pub fn bump_session(
let mut category: categories::V = deserialize(&data).unwrap();
match category.started.as_mut() {
Some(started) => {
*started -= duration;
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
if let Some(new_started) = started.checked_sub_signed(duration) {
*started = new_started;
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
} else {
return Ok(Err(StatusJson::new(
Status::BadRequest,
"Duration subtract resulted in overflow",
)))
}
Ok(Ok(Redirect::to(uri!(pages::index))))
}
None => {
@ -92,15 +102,15 @@ pub fn toggle_category_session(
let category_uuid_s = sled::IVec::from(serialize(&category_uuid.into_inner())?);
let categories_tree = db.open_tree(categories::NAME)?;
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
let sessions_tree = db.open_tree(sessions::NAME)?;
Ok((&categories_tree, &past_sessions_tree).transaction(
|(tx_categories, tx_past_sessions)| {
Ok((&categories_tree, &sessions_tree).transaction(
|(tx_categories, tx_sessions)| {
match tx_categories.get(&category_uuid_s)? {
None => return Ok(Err(Status::NotFound)),
Some(data) => {
let mut category: categories::V = deserialize(&data).unwrap();
let now = Utc::now().naive_utc();
let now = Local::now();
match (set_active, category.started.take()) {
(false, Some(started)) => {
@ -108,13 +118,14 @@ pub fn toggle_category_session(
let duration = now - started;
if duration > Duration::minutes(5) {
let session_uuid = serialize(&uuid::Uuid::new_v4()).unwrap();
let past_session = past_sessions::V {
let session = sessions::V {
category: category_uuid.into_inner(),
started,
ended: now,
deleted: category.deleted,
};
tx_past_sessions
.insert(session_uuid, serialize(&past_session).unwrap())?;
tx_sessions
.insert(session_uuid, serialize(&session).unwrap())?;
}
}
(true, None) => {
@ -140,6 +151,7 @@ pub struct EditSession {
category: Uuid,
started: String,
ended: String,
deleted: bool,
}
#[post("/session/<session_uuid>/edit", data = "<session>")]
@ -150,12 +162,11 @@ pub fn edit_session(
) -> Result<Redirect, StatusJson> {
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
dbg!(&session);
let session = past_sessions::V {
let session = sessions::V {
category: session.category.into_inner(),
started: NaiveDateTime::parse_from_str(&session.started, "%Y-%m-%d %H:%M")?,
ended: NaiveDateTime::parse_from_str(&session.ended, "%Y-%m-%d %H:%M")?,
started: Local.from_local_datetime(&NaiveDateTime::parse_from_str(&session.started, "%Y-%m-%d %H:%M")?).unwrap(),
ended: Local.from_local_datetime(&NaiveDateTime::parse_from_str(&session.ended, "%Y-%m-%d %H:%M")?).unwrap(),
deleted: session.deleted,
};
if session.started >= session.ended {
@ -165,7 +176,7 @@ pub fn edit_session(
));
}
db.open_tree(past_sessions::NAME)?
db.open_tree(sessions::NAME)?
.insert(session_uuid_s, serialize(&session)?)?;
// FIXME: Uuid does not implement FromUriParam for some reason... File an issue?
@ -177,19 +188,19 @@ pub fn edit_session(
pub fn delete_session(session_uuid: Uuid, db: State<'_, sled::Db>) -> Result<Redirect, StatusJson> {
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
let sessions_tree = db.open_tree(sessions::NAME)?;
match past_sessions_tree.remove(session_uuid_s)? {
match sessions_tree.remove(session_uuid_s)? {
Some(_) => Ok(Redirect::to(uri!(pages::history))),
None => Err(Status::NotFound.into()),
}
// TODO: mark as deleted instead of removing
// Ok(past_sessions_tree.transaction(|tx_past_sessions| {
// match tx_past_sessions.get(&session_uuid_s)? {
// Ok(sessions_tree.transaction(|tx_sessions| {
// match tx_sessions.get(&session_uuid_s)? {
// None => return Ok(Err(Status::NotFound)),
// Some(data) => {
// let mut past_session: past_sessions::V = deserialize(&data).unwrap();
// let mut session: sessions::V = deserialize(&data).unwrap();
// }
// }
// Ok(Ok(Redirect::to(uri!(pages::history))))

View File

@ -1,4 +1,4 @@
use crate::database::latest::trees::{categories, past_sessions};
use crate::database::latest::trees::{categories, sessions};
use crate::status_json::StatusJson;
use bincode::deserialize;
use bincode::serialize;
@ -35,14 +35,14 @@ pub fn index(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
pub fn session_edit(session_uuid: Uuid, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct SessionPageContext {
session: past_sessions::V,
session: sessions::V,
session_id: uuid::Uuid,
}
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
match past_sessions_tree.get(session_uuid_s)? {
let sessions_tree = db.open_tree(sessions::NAME)?;
match sessions_tree.get(session_uuid_s)? {
None => Err(Status::NotFound)?,
Some(data) => {
let context = SessionPageContext {
@ -60,7 +60,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct HistoryEntryContext {
category: categories::V,
session: past_sessions::V,
session: sessions::V,
session_id: uuid::Uuid,
duration: Duration,
}
@ -71,7 +71,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
}
let categories_tree = db.open_tree(categories::NAME)?;
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
let sessions_tree = db.open_tree(sessions::NAME)?;
let categories: HashMap<categories::K, categories::V> = categories_tree
.iter()
@ -80,7 +80,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
})
.collect::<Result<Result<_, _>, _>>()??;
let past_sessions: HashMap<past_sessions::K, past_sessions::V> = past_sessions_tree
let sessions: HashMap<sessions::K, sessions::V> = sessions_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
@ -88,7 +88,7 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
.collect::<Result<Result<_, _>, _>>()??;
let mut context = TemplateContext {
entries: past_sessions
entries: sessions
.into_iter()
.map(|(session_id, session)| HistoryEntryContext {
duration: (session.ended - session.started).to_std().unwrap(),

View File

@ -20,7 +20,8 @@
<br>
<br>
<form action="/session/{{session_id}}/edit" method="post">
<input type="hidden" id="category" name="category" value="{{session.category}}">
<input type="hidden" id="category" name="category" value="{{session.category}}">
<input type="hidden" id="deleted" name="deleted" value="{{session.deleted}}">
<span>Started:</span>
<input type="text" id="started" name="started" value="{{pretty_datetime session.started}}"></input>
<br>