Merge pull request #211 from SeaQL/error-no-record-updated

Throw error if none of the db rows are affected
This commit is contained in:
Chris Tsang 2021-10-05 22:01:10 +08:00 committed by GitHub
commit 125ee40161
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 187 additions and 10 deletions

View File

@ -3,6 +3,7 @@ pub enum DbErr {
Conn(String), Conn(String),
Exec(String), Exec(String),
Query(String), Query(String),
RecordNotFound(String),
} }
impl std::error::Error for DbErr {} 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::Conn(s) => write!(f, "Connection Error: {}", s),
Self::Exec(s) => write!(f, "Execution Error: {}", s), Self::Exec(s) => write!(f, "Execution Error: {}", s),
Self::Query(s) => write!(f, "Query Error: {}", s), Self::Query(s) => write!(f, "Query Error: {}", s),
Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s),
} }
} }
} }

View File

@ -7,9 +7,10 @@ use std::future::Future;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Updater { pub struct Updater {
query: UpdateStatement, query: UpdateStatement,
check_record_exists: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct UpdateResult { pub struct UpdateResult {
pub rows_affected: u64, pub rows_affected: u64,
} }
@ -39,7 +40,15 @@ where
impl Updater { impl Updater {
pub fn new(query: UpdateStatement) -> Self { pub fn new(query: UpdateStatement) -> Self {
Self { query } Self {
query,
check_record_exists: false,
}
}
pub fn check_record_exists(mut self) -> Self {
self.check_record_exists = true;
self
} }
pub fn exec( pub fn exec(
@ -47,7 +56,7 @@ impl Updater {
db: &DatabaseConnection, db: &DatabaseConnection,
) -> impl Future<Output = Result<UpdateResult, DbErr>> + '_ { ) -> impl Future<Output = Result<UpdateResult, DbErr>> + '_ {
let builder = db.get_database_backend(); let builder = db.get_database_backend();
exec_update(builder.build(&self.query), db) exec_update(builder.build(&self.query), db, self.check_record_exists)
} }
} }
@ -66,14 +75,160 @@ async fn exec_update_and_return_original<A>(
where where
A: ActiveModelTrait, A: ActiveModelTrait,
{ {
Updater::new(query).exec(db).await?; Updater::new(query).check_record_exists().exec(db).await?;
Ok(model) Ok(model)
} }
// Only Statement impl Send // Only Statement impl Send
async fn exec_update(statement: Statement, db: &DatabaseConnection) -> Result<UpdateResult, DbErr> { async fn exec_update(
statement: Statement,
db: &DatabaseConnection,
check_record_exists: bool,
) -> Result<UpdateResult, DbErr> {
let result = db.execute(statement).await?; 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 { Ok(UpdateResult {
rows_affected: result.rows_affected(), 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![
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
vec!["Cheese Cake".into(), 1i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
vec!["Cheese Cake".into(), 2i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
vec!["Cheese Cake".into(), 2i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
vec!["Cheese Cake".into(), 2i32.into()]
),
Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#,
vec!["Cheese Cake".into(), 2i32.into()]
),
]
);
Ok(())
}
}

View File

@ -1,5 +1,6 @@
pub use super::*; pub use super::*;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
use sea_orm::DbErr;
use uuid::Uuid; use uuid::Uuid;
pub async fn test_update_cake(db: &DbConn) { pub async fn test_update_cake(db: &DbConn) {
@ -119,10 +120,14 @@ pub async fn test_update_deleted_customer(db: &DbConn) {
..Default::default() ..Default::default()
}; };
let _customer_update_res: customer::ActiveModel = customer let customer_update_res = customer.update(db).await;
.update(db)
.await assert_eq!(
.expect("could not update customer"); 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); assert_eq!(Customer::find().count(db).await.unwrap(), init_n_customers);

View File

@ -1,7 +1,7 @@
pub mod common; pub mod common;
pub use common::{bakery_chain::*, setup::*, TestContext}; 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] #[sea_orm_macros::test]
#[cfg(any( #[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(()) Ok(())
} }