diff --git a/sea-orm-sync/src/executor/insert.rs b/sea-orm-sync/src/executor/insert.rs index 38a596989..f378baf3b 100644 --- a/sea-orm-sync/src/executor/insert.rs +++ b/sea-orm-sync/src/executor/insert.rs @@ -42,12 +42,26 @@ where pub last_insert_id: Option< 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 { - /// 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), @@ -57,7 +71,11 @@ impl TryInsertResult> 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 as PrimaryKeyTrait>::ValueType>, DbErr> { diff --git a/sea-orm-sync/src/query/insert.rs b/sea-orm-sync/src/query/insert.rs index 3d22b5470..a2d26a25e 100644 --- a/sea-orm-sync/src/query/insert.rs +++ b/sea-orm-sync/src/query/insert.rs @@ -28,9 +28,14 @@ where pub(crate) model: PhantomData, } -/// 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`. +/// 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 where @@ -206,7 +211,23 @@ 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(mut self, columns: I) -> TryInsert + where + I: IntoIterator::Column>, + { + let primary_keys = ::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 where A: ActiveModelTrait, @@ -214,7 +235,11 @@ where 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 where A: ActiveModelTrait, @@ -348,7 +373,44 @@ 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(mut self, columns: I) -> TryInsert + where + I: IntoIterator::Column>, + { + let primary_keys = ::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 where A: ActiveModelTrait, @@ -356,7 +418,11 @@ where 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 where A: ActiveModelTrait, @@ -492,6 +558,18 @@ where self } + + /// Set ON CONFLICT do nothing, but with MySQL specific polyfill. + pub fn on_conflict_do_nothing_on(mut self, columns: I) -> Self + where + I: IntoIterator::Column>, + { + let primary_keys = ::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 QueryTrait for TryInsert @@ -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}; diff --git a/sea-orm-sync/tests/upsert_tests.rs b/sea-orm-sync/tests/upsert_tests.rs index b9912e9b4..6b1f1265b 100644 --- a/sea-orm-sync/tests/upsert_tests.rs +++ b/sea-orm-sync/tests/upsert_tests.rs @@ -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))); diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 70efbb133..622f429bd 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -42,12 +42,26 @@ where pub last_insert_id: Option< 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 { - /// 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), @@ -57,7 +71,11 @@ impl TryInsertResult> 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 as PrimaryKeyTrait>::ValueType>, DbErr> { diff --git a/src/query/insert.rs b/src/query/insert.rs index 40b8bcbef..44fc3de02 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -28,9 +28,14 @@ where pub(crate) model: PhantomData, } -/// 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`. +/// 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 where @@ -206,7 +211,23 @@ 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(mut self, columns: I) -> TryInsert + where + I: IntoIterator::Column>, + { + let primary_keys = ::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 where A: ActiveModelTrait, @@ -214,7 +235,11 @@ where 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 where A: ActiveModelTrait, @@ -348,7 +373,44 @@ 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(mut self, columns: I) -> TryInsert + where + I: IntoIterator::Column>, + { + let primary_keys = ::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 where A: ActiveModelTrait, @@ -356,7 +418,11 @@ where 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 where A: ActiveModelTrait, @@ -492,6 +558,18 @@ where self } + + /// Set ON CONFLICT do nothing, but with MySQL specific polyfill. + pub fn on_conflict_do_nothing_on(mut self, columns: I) -> Self + where + I: IntoIterator::Column>, + { + let primary_keys = ::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 QueryTrait for TryInsert @@ -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`"#, + ); + } + #[smol_potat::test] async fn insert_8() -> Result<(), DbErr> { use crate::{DbBackend, MockDatabase, Statement, Transaction}; diff --git a/tests/upsert_tests.rs b/tests/upsert_tests.rs index 539f2322e..df1169a33 100644 --- a/tests/upsert_tests.rs +++ b/tests/upsert_tests.rs @@ -72,8 +72,7 @@ pub async 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) .await;