Skip to content
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
26 changes: 22 additions & 4 deletions sea-orm-sync/src/executor/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,26 @@ where
pub last_insert_id: Option<<PrimaryKey<A> as PrimaryKeyTrait>::ValueType>,
}

/// The types of results for an INSERT operation
/// The result of executing a [`crate::TryInsert`].
///
/// This enum represents no‑op inserts (e.g. conflict `DO NOTHING`) without treating
/// them as errors.
#[derive(Debug)]
pub enum TryInsertResult<T> {
/// The INSERT statement did not have any value to insert
/// There was nothing to insert, so no SQL was executed.
///
/// This typically happens when creating a [`crate::TryInsert`] from an empty iterator or None.
Empty,
/// The INSERT operation did not insert any valid value
/// The statement was executed, but SeaORM could not get the inserted row / insert id.
///
/// This is commonly caused by `ON CONFLICT ... DO NOTHING` (Postgres / SQLite) or the MySQL
/// polyfill (`ON DUPLICATE KEY UPDATE pk = pk`).
///
/// Note that this variant maps from `DbErr::RecordNotInserted`, so it can also represent other
/// situations where the backend/driver reports no inserted row (e.g. an empty `RETURNING`
/// result set or a "no-op" update in MySQL where `last_insert_id` is reported as `0`). In rare
/// cases, this can be a false negative where a row was inserted but the backend did not report
/// it.
Conflicted,
/// Successfully inserted
Inserted(T),
Expand All @@ -57,7 +71,11 @@ impl<A> TryInsertResult<InsertResult<A>>
where
A: ActiveModelTrait,
{
/// Empty: `Ok(None)`. Inserted: `Ok(Some(last_insert_id))`. Conflicted: `Err(DbErr::RecordNotInserted)`.
/// Extract the last inserted id.
///
/// - [`TryInsertResult::Empty`] => `Ok(None)`
/// - [`TryInsertResult::Inserted`] => `Ok(Some(last_insert_id))`
/// - [`TryInsertResult::Conflicted`] => `Err(DbErr::RecordNotInserted)`
pub fn last_insert_id(
self,
) -> Result<Option<<PrimaryKey<A> as PrimaryKeyTrait>::ValueType>, DbErr> {
Expand Down
113 changes: 107 additions & 6 deletions sea-orm-sync/src/query/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ where
pub(crate) model: PhantomData<A>,
}

/// Performs INSERT operations on one or more ActiveModels, will do nothing if input is empty.
/// Wrapper of [`Insert`] / [`InsertMany`], treats "no row inserted/id returned" as a normal outcome.
///
/// All interfaces works the same as `Insert<A>`.
/// Its `exec*` methods return [`crate::TryInsertResult`].
/// Mapping empty input to [`crate::TryInsertResult::Empty`] (no SQL executed) and
/// `DbErr::RecordNotInserted` to [`crate::TryInsertResult::Conflicted`].
///
/// Useful for idempotent inserts such as `ON CONFLICT ... DO NOTHING` (Postgres / SQLite) or the
/// MySQL polyfill (`ON DUPLICATE KEY UPDATE pk = pk`).
#[derive(Debug)]
pub struct TryInsert<A>
where
Expand Down Expand Up @@ -206,15 +211,35 @@ where
self
}

/// Allow insert statement to return without error if nothing's been inserted
/// Set ON CONFLICT do nothing, but with MySQL specific polyfill.
pub fn on_conflict_do_nothing_on<I>(mut self, columns: I) -> TryInsert<A>
where
I: IntoIterator<Item = <A::Entity as EntityTrait>::Column>,
{
let primary_keys = <A::Entity as EntityTrait>::PrimaryKey::iter();
let mut on_conflict = OnConflict::columns(columns);
on_conflict.do_nothing_on(primary_keys);
self.query.on_conflict(on_conflict);
TryInsert::from_one(self)
}

/// Allow insert statement to return without error if nothing's been inserted.
#[deprecated(
since = "2.0.0",
note = "Please use [`TryInsert::one`] or `on_conflict_do_nothing*` methods that return [`TryInsert`]"
)]
pub fn do_nothing(self) -> TryInsert<A>
where
A: ActiveModelTrait,
{
TryInsert::from_one(self)
}

/// Alias to `do_nothing`
/// Alias to [`Insert::do_nothing`].
#[deprecated(
since = "2.0.0",
note = "Please use [`TryInsert::one`] or `on_conflict_do_nothing*` methods that return [`TryInsert`]"
)]
pub fn on_empty_do_nothing(self) -> TryInsert<A>
where
A: ActiveModelTrait,
Expand Down Expand Up @@ -348,15 +373,56 @@ where
self
}

/// Allow insert statement to return without error if nothing's been inserted
/// Set ON CONFLICT do nothing, but with MySQL specific polyfill.
/// ```
/// use sea_orm::{entity::*, query::*, sea_query::OnConflict, tests_cfg::cake, DbBackend};
/// let orange = cake::ActiveModel {
/// id: ActiveValue::set(2),
/// name: ActiveValue::set("Orange".to_owned()),
/// };
/// assert_eq!(
/// cake::Entity::insert(orange.clone())
/// .on_conflict_do_nothing_on([cake::Column::Name])
/// .build(DbBackend::Postgres)
/// .to_string(),
/// r#"INSERT INTO "cake" ("id", "name") VALUES (2, 'Orange') ON CONFLICT ("name") DO NOTHING"#,
/// );
/// assert_eq!(
/// cake::Entity::insert(orange)
/// .on_conflict_do_nothing_on([cake::Column::Name])
/// .build(DbBackend::MySql)
/// .to_string(),
/// r#"INSERT INTO `cake` (`id`, `name`) VALUES (2, 'Orange') ON DUPLICATE KEY UPDATE `id` = `id`"#,
/// );
/// ```
pub fn on_conflict_do_nothing_on<I>(mut self, columns: I) -> TryInsert<A>
where
I: IntoIterator<Item = <A::Entity as EntityTrait>::Column>,
{
let primary_keys = <A::Entity as EntityTrait>::PrimaryKey::iter();
let mut on_conflict = OnConflict::columns(columns);
on_conflict.do_nothing_on(primary_keys);
self.query.on_conflict(on_conflict);
TryInsert::from_many(self)
}

/// Allow insert statement to return without error if nothing's been inserted.
#[deprecated(
since = "2.0.0",
note = "Please use [`TryInsert::many`] or `on_conflict_do_nothing*` methods that return [`TryInsert`]"
)]
pub fn do_nothing(self) -> TryInsert<A>
where
A: ActiveModelTrait,
{
TryInsert::from_many(self)
}

/// Alias to `do_nothing`
/// Alias to [`InsertMany::do_nothing`].
#[deprecated(
since = "2.0.0",
note = "Empty input is already handled by [`InsertMany::exec`] (no SQL executed). For conflict handling, use [`InsertMany::on_conflict_do_nothing`] or [`InsertMany::on_conflict_do_nothing_on`]."
)]
pub fn on_empty_do_nothing(self) -> TryInsert<A>
where
A: ActiveModelTrait,
Expand Down Expand Up @@ -492,6 +558,18 @@ where

self
}

/// Set ON CONFLICT do nothing, but with MySQL specific polyfill.
pub fn on_conflict_do_nothing_on<I>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = <A::Entity as EntityTrait>::Column>,
{
let primary_keys = <A::Entity as EntityTrait>::PrimaryKey::iter();
let mut on_conflict = OnConflict::columns(columns);
on_conflict.do_nothing_on(primary_keys);
self.insert_struct.query.on_conflict(on_conflict);
self
}
}

impl<A> QueryTrait for TryInsert<A>
Expand Down Expand Up @@ -665,6 +743,29 @@ mod tests {
);
}

#[test]
fn test_on_conflict_do_nothing_on() {
let orange = cake::ActiveModel {
id: ActiveValue::set(2),
name: ActiveValue::set("Orange".to_owned()),
};

assert_eq!(
cake::Entity::insert(orange.clone())
.on_conflict_do_nothing_on([cake::Column::Name])
.build(DbBackend::Postgres)
.to_string(),
r#"INSERT INTO "cake" ("id", "name") VALUES (2, 'Orange') ON CONFLICT ("name") DO NOTHING"#,
);
assert_eq!(
cake::Entity::insert(orange)
.on_conflict_do_nothing_on([cake::Column::Name])
.build(DbBackend::MySql)
.to_string(),
r#"INSERT INTO `cake` (`id`, `name`) VALUES (2, 'Orange') ON DUPLICATE KEY UPDATE `id` = `id`"#,
);
}

#[test]
fn insert_8() -> Result<(), DbErr> {
use crate::{DbBackend, MockDatabase, Statement, Transaction};
Expand Down
3 changes: 1 addition & 2 deletions sea-orm-sync/tests/upsert_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ pub fn create_insert_default(db: &DatabaseConnection) -> Result<(), DbErr> {
assert!(matches!(res, Err(DbErr::RecordNotInserted)));

let res = Entity::insert_many([ActiveModel { id: Set(3) }, ActiveModel { id: Set(4) }])
.on_conflict(on_conflict)
.do_nothing()
.on_conflict_do_nothing_on([Column::Id])
.exec(db);

assert!(matches!(res, Ok(TryInsertResult::Conflicted)));
Expand Down
26 changes: 22 additions & 4 deletions src/executor/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,26 @@ where
pub last_insert_id: Option<<PrimaryKey<A> as PrimaryKeyTrait>::ValueType>,
}

/// The types of results for an INSERT operation
/// The result of executing a [`crate::TryInsert`].
///
/// This enum represents no‑op inserts (e.g. conflict `DO NOTHING`) without treating
/// them as errors.
#[derive(Debug)]
pub enum TryInsertResult<T> {
/// The INSERT statement did not have any value to insert
/// There was nothing to insert, so no SQL was executed.
///
/// This typically happens when creating a [`crate::TryInsert`] from an empty iterator or None.
Empty,
/// The INSERT operation did not insert any valid value
/// The statement was executed, but SeaORM could not get the inserted row / insert id.
///
/// This is commonly caused by `ON CONFLICT ... DO NOTHING` (Postgres / SQLite) or the MySQL
/// polyfill (`ON DUPLICATE KEY UPDATE pk = pk`).
///
/// Note that this variant maps from `DbErr::RecordNotInserted`, so it can also represent other
/// situations where the backend/driver reports no inserted row (e.g. an empty `RETURNING`
/// result set or a "no-op" update in MySQL where `last_insert_id` is reported as `0`). In rare
/// cases, this can be a false negative where a row was inserted but the backend did not report
/// it.
Conflicted,
/// Successfully inserted
Inserted(T),
Expand All @@ -57,7 +71,11 @@ impl<A> TryInsertResult<InsertResult<A>>
where
A: ActiveModelTrait,
{
/// Empty: `Ok(None)`. Inserted: `Ok(Some(last_insert_id))`. Conflicted: `Err(DbErr::RecordNotInserted)`.
/// Extract the last inserted id.
///
/// - [`TryInsertResult::Empty`] => `Ok(None)`
/// - [`TryInsertResult::Inserted`] => `Ok(Some(last_insert_id))`
/// - [`TryInsertResult::Conflicted`] => `Err(DbErr::RecordNotInserted)`
pub fn last_insert_id(
self,
) -> Result<Option<<PrimaryKey<A> as PrimaryKeyTrait>::ValueType>, DbErr> {
Expand Down
Loading
Loading