diff --git a/src/error.rs b/src/error.rs index 09f80b0a..f8aff775 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ pub enum DbErr { Conn(String), Exec(String), Query(String), + RecordNotFound(String), } impl std::error::Error for DbErr {} @@ -13,6 +14,7 @@ impl std::fmt::Display for DbErr { Self::Conn(s) => write!(f, "Connection Error: {}", s), Self::Exec(s) => write!(f, "Execution Error: {}", s), Self::Query(s) => write!(f, "Query Error: {}", s), + Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s), } } } diff --git a/src/executor/update.rs b/src/executor/update.rs index 9e36de57..dd0449c6 100644 --- a/src/executor/update.rs +++ b/src/executor/update.rs @@ -7,9 +7,10 @@ use std::future::Future; #[derive(Clone, Debug)] pub struct Updater { query: UpdateStatement, + check_record_exists: bool, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct UpdateResult { pub rows_affected: u64, } @@ -42,15 +43,26 @@ where impl Updater { pub fn new(query: UpdateStatement) -> Self { - Self { query } + Self { + query, + check_record_exists: false, + } } - pub async fn exec<'a, C>(self, db: &'a C) -> Result + pub fn check_record_exists(mut self) -> Self { + self.check_record_exists = true; + self + } + + pub fn exec<'a, C>( + self, + db: &'a C + ) -> impl Future> + '_ where C: ConnectionTrait<'a>, { let builder = db.get_database_backend(); - exec_update(builder.build(&self.query), db).await + exec_update(builder.build(&self.query), db, self.check_record_exists) } } @@ -70,17 +82,163 @@ where A: ActiveModelTrait, C: ConnectionTrait<'a>, { - Updater::new(query).exec(db).await?; + Updater::new(query).check_record_exists().exec(db).await?; Ok(model) } // Only Statement impl Send -async fn exec_update<'a, C>(statement: Statement, db: &'a C) -> Result +async fn exec_update<'a, C>( + statement: Statement, + db: &'a C, + check_record_exists: bool, +) -> Result where C: ConnectionTrait<'a>, { let result = db.execute(statement).await?; + if check_record_exists && result.rows_affected() == 0 { + return Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned(), + )); + } Ok(UpdateResult { rows_affected: result.rows_affected(), }) } + +#[cfg(test)] +mod tests { + use crate::{entity::prelude::*, tests_cfg::*, *}; + use pretty_assertions::assert_eq; + use sea_query::Expr; + + #[smol_potat::test] + async fn update_record_not_found_1() -> Result<(), DbErr> { + let db = MockDatabase::new(DbBackend::Postgres) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 0, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 0, + rows_affected: 0, + }, + MockExecResult { + last_insert_id: 0, + rows_affected: 0, + }, + MockExecResult { + last_insert_id: 0, + rows_affected: 0, + }, + MockExecResult { + last_insert_id: 0, + rows_affected: 0, + }, + ]) + .into_connection(); + + let model = cake::Model { + id: 1, + name: "New York Cheese".to_owned(), + }; + + assert_eq!( + cake::ActiveModel { + name: Set("Cheese Cake".to_owned()), + ..model.into_active_model() + } + .update(&db) + .await?, + cake::Model { + id: 1, + name: "Cheese Cake".to_owned(), + } + .into_active_model() + ); + + let model = cake::Model { + id: 2, + name: "New York Cheese".to_owned(), + }; + + assert_eq!( + cake::ActiveModel { + name: Set("Cheese Cake".to_owned()), + ..model.clone().into_active_model() + } + .update(&db) + .await, + Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned() + )) + ); + + assert_eq!( + cake::Entity::update(cake::ActiveModel { + name: Set("Cheese Cake".to_owned()), + ..model.clone().into_active_model() + }) + .exec(&db) + .await, + Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned() + )) + ); + + assert_eq!( + Update::one(cake::ActiveModel { + name: Set("Cheese Cake".to_owned()), + ..model.into_active_model() + }) + .exec(&db) + .await, + Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned() + )) + ); + + assert_eq!( + Update::many(cake::Entity) + .col_expr(cake::Column::Name, Expr::value("Cheese Cake".to_owned())) + .filter(cake::Column::Id.eq(2)) + .exec(&db) + .await, + Ok(UpdateResult { rows_affected: 0 }) + ); + + assert_eq!( + db.into_transaction_log(), + vec![ + MockTransaction::from_sql_and_values( + DbBackend::Postgres, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + vec!["Cheese Cake".into(), 1i32.into()] + ), + MockTransaction::from_sql_and_values( + DbBackend::Postgres, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + vec!["Cheese Cake".into(), 2i32.into()] + ), + MockTransaction::from_sql_and_values( + DbBackend::Postgres, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + vec!["Cheese Cake".into(), 2i32.into()] + ), + MockTransaction::from_sql_and_values( + DbBackend::Postgres, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + vec!["Cheese Cake".into(), 2i32.into()] + ), + MockTransaction::from_sql_and_values( + DbBackend::Postgres, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + vec!["Cheese Cake".into(), 2i32.into()] + ), + ] + ); + + Ok(()) + } +} diff --git a/tests/crud/updates.rs b/tests/crud/updates.rs index c2048f9b..262031ef 100644 --- a/tests/crud/updates.rs +++ b/tests/crud/updates.rs @@ -1,5 +1,6 @@ pub use super::*; use rust_decimal_macros::dec; +use sea_orm::DbErr; use uuid::Uuid; pub async fn test_update_cake(db: &DbConn) { @@ -119,10 +120,14 @@ pub async fn test_update_deleted_customer(db: &DbConn) { ..Default::default() }; - let _customer_update_res: customer::ActiveModel = customer - .update(db) - .await - .expect("could not update customer"); + let customer_update_res = customer.update(db).await; + + assert_eq!( + customer_update_res, + Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned() + )) + ); assert_eq!(Customer::find().count(db).await.unwrap(), init_n_customers); diff --git a/tests/uuid_tests.rs b/tests/uuid_tests.rs index b7fc497a..752d55da 100644 --- a/tests/uuid_tests.rs +++ b/tests/uuid_tests.rs @@ -1,7 +1,7 @@ pub mod common; pub use common::{bakery_chain::*, setup::*, TestContext}; -use sea_orm::{entity::prelude::*, DatabaseConnection, IntoActiveModel}; +use sea_orm::{entity::prelude::*, DatabaseConnection, IntoActiveModel, Set}; #[sea_orm_macros::test] #[cfg(any( @@ -43,5 +43,20 @@ pub async fn create_metadata(db: &DatabaseConnection) -> Result<(), DbErr> { } ); + let update_res = Metadata::update(metadata::ActiveModel { + value: Set("0.22".to_owned()), + ..metadata.clone().into_active_model() + }) + .filter(metadata::Column::Uuid.eq(Uuid::default())) + .exec(db) + .await; + + assert_eq!( + update_res, + Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned() + )) + ); + Ok(()) }