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>; fn meta_query(&self, query: &str, params: &[&dyn ToSql]) -> Result>; // ---------- // 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) ) ", &[])?; Ok(()) } // ------------- // Entry queries // ------------- fn entries_by_sheet(&self, sheet: &str) -> Result> { self.entry_query("select * from entries where sheet=?1 order by start asc", &[&sheet]) } fn entries_all_visible(&self) -> Result> { self.entry_query("select * from entries where sheet not like '!_%' escape \"!\" order by sheet asc, start asc", &[]) } fn entries_full(&self) -> Result> { self.entry_query("select * from entries order by sheet asc, start asc", &[]) } fn entry_insert(&mut self, start: DateTime, end: Option>, note: Option, sheet: String) -> Result<()> { self.execute("insert into entries (start, end, note, sheet) values (?1, ?2, ?3, ?4)", &[ &start, &end, ¬e, &sheet, ]) } // Meta queries fn current_sheet(&self) -> Result> { let results = self.meta_query("select * from meta where key='current_sheet'", &[])?; Ok(results.into_iter().next().map(|m| m.value)) } fn version(&self) -> Result { 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 { Ok(SqliteDatabase { connection: Connection::open_in_memory()?, }) } pub fn from_path>(path: P) -> Result { Ok(SqliteDatabase { connection: Connection::open(path)?, }) } } 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> { let mut stmt = self.connection.prepare(query)?; let results = stmt.query_map(params, |row| { let x = Ok(Entry { id: row.get("id")?, note: row.get("note")?, start: row.get("start")?, end: row.get("end")?, sheet: row.get("sheet")?, }); x})?.filter_map(|r| r.ok()).collect(); Ok(results) } fn meta_query(&self, query: &str, params: &[&dyn ToSql]) -> Result> { 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) } }