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
This commit is contained in:
darkmmon 2023-06-20 17:17:18 +08:00 committed by GitHub
parent 7c6ab8fa2d
commit 92ea837cdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 228 additions and 2 deletions

View File

@ -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: <<<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType,
}
/// The types of results for an INSERT operation
#[derive(Debug)]
pub enum TryInsertResult<T> {
/// The INSERT operation did not insert any value
Empty,
/// Reserved
Conflicted,
/// Successfully inserted
Inserted(T),
}
impl<A> TryInsert<A>
where
A: ActiveModelTrait,
{
/// Execute an insert operation
#[allow(unused_mut)]
pub async fn exec<'a, C>(self, db: &'a C) -> TryInsertResult<Result<InsertResult<A>, 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<Result<u64, DbErr>>
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
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<Result<<A::Entity as EntityTrait>::Model, DbErr>>
where
<A::Entity as EntityTrait>::Model: IntoActiveModel<A>,
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<A> Insert<A>
where
A: ActiveModelTrait,

View File

@ -72,7 +72,7 @@ where
/// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#,
/// );
/// ```
pub fn one<M>(m: M) -> Insert<A>
pub fn one<M>(m: M) -> Self
where
M: IntoActiveModel<A>,
{
@ -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<A>
where
A: ActiveModelTrait,
{
TryInsert::from_insert(self)
}
}
impl<A> QueryTrait for Insert<A>
@ -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<A>. Please refer to Insert<A> page for more information
#[derive(Debug)]
pub struct TryInsert<A>
where
A: ActiveModelTrait,
{
pub(crate) insert_struct: Insert<A>,
}
impl<A> Default for TryInsert<A>
where
A: ActiveModelTrait,
{
fn default() -> Self {
Self::new()
}
}
#[allow(missing_docs)]
impl<A> TryInsert<A>
where
A: ActiveModelTrait,
{
pub(crate) fn new() -> Self {
Self {
insert_struct: Insert::new(),
}
}
pub fn one<M>(m: M) -> Self
where
M: IntoActiveModel<A>,
{
Self::new().add(m)
}
pub fn many<M, I>(models: I) -> Self
where
M: IntoActiveModel<A>,
I: IntoIterator<Item = M>,
{
Self::new().add_many(models)
}
#[allow(clippy::should_implement_trait)]
pub fn add<M>(mut self, m: M) -> Self
where
M: IntoActiveModel<A>,
{
self.insert_struct = self.insert_struct.add(m);
self
}
pub fn add_many<M, I>(mut self, models: I) -> Self
where
M: IntoActiveModel<A>,
I: IntoIterator<Item = M>,
{
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<A>
pub fn from_insert(insert: Insert<A>) -> Self {
Self {
insert_struct: insert,
}
}
}
impl<A> QueryTrait for TryInsert<A>
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]

View File

@ -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));
}