diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 40c20d019..8b1ed8821 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -288,6 +288,7 @@ jobs: name: Examples runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest] path: [basic, actix_example, actix4_example, axum_example, rocket_example] @@ -312,6 +313,7 @@ jobs: if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-issues == 'true') }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest] path: [86, 249, 262, 319] @@ -350,6 +352,7 @@ jobs: env: DATABASE_URL: "sqlite::memory:" strategy: + fail-fast: false matrix: runtime: [async-std, actix, tokio] tls: [native-tls, rustls] @@ -392,6 +395,7 @@ jobs: env: DATABASE_URL: "mysql://root:@localhost" strategy: + fail-fast: false matrix: version: [8.0, 5.7] runtime: [async-std, actix, tokio] @@ -452,8 +456,9 @@ jobs: env: DATABASE_URL: "mysql://root:@localhost" strategy: + fail-fast: false matrix: - version: [10.6] + version: [10.6, 10.5, 10.4] runtime: [async-std, actix, tokio] tls: [native-tls] services: @@ -512,8 +517,9 @@ jobs: env: DATABASE_URL: "postgres://root:root@localhost" strategy: + fail-fast: false matrix: - version: [13.3, 12.7, 11.12, 10.17, 9.6.22] + version: [13, 12, 11, 10, 9] runtime: [tokio] tls: [native-tls] services: diff --git a/src/database/connection.rs b/src/database/connection.rs index 34b82081a..8c409999b 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -45,6 +45,12 @@ pub trait ConnectionTrait<'a>: Sync { T: Send, E: std::error::Error + Send; + /// Check if the connection supports `RETURNING` syntax on insert and update + fn support_returning(&self) -> bool { + let db_backend = self.get_database_backend(); + db_backend.support_returning() + } + /// Check if the connection is a test connection for the Mock database fn is_mock_connection(&self) -> bool { false diff --git a/src/database/db_connection.rs b/src/database/db_connection.rs index 681903ddd..99de8633f 100644 --- a/src/database/db_connection.rs +++ b/src/database/db_connection.rs @@ -267,6 +267,11 @@ impl DbBackend { Self::Sqlite => Box::new(SqliteQueryBuilder), } } + + /// Check if the database supports `RETURNING` syntax on insert and update + pub fn support_returning(&self) -> bool { + matches!(self, Self::Postgres) + } } #[cfg(test)] diff --git a/src/docs.rs b/src/docs.rs index 4d1226c3a..3870e716f 100644 --- a/src/docs.rs +++ b/src/docs.rs @@ -3,7 +3,12 @@ //! Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. //! //! ``` -//! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*, DatabaseConnection, DbBackend, MockDatabase, Transaction, IntoMockRow}; +//! # use sea_orm::{error::*, tests_cfg::*, *}; +//! # +//! # #[smol_potat::main] +//! # #[cfg(feature = "mock")] +//! # pub async fn main() -> Result<(), DbErr> { +//! # //! # let db = MockDatabase::new(DbBackend::Postgres) //! # .append_query_results(vec![ //! # vec![cake::Model { @@ -19,7 +24,7 @@ //! # .into_mock_row()], //! # ]) //! # .into_connection(); -//! # let _: Result<(), DbErr> = smol::block_on(async { +//! # //! // execute multiple queries in parallel //! let cakes_and_fruits: (Vec, Vec) = //! futures::try_join!(Cake::find().all(&db), Fruit::find().all(&db))?; @@ -53,7 +58,7 @@ //! # ] //! # ); //! # Ok(()) -//! # }); +//! # } //! ``` //! //! 2. Dynamic diff --git a/src/driver/mock.rs b/src/driver/mock.rs index c0ffa0a23..cdded50c8 100644 --- a/src/driver/mock.rs +++ b/src/driver/mock.rs @@ -19,7 +19,8 @@ pub struct MockDatabaseConnector; /// Defines a connection for the [MockDatabase] #[derive(Debug)] pub struct MockDatabaseConnection { - counter: AtomicUsize, + execute_counter: AtomicUsize, + query_counter: AtomicUsize, mocker: Mutex>, } @@ -100,7 +101,8 @@ impl MockDatabaseConnection { M: MockDatabaseTrait, { Self { - counter: AtomicUsize::new(0), + execute_counter: AtomicUsize::new(0), + query_counter: AtomicUsize::new(0), mocker: Mutex::new(Box::new(m)), } } @@ -117,14 +119,14 @@ impl MockDatabaseConnection { /// Execute the SQL statement in the [MockDatabase] pub fn execute(&self, statement: Statement) -> Result { debug_print!("{}", statement); - let counter = self.counter.fetch_add(1, Ordering::SeqCst); + let counter = self.execute_counter.fetch_add(1, Ordering::SeqCst); self.mocker.lock().unwrap().execute(counter, statement) } /// Return one [QueryResult] if the query was successful pub fn query_one(&self, statement: Statement) -> Result, DbErr> { debug_print!("{}", statement); - let counter = self.counter.fetch_add(1, Ordering::SeqCst); + let counter = self.query_counter.fetch_add(1, Ordering::SeqCst); let result = self.mocker.lock().unwrap().query(counter, statement)?; Ok(result.into_iter().next()) } @@ -132,7 +134,7 @@ impl MockDatabaseConnection { /// Return all [QueryResult]s if the query was successful pub fn query_all(&self, statement: Statement) -> Result, DbErr> { debug_print!("{}", statement); - let counter = self.counter.fetch_add(1, Ordering::SeqCst); + let counter = self.query_counter.fetch_add(1, Ordering::SeqCst); self.mocker.lock().unwrap().query(counter, statement) } diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index c58ea5a9d..06e01fcbe 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -140,6 +140,113 @@ pub trait ActiveModelTrait: Clone + Debug { } /// Perform an `INSERT` operation on the ActiveModel + /// + /// # Example (Postgres) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 15, + /// # name: "Apple Pie".to_owned(), + /// # }], + /// # ]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// let apple = cake::ActiveModel { + /// name: Set("Apple Pie".to_owned()), + /// ..Default::default() + /// }; + /// + /// assert_eq!( + /// apple.insert(&db).await?, + /// cake::Model { + /// id: 15, + /// name: "Apple Pie".to_owned(), + /// } + /// .into_active_model() + /// ); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"INSERT INTO "cake" ("name") VALUES ($1) RETURNING "id", "name""#, + /// vec!["Apple Pie".into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example (MySQL) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::MySql) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 15, + /// # name: "Apple Pie".to_owned(), + /// # }], + /// # ]) + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 15, + /// # rows_affected: 1, + /// # }, + /// # ]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// let apple = cake::ActiveModel { + /// name: Set("Apple Pie".to_owned()), + /// ..Default::default() + /// }; + /// + /// assert_eq!( + /// apple.insert(&db).await?, + /// cake::Model { + /// id: 15, + /// name: "Apple Pie".to_owned(), + /// } + /// .into_active_model() + /// ); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![ + /// Transaction::from_sql_and_values( + /// DbBackend::MySql, + /// r#"INSERT INTO `cake` (`name`) VALUES (?)"#, + /// vec!["Apple Pie".into()] + /// ), + /// Transaction::from_sql_and_values( + /// DbBackend::MySql, + /// r#"SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` = ? LIMIT ?"#, + /// vec![15.into(), 1u64.into()] + /// ) + /// ] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` async fn insert<'a, C>(self, db: &'a C) -> Result where ::Model: IntoActiveModel, @@ -147,20 +254,126 @@ pub trait ActiveModelTrait: Clone + Debug { C: ConnectionTrait<'a>, { let am = ActiveModelBehavior::before_save(self, true)?; - let res = ::insert(am).exec(db).await?; - let found = ::find_by_id(res.last_insert_id) - .one(db) + let am = ::insert(am) + .exec_with_returning(db) .await?; - let am = match found { - Some(model) => model.into_active_model(), - None => return Err(DbErr::Exec("Failed to find inserted item".to_owned())), - }; ActiveModelBehavior::after_save(am, true) } /// Perform the `UPDATE` operation on an ActiveModel + /// + /// # Example (Postgres) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![fruit::Model { + /// # id: 1, + /// # name: "Orange".to_owned(), + /// # cake_id: None, + /// # }], + /// # ]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; + /// + /// let orange = fruit::ActiveModel { + /// id: Set(1), + /// name: Set("Orange".to_owned()), + /// ..Default::default() + /// }; + /// + /// assert_eq!( + /// orange.update(&db).await?, + /// fruit::Model { + /// id: 1, + /// name: "Orange".to_owned(), + /// cake_id: None, + /// } + /// .into_active_model() + /// ); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2 RETURNING "id", "name", "cake_id""#, + /// vec!["Orange".into(), 1i32.into()] + /// )]); + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example (MySQL) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::MySql) + /// # .append_query_results(vec![ + /// # vec![fruit::Model { + /// # id: 1, + /// # name: "Orange".to_owned(), + /// # cake_id: None, + /// # }], + /// # ]) + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 0, + /// # rows_affected: 1, + /// # }, + /// # ]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; + /// + /// let orange = fruit::ActiveModel { + /// id: Set(1), + /// name: Set("Orange".to_owned()), + /// ..Default::default() + /// }; + /// + /// assert_eq!( + /// orange.update(&db).await?, + /// fruit::Model { + /// id: 1, + /// name: "Orange".to_owned(), + /// cake_id: None, + /// } + /// .into_active_model() + /// ); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![ + /// Transaction::from_sql_and_values( + /// DbBackend::MySql, + /// r#"UPDATE `fruit` SET `name` = ? WHERE `fruit`.`id` = ?"#, + /// vec!["Orange".into(), 1i32.into()] + /// ), + /// Transaction::from_sql_and_values( + /// DbBackend::MySql, + /// r#"SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` WHERE `fruit`.`id` = ? LIMIT ?"#, + /// vec![1i32.into(), 1u64.into()] + /// )]); + /// # + /// # Ok(()) + /// # } + /// ``` async fn update<'a, C>(self, db: &'a C) -> Result where + ::Model: IntoActiveModel, Self: ActiveModelBehavior + 'a, C: ConnectionTrait<'a>, { @@ -195,6 +408,48 @@ pub trait ActiveModelTrait: Clone + Debug { } /// Delete an active model by its primary key + /// + /// # Example + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 0, + /// # rows_affected: 1, + /// # }, + /// # ]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; + /// + /// let orange = fruit::ActiveModel { + /// id: Set(3), + /// ..Default::default() + /// }; + /// + /// let delete_result = orange.delete(&db).await?; + /// + /// assert_eq!(delete_result.rows_affected, 1); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, + /// vec![3i32.into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` async fn delete<'a, C>(self, db: &'a C) -> Result where Self: ActiveModelBehavior + 'a, diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index 9c8a921de..e6247d352 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -95,8 +95,11 @@ pub trait EntityTrait: EntityName { /// # Example /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![ @@ -121,8 +124,6 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// assert_eq!( /// cake::Entity::find().one(&db).await?, /// Some(cake::Model { @@ -144,9 +145,6 @@ pub trait EntityTrait: EntityName { /// }, /// ] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -163,6 +161,9 @@ pub trait EntityTrait: EntityName { /// ), /// ] /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn find() -> Select { Select::new() @@ -173,8 +174,11 @@ pub trait EntityTrait: EntityName { /// # Example /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![ @@ -189,8 +193,6 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// assert_eq!( /// cake::Entity::find_by_id(11).all(&db).await?, /// vec![cake::Model { @@ -198,9 +200,6 @@ pub trait EntityTrait: EntityName { /// name: "Sponge Cake".to_owned(), /// }] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -210,11 +209,17 @@ pub trait EntityTrait: EntityName { /// vec![11i32.into()] /// )] /// ); + /// # + /// # Ok(()) + /// # } /// ``` /// Find by composite key /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![ @@ -229,8 +234,6 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake_filling}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// assert_eq!( /// cake_filling::Entity::find_by_id((2, 3)).all(&db).await?, /// vec![cake_filling::Model { @@ -238,9 +241,6 @@ pub trait EntityTrait: EntityName { /// filling_id: 3, /// }] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -252,6 +252,9 @@ pub trait EntityTrait: EntityName { /// ].join(" ").as_str(), /// vec![2i32.into(), 3i32.into()] /// )]); + /// # + /// # Ok(()) + /// # } /// ``` fn find_by_id(values: ::ValueType) -> Select { let mut select = Self::find(); @@ -272,13 +275,55 @@ pub trait EntityTrait: EntityName { /// Insert an model into database /// - /// # Example + /// # Example (Postgres) /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![maplit::btreemap! { + /// # "id" => Into::::into(15), + /// # }]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// let apple = cake::ActiveModel { + /// name: Set("Apple Pie".to_owned()), + /// ..Default::default() + /// }; + /// + /// let insert_result = cake::Entity::insert(apple).exec(&db).await?; + /// + /// assert_eq!(dbg!(insert_result.last_insert_id), 15); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"INSERT INTO "cake" ("name") VALUES ($1) RETURNING "id""#, + /// vec!["Apple Pie".into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example (MySQL) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::MySql) /// # .append_exec_results(vec![ /// # MockExecResult { /// # last_insert_id: 15, @@ -294,21 +339,21 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let insert_result = cake::Entity::insert(apple).exec(&db).await?; /// /// assert_eq!(insert_result.last_insert_id, 15); - /// // assert_eq!(insert_result.rows_affected, 1); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1) RETURNING "id""#, vec!["Apple Pie".into()] - /// )]); + /// DbBackend::MySql, + /// r#"INSERT INTO `cake` (`name`) VALUES (?)"#, + /// vec!["Apple Pie".into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn insert(model: A) -> Insert where @@ -319,13 +364,61 @@ pub trait EntityTrait: EntityName { /// Insert many models into database /// - /// # Example + /// # Example (Postgres) /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![maplit::btreemap! { + /// # "id" => Into::::into(28), + /// # }]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// let apple = cake::ActiveModel { + /// name: Set("Apple Pie".to_owned()), + /// ..Default::default() + /// }; + /// let orange = cake::ActiveModel { + /// name: Set("Orange Scone".to_owned()), + /// ..Default::default() + /// }; + /// + /// let insert_result = cake::Entity::insert_many(vec![apple, orange]) + /// .exec(&db) + /// .await?; + /// + /// assert_eq!(insert_result.last_insert_id, 28); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"INSERT INTO "cake" ("name") VALUES ($1), ($2) RETURNING "id""#, + /// vec!["Apple Pie".into(), "Orange Scone".into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example (MySQL) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::MySql) /// # .append_exec_results(vec![ /// # MockExecResult { /// # last_insert_id: 28, @@ -345,22 +438,23 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # - /// let insert_result = cake::Entity::insert_many(vec![apple, orange]).exec(&db).await?; + /// let insert_result = cake::Entity::insert_many(vec![apple, orange]) + /// .exec(&db) + /// .await?; /// /// assert_eq!(insert_result.last_insert_id, 28); - /// // assert_eq!(insert_result.rows_affected, 2); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"INSERT INTO "cake" ("name") VALUES ($1), ($2) RETURNING "id""#, + /// DbBackend::MySql, + /// r#"INSERT INTO `cake` (`name`) VALUES (?), (?)"#, /// vec!["Apple Pie".into(), "Orange Scone".into()] - /// )]); + /// )] + /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn insert_many(models: I) -> Insert where @@ -374,19 +468,81 @@ pub trait EntityTrait: EntityName { /// /// - To apply where conditions / filters, see [`QueryFilter`](crate::query::QueryFilter) /// - /// # Example + /// # Example (Postgres) /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![fruit::Model { + /// # id: 1, + /// # name: "Orange".to_owned(), + /// # cake_id: None, + /// # }], + /// # ]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; + /// + /// let orange = fruit::ActiveModel { + /// id: Set(1), + /// name: Set("Orange".to_owned()), + /// ..Default::default() + /// }; + /// + /// assert_eq!( + /// fruit::Entity::update(orange.clone()) + /// .filter(fruit::Column::Name.contains("orange")) + /// .exec(&db) + /// .await?, + /// fruit::Model { + /// id: 1, + /// name: "Orange".to_owned(), + /// cake_id: None, + /// } + /// .into_active_model(), + /// ); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2 AND "fruit"."name" LIKE $3 RETURNING "id", "name", "cake_id""#, + /// vec!["Orange".into(), 1i32.into(), "%orange%".into()] + /// )]); + /// # + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example (MySQL) + /// + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] + /// # #[cfg(feature = "mock")] + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let db = MockDatabase::new(DbBackend::MySql) /// # .append_exec_results(vec![ /// # MockExecResult { /// # last_insert_id: 0, /// # rows_affected: 1, /// # }, /// # ]) + /// # .append_query_results(vec![ + /// # vec![fruit::Model { + /// # id: 1, + /// # name: "Orange".to_owned(), + /// # cake_id: None, + /// # }], + /// # ]) /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; @@ -397,25 +553,35 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// assert_eq!( /// fruit::Entity::update(orange.clone()) /// .filter(fruit::Column::Name.contains("orange")) /// .exec(&db) /// .await?, - /// orange + /// fruit::Model { + /// id: 1, + /// name: "Orange".to_owned(), + /// cake_id: None, + /// } + /// .into_active_model(), /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), - /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2 AND "fruit"."name" LIKE $3"#, - /// vec!["Orange".into(), 1i32.into(), "%orange%".into()] - /// )]); + /// vec![ + /// Transaction::from_sql_and_values( + /// DbBackend::MySql, + /// r#"UPDATE `fruit` SET `name` = ? WHERE `fruit`.`id` = ? AND `fruit`.`name` LIKE ?"#, + /// vec!["Orange".into(), 1i32.into(), "%orange%".into()] + /// ), + /// Transaction::from_sql_and_values( + /// DbBackend::MySql, + /// r#"SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` WHERE `fruit`.`id` = ? LIMIT ?"#, + /// vec![1i32.into(), 1u64.into()] + /// )]); + /// # + /// # Ok(()) + /// # } /// ``` fn update(model: A) -> UpdateOne where @@ -431,8 +597,11 @@ pub trait EntityTrait: EntityName { /// # Example /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_exec_results(vec![ @@ -443,10 +612,13 @@ pub trait EntityTrait: EntityName { /// # ]) /// # .into_connection(); /// # - /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::{Expr, Value}}; + /// use sea_orm::{ + /// entity::*, + /// query::*, + /// sea_query::{Expr, Value}, + /// tests_cfg::fruit, + /// }; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let update_result = fruit::Entity::update_many() /// .col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None))) /// .filter(fruit::Column::Name.contains("Apple")) @@ -454,15 +626,18 @@ pub trait EntityTrait: EntityName { /// .await?; /// /// assert_eq!(update_result.rows_affected, 5); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, vec![Value::Int(None), "%Apple%".into()] - /// )]); + /// DbBackend::Postgres, + /// r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, + /// vec![Value::Int(None), "%Apple%".into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn update_many() -> UpdateMany { Update::many(Self::default()) @@ -475,8 +650,11 @@ pub trait EntityTrait: EntityName { /// # Example /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_exec_results(vec![ @@ -494,20 +672,21 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let delete_result = fruit::Entity::delete(orange).exec(&db).await?; /// /// assert_eq!(delete_result.rows_affected, 1); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, vec![3i32.into()] - /// )]); + /// DbBackend::Postgres, + /// r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, + /// vec![3i32.into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn delete(model: A) -> DeleteOne where @@ -523,8 +702,11 @@ pub trait EntityTrait: EntityName { /// # Example /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{entity::*, error::*, query::*, tests_cfg::*, MockDatabase, MockExecResult, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_exec_results(vec![ @@ -533,27 +715,34 @@ pub trait EntityTrait: EntityName { /// # rows_affected: 5, /// # }, /// # ]) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 15, + /// # name: "Apple Pie".to_owned(), + /// # }], + /// # ]) /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let delete_result = fruit::Entity::delete_many() /// .filter(fruit::Column::Name.contains("Apple")) /// .exec(&db) /// .await?; /// /// assert_eq!(delete_result.rows_affected, 5); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"DELETE FROM "fruit" WHERE "fruit"."name" LIKE $1"#, vec!["%Apple%".into()] - /// )]); + /// DbBackend::Postgres, + /// r#"DELETE FROM "fruit" WHERE "fruit"."name" LIKE $1"#, + /// vec!["%Apple%".into()] + /// )] + /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn delete_many() -> DeleteMany { Delete::many(Self::default()) diff --git a/src/entity/model.rs b/src/entity/model.rs index b11a700ba..acea83d63 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -47,8 +47,11 @@ pub trait FromQueryResult: Sized { } /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![vec![ @@ -67,8 +70,6 @@ pub trait FromQueryResult: Sized { /// num_of_cakes: i32, /// } /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let res: Vec = SelectResult::find_by_statement(Statement::from_sql_and_values( /// DbBackend::Postgres, /// r#"SELECT "name", COUNT(*) AS "num_of_cakes" FROM "cake" GROUP BY("name")"#, @@ -85,8 +86,6 @@ pub trait FromQueryResult: Sized { /// },] /// ); /// # - /// # Ok(()) - /// # }); /// # assert_eq!( /// # db.into_transaction_log(), /// # vec![Transaction::from_sql_and_values( @@ -95,6 +94,9 @@ pub trait FromQueryResult: Sized { /// # vec![] /// # ),] /// # ); + /// # + /// # Ok(()) + /// # } /// ``` fn find_by_statement(stmt: Statement) -> SelectorRaw> { SelectorRaw::>::from_statement(stmt) diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 64b6c145b..a6dbcbd53 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -1,8 +1,10 @@ use crate::{ - error::*, ActiveModelTrait, ConnectionTrait, DbBackend, EntityTrait, Insert, PrimaryKeyTrait, - Statement, TryFromU64, + error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, Insert, IntoActiveModel, + Iterable, PrimaryKeyTrait, SelectModel, SelectorRaw, Statement, TryFromU64, +}; +use sea_query::{ + Alias, Expr, FromValueTuple, Iden, InsertStatement, IntoColumnRef, Query, ValueTuple, }; -use sea_query::{FromValueTuple, InsertStatement, ValueTuple}; use std::{future::Future, marker::PhantomData}; /// Defines a structure to perform INSERT operations in an ActiveModel @@ -39,18 +41,28 @@ where { // so that self is dropped before entering await let mut query = self.query; - if db.get_database_backend() == DbBackend::Postgres { - use crate::{sea_query::Query, Iterable}; - if ::PrimaryKey::iter().count() > 0 { - query.returning( - Query::select() - .columns(::PrimaryKey::iter()) - .take(), - ); - } + if db.support_returning() && ::PrimaryKey::iter().count() > 0 { + let mut returning = Query::select(); + returning.columns( + ::PrimaryKey::iter().map(|c| c.into_column_ref()), + ); + query.returning(returning); } Inserter::::new(self.primary_key, query).exec(db) } + + /// Execute an insert operation and return the inserted model (use `RETURNING` syntax if database supported) + pub fn exec_with_returning<'a, C>( + self, + db: &'a C, + ) -> impl Future> + '_ + where + ::Model: IntoActiveModel, + C: ConnectionTrait<'a>, + A: 'a, + { + Inserter::::new(self.primary_key, self.query).exec_with_returning(db) + } } impl Inserter @@ -75,6 +87,19 @@ where let builder = db.get_database_backend(); exec_insert(self.primary_key, builder.build(&self.query), db) } + + /// Execute an insert operation and return the inserted model (use `RETURNING` syntax if database supported) + pub fn exec_with_returning<'a, C>( + self, + db: &'a C, + ) -> impl Future> + '_ + where + ::Model: IntoActiveModel, + C: ConnectionTrait<'a>, + A: 'a, + { + exec_insert_with_returning(self.primary_key, self.query, db) + } } async fn exec_insert<'a, A, C>( @@ -88,16 +113,15 @@ where { type PrimaryKey = <::Entity as EntityTrait>::PrimaryKey; type ValueTypeOf = as PrimaryKeyTrait>::ValueType; - let last_insert_id_opt = match db.get_database_backend() { - DbBackend::Postgres => { - use crate::{sea_query::Iden, Iterable}; + let last_insert_id_opt = match db.support_returning() { + true => { let cols = PrimaryKey::::iter() .map(|col| col.to_string()) .collect::>(); let res = db.query_one(statement).await?.unwrap(); res.try_get_many("", cols.as_ref()).ok() } - _ => { + false => { let last_insert_id = db.execute(statement).await?.last_insert_id(); ValueTypeOf::::try_from_u64(last_insert_id).ok() } @@ -111,3 +135,47 @@ where }; Ok(InsertResult { last_insert_id }) } + +async fn exec_insert_with_returning<'a, A, C>( + primary_key: Option, + mut insert_statement: InsertStatement, + db: &'a C, +) -> Result +where + ::Model: IntoActiveModel, + C: ConnectionTrait<'a>, + A: ActiveModelTrait, +{ + let db_backend = db.get_database_backend(); + let found = match db.support_returning() { + true => { + let mut returning = Query::select(); + returning.exprs(::Column::iter().map(|c| { + let col = Expr::col(c); + let col_def = ColumnTrait::def(&c); + let col_type = col_def.get_column_type(); + match col_type.get_enum_name() { + Some(_) => col.as_enum(Alias::new("text")), + None => col.into(), + } + })); + insert_statement.returning(returning); + SelectorRaw::::Model>>::from_statement( + db_backend.build(&insert_statement), + ) + .one(db) + .await? + } + false => { + let insert_res = + exec_insert::(primary_key, db_backend.build(&insert_statement), db).await?; + ::find_by_id(insert_res.last_insert_id) + .one(db) + .await? + } + }; + match found { + Some(model) => Ok(model.into_active_model()), + None => Err(DbErr::Exec("Failed to find inserted item".to_owned())), + } +} diff --git a/src/executor/paginator.rs b/src/executor/paginator.rs index f52b3bd86..e8f37bea7 100644 --- a/src/executor/paginator.rs +++ b/src/executor/paginator.rs @@ -96,12 +96,23 @@ where /// Fetch one page and increment the page counter /// - /// ```rust + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, MockDatabase, DbBackend}; - /// # let owned_db = MockDatabase::new(DbBackend::Postgres).into_connection(); + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let owned_db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 1, + /// # name: "Cake".to_owned(), + /// # }], + /// # vec![], + /// # ]) + /// # .into_connection(); /// # let db = &owned_db; - /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// let mut cake_pages = cake::Entity::find() @@ -113,7 +124,7 @@ where /// } /// # /// # Ok(()) - /// # }); + /// # } /// ``` pub async fn fetch_and_next(&mut self) -> Result>, DbErr> { let vec = self.fetch().await?; @@ -124,12 +135,23 @@ where /// Convert self into an async stream /// - /// ```rust + /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, MockDatabase, DbBackend}; - /// # let owned_db = MockDatabase::new(DbBackend::Postgres).into_connection(); + /// # pub async fn main() -> Result<(), DbErr> { + /// # + /// # let owned_db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 1, + /// # name: "Cake".to_owned(), + /// # }], + /// # vec![], + /// # ]) + /// # .into_connection(); /// # let db = &owned_db; - /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// use futures::TryStreamExt; /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; @@ -143,7 +165,7 @@ where /// } /// # /// # Ok(()) - /// # }); + /// # } /// ``` pub fn into_stream(mut self) -> PinBoxStream<'db, Result, DbErr>> { Box::pin(stream! { diff --git a/src/executor/query.rs b/src/executor/query.rs index 3c1a9184d..12d0c7cfa 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -320,8 +320,11 @@ pub trait TryGetableMany: Sized { fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result; /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(all(feature = "mock", feature = "macros"))] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![vec![ @@ -344,8 +347,6 @@ pub trait TryGetableMany: Sized { /// NumOfCakes, /// } /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let res: Vec<(String, i32)> = /// <(String, i32)>::find_by_statement::(Statement::from_sql_and_values( /// DbBackend::Postgres, @@ -362,9 +363,6 @@ pub trait TryGetableMany: Sized { /// ("New York Cheese".to_owned(), 1), /// ] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -374,6 +372,9 @@ pub trait TryGetableMany: Sized { /// vec![] /// ),] /// ); + /// # + /// # Ok(()) + /// # } /// ``` fn find_by_statement(stmt: Statement) -> SelectorRaw> where diff --git a/src/executor/select.rs b/src/executor/select.rs index 9fb906cf6..4c0d8ecfc 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -143,8 +143,11 @@ where } /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(all(feature = "mock", feature = "macros"))] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![vec![ @@ -164,8 +167,6 @@ where /// CakeName, /// } /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let res: Vec = cake::Entity::find() /// .select_only() /// .column_as(cake::Column::Name, QueryAs::CakeName) @@ -177,9 +178,6 @@ where /// res, /// vec!["Chocolate Forest".to_owned(), "New York Cheese".to_owned()] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -189,11 +187,17 @@ where /// vec![] /// )] /// ); + /// # + /// # Ok(()) + /// # } /// ``` /// /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(all(feature = "mock", feature = "macros"))] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![vec![ @@ -212,8 +216,6 @@ where /// NumOfCakes, /// } /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let res: Vec<(String, i64)> = cake::Entity::find() /// .select_only() /// .column_as(cake::Column::Name, QueryAs::CakeName) @@ -224,9 +226,6 @@ where /// .await?; /// /// assert_eq!(res, vec![("Chocolate Forest".to_owned(), 2i64)]); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -241,6 +240,9 @@ where /// vec![] /// )] /// ); + /// # + /// # Ok(()) + /// # } /// ``` pub fn into_values(self) -> Selector> where @@ -490,8 +492,11 @@ where } /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![vec![ @@ -514,8 +519,6 @@ where /// num_of_cakes: i32, /// } /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let res: Vec = cake::Entity::find() /// .from_raw_sql(Statement::from_sql_and_values( /// DbBackend::Postgres, @@ -539,9 +542,6 @@ where /// }, /// ] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -551,6 +551,9 @@ where /// vec![] /// ),] /// ); + /// # + /// # Ok(()) + /// # } /// ``` pub fn into_model(self) -> SelectorRaw> where @@ -563,8 +566,11 @@ where } /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # /// # let db = MockDatabase::new(DbBackend::Postgres) /// # .append_query_results(vec![vec![ @@ -581,8 +587,6 @@ where /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let res: Vec = cake::Entity::find().from_raw_sql( /// Statement::from_sql_and_values( /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] @@ -605,9 +609,6 @@ where /// }), /// ] /// ); - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -616,6 +617,9 @@ where /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] /// ), /// ]); + /// # + /// # Ok(()) + /// # } /// ``` #[cfg(feature = "with-json")] pub fn into_json(self) -> SelectorRaw> { @@ -627,15 +631,23 @@ where /// Get an item from the Select query /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # - /// # let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 1, + /// # name: "Cake".to_owned(), + /// # }], + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let _: Option = cake::Entity::find() /// .from_raw_sql(Statement::from_sql_and_values( /// DbBackend::Postgres, @@ -644,9 +656,6 @@ where /// )) /// .one(&db) /// .await?; - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -656,6 +665,9 @@ where /// vec![1.into()] /// ),] /// ); + /// # + /// # Ok(()) + /// # } /// ``` pub async fn one<'a, C>(self, db: &C) -> Result, DbErr> where @@ -670,15 +682,23 @@ where /// Get all items from the Select query /// ``` + /// # use sea_orm::{error::*, tests_cfg::*, *}; + /// # + /// # #[smol_potat::main] /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # pub async fn main() -> Result<(), DbErr> { /// # - /// # let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![ + /// # vec![cake::Model { + /// # id: 1, + /// # name: "Cake".to_owned(), + /// # }], + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = smol::block_on(async { - /// # /// let _: Vec = cake::Entity::find() /// .from_raw_sql(Statement::from_sql_and_values( /// DbBackend::Postgres, @@ -687,9 +707,6 @@ where /// )) /// .all(&db) /// .await?; - /// # - /// # Ok(()) - /// # }); /// /// assert_eq!( /// db.into_transaction_log(), @@ -699,6 +716,9 @@ where /// vec![] /// ),] /// ); + /// # + /// # Ok(()) + /// # } /// ``` pub async fn all<'a, C>(self, db: &C) -> Result, DbErr> where diff --git a/src/executor/update.rs b/src/executor/update.rs index 402f29acd..83747e2d6 100644 --- a/src/executor/update.rs +++ b/src/executor/update.rs @@ -1,7 +1,8 @@ use crate::{ - error::*, ActiveModelTrait, ConnectionTrait, EntityTrait, Statement, UpdateMany, UpdateOne, + error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, IntoActiveModel, + Iterable, SelectModel, SelectorRaw, Statement, UpdateMany, UpdateOne, }; -use sea_query::UpdateStatement; +use sea_query::{Alias, Expr, FromValueTuple, Query, UpdateStatement}; use std::future::Future; /// Defines an update operation @@ -25,10 +26,11 @@ where /// Execute an update operation on an ActiveModel pub async fn exec<'b, C>(self, db: &'b C) -> Result where + ::Model: IntoActiveModel, C: ConnectionTrait<'b>, { // so that self is dropped before entering await - exec_update_and_return_original(self.query, self.model, db).await + exec_update_and_return_updated(self.query, self.model, db).await } } @@ -78,17 +80,61 @@ where Updater::new(query).exec(db).await } -async fn exec_update_and_return_original<'a, A, C>( - query: UpdateStatement, +async fn exec_update_and_return_updated<'a, A, C>( + mut query: UpdateStatement, model: A, db: &'a C, ) -> Result where + ::Model: IntoActiveModel, A: ActiveModelTrait, C: ConnectionTrait<'a>, { - Updater::new(query).check_record_exists().exec(db).await?; - Ok(model) + match db.support_returning() { + true => { + let mut returning = Query::select(); + returning.exprs(::Column::iter().map(|c| { + let col = Expr::col(c); + let col_def = c.def(); + let col_type = col_def.get_column_type(); + match col_type.get_enum_name() { + Some(_) => col.as_enum(Alias::new("text")), + None => col.into(), + } + })); + query.returning(returning); + let db_backend = db.get_database_backend(); + let found: Option<::Model> = + SelectorRaw::::Model>>::from_statement( + db_backend.build(&query), + ) + .one(db) + .await?; + // If we got `None` then we are updating a row that does not exist. + match found { + Some(model) => Ok(model.into_active_model()), + None => Err(DbErr::RecordNotFound( + "None of the database rows are affected".to_owned(), + )), + } + } + false => { + // If we updating a row that does not exist then an error will be thrown here. + Updater::new(query).check_record_exists().exec(db).await?; + let primary_key_value = match model.get_primary_key_value() { + Some(val) => FromValueTuple::from_value_tuple(val), + None => return Err(DbErr::Exec("Fail to get primary key from model".to_owned())), + }; + let found = ::find_by_id(primary_key_value) + .one(db) + .await?; + // If we cannot select the updated row from db by the cached primary key + match found { + Some(model) => Ok(model.into_active_model()), + None => Err(DbErr::Exec("Failed to find inserted item".to_owned())), + } + } + } } async fn exec_update<'a, C>( @@ -119,23 +165,16 @@ mod tests { #[smol_potat::test] async fn update_record_not_found_1() -> Result<(), DbErr> { let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results(vec![ + vec![cake::Model { + id: 1, + name: "Cheese Cake".to_owned(), + }], + vec![], + vec![], + vec![], + ]) .append_exec_results(vec![ - MockExecResult { - last_insert_id: 0, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 0, - rows_affected: 0, - }, - MockExecResult { - last_insert_id: 0, - rows_affected: 0, - }, - MockExecResult { - last_insert_id: 0, - rows_affected: 0, - }, MockExecResult { last_insert_id: 0, rows_affected: 0, @@ -217,22 +256,22 @@ mod tests { vec![ Transaction::from_sql_and_values( DbBackend::Postgres, - r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#, vec!["Cheese Cake".into(), 1i32.into()] ), Transaction::from_sql_and_values( DbBackend::Postgres, - r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#, vec!["Cheese Cake".into(), 2i32.into()] ), Transaction::from_sql_and_values( DbBackend::Postgres, - r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#, vec!["Cheese Cake".into(), 2i32.into()] ), Transaction::from_sql_and_values( DbBackend::Postgres, - r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2"#, + r#"UPDATE "cake" SET "name" = $1 WHERE "cake"."id" = $2 RETURNING "id", "name""#, vec!["Cheese Cake".into(), 2i32.into()] ), Transaction::from_sql_and_values( diff --git a/tests/returning_tests.rs b/tests/returning_tests.rs new file mode 100644 index 000000000..7fa0447b4 --- /dev/null +++ b/tests/returning_tests.rs @@ -0,0 +1,69 @@ +pub mod common; + +pub use common::{bakery_chain::*, setup::*, TestContext}; +pub use sea_orm::{entity::prelude::*, *}; +pub use sea_query::Query; + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +async fn main() -> Result<(), DbErr> { + use bakery::*; + + let ctx = TestContext::new("returning_tests").await; + let db = &ctx.db; + let builder = db.get_database_backend(); + + let mut insert = Query::insert(); + insert + .into_table(Entity) + .columns(vec![Column::Name, Column::ProfitMargin]) + .values_panic(vec!["Bakery Shop".into(), 0.5.into()]); + + let mut update = Query::update(); + update + .table(Entity) + .values(vec![ + (Column::Name, "Bakery Shop".into()), + (Column::ProfitMargin, 0.5.into()), + ]) + .and_where(Column::Id.eq(1)); + + let mut returning = Query::select(); + returning.columns(vec![Column::Id, Column::Name, Column::ProfitMargin]); + + create_tables(db).await?; + + if db.support_returning() { + insert.returning(returning.clone()); + let insert_res = db + .query_one(builder.build(&insert)) + .await? + .expect("Insert failed with query_one"); + let _id: i32 = insert_res.try_get("", "id")?; + let _name: String = insert_res.try_get("", "name")?; + let _profit_margin: f64 = insert_res.try_get("", "profit_margin")?; + + update.returning(returning.clone()); + let update_res = db + .query_one(builder.build(&update)) + .await? + .expect("Update filed with query_one"); + let _id: i32 = update_res.try_get("", "id")?; + let _name: String = update_res.try_get("", "name")?; + let _profit_margin: f64 = update_res.try_get("", "profit_margin")?; + } else { + let insert_res = db.execute(builder.build(&insert)).await?; + assert!(insert_res.rows_affected() > 0); + + let update_res = db.execute(builder.build(&update)).await?; + assert!(update_res.rows_affected() > 0); + } + + ctx.delete().await; + + Ok(()) +}