X-Git-Url: http://git.euphorik.ch/?a=blobdiff_plain;f=backend%2Fsrc%2Fdb.rs;h=2bf383b4e395c49550c58689a316a6cfe79adbbe;hb=cdb883c3c4ccbb82774ecfbfad059f3392e75432;hp=6458c7eb7308d384a695f7af38893d0c0e2fb821;hpb=eab43f8995eff5b8a4f6c4ded6a655866feddedb;p=recipes.git diff --git a/backend/src/db.rs b/backend/src/db.rs index 6458c7e..2bf383b 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -1,48 +1,158 @@ -use std::path::Path; -use std::fs; +use std::{fs::{self, File}, path::Path, io::Read}; +use itertools::Itertools; //use rusqlite::types::ToSql; //use rusqlite::{Connection, Result, NO_PARAMS}; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; + +use crate::consts; +use crate::model; const CURRENT_DB_VERSION: u32 = 1; +#[derive(Debug)] +pub enum DBError { + SqliteError(rusqlite::Error), + R2d2Error(r2d2::Error), + UnsupportedVersion(u32), + Other(String), +} + +impl From for DBError { + fn from(error: rusqlite::Error) -> Self { + DBError::SqliteError(error) + } +} + +impl From for DBError { + fn from(error: r2d2::Error) -> Self { + DBError::R2d2Error(error) + } +} + +type Result = std::result::Result; + +#[derive(Clone)] pub struct Connection { - con: rusqlite::Connection + //con: rusqlite::Connection + pool: Pool } impl Connection { - pub fn new() -> Connection { + pub fn new() -> Result { - // TODO: use a constant in consts module. - let data_dir = Path::new("data"); + let data_dir = Path::new(consts::DB_DIRECTORY); if !data_dir.exists() { fs::DirBuilder::new().create(data_dir).unwrap(); } - Connection { con: rusqlite::Connection::open(data_dir.join("recipes.sqlite")).unwrap() } - } + let manager = SqliteConnectionManager::file(consts::DB_FILENAME); + let pool = r2d2::Pool::new(manager).unwrap(); - pub fn create_or_update(self: &Self) -> rusqlite::Result<&str> { - //let connection = Connection::new(); - // let mut stmt = connection.sqlite_con.prepare("SELECT * FROM versions ORDER BY date").unwrap(); - // let mut stmt = connection.sqlite_con..prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='versions'").unwrap(); + let connection = Connection { pool }; + connection.create_or_update()?; + Ok(connection) + } + /* + * Called after the connection has been established for creating or updating the database. + * The 'Version' table tracks the current state of the database. + */ + fn create_or_update(&self) -> Result<()> { // Check the Database version. - let version = { - let stmt_version_table = self.con.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='versions'")?; - /*if stmt_version_table.query(rusqlite::NO_PARAMS)?.count() == 0 { - 0 - } else { - 1 // let stmt_versions = self.con.prepare("SELECT number FROM [") - }*/ - 0 + let mut con = self.pool.get()?; + let tx = con.transaction()?; + + // Version 0 corresponds to an empty database. + let mut version = { + match tx.query_row( + "SELECT [name] FROM [sqlite_master] WHERE [type] = 'table' AND [name] = 'Version'", + [], + |row| row.get::(0) + ) { + Ok(_) => tx.query_row("SELECT [version] FROM [Version] ORDER BY [id] DESC", [], |row| row.get(0)).unwrap_or_default(), + Err(_) => 0 + } }; - self.con.query_row( - "SELECT name FROM sqlite_master WHERE type='table' AND name='versions'", - rusqlite::NO_PARAMS, - |row| Ok(dbg!("test")) - ) + while Connection::update_to_next_version(version, &tx)? { + version += 1; + } + + tx.commit()?; + + Ok(()) } + + fn update_to_next_version(current_version: u32, tx: &rusqlite::Transaction) -> Result { + let next_version = current_version + 1; + + if next_version <= CURRENT_DB_VERSION { + println!("Update to version {}...", next_version); + } + + fn update_version(to_version: u32, tx: &rusqlite::Transaction) -> Result<()> { + tx.execute("INSERT INTO [Version] ([version], [datetime]) VALUES (?1, datetime('now'))", [to_version]).map(|_| ()).map_err(DBError::from) + } + + fn ok(updated: bool) -> Result { + if updated { + println!("Version updated"); + } + Ok(updated) + } + + match next_version { + 1 => { + tx.execute_batch(&load_sql_file(next_version)?)?; + update_version(next_version, tx)?; + + ok(true) + } + + // Version 1 doesn't exist yet. + 2 => + ok(false), + + v => + Err(DBError::UnsupportedVersion(v)), + } + } + + pub fn get_all_recipe_titles(&self) -> Result> { + let con = self.pool.get()?; + let mut stmt = con.prepare("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")?; + let titles = + stmt.query_map([], |row| { + Ok((row.get(0)?, row.get(1)?)) + })?.map(|r| r.unwrap()).collect_vec(); // TODO: remove unwrap. + Ok(titles) + } + + pub fn get_all_recipes(&self) -> Result> { + let con = self.pool.get()?; + let mut stmt = con.prepare("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")?; + let recipes = + stmt.query_map([], |row| { + Ok(model::Recipe::new(row.get(0)?, row.get(1)?)) + })?.map(|r| r.unwrap()).collect_vec(); // TODO: remove unwrap. + Ok(recipes) + } + + pub fn get_recipe(&self, id: i32) -> Result { + let con = self.pool.get()?; + con.query_row("SELECT [id], [title] FROM [Recipe] WHERE [id] = ?1", [id], |row| { + Ok(model::Recipe::new(row.get(0)?, row.get(1)?)) + }).map_err(DBError::from) + } +} + +fn load_sql_file(version: u32) -> Result { + let sql_file = consts::SQL_FILENAME.replace("{VERSION}", &version.to_string()); + let mut file = File::open(&sql_file).map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err.to_string())))?; + let mut sql = String::new(); + file.read_to_string(&mut sql).map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err.to_string())))?; + Ok(sql) } \ No newline at end of file