// Copyright 2021 Jeremy Wall // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /// Storage backend for recipes. use std::convert::From; use std::path::Path; use rusqlite::{params, Connection, DropBehavior, Result as SqliteResult, Transaction}; use recipes::{unit::Measure, Ingredient, Recipe, Step}; // TODO Model the error domain of our storage layer. #[derive(Debug)] pub struct StorageError { message: String, } impl From for StorageError { fn from(e: rusqlite::Error) -> Self { match e { rusqlite::Error::SqliteFailure(e, msg) => StorageError { message: format!("{}: {}", e, msg.unwrap_or_default()), }, rusqlite::Error::SqliteSingleThreadedMode => unimplemented!(), rusqlite::Error::FromSqlConversionFailure(_, _, _) => unimplemented!(), rusqlite::Error::IntegralValueOutOfRange(_, _) => todo!(), rusqlite::Error::Utf8Error(_) => todo!(), rusqlite::Error::NulError(_) => todo!(), rusqlite::Error::InvalidParameterName(_) => todo!(), rusqlite::Error::InvalidPath(_) => todo!(), rusqlite::Error::ExecuteReturnedResults => todo!(), rusqlite::Error::QueryReturnedNoRows => todo!(), rusqlite::Error::InvalidColumnIndex(_) => todo!(), rusqlite::Error::InvalidColumnName(_) => todo!(), rusqlite::Error::InvalidColumnType(_, _, _) => todo!(), rusqlite::Error::StatementChangedRows(_) => todo!(), rusqlite::Error::ToSqlConversionFailure(_) => todo!(), rusqlite::Error::InvalidQuery => todo!(), rusqlite::Error::MultipleStatement => todo!(), rusqlite::Error::InvalidParameterCount(_, _) => todo!(), _ => todo!(), } } } pub enum IterResult { Some(Entity), Err(StorageError), None, } pub trait RecipeStore { fn store_recipe(&mut self, e: &mut Recipe) -> Result<(), StorageError>; fn fetch_all_recipes(&self) -> Result, StorageError>; fn fetch_recipe_steps(&self, recipe_id: i64) -> Result>, StorageError>; fn fetch_recipe(&mut self, k: &str) -> Result, StorageError>; fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result, StorageError>; } pub struct SqliteBackend { conn: Connection, } impl SqliteBackend { pub fn new>(path: P) -> SqliteResult { Ok(Self { conn: Connection::open(path)?, }) } pub fn new_in_memory() -> SqliteResult { Ok(Self { conn: Connection::open_in_memory()?, }) } pub fn get_schema_version(&self) -> SqliteResult> { let mut stmt = self .conn .prepare("SELECT max(version) from schema_version")?; Ok(stmt.query_row([], |r| r.get(0))?) } pub fn start_transaction<'a>(&'a mut self) -> SqliteResult> { self.conn.transaction().map(|mut tx| { tx.set_drop_behavior(DropBehavior::Commit); tx }) } pub fn create_schema(&self) -> SqliteResult<()> { self.conn.execute( "CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY )", [], )?; let version = self.get_schema_version()?; if let None = version { self.conn .execute("INSERT INTO schema_version ( version ) values ( 0 )", [])?; } else { return Ok(()); } self.conn.execute( "CREATE TABLE IF NOT EXISTS recipes ( id INTEGER PRIMARY KEY, title TEXT UNIQUE NOT NULL, desc TEXT NOT NULL )", [], )?; self.conn.execute( "CREATE TABLE IF NOT EXISTS steps ( recipe_id INTEGER NOT NULL, step_idx INTEGER NOT NULL, prep_time INTEGER, -- in seconds instructions TEXT NOT NULL, FOREIGN KEY(recipe_id) REFERENCES recipes(id) CONSTRAINT step_key PRIMARY KEY (recipe_id, step_idx) )", [], )?; self.conn.execute( "CREATE TABLE IF NOT EXISTS step_ingredients ( recipe_id INTEGER NOT NULL, step_idx INTEGER NOT NULL, ingredient_idx INTEGER NOT NULL, name TEXT NOT NULL, amt TEXT NOT NULL, category TEXT NOT NULL, form TEXT, FOREIGN KEY(recipe_id, step_idx) REFERENCES steps(recipe_id, step_idx), CONSTRAINT step_ingredients_key PRIMARY KEY (recipe_id, step_idx, name, form) )", [], )?; Ok(()) } pub fn serialize_step_stmt_rows( mut stmt: rusqlite::Statement, recipe_id: i64, ) -> Result>, StorageError> { if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| { let prep_time: Option = row.get(2)?; let instructions: String = row.get(3)?; Ok(Step::new( prep_time.map(|i| std::time::Duration::from_secs(i as u64)), instructions, )) }) { let mut steps = Vec::new(); for step in step_iter { steps.push(step?); } return Ok(Some(steps)); } return Ok(None); } fn map_recipe_row(r: &rusqlite::Row) -> Result { let id: i64 = r.get(0)?; let title: String = r.get(1)?; let desc: String = r.get(2)?; Ok(Recipe::new_id(id, title, desc)) } fn map_ingredient_row(r: &rusqlite::Row) -> Result { let name: String = r.get(3)?; let amt: String = r.get(4)?; let category = r.get(5)?; let form = r.get(6)?; Ok(Ingredient::new( name, form, dbg!(Measure::parse(dbg!(&amt)).unwrap()), category, )) } } impl RecipeStore for SqliteBackend { fn fetch_all_recipes(&self) -> Result, StorageError> { let mut stmt = self.conn.prepare("SELECT * FROM recipes")?; let recipe_iter = stmt.query_map([], |r| Self::map_recipe_row(r))?; let mut recipes = Vec::new(); for next in recipe_iter { recipes.push(next?); } Ok(recipes) } fn store_recipe(&mut self, recipe: &mut Recipe) -> Result<(), StorageError> { // If we don't have a transaction already we should start one. let tx = self.start_transaction()?; if let Some(id) = recipe.id { tx.execute( "INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)", params![id, recipe.title, recipe.desc], )?; } else { tx.execute( "INSERT INTO recipes (title, desc) VALUES (?1, ?2)", params![recipe.title, recipe.desc], )?; let mut stmt = tx.prepare("select id from recipes where title = ?1")?; let id = stmt.query_row(params![recipe.title], |row| Ok(row.get(0)?))?; recipe.id = Some(id); } for (idx, step) in recipe.steps.iter().enumerate() { tx.execute("INSERT INTO steps (recipe_id, step_idx, prep_time, instructions) VALUES (?1, ?2, ?3, ?4)", params![recipe.id, dbg!(idx), step.prep_time.map(|v| v.as_secs()) , step.instructions])?; for (ing_idx, ing) in step.ingredients.iter().enumerate() { dbg!(tx.execute( "INSERT INTO step_ingredients (recipe_id, step_idx, ingredient_idx, name, amt, category, form) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![recipe.id, dbg!(idx), ing_idx, ing.name, format!("{}", ing.amt), ing.category, ing.form])?); } } Ok(()) } fn fetch_recipe_steps(&self, recipe_id: i64) -> Result>, StorageError> { let stmt = self .conn .prepare("SELECT * from steps WHERE recipe_id = ?1 ORDER BY step_idx")?; SqliteBackend::serialize_step_stmt_rows(stmt, recipe_id) } fn fetch_recipe(&mut self, key: &str) -> Result, StorageError> { let tx = self.start_transaction()?; let mut stmt = tx.prepare("SELECT * FROM recipes WHERE title = ?1")?; let recipe_iter = stmt.query_map(params![key], |r| Self::map_recipe_row(r))?; let mut recipe = recipe_iter .filter(|res| res.is_ok()) // TODO(jwall): What about failures here? .map(|r| r.unwrap()) .next(); if let Some(recipe) = recipe.as_mut() { // We know the recipe.id has to exist since we filled it when it came from the database. let stmt = tx.prepare("SELECT * FROM steps WHERE recipe_id = ?1 ORDER BY step_idx")?; let steps = SqliteBackend::serialize_step_stmt_rows(stmt, recipe.id.unwrap())?; let mut stmt = tx.prepare("SELECT * from step_ingredients WHERE recipe_id = ?1 and step_idx = ?2 ORDER BY ingredient_idx")?; if let Some(mut steps) = steps { for (step_idx, mut step) in steps.drain(0..).enumerate() { // TODO(jwall): Fetch the ingredients. let ing_iter = stmt.query_map(params![recipe.id, step_idx], |row| { Self::map_ingredient_row(row) })?; for ing in ing_iter { step.ingredients.push(ing?); } recipe.add_step(step); } } } return Ok(recipe); } fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result, StorageError> { let mut stmt = self.conn.prepare( "SELECT * FROM step_ingredients WHERE recipe_id = ?1 ORDER BY step_idx, ingredient_idx", )?; let ing_iter = stmt.query_map(params![recipe_id], |row| Self::map_ingredient_row(row))?; let mut ingredients = Vec::new(); for i in ing_iter { ingredients.push(i?); } Ok(ingredients) } } #[cfg(test)] mod tests;