From 92ea837cdfed6d6789697710f18743e733a32125 Mon Sep 17 00:00:00 2001 From: darkmmon Date: Tue, 20 Jun 2023 17:17:18 +0800 Subject: [PATCH] Make insert statement not panic when inserting nothing (#1708) * end-of-day commit (WIP) * progress commit (WIP) * refactored and added InsertAttempt * async asjusting * completed implementation for insertAttempt in execution Added in tests for insertAttempt * updated wording for new INSERT type * removed InsertTrait --- src/executor/insert.rs | 66 ++++++++++++++++++++++ src/query/insert.rs | 110 +++++++++++++++++++++++++++++++++++- tests/empty_insert_tests.rs | 54 ++++++++++++++++++ 3 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 tests/empty_insert_tests.rs diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 96ad56cc..be00dbed 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -1,6 +1,7 @@ use crate::{ error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, Insert, IntoActiveModel, Iterable, PrimaryKeyToColumn, PrimaryKeyTrait, SelectModel, SelectorRaw, Statement, TryFromU64, + TryInsert, }; use sea_query::{Expr, FromValueTuple, Iden, InsertStatement, IntoColumnRef, Query, ValueTuple}; use std::{future::Future, marker::PhantomData}; @@ -26,6 +27,71 @@ where pub last_insert_id: <<::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType, } +/// The types of results for an INSERT operation +#[derive(Debug)] +pub enum TryInsertResult { + /// The INSERT operation did not insert any value + Empty, + /// Reserved + Conflicted, + /// Successfully inserted + Inserted(T), +} + +impl TryInsert +where + A: ActiveModelTrait, +{ + /// Execute an insert operation + #[allow(unused_mut)] + pub async fn exec<'a, C>(self, db: &'a C) -> TryInsertResult, DbErr>> + where + C: ConnectionTrait, + A: 'a, + { + if self.insert_struct.columns.is_empty() { + TryInsertResult::Empty + } else { + TryInsertResult::Inserted(self.insert_struct.exec(db).await) + } + } + + /// Execute an insert operation without returning (don't use `RETURNING` syntax) + /// Number of rows affected is returned + pub async fn exec_without_returning<'a, C>( + self, + db: &'a C, + ) -> TryInsertResult> + where + ::Model: IntoActiveModel, + C: ConnectionTrait, + A: 'a, + { + if self.insert_struct.columns.is_empty() { + TryInsertResult::Empty + } else { + TryInsertResult::Inserted(self.insert_struct.exec_without_returning(db).await) + } + } + + /// Execute an insert operation and return the inserted model (use `RETURNING` syntax if database supported) + pub async fn exec_with_returning<'a, C>( + self, + db: &'a C, + ) -> TryInsertResult::Model, DbErr>> + where + ::Model: IntoActiveModel, + C: ConnectionTrait, + A: 'a, + { + if self.insert_struct.columns.is_empty() { + TryInsertResult::Empty + } else { + TryInsertResult::Inserted(self.insert_struct.exec_with_returning(db).await) + } + } +} + impl Insert where A: ActiveModelTrait, diff --git a/src/query/insert.rs b/src/query/insert.rs index 64dc248b..3fdb343c 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -72,7 +72,7 @@ where /// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#, /// ); /// ``` - pub fn one(m: M) -> Insert + pub fn one(m: M) -> Self where M: IntoActiveModel, { @@ -208,6 +208,15 @@ where self.query.on_conflict(on_conflict); self } + + /// Allow insert statement return safely if inserting nothing. + /// The database will not be affected. + pub fn on_empty_do_nothing(self) -> TryInsert + where + A: ActiveModelTrait, + { + TryInsert::from_insert(self) + } } impl QueryTrait for Insert @@ -229,11 +238,108 @@ where } } +/// Performs INSERT operations on a ActiveModel, will do nothing if input is empty. +/// +/// All functions works the same as if it is Insert. Please refer to Insert page for more information +#[derive(Debug)] +pub struct TryInsert +where + A: ActiveModelTrait, +{ + pub(crate) insert_struct: Insert, +} + +impl Default for TryInsert +where + A: ActiveModelTrait, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(missing_docs)] +impl TryInsert +where + A: ActiveModelTrait, +{ + pub(crate) fn new() -> Self { + Self { + insert_struct: Insert::new(), + } + } + + pub fn one(m: M) -> Self + where + M: IntoActiveModel, + { + Self::new().add(m) + } + + pub fn many(models: I) -> Self + where + M: IntoActiveModel, + I: IntoIterator, + { + Self::new().add_many(models) + } + + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, m: M) -> Self + where + M: IntoActiveModel, + { + self.insert_struct = self.insert_struct.add(m); + self + } + + pub fn add_many(mut self, models: I) -> Self + where + M: IntoActiveModel, + I: IntoIterator, + { + for model in models.into_iter() { + self.insert_struct = self.insert_struct.add(model); + } + self + } + + pub fn on_conflict(mut self, on_conflict: OnConflict) -> Self { + self.insert_struct.query.on_conflict(on_conflict); + self + } + + // helper function for on_empty_do_nothing in Insert + pub fn from_insert(insert: Insert) -> Self { + Self { + insert_struct: insert, + } + } +} + +impl QueryTrait for TryInsert +where + A: ActiveModelTrait, +{ + type QueryStatement = InsertStatement; + + fn query(&mut self) -> &mut InsertStatement { + &mut self.insert_struct.query + } + + fn as_query(&self) -> &InsertStatement { + &self.insert_struct.query + } + + fn into_query(self) -> InsertStatement { + self.insert_struct.query + } +} #[cfg(test)] mod tests { use sea_query::OnConflict; - use crate::tests_cfg::cake; + use crate::tests_cfg::cake::{self, ActiveModel}; use crate::{ActiveValue, DbBackend, DbErr, EntityTrait, Insert, IntoActiveModel, QueryTrait}; #[test] diff --git a/tests/empty_insert_tests.rs b/tests/empty_insert_tests.rs new file mode 100644 index 00000000..962947a9 --- /dev/null +++ b/tests/empty_insert_tests.rs @@ -0,0 +1,54 @@ +pub mod common; +mod crud; + +pub use common::{bakery_chain::*, setup::*, TestContext}; +pub use sea_orm::{ + entity::*, error::DbErr, tests_cfg, DatabaseConnection, DbBackend, EntityName, ExecResult, +}; + +pub use crud::*; +// use common::bakery_chain::*; +use sea_orm::{DbConn, TryInsertResult}; + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +async fn main() { + let ctx = TestContext::new("bakery_chain_empty_insert_tests").await; + create_tables(&ctx.db).await.unwrap(); + test(&ctx.db).await; + ctx.delete().await; +} + +pub async fn test(db: &DbConn) { + let seaside_bakery = bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + profit_margin: Set(10.4), + ..Default::default() + }; + + let res = Bakery::insert(seaside_bakery) + .on_empty_do_nothing() + .exec(db) + .await; + + assert!(matches!(res, TryInsertResult::Inserted(_))); + + let empty_iterator = [bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + profit_margin: Set(10.4), + ..Default::default() + }] + .into_iter() + .filter(|_| false); + + let empty_insert = Bakery::insert_many(empty_iterator) + .on_empty_do_nothing() + .exec(db) + .await; + + assert!(matches!(empty_insert, TryInsertResult::Empty)); +}