Skip to content

Commit

Permalink
Make insert statement not panic when inserting nothing (#1708)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
darkmmon authored Jun 20, 2023
1 parent 7c6ab8f commit 92ea837
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 2 deletions.
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)
.on_empty_do_nothing()
.exec(db)
.await;

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

0 comments on commit 92ea837

Please sign in to comment.