tiempo-rs/src/database.rs

433 lines
15 KiB
Rust

use std::path::Path;
use rusqlite::{Connection, ToSql};
use chrono::{DateTime, Utc};
use crate::error::{Error, Result};
use crate::models::{Entry, Meta};
pub enum DBVersion {
Timetrap,
Version(u16),
}
pub trait Database {
/// This is used to create tables and insert rows
fn execute(&mut self, query: &str, params: &[&dyn ToSql]) -> Result<()>;
/// And this is used to retrieve data
fn entry_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Entry>>;
fn meta_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Meta>>;
// ----------
// Migrations
// ----------
fn init(&mut self) -> Result<()> {
self.execute("CREATE TABLE `entries`
(
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,\
`note` varchar(255),
`start` timestamp,
`end` timestamp,
`sheet` varchar(255)
)
", &[])?;
self.execute("CREATE TABLE `meta`
(
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`key` varchar(255),
`value` varchar(255)
)
", &[])?;
self.execute("INSERT INTO meta (key, value) VALUES ('database_version', 1)", &[])?;
Ok(())
}
// -------------
// Entry queries
// -------------
fn entries_by_sheet(&self, sheet: &str, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> Result<Vec<Entry>> {
match (start, end) {
(Some(start), Some(end)) => {
self.entry_query(
"select * from entries where sheet=?1 and start >= ?2 and start <= ?3 order by start asc",
&[&sheet, &start, &end]
)
}
(Some(start), None) => {
self.entry_query(
"select * from entries where sheet=?1 and start >= ?2 order by start asc",
&[&sheet, &start]
)
}
(None, Some(end)) => {
self.entry_query(
"select * from entries where sheet=?1 and start <= ?2 order by start asc",
&[&sheet, &end]
)
}
(None, None) => {
self.entry_query(
"select * from entries where sheet=?1 order by start asc",
&[&sheet]
)
}
}
}
fn entries_all_visible(&self, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> Result<Vec<Entry>> {
match (start, end) {
(Some(start), Some(end)) => {
self.entry_query(
"select * from entries where sheet not like '!_%' escape \"!\" and start >= ?1 and start <= ?2 order by start asc",
&[&start, &end]
)
}
(Some(start), None) => {
self.entry_query(
"select * from entries where sheet not like '!_%' escape \"!\" and start >= ?1 order by start asc",
&[&start]
)
}
(None, Some(end)) => {
self.entry_query(
"select * from entries where sheet not like '!_%' escape \"!\" and start <= ?1 order by start asc",
&[&end]
)
}
(None, None) => {
self.entry_query(
"select * from entries where sheet not like '!_%' escape \"!\" order by start asc",
&[]
)
}
}
}
fn entries_full(&self, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> Result<Vec<Entry>> {
match (start, end) {
(Some(start), Some(end)) => {
self.entry_query(
"select * from entries where start >= ?1 and start <= ?2 order by start asc",
&[&start, &end]
)
}
(Some(start), None) => {
self.entry_query(
"select * from entries where start >= ?1 order by start asc",
&[&start]
)
}
(None, Some(end)) => {
self.entry_query(
"select * from entries where start <= ?1 order by start asc",
&[&end]
)
}
(None, None) => {
self.entry_query(
"select * from entries order by start asc",
&[]
)
}
}
}
fn entry_insert(&mut self, start: DateTime<Utc>, end: Option<DateTime<Utc>>, note: Option<String>, sheet: String) -> Result<()> {
self.execute("insert into entries (start, end, note, sheet) values (?1, ?2, ?3, ?4)", &[
&start, &end, &note, &sheet,
])
}
// Meta queries
fn current_sheet(&self) -> Result<Option<String>> {
let results = self.meta_query("select * from meta where key='current_sheet'", &[])?;
Ok(results.into_iter().next().map(|m| m.value))
}
fn last_sheet(&self) -> Result<Option<String>> {
let results = self.meta_query("select * from meta where key='last_sheet'", &[])?;
Ok(results.into_iter().next().map(|m| m.value))
}
fn set_current_sheet(&mut self, sheet: &str) -> Result<()> {
self.execute("INSERT INTO meta (key, value) VALUES ('current_sheet', ?1)", &[&sheet])?;
Ok(())
}
fn set_last_sheet(&mut self, sheet: &str) -> Result<()> {
self.execute("INSERT INTO meta (key, value) VALUES ('last_sheet', ?1)", &[&sheet])?;
Ok(())
}
fn version(&self) -> Result<DBVersion> {
let results = self.meta_query("select * from meta where key='database_version'", &[])?;
if let Some(v) = results.into_iter().next().map(|m| m.value) {
Ok(DBVersion::Version(v.parse().map_err(|_| {
Error::CorruptedData(format!(
"Found value '{}' for key 'database_version' in meta table, which is not a valid integer",
v
))
})?))
} else {
Ok(DBVersion::Timetrap)
}
}
}
pub struct SqliteDatabase {
connection: Connection,
}
impl SqliteDatabase {
pub fn from_memory() -> Result<impl Database> {
Ok(SqliteDatabase {
connection: Connection::open_in_memory()?,
})
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<impl Database> {
Ok(SqliteDatabase {
connection: Connection::open(path)?,
})
}
pub fn from_path_or_create<P: AsRef<Path>>(path: P) -> Result<impl Database> {
if path.as_ref().is_file() {
Self::from_path(path)
} else {
let mut db = Self::from_path(path)?;
db.init()?;
Ok(db)
}
}
}
impl Database for SqliteDatabase {
fn execute(&mut self, query: &str, params: &[&dyn ToSql]) -> Result<()> {
self.connection.execute(query, params)?;
Ok(())
}
fn entry_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Entry>> {
let mut stmt = self.connection.prepare(query)?;
let entries = stmt.query_and_then(params, |row| {
let id: u64 = row.get("id")?;
let note = row.get("note")?;
let sheet = row.get("sheet")?;
let start = row.get("start").map_err(|_| {
Error::InvalidTimeInDatabase {
id: id.to_string(),
col: "start".into(),
}
})?;
let end = row.get("end").map_err(|_| {
Error::InvalidTimeInDatabase {
id: id.to_string(),
col: "start".into(),
}
})?;
Ok(Entry {
id, note, start, end, sheet,
})
})?.collect();
entries
}
fn meta_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Meta>> {
let mut stmt = self.connection.prepare(query)?;
let results = stmt.query_map(params, |row| Ok(Meta {
id: row.get("id")?,
key: row.get("key")?,
value: row.get("value")?,
}))?.filter_map(|r| r.ok()).collect();
Ok(results)
}
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
fn test_entries_by_sheet() {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "OOO".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "OOO".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "OOO".into()).unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
// filter by start and end
assert_eq!(
db.entries_by_sheet("XXX", Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
]
);
// filter only by start
assert_eq!(
db.entries_by_sheet("XXX", Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
]
);
// filter only by end
assert_eq!(
db.entries_by_sheet("XXX", None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
]
);
// no filter
assert_eq!(
db.entries_by_sheet("XXX", None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
]
);
}
#[test]
fn test_entries_all() {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "_OO".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "_OO".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "_OO".into()).unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
// filter by start and end
assert_eq!(
db.entries_all_visible(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
]
);
// filter only by start
assert_eq!(
db.entries_all_visible(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
]
);
// filter only by end
assert_eq!(
db.entries_all_visible(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
]
);
// no filter
assert_eq!(
db.entries_all_visible(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
]
);
}
#[test]
fn test_entries_full() {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "_OO".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "_OO".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX".into()).unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "_OO".into()).unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
// filter by start and end
assert_eq!(
db.entries_full(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
]
);
// filter only by start
assert_eq!(
db.entries_full(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
]
);
// filter only by end
assert_eq!(
db.entries_full(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
]
);
// no filter
assert_eq!(
db.entries_full(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
]
);
}
}