Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make insert statement not panic when inserting nothing #1708

Merged
merged 7 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/executor/insert.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
Expand Down
110 changes: 108 additions & 2 deletions src/query/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
{
Expand Down Expand Up @@ -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>
Expand All @@ -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]
Expand Down
54 changes: 54 additions & 0 deletions tests/empty_insert_tests.rs
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

@tyt2y3 tyt2y3 Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.on_empty_do_nothing()
.exec(db)
.await;

assert!(matches!(empty_insert, TryInsertResult::Empty));
}