From ffb1b42f423cf166d5fe3681c9cfb10349928a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:34:01 +0200 Subject: [PATCH] refactor db complete --- rust/src/lib.rs | 2 +- rust/src/models/inventory.rs | 34 +-- rust/src/models/trips.rs | 388 ++++++++++++++++++++++------------- rust/src/models/user.rs | 10 +- rust/src/sqlite.rs | 16 +- 5 files changed, 282 insertions(+), 168 deletions(-) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4072b4d..93d955c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -17,7 +17,7 @@ pub use error::{AuthError, CommandError, Error, RequestError, StartError}; #[derive(Clone, Debug)] pub struct AppState { - pub database_pool: sqlite::Pool, + pub database_pool: sqlite::Pool, pub client_state: ClientState, pub auth_config: auth::Config, } diff --git a/rust/src/models/inventory.rs b/rust/src/models/inventory.rs index 7d5add7..8b1b1e3 100644 --- a/rust/src/models/inventory.rs +++ b/rust/src/models/inventory.rs @@ -10,7 +10,7 @@ pub struct Inventory { impl Inventory { #[tracing::instrument] - pub async fn load(ctx: &Context, pool: &sqlx::Pool) -> Result { + pub async fn load(ctx: &Context, pool: &sqlite::Pool) -> Result { let user_id = ctx.user.id.to_string(); let mut categories = crate::query_all!( @@ -70,7 +70,7 @@ impl Category { #[tracing::instrument] pub async fn _find( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, ) -> Result, Error> { let id_param = id.to_string(); @@ -98,11 +98,7 @@ impl Category { } #[tracing::instrument] - pub async fn save( - ctx: &Context, - pool: &sqlx::Pool, - name: &str, - ) -> Result { + pub async fn save(ctx: &Context, pool: &sqlite::Pool, name: &str) -> Result { let id = Uuid::new_v4(); let id_param = id.to_string(); let user_id = ctx.user.id.to_string(); @@ -141,7 +137,7 @@ impl Category { pub async fn populate_items( &mut self, ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, ) -> Result<(), Error> { let id = self.id.to_string(); let user_id = ctx.user.id.to_string(); @@ -237,11 +233,7 @@ impl TryFrom for InventoryItem { impl InventoryItem { #[tracing::instrument] - pub async fn find( - ctx: &Context, - pool: &sqlx::Pool, - id: Uuid, - ) -> Result, Error> { + pub async fn find(ctx: &Context, pool: &sqlite::Pool, id: Uuid) -> Result, Error> { let id_param = id.to_string(); let user_id = ctx.user.id.to_string(); @@ -282,7 +274,7 @@ impl InventoryItem { #[tracing::instrument] pub async fn name_exists( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, name: &str, ) -> Result { let user_id = ctx.user.id.to_string(); @@ -304,11 +296,7 @@ impl InventoryItem { } #[tracing::instrument] - pub async fn delete( - ctx: &Context, - pool: &sqlx::Pool, - id: Uuid, - ) -> Result { + pub async fn delete(ctx: &Context, pool: &sqlite::Pool, id: Uuid) -> Result { let id_param = id.to_string(); let user_id = ctx.user.id.to_string(); let results = crate::execute!( @@ -332,7 +320,7 @@ impl InventoryItem { #[tracing::instrument] pub async fn update( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, name: &str, weight: u32, @@ -367,7 +355,7 @@ impl InventoryItem { #[tracing::instrument] pub async fn save( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, name: &str, category_id: Uuid, weight: u32, @@ -402,7 +390,7 @@ impl InventoryItem { #[tracing::instrument] pub async fn get_category_max_weight( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, category_id: Uuid, ) -> Result { let user_id = ctx.user.id.to_string(); @@ -468,7 +456,7 @@ impl Item { #[tracing::instrument] pub async fn _get_category_total_picked_weight( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, category_id: Uuid, ) -> Result { let user_id = ctx.user.id.to_string(); diff --git a/rust/src/models/trips.rs b/rust/src/models/trips.rs index 93e1319..e9672c0 100644 --- a/rust/src/models/trips.rs +++ b/rust/src/models/trips.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use time; use uuid::Uuid; -#[derive(sqlx::Type, PartialEq, PartialOrd, Deserialize, Debug)] +#[derive(sqlite::Type, PartialEq, PartialOrd, Deserialize, Debug)] pub enum TripState { Init, Planning, @@ -131,18 +131,80 @@ impl TripCategory { #[tracing::instrument] pub async fn find( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, trip_id: Uuid, category_id: Uuid, ) -> Result, Error> { - let mut category: Option = None; - let user_id = ctx.user.id.to_string(); let trip_id_param = trip_id.to_string(); let category_id_param = category_id.to_string(); - sqlx::query!( - " + struct Row { + category_id: String, + category_name: String, + category_description: Option, + #[allow(dead_code)] + trip_id: Option, + item_id: Option, + item_name: Option, + item_description: Option, + item_weight: Option, + item_is_picked: Option, + item_is_packed: Option, + item_is_ready: Option, + item_is_new: Option, + } + + struct RowParsed { + category: TripCategory, + item: Option, + } + + impl TryFrom for RowParsed { + type Error = Error; + + fn try_from(row: Row) -> Result { + let category = inventory::Category { + id: Uuid::try_parse(&row.category_id)?, + name: row.category_name, + description: row.category_description, + items: None, + }; + Ok(Self { + category: TripCategory { + category, + items: None, + }, + + item: match row.item_id { + Some(item_id) => Some(TripItem { + item: inventory::Item { + id: Uuid::try_parse(&item_id)?, + name: row.item_name.unwrap(), + description: row.item_description, + weight: row.item_weight.unwrap(), + category_id: Uuid::try_parse(&row.category_id)?, + }, + picked: row.item_is_picked.unwrap(), + packed: row.item_is_packed.unwrap(), + ready: row.item_is_ready.unwrap(), + new: row.item_is_new.unwrap(), + }), + None => None, + }, + }) + } + } + + let mut rows = crate::query_all!( + sqlite::QueryClassification { + query_type: sqlite::QueryType::Select, + component: sqlite::Component::Trips, + }, + pool, + Row, + RowParsed, + r#" SELECT category.id as category_id, category.name as category_name, @@ -182,67 +244,34 @@ impl TripCategory { ) AS inner ON inner.category_id = category.id WHERE category.id = ? - ", + "#, trip_id_param, user_id, category_id_param ) - .fetch(pool) - .map_ok(|row| -> Result<(), Error> { - match &category { - Some(_) => (), - None => { - category = Some(TripCategory { - category: inventory::Category { - id: Uuid::try_parse(&row.category_id)?, - name: row.category_name, - description: row.category_description, - items: None, - }, - items: None, - }); - } + .await?; + + let mut category = match rows.pop() { + None => return Ok(None), + Some(initial) => TripCategory { + category: initial.category.category, + items: initial.item.map(|item| vec![item]).or_else(|| Some(vec![])), + }, + }; + + for row in rows { + let item = row.item; + category.items = category.items.or_else(|| Some(vec![])); + + if let Some(item) = item { + category.items = category.items.map(|mut c| { + c.push(item); + c + }); }; + } - match row.item_id { - None => { - // we have an empty (unused) category which has NULL values - // for the item_id column - category.as_mut().unwrap().items = Some(vec![]); - category.as_mut().unwrap().category.items = Some(vec![]); - } - Some(item_id) => { - let item = TripItem { - item: inventory::Item { - id: Uuid::try_parse(&item_id)?, - name: row.item_name.unwrap(), - description: row.item_description, - weight: row.item_weight.unwrap(), - category_id: category.as_ref().unwrap().category.id, - }, - picked: row.item_is_picked.unwrap(), - packed: row.item_is_packed.unwrap(), - ready: row.item_is_ready.unwrap(), - new: row.item_is_new.unwrap(), - }; - - match &mut category.as_mut().unwrap().items { - None => category.as_mut().unwrap().items = Some(vec![item]), - Some(ref mut items) => items.push(item), - } - } - } - - Ok(()) - }) - .try_collect::>>() - .await? - .into_iter() - .collect::>()?; - - // this may be None if there are no results (which - // means that the category was not found) - Ok(category) + Ok(Some(category)) } } @@ -292,7 +321,7 @@ impl TripItem { #[tracing::instrument] pub async fn find( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, trip_id: Uuid, item_id: Uuid, ) -> Result, Error> { @@ -335,7 +364,7 @@ impl TripItem { #[tracing::instrument] pub async fn set_state( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, trip_id: Uuid, item_id: Uuid, key: TripItemStateKey, @@ -477,7 +506,7 @@ pub enum TripAttribute { impl Trip { #[tracing::instrument] - pub async fn all(ctx: &Context, pool: &sqlx::Pool) -> Result, Error> { + pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result, Error> { let user_id = ctx.user.id.to_string(); crate::query_all!( sqlite::QueryClassification { @@ -507,7 +536,7 @@ impl Trip { #[tracing::instrument] pub async fn find( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, trip_id: Uuid, ) -> Result, Error> { let trip_id_param = trip_id.to_string(); @@ -541,7 +570,7 @@ impl Trip { #[tracing::instrument] pub async fn trip_type_remove( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, type_id: Uuid, ) -> Result { @@ -576,7 +605,7 @@ impl Trip { #[tracing::instrument] pub async fn trip_type_add( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, type_id: Uuid, ) -> Result<(), Error> { @@ -614,7 +643,7 @@ impl Trip { #[tracing::instrument] pub async fn set_state( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, new_state: &TripState, ) -> Result { @@ -641,7 +670,7 @@ impl Trip { #[tracing::instrument] pub async fn set_comment( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, new_comment: &str, ) -> Result { @@ -668,7 +697,7 @@ impl Trip { #[tracing::instrument] pub async fn set_attribute( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, trip_id: Uuid, attribute: TripAttribute, value: &str, @@ -785,7 +814,7 @@ impl Trip { #[tracing::instrument] pub async fn save( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, name: &str, date_start: time::Date, date_end: time::Date, @@ -823,7 +852,7 @@ impl Trip { #[tracing::instrument] pub async fn find_total_picked_weight( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, trip_id: Uuid, ) -> Result { let user_id = ctx.user.id.to_string(); @@ -890,7 +919,7 @@ impl Trip { pub async fn load_trips_types( &mut self, ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, ) -> Result<(), Error> { struct Row { id: String, @@ -956,7 +985,7 @@ impl Trip { pub async fn sync_trip_items_with_inventory( &mut self, ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, ) -> Result<(), Error> { // we need to get all items that are part of the inventory but not // part of the trip items @@ -1009,9 +1038,6 @@ impl Trip { ) .await?; - // looks like there is currently no nice way to do multiple inserts - // with sqlx. whatever, this won't matter - // only mark as new when the trip is already underway let mark_as_new = self.state != TripState::new(); @@ -1056,15 +1082,80 @@ impl Trip { pub async fn load_categories( &mut self, ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, ) -> Result<(), Error> { let mut categories: Vec = vec![]; // we can ignore the return type as we collect into `categories` // in the `map_ok()` closure let id = self.id.to_string(); let user_id = ctx.user.id.to_string(); - sqlx::query!( - " + + struct Row { + category_id: String, + category_name: String, + category_description: Option, + #[allow(dead_code)] + trip_id: Option, + item_id: Option, + item_name: Option, + item_description: Option, + item_weight: Option, + item_is_picked: Option, + item_is_packed: Option, + item_is_ready: Option, + item_is_new: Option, + } + + struct RowParsed { + category: TripCategory, + item: Option, + } + + impl TryFrom for RowParsed { + type Error = Error; + + fn try_from(row: Row) -> Result { + let category = inventory::Category { + id: Uuid::try_parse(&row.category_id)?, + name: row.category_name, + description: row.category_description, + items: None, + }; + Ok(Self { + category: TripCategory { + category, + items: None, + }, + + item: match row.item_id { + Some(item_id) => Some(TripItem { + item: inventory::Item { + id: Uuid::try_parse(&item_id)?, + name: row.item_name.unwrap(), + description: row.item_description, + weight: row.item_weight.unwrap(), + category_id: Uuid::try_parse(&row.category_id)?, + }, + picked: row.item_is_picked.unwrap(), + packed: row.item_is_packed.unwrap(), + ready: row.item_is_ready.unwrap(), + new: row.item_is_new.unwrap(), + }), + None => None, + }, + }) + } + } + + let rows = crate::query_all!( + sqlite::QueryClassification { + query_type: sqlite::QueryType::Select, + component: sqlite::Component::Trips, + }, + pool, + Row, + RowParsed, + r#" SELECT category.id as category_id, category.name as category_name, @@ -1103,70 +1194,93 @@ impl Trip { ) AS inner ON inner.category_id = category.id WHERE category.user_id = ? - ", + "#, id, user_id, user_id, ) - .fetch(pool) - .map_ok(|row| -> Result<(), Error> { - let mut category = TripCategory { - category: inventory::Category { - id: Uuid::try_parse(&row.category_id)?, - name: row.category_name, - description: row.category_description, + .await?; - items: None, - }, - items: None, - }; + for row in rows { + match categories + .iter_mut() + .find(|cat| cat.category.id == row.category.category.id) + { + Some(ref mut existing_category) => { + // taking and then readding later + let mut items = existing_category.items.take().unwrap_or(vec![]); - match row.item_id { - None => { - // we have an empty (unused) category which has NULL values - // for the item_id column - category.items = Some(vec![]); - categories.push(category); - } - Some(item_id) => { - let item = TripItem { - item: inventory::Item { - id: Uuid::try_parse(&item_id)?, - name: row.item_name.unwrap(), - description: row.item_description, - weight: row.item_weight.unwrap(), - category_id: category.category.id, - }, - picked: row.item_is_picked.unwrap(), - packed: row.item_is_packed.unwrap(), - ready: row.item_is_ready.unwrap(), - new: row.item_is_new.unwrap(), - }; - - if let Some(&mut ref mut c) = categories - .iter_mut() - .find(|c| c.category.id == category.category.id) - { - // we always populate c.items when we add a new category, so - // it's safe to unwrap here - c.items.as_mut().unwrap().push(item); - } else { - category.items = Some(vec![item]); - categories.push(category); + if let Some(item) = row.item { + items.push(item); } - } - } - Ok(()) - }) - .try_collect::>>() - .await? - .into_iter() - .collect::>()?; + existing_category.items = Some(items); + } + None => categories.push(TripCategory { + category: row.category.category, + items: row.item.map(|item| vec![item]).or_else(|| Some(vec![])), + }), + } + } self.categories = Some(categories); Ok(()) + // .fetch(pool) + // .map_ok(|row| -> Result<(), Error> { + // let mut category = TripCategory { + // category: inventory::Category { + // id: Uuid::try_parse(&row.category_id)?, + // name: row.category_name, + // description: row.category_description, + + // items: None, + // }, + // items: None, + // }; + + // match row.item_id { + // None => { + // // we have an empty (unused) category which has NULL values + // // for the item_id column + // category.items = Some(vec![]); + // categories.push(category); + // } + // Some(item_id) => { + // let item = TripItem { + // item: inventory::Item { + // id: Uuid::try_parse(&item_id)?, + // name: row.item_name.unwrap(), + // description: row.item_description, + // weight: row.item_weight.unwrap(), + // category_id: category.category.id, + // }, + // picked: row.item_is_picked.unwrap(), + // packed: row.item_is_packed.unwrap(), + // ready: row.item_is_ready.unwrap(), + // new: row.item_is_new.unwrap(), + // }; + + // if let Some(&mut ref mut c) = categories + // .iter_mut() + // .find(|c| c.category.id == category.category.id) + // { + // // we always populate c.items when we add a new category, so + // // it's safe to unwrap here + // c.items.as_mut().unwrap().push(item); + // } else { + // category.items = Some(vec![item]); + // categories.push(category); + // } + // } + // } + + // Ok(()) + // }) + // .try_collect::>>() + // .await? + // .into_iter() + // .collect::>()?; } } @@ -1179,7 +1293,7 @@ pub struct TripType { impl TripsType { #[tracing::instrument] - pub async fn all(ctx: &Context, pool: &sqlx::Pool) -> Result, Error> { + pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result, Error> { let user_id = ctx.user.id.to_string(); crate::query_all!( sqlite::QueryClassification { @@ -1200,11 +1314,7 @@ impl TripsType { } #[tracing::instrument] - pub async fn save( - ctx: &Context, - pool: &sqlx::Pool, - name: &str, - ) -> Result { + pub async fn save(ctx: &Context, pool: &sqlite::Pool, name: &str) -> Result { let user_id = ctx.user.id.to_string(); let id = Uuid::new_v4(); let id_param = id.to_string(); @@ -1230,7 +1340,7 @@ impl TripsType { #[tracing::instrument] pub async fn set_name( ctx: &Context, - pool: &sqlx::Pool, + pool: &sqlite::Pool, id: Uuid, new_name: &str, ) -> Result { diff --git a/rust/src/models/user.rs b/rust/src/models/user.rs index 47138bd..e9c6348 100644 --- a/rust/src/models/user.rs +++ b/rust/src/models/user.rs @@ -57,11 +57,16 @@ impl User { } #[tracing::instrument] -pub async fn create(pool: &sqlx::Pool, user: NewUser<'_>) -> Result { +pub async fn create(pool: &sqlite::Pool, user: NewUser<'_>) -> Result { let id = Uuid::new_v4(); let id_param = id.to_string(); - sqlx::query!( + crate::execute!( + sqlite::QueryClassification { + query_type: sqlite::QueryType::Insert, + component: sqlite::Component::User, + }, + pool, "INSERT INTO users (id, username, fullname) VALUES @@ -70,7 +75,6 @@ pub async fn create(pool: &sqlx::Pool, user: NewUser<'_>) -> Resul user.username, user.fullname ) - .execute(pool) .await?; Ok(id) diff --git a/rust/src/sqlite.rs b/rust/src/sqlite.rs index 4106705..6fc3566 100644 --- a/rust/src/sqlite.rs +++ b/rust/src/sqlite.rs @@ -7,14 +7,26 @@ use tracing::Instrument; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::ConnectOptions; -pub use sqlx::{Pool, Sqlite}; +pub use sqlx::{Pool as SqlitePool, Sqlite}; use std::str::FromStr as _; +pub use sqlx::Type; + use crate::StartError; +pub type Pool = sqlx::Pool; + +pub fn int_to_bool(value: i32) -> bool { + match value { + 0 => false, + 1 => true, + _ => panic!("got invalid boolean from sqlite"), + } +} + #[tracing::instrument] -pub async fn init_database_pool(url: &str) -> Result, StartError> { +pub async fn init_database_pool(url: &str) -> Result { async { SqlitePoolOptions::new() .max_connections(5)