diff --git a/issues/1790/Cargo.toml b/issues/1790/Cargo.toml new file mode 100644 index 000000000..e3872e255 --- /dev/null +++ b/issues/1790/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-orm-issues-1790" +version = "0.1.0" +edition = "2023" +publish = false + +[dependencies] +anyhow = "1" +serde = "1" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } + +[dependencies.sea-orm] +path = "../../" +default-features = false +features = ["macros", "runtime-tokio-native-tls", "sqlx-sqlite"] diff --git a/issues/1790/insert_test.rs b/issues/1790/insert_test.rs new file mode 100644 index 000000000..cf30e58c1 --- /dev/null +++ b/issues/1790/insert_test.rs @@ -0,0 +1,58 @@ +mod tests { + // currently ok + #[test] + fn insert_do_nothing_postgres() { + assert_eq!( + Insert::::new() + .add(cake::Model { + id: 1, + name: "Apple Pie".to_owned(), + }) + .on_conflict(OnConflict::new() + .do_nothing() + .to_owned() + ) + .build(DbBackend::Postgres) + .to_string(), + r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie') ON CONFLICT DO NOTHING"#, + ); + } + + //failed to run + #[test] + fn insert_do_nothing_mysql() { + assert_eq!( + Insert::::new() + .add(cake::Model { + id: 1, + name: "Apple Pie".to_owned(), + }) + .on_conflict(OnConflict::new() + .do_nothing() + .to_owned() + ) + .build(DbBackend::Mysql) + .to_string(), + r#"INSERT IGNORE INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"#, + ); + } + + // currently ok + #[test] + fn insert_do_nothing() { + assert_eq!( + Insert::::new() + .add(cake::Model { + id: 1, + name: "Apple Pie".to_owned(), + }) + .on_conflict(OnConflict::new() + .do_nothing() + .to_owned() + ) + .build(DbBackend::Sqlite) + .to_string(), + r#"INSERT IGNORE INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"#, + ); + } +} \ No newline at end of file diff --git a/src/database/mock.rs b/src/database/mock.rs index df222d8ee..e760ceaf7 100644 --- a/src/database/mock.rs +++ b/src/database/mock.rs @@ -285,6 +285,33 @@ where } } +impl IntoMockRow for (M, Option) +where + M: ModelTrait, + N: ModelTrait, +{ + fn into_mock_row(self) -> MockRow { + let mut mapped_join = BTreeMap::new(); + + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectA.as_str(), column.as_str()), + self.0.get(column), + ); + } + if let Some(b_entity) = self.1 { + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectB.as_str(), column.as_str()), + b_entity.get(column), + ); + } + } + + mapped_join.into_mock_row() + } +} + impl IntoMockRow for BTreeMap where T: Into, diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index 02917749f..5f7422e33 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -960,7 +960,6 @@ mod tests { ); } - delete_by_id("UUID".to_string()); delete_by_id("UUID".to_string()); delete_by_id("UUID"); delete_by_id(Cow::from("UUID")); diff --git a/src/executor/select.rs b/src/executor/select.rs index cd7d122cb..ae2ac5eb2 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -595,7 +595,7 @@ where /// /// > `SelectTwoMany::one()` method has been dropped (#486) /// > - /// > You can get `(Entity, Vec)` by first querying a single model from Entity, + /// > You can get `(Entity, Vec)` by first querying a single model from Entity, /// > then use [`ModelTrait::find_related`] on the model. /// > /// > See https://www.sea-ql.org/SeaORM/docs/basic-crud/select#lazy-loading for details. @@ -1107,3 +1107,565 @@ where } acc } + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + fn cake_fruit_model( + cake_id: i32, + fruit_id: i32, + ) -> ( + sea_orm::tests_cfg::cake::Model, + sea_orm::tests_cfg::fruit::Model, + ) { + (cake_model(cake_id), fruit_model(fruit_id, Some(cake_id))) + } + + fn cake_model(id: i32) -> sea_orm::tests_cfg::cake::Model { + let name = match id { + 1 => "apple cake", + 2 => "orange cake", + 3 => "fruit cake", + 4 => "chocolate cake", + _ => "", + } + .to_string(); + sea_orm::tests_cfg::cake::Model { id, name } + } + + fn fruit_model(id: i32, cake_id: Option) -> sea_orm::tests_cfg::fruit::Model { + let name = match id { + 1 => "apple", + 2 => "orange", + 3 => "grape", + 4 => "strawberry", + _ => "", + } + .to_string(); + sea_orm::tests_cfg::fruit::Model { id, name, cake_id } + } + + fn cake_vendor_link( + cake_id: i32, + vendor_id: i32, + ) -> ( + sea_orm::tests_cfg::cake::Model, + sea_orm::tests_cfg::vendor::Model, + ) { + (cake_model(cake_id), vendor_model(vendor_id)) + } + + fn vendor_model(id: i32) -> sea_orm::tests_cfg::vendor::Model { + let name = match id { + 1 => "Apollo", + 2 => "Benny", + 3 => "Christine", + 4 => "David", + _ => "", + } + .to_string(); + sea_orm::tests_cfg::vendor::Model { id, name } + } + + #[smol_potat::test] + pub async fn also_related() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase, Statement, Transaction}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[cake_fruit_model(1, 1)]]) + .into_connection(); + + assert_eq!( + Cake::find().find_also_related(Fruit).all(&db).await?, + [(cake_model(1), Some(fruit_model(1, Some(1))))] + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id""#, + r#"FROM "cake""#, + r#"LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + ] + .join(" ") + .as_str(), + [] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_related_2() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[cake_fruit_model(1, 1), cake_fruit_model(1, 2)]]) + .into_connection(); + + assert_eq!( + Cake::find().find_also_related(Fruit).all(&db).await?, + [ + (cake_model(1), Some(fruit_model(1, Some(1)))), + (cake_model(1), Some(fruit_model(2, Some(1)))) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_related_3() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_fruit_model(1, 1), + cake_fruit_model(1, 2), + cake_fruit_model(2, 2), + ]]) + .into_connection(); + + assert_eq!( + Cake::find().find_also_related(Fruit).all(&db).await?, + [ + (cake_model(1), Some(fruit_model(1, Some(1)))), + (cake_model(1), Some(fruit_model(2, Some(1)))), + (cake_model(2), Some(fruit_model(2, Some(2)))) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_related_4() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_fruit_model(1, 1).into_mock_row(), + cake_fruit_model(1, 2).into_mock_row(), + cake_fruit_model(2, 2).into_mock_row(), + (cake_model(3), None::).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find().find_also_related(Fruit).all(&db).await?, + [ + (cake_model(1), Some(fruit_model(1, Some(1)))), + (cake_model(1), Some(fruit_model(2, Some(1)))), + (cake_model(2), Some(fruit_model(2, Some(2)))), + (cake_model(3), None) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn with_related() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase, Statement, Transaction}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_fruit_model(1, 1), + cake_fruit_model(2, 2), + cake_fruit_model(2, 3), + ]]) + .into_connection(); + + assert_eq!( + Cake::find().find_with_related(Fruit).all(&db).await?, + [ + (cake_model(1), vec![fruit_model(1, Some(1))]), + ( + cake_model(2), + vec![fruit_model(2, Some(2)), fruit_model(3, Some(2))] + ) + ] + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id""#, + r#"FROM "cake""#, + r#"LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + r#"ORDER BY "cake"."id" ASC"# + ] + .join(" ") + .as_str(), + [] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn with_related_2() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_fruit_model(1, 1).into_mock_row(), + cake_fruit_model(2, 1).into_mock_row(), + cake_fruit_model(2, 2).into_mock_row(), + cake_fruit_model(2, 3).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find().find_with_related(Fruit).all(&db).await?, + [ + (cake_model(1), vec![fruit_model(1, Some(1)),]), + ( + cake_model(2), + vec![ + fruit_model(1, Some(2)), + fruit_model(2, Some(2)), + fruit_model(3, Some(2)), + ] + ), + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn with_related_empty() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_fruit_model(1, 1).into_mock_row(), + cake_fruit_model(2, 1).into_mock_row(), + cake_fruit_model(2, 2).into_mock_row(), + cake_fruit_model(2, 3).into_mock_row(), + (cake_model(3), None::).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find().find_with_related(Fruit).all(&db).await?, + [ + (cake_model(1), vec![fruit_model(1, Some(1)),]), + ( + cake_model(2), + vec![ + fruit_model(1, Some(2)), + fruit_model(2, Some(2)), + fruit_model(3, Some(2)), + ] + ), + (cake_model(3), vec![]) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_linked_base() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase, Statement, Transaction}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[cake_vendor_link(1, 1)]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [(cake_model(1), Some(vendor_model(1)))] + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""r2"."id" AS "B_id", "r2"."name" AS "B_name""#, + r#"FROM "cake""#, + r#"LEFT JOIN "cake_filling" AS "r0" ON "cake"."id" = "r0"."cake_id""#, + r#"LEFT JOIN "filling" AS "r1" ON "r0"."filling_id" = "r1"."id""#, + r#"LEFT JOIN "vendor" AS "r2" ON "r1"."vendor_id" = "r2"."id""#, + ] + .join(" ") + .as_str(), + [] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_linked_same_cake() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1), + cake_vendor_link(1, 2), + cake_vendor_link(2, 3), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), Some(vendor_model(1))), + (cake_model(1), Some(vendor_model(2))), + (cake_model(2), Some(vendor_model(3))) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_linked_same_vendor() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(2, 1).into_mock_row(), + cake_vendor_link(3, 2).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), Some(vendor_model(1))), + (cake_model(2), Some(vendor_model(1))), + (cake_model(3), Some(vendor_model(2))), + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_linked_many_to_many() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(1, 2).into_mock_row(), + cake_vendor_link(1, 3).into_mock_row(), + cake_vendor_link(2, 1).into_mock_row(), + cake_vendor_link(2, 2).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), Some(vendor_model(1))), + (cake_model(1), Some(vendor_model(2))), + (cake_model(1), Some(vendor_model(3))), + (cake_model(2), Some(vendor_model(1))), + (cake_model(2), Some(vendor_model(2))), + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn also_linked_empty() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(2, 2).into_mock_row(), + cake_vendor_link(3, 3).into_mock_row(), + (cake_model(4), None::).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), Some(vendor_model(1))), + (cake_model(2), Some(vendor_model(2))), + (cake_model(3), Some(vendor_model(3))), + (cake_model(4), None) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn with_linked_base() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, MockDatabase, Statement, Transaction}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1), + cake_vendor_link(2, 2), + cake_vendor_link(2, 3), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_with_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), vec![vendor_model(1)]), + (cake_model(2), vec![vendor_model(2), vendor_model(3)]) + ] + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""r2"."id" AS "B_id", "r2"."name" AS "B_name" FROM "cake""#, + r#"LEFT JOIN "cake_filling" AS "r0" ON "cake"."id" = "r0"."cake_id""#, + r#"LEFT JOIN "filling" AS "r1" ON "r0"."filling_id" = "r1"."id""#, + r#"LEFT JOIN "vendor" AS "r2" ON "r1"."vendor_id" = "r2"."id""#, + ] + .join(" ") + .as_str(), + [] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn with_linked_same_vendor() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(2, 2).into_mock_row(), + cake_vendor_link(3, 2).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_with_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), vec![vendor_model(1)]), + (cake_model(2), vec![vendor_model(2)]), + (cake_model(3), vec![vendor_model(2)]) + ] + ); + + Ok(()) + } + + #[smol_potat::test] + pub async fn with_linked_empty() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(2, 1).into_mock_row(), + cake_vendor_link(2, 2).into_mock_row(), + (cake_model(3), None::).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_with_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), vec![vendor_model(1)]), + (cake_model(2), vec![vendor_model(1), vendor_model(2)]), + (cake_model(3), vec![]) + ] + ); + + Ok(()) + } + + // normally would not happen + #[smol_potat::test] + pub async fn with_linked_repeated() -> Result<(), sea_orm::DbErr> { + use sea_orm::tests_cfg::*; + use sea_orm::{DbBackend, EntityTrait, IntoMockRow, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(1, 1).into_mock_row(), + cake_vendor_link(2, 1).into_mock_row(), + cake_vendor_link(2, 2).into_mock_row(), + ]]) + .into_connection(); + + assert_eq!( + Cake::find() + .find_with_linked(entity_linked::CakeToFillingVendor) + .all(&db) + .await?, + [ + (cake_model(1), vec![vendor_model(1), vendor_model(1)]), + (cake_model(2), vec![vendor_model(1), vendor_model(2)]), + ] + ); + + Ok(()) + } +} diff --git a/src/query/loader.rs b/src/query/loader.rs index 28663898e..2f3693a30 100644 --- a/src/query/loader.rs +++ b/src/query/loader.rs @@ -440,72 +440,138 @@ fn table_column(tbl: &TableRef, col: &DynIden) -> ColumnRef { #[cfg(test)] mod tests { + fn cake_model(id: i32) -> sea_orm::tests_cfg::cake::Model { + let name = match id { + 1 => "apple cake", + 2 => "orange cake", + 3 => "fruit cake", + 4 => "chocolate cake", + _ => "", + } + .to_string(); + sea_orm::tests_cfg::cake::Model { id, name } + } + + fn fruit_model(id: i32, cake_id: Option) -> sea_orm::tests_cfg::fruit::Model { + let name = match id { + 1 => "apple", + 2 => "orange", + 3 => "grape", + 4 => "strawberry", + _ => "", + } + .to_string(); + sea_orm::tests_cfg::fruit::Model { id, name, cake_id } + } + + fn filling_model(id: i32) -> sea_orm::tests_cfg::filling::Model { + let name = match id { + 1 => "apple juice", + 2 => "orange jam", + 3 => "chocolate crust", + 4 => "strawberry jam", + _ => "", + } + .to_string(); + sea_orm::tests_cfg::filling::Model { + id, + name, + vendor_id: Some(1), + ignored_attr: 0, + } + } + + fn cake_filling_model( + cake_id: i32, + filling_id: i32, + ) -> sea_orm::tests_cfg::cake_filling::Model { + sea_orm::tests_cfg::cake_filling::Model { + cake_id, + filling_id, + } + } + #[tokio::test] async fn test_load_one() { - use crate::{ - entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase, - }; + use sea_orm::{entity::prelude::*, tests_cfg::*, DbBackend, LoaderTrait, MockDatabase}; let db = MockDatabase::new(DbBackend::Postgres) - .append_query_results([[ - cake::Model { - id: 1, - name: "New York Cheese".to_owned(), - } - .into_mock_row(), - cake::Model { - id: 2, - name: "London Cheese".to_owned(), - } - .into_mock_row(), - ]]) + .append_query_results([[cake_model(1), cake_model(2)]]) .into_connection(); - let fruits = vec![fruit::Model { - id: 1, - name: "Apple".to_owned(), - cake_id: Some(1), - }]; + let fruits = vec![fruit_model(1, Some(1))]; let cakes = fruits .load_one(cake::Entity::find(), &db) .await .expect("Should return something"); - assert_eq!( - cakes, - [Some(cake::Model { - id: 1, - name: "New York Cheese".to_owned(), - })] - ); + assert_eq!(cakes, [Some(cake_model(1))]); + } + + #[tokio::test] + async fn test_load_one_same_cake() { + use sea_orm::{entity::prelude::*, tests_cfg::*, DbBackend, LoaderTrait, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[cake_model(1), cake_model(2)]]) + .into_connection(); + + let fruits = vec![fruit_model(1, Some(1)), fruit_model(2, Some(1))]; + + let cakes = fruits + .load_one(cake::Entity::find(), &db) + .await + .expect("Should return something"); + + assert_eq!(cakes, [Some(cake_model(1)), Some(cake_model(1))]); + } + + #[tokio::test] + async fn test_load_one_empty() { + use sea_orm::{entity::prelude::*, tests_cfg::*, DbBackend, LoaderTrait, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[cake_model(1), cake_model(2)]]) + .into_connection(); + + let fruits: Vec = vec![]; + + let cakes = fruits + .load_one(cake::Entity::find(), &db) + .await + .expect("Should return something"); + + assert_eq!(cakes, []); } #[tokio::test] async fn test_load_many() { - use crate::{ - entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase, - }; + use sea_orm::{entity::prelude::*, tests_cfg::*, DbBackend, LoaderTrait, MockDatabase}; let db = MockDatabase::new(DbBackend::Postgres) - .append_query_results([[fruit::Model { - id: 1, - name: "Apple".to_owned(), - cake_id: Some(1), - } - .into_mock_row()]]) + .append_query_results([[fruit_model(1, Some(1))]]) .into_connection(); - let cakes = vec![ - cake::Model { - id: 1, - name: "New York Cheese".to_owned(), - }, - cake::Model { - id: 2, - name: "London Cheese".to_owned(), - }, - ]; + let cakes = vec![cake_model(1), cake_model(2)]; + + let fruits = cakes + .load_many(fruit::Entity::find(), &db) + .await + .expect("Should return something"); + + assert_eq!(fruits, [vec![fruit_model(1, Some(1))], vec![]]); + } + + #[tokio::test] + async fn test_load_many_same_fruit() { + use sea_orm::{entity::prelude::*, tests_cfg::*, DbBackend, LoaderTrait, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[fruit_model(1, Some(1)), fruit_model(2, Some(1))]]) + .into_connection(); + + let cakes = vec![cake_model(1), cake_model(2)]; let fruits = cakes .load_many(fruit::Entity::find(), &db) @@ -515,13 +581,120 @@ mod tests { assert_eq!( fruits, [ - vec![fruit::Model { - id: 1, - name: "Apple".to_owned(), - cake_id: Some(1), - }], + vec![fruit_model(1, Some(1)), fruit_model(2, Some(1))], vec![] ] ); } + + // FIXME: load many with empty vector will panic + // #[tokio::test] + async fn test_load_many_empty() { + use sea_orm::{entity::prelude::*, tests_cfg::*, DbBackend, MockDatabase}; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[fruit_model(1, Some(1)), fruit_model(2, Some(1))]]) + .into_connection(); + + let cakes: Vec = vec![]; + + let fruits = cakes + .load_many(fruit::Entity::find(), &db) + .await + .expect("Should return something"); + + let empty_vec: Vec> = vec![]; + + assert_eq!(fruits, empty_vec); + } + + #[tokio::test] + async fn test_load_many_to_many_base() { + use sea_orm::{ + entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase, + }; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([ + [cake_filling_model(1, 1).into_mock_row()], + [filling_model(1).into_mock_row()], + ]) + .into_connection(); + + let cakes = vec![cake_model(1)]; + + let fillings = cakes + .load_many_to_many(Filling, CakeFilling, &db) + .await + .expect("Should return something"); + + assert_eq!(fillings, vec![vec![filling_model(1)]]); + } + + #[tokio::test] + async fn test_load_many_to_many_complex() { + use sea_orm::{ + entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase, + }; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([ + [ + cake_filling_model(1, 1).into_mock_row(), + cake_filling_model(1, 2).into_mock_row(), + cake_filling_model(1, 3).into_mock_row(), + cake_filling_model(2, 1).into_mock_row(), + cake_filling_model(2, 2).into_mock_row(), + ], + [ + filling_model(1).into_mock_row(), + filling_model(2).into_mock_row(), + filling_model(3).into_mock_row(), + filling_model(4).into_mock_row(), + filling_model(5).into_mock_row(), + ], + ]) + .into_connection(); + + let cakes = vec![cake_model(1), cake_model(2), cake_model(3)]; + + let fillings = cakes + .load_many_to_many(Filling, CakeFilling, &db) + .await + .expect("Should return something"); + + assert_eq!( + fillings, + vec![ + vec![filling_model(1), filling_model(2), filling_model(3)], + vec![filling_model(1), filling_model(2)], + vec![], + ] + ); + } + + #[tokio::test] + async fn test_load_many_to_many_empty() { + use sea_orm::{ + entity::prelude::*, tests_cfg::*, DbBackend, IntoMockRow, LoaderTrait, MockDatabase, + }; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([ + [cake_filling_model(1, 1).into_mock_row()], + [filling_model(1).into_mock_row()], + ]) + .into_connection(); + + let cakes: Vec = vec![]; + + let fillings = cakes + .load_many_to_many(Filling, CakeFilling, &db) + .await + .expect("Should return something"); + + let empty_vec: Vec> = vec![]; + + assert_eq!(fillings, empty_vec); + } } diff --git a/tests/active_enum_tests.rs b/tests/active_enum_tests.rs index 14ebb436b..e1aff3f98 100644 --- a/tests/active_enum_tests.rs +++ b/tests/active_enum_tests.rs @@ -371,6 +371,27 @@ pub async fn find_linked_active_enum(db: &DatabaseConnection) -> Result<(), DbEr }) )] ); + assert_eq!( + ActiveEnum::find() + .find_with_linked(active_enum::ActiveEnumChildLink) + .all(db) + .await?, + [( + active_enum::Model { + id: 2, + category: Some(Category::Small), + color: Some(Color::White), + tea: Some(Tea::BreakfastTea), + }, + vec![active_enum_child::Model { + id: 1, + parent_id: 2, + category: Some(Category::Big), + color: Some(Color::Black), + tea: Some(Tea::EverydayTea), + }] + )] + ); assert_eq!( active_enum_child::Model { @@ -411,6 +432,27 @@ pub async fn find_linked_active_enum(db: &DatabaseConnection) -> Result<(), DbEr }) )] ); + assert_eq!( + ActiveEnumChild::find() + .find_with_linked(active_enum_child::ActiveEnumLink) + .all(db) + .await?, + [( + active_enum_child::Model { + id: 1, + parent_id: 2, + category: Some(Category::Big), + color: Some(Color::Black), + tea: Some(Tea::EverydayTea), + }, + vec![active_enum::Model { + id: 2, + category: Some(Category::Small), + color: Some(Color::White), + tea: Some(Tea::BreakfastTea), + }] + )] + ); Ok(()) } diff --git a/tests/crud/updates.rs b/tests/crud/updates.rs index 5c1f39d90..6dbd29e93 100644 --- a/tests/crud/updates.rs +++ b/tests/crud/updates.rs @@ -52,6 +52,7 @@ pub async fn test_update_cake(db: &DbConn) { let cake_model = cake.unwrap(); assert_eq!(cake_model.name, "Extra chocolate mud cake"); assert_eq!(cake_model.price, dec!(20.00)); + assert!(!cake_model.gluten_free); } pub async fn test_update_bakery(db: &DbConn) { diff --git a/tests/empty_insert_tests.rs b/tests/empty_insert_tests.rs index e3a8bfa76..ea61a37fb 100644 --- a/tests/empty_insert_tests.rs +++ b/tests/empty_insert_tests.rs @@ -9,6 +9,7 @@ pub use sea_orm::{ pub use crud::*; // use common::bakery_chain::*; use sea_orm::{DbConn, TryInsertResult}; +use sea_query::OnConflict; #[sea_orm_macros::test] #[cfg(any( @@ -37,6 +38,12 @@ pub async fn test(db: &DbConn) { assert!(matches!(res, Ok(TryInsertResult::Inserted(_)))); + let double_seaside_bakery = bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + profit_margin: Set(10.4), + id: Set(1), + }; + let empty_insert = Bakery::insert_many(std::iter::empty::()) .on_empty_do_nothing() .exec(db) diff --git a/tests/loader_tests.rs b/tests/loader_tests.rs index c0cb408b1..67341fdc6 100644 --- a/tests/loader_tests.rs +++ b/tests/loader_tests.rs @@ -1,7 +1,7 @@ pub mod common; pub use common::{bakery_chain::*, setup::*, TestContext}; -use sea_orm::{entity::*, query::*, DbConn, DbErr}; +use sea_orm::{entity::*, query::*, DbConn, DbErr, RuntimeErr}; #[sea_orm_macros::test] #[cfg(any( @@ -32,6 +32,17 @@ async fn loader_load_one() -> Result<(), DbErr> { assert_eq!(bakers, [baker_1, baker_2, baker_3]); assert_eq!(bakeries, [Some(bakery_0.clone()), Some(bakery_0), None]); + // has many find, should use load_many instead + let bakeries = bakery::Entity::find().all(&ctx.db).await?; + let bakers = bakeries.load_one(baker::Entity, &ctx.db).await; + + assert_eq!( + bakers, + Err(DbErr::Query(RuntimeErr::Internal( + "Relation is HasMany instead of HasOne".to_string() + ))) + ); + Ok(()) } @@ -47,6 +58,7 @@ async fn loader_load_many() -> Result<(), DbErr> { let bakery_1 = insert_bakery(&ctx.db, "SeaSide Bakery").await?; let bakery_2 = insert_bakery(&ctx.db, "Offshore Bakery").await?; + let bakery_3 = insert_bakery(&ctx.db, "Rocky Bakery").await?; let baker_1 = insert_baker(&ctx.db, "Baker 1", bakery_1.id).await?; let baker_2 = insert_baker(&ctx.db, "Baker 2", bakery_1.id).await?; @@ -57,12 +69,16 @@ async fn loader_load_many() -> Result<(), DbErr> { let bakeries = bakery::Entity::find().all(&ctx.db).await?; let bakers = bakeries.load_many(baker::Entity, &ctx.db).await?; - assert_eq!(bakeries, [bakery_1.clone(), bakery_2.clone()]); + assert_eq!( + bakeries, + [bakery_1.clone(), bakery_2.clone(), bakery_3.clone()] + ); assert_eq!( bakers, [ - [baker_1.clone(), baker_2.clone()], - [baker_3.clone(), baker_4.clone()] + vec![baker_1.clone(), baker_2.clone()], + vec![baker_3.clone(), baker_4.clone()], + vec![] ] ); @@ -79,7 +95,8 @@ async fn loader_load_many() -> Result<(), DbErr> { bakers, [ vec![baker_1.clone(), baker_2.clone()], - vec![baker_4.clone()] + vec![baker_4.clone()], + vec![] ] ); @@ -150,6 +167,7 @@ async fn loader_load_many_to_many() -> Result<(), DbErr> { let baker_1 = insert_baker(&ctx.db, "Jane", bakery_1.id).await?; let baker_2 = insert_baker(&ctx.db, "Peter", bakery_1.id).await?; + let baker_3 = insert_baker(&ctx.db, "Fred", bakery_1.id).await?; // does not make cake let cake_1 = insert_cake(&ctx.db, "Cheesecake", None).await?; let cake_2 = insert_cake(&ctx.db, "Coffee", None).await?; @@ -166,12 +184,13 @@ async fn loader_load_many_to_many() -> Result<(), DbErr> { .load_many_to_many(cake::Entity, cakes_bakers::Entity, &ctx.db) .await?; - assert_eq!(bakers, [baker_1.clone(), baker_2.clone()]); + assert_eq!(bakers, [baker_1.clone(), baker_2.clone(), baker_3.clone()]); assert_eq!( cakes, [ vec![cake_1.clone(), cake_2.clone()], - vec![cake_2.clone(), cake_3.clone()] + vec![cake_2.clone(), cake_3.clone()], + vec![] ] ); @@ -184,7 +203,7 @@ async fn loader_load_many_to_many() -> Result<(), DbErr> { &ctx.db, ) .await?; - assert_eq!(cakes, [vec![cake_1.clone()], vec![cake_3.clone()]]); + assert_eq!(cakes, [vec![cake_1.clone()], vec![cake_3.clone()], vec![]]); // now, start again from cakes diff --git a/tests/relational_tests.rs b/tests/relational_tests.rs index a2994ee84..c8c41b17a 100644 --- a/tests/relational_tests.rs +++ b/tests/relational_tests.rs @@ -494,6 +494,255 @@ pub async fn having() { ctx.delete().await; } +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +pub async fn related() -> Result<(), DbErr> { + use sea_orm::{SelectA, SelectB}; + + let ctx = TestContext::new("test_related").await; + create_tables(&ctx.db).await?; + + // SeaSide Bakery + let seaside_bakery = bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + profit_margin: Set(10.4), + ..Default::default() + }; + let seaside_bakery_res = Bakery::insert(seaside_bakery).exec(&ctx.db).await?; + + // Bob's Baker + let baker_bob = baker::ActiveModel { + name: Set("Baker Bob".to_owned()), + contact_details: Set(serde_json::json!({ + "mobile": "+61424000000", + "home": "0395555555", + "address": "12 Test St, Testville, Vic, Australia" + })), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id)), + ..Default::default() + }; + let _baker_bob_res = Baker::insert(baker_bob).exec(&ctx.db).await?; + + // Bobby's Baker + let baker_bobby = baker::ActiveModel { + name: Set("Baker Bobby".to_owned()), + contact_details: Set(serde_json::json!({ + "mobile": "+85212345678", + })), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id)), + ..Default::default() + }; + let _baker_bobby_res = Baker::insert(baker_bobby).exec(&ctx.db).await?; + + // Terres Bakery + let terres_bakery = bakery::ActiveModel { + name: Set("Terres Bakery".to_owned()), + profit_margin: Set(13.5), + ..Default::default() + }; + let terres_bakery_res = Bakery::insert(terres_bakery).exec(&ctx.db).await?; + + // Ada's Baker + let baker_ada = baker::ActiveModel { + name: Set("Baker Ada".to_owned()), + contact_details: Set(serde_json::json!({ + "mobile": "+61424000000", + "home": "0395555555", + "address": "12 Test St, Testville, Vic, Australia" + })), + bakery_id: Set(Some(terres_bakery_res.last_insert_id)), + ..Default::default() + }; + let _baker_ada_res = Baker::insert(baker_ada).exec(&ctx.db).await?; + + // Stone Bakery, with no baker + let stone_bakery = bakery::ActiveModel { + name: Set("Stone Bakery".to_owned()), + profit_margin: Set(13.5), + ..Default::default() + }; + let _stone_bakery_res = Bakery::insert(stone_bakery).exec(&ctx.db).await?; + + #[derive(Debug, FromQueryResult, PartialEq)] + struct BakerLite { + name: String, + } + + #[derive(Debug, FromQueryResult, PartialEq)] + struct BakeryLite { + name: String, + } + + // get all bakery and baker's name and put them into tuples + let bakers_in_bakery: Vec<(BakeryLite, Option)> = Bakery::find() + .find_also_related(Baker) + .select_only() + .column_as(bakery::Column::Name, (SelectA, bakery::Column::Name)) + .column_as(baker::Column::Name, (SelectB, baker::Column::Name)) + .order_by_asc(bakery::Column::Id) + .order_by_asc(baker::Column::Id) + .into_model() + .all(&ctx.db) + .await?; + + assert_eq!( + bakers_in_bakery, + [ + ( + BakeryLite { + name: "SeaSide Bakery".to_owned(), + }, + Some(BakerLite { + name: "Baker Bob".to_owned(), + }) + ), + ( + BakeryLite { + name: "SeaSide Bakery".to_owned(), + }, + Some(BakerLite { + name: "Baker Bobby".to_owned(), + }) + ), + ( + BakeryLite { + name: "Terres Bakery".to_owned(), + }, + Some(BakerLite { + name: "Baker Ada".to_owned(), + }) + ), + ( + BakeryLite { + name: "Stone Bakery".to_owned(), + }, + None, + ), + ] + ); + + let seaside_bakery = Bakery::find() + .filter(bakery::Column::Id.eq(1)) + .one(&ctx.db) + .await? + .unwrap(); + + let bakers = seaside_bakery.find_related(Baker).all(&ctx.db).await?; + + assert_eq!( + bakers, + [ + baker::Model { + id: 1, + name: "Baker Bob".to_owned(), + contact_details: serde_json::json!({ + "mobile": "+61424000000", + "home": "0395555555", + "address": "12 Test St, Testville, Vic, Australia" + }), + bakery_id: Some(1), + }, + baker::Model { + id: 2, + name: "Baker Bobby".to_owned(), + contact_details: serde_json::json!({ + "mobile": "+85212345678", + }), + bakery_id: Some(1), + } + ] + ); + + let select_bakery_with_baker = Bakery::find() + .find_with_related(Baker) + .order_by_asc(baker::Column::Id); + + assert_eq!( + select_bakery_with_baker + .build(sea_orm::DatabaseBackend::MySql) + .to_string(), + [ + "SELECT `bakery`.`id` AS `A_id`,", + "`bakery`.`name` AS `A_name`,", + "`bakery`.`profit_margin` AS `A_profit_margin`,", + "`baker`.`id` AS `B_id`,", + "`baker`.`name` AS `B_name`,", + "`baker`.`contact_details` AS `B_contact_details`,", + "`baker`.`bakery_id` AS `B_bakery_id`", + "FROM `bakery`", + "LEFT JOIN `baker` ON `bakery`.`id` = `baker`.`bakery_id`", + "ORDER BY `bakery`.`id` ASC, `baker`.`id` ASC" + ] + .join(" ") + ); + + assert_eq!( + select_bakery_with_baker.all(&ctx.db).await?, + [ + ( + bakery::Model { + id: 1, + name: "SeaSide Bakery".to_owned(), + profit_margin: 10.4, + }, + vec![ + baker::Model { + id: 1, + name: "Baker Bob".to_owned(), + contact_details: serde_json::json!({ + "mobile": "+61424000000", + "home": "0395555555", + "address": "12 Test St, Testville, Vic, Australia" + }), + bakery_id: Some(seaside_bakery_res.last_insert_id), + }, + baker::Model { + id: 2, + name: "Baker Bobby".to_owned(), + contact_details: serde_json::json!({ + "mobile": "+85212345678", + }), + bakery_id: Some(seaside_bakery_res.last_insert_id), + } + ] + ), + ( + bakery::Model { + id: 2, + name: "Terres Bakery".to_owned(), + profit_margin: 13.5, + }, + vec![baker::Model { + id: 3, + name: "Baker Ada".to_owned(), + contact_details: serde_json::json!({ + "mobile": "+61424000000", + "home": "0395555555", + "address": "12 Test St, Testville, Vic, Australia" + }), + bakery_id: Some(terres_bakery_res.last_insert_id), + }] + ), + ( + bakery::Model { + id: 3, + name: "Stone Bakery".to_owned(), + profit_margin: 13.5, + }, + vec![] + ), + ] + ); + + ctx.delete().await; + + Ok(()) +} + #[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", @@ -586,6 +835,17 @@ pub async fn linked() -> Result<(), DbErr> { .exec(&ctx.db) .await?; + // Freerider's Baker, no cake baked + let baker_freerider = baker::ActiveModel { + name: Set("Freerider".to_owned()), + contact_details: Set(serde_json::json!({ + "mobile": "+85298765432", + })), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id)), + ..Default::default() + }; + let _baker_freerider_res = Baker::insert(baker_freerider).exec(&ctx.db).await?; + // Kate's Customer, Order & Line Item let customer_kate = customer::ActiveModel { name: Set("Kate".to_owned()), @@ -680,6 +940,7 @@ pub async fn linked() -> Result<(), DbErr> { name: String, } + // filtered find let baked_for_customers: Vec<(BakerLite, Option)> = Baker::find() .find_also_linked(baker::BakedForCustomer) .select_only() @@ -725,9 +986,16 @@ pub async fn linked() -> Result<(), DbErr> { name: "Kara".to_owned(), }) ), + ( + BakerLite { + name: "Freerider".to_owned(), + }, + None, + ), ] ); + // try to use find_linked instead let baker_bob = Baker::find() .filter(baker::Column::Id.eq(1)) .one(&ctx.db) @@ -748,6 +1016,7 @@ pub async fn linked() -> Result<(), DbErr> { }] ); + // find full model using with_linked let select_baker_with_customer = Baker::find() .find_with_linked(baker::BakedForCustomer) .order_by_asc(baker::Column::Id) @@ -823,6 +1092,17 @@ pub async fn linked() -> Result<(), DbErr> { }, ] ), + ( + baker::Model { + id: 3, + name: "Freerider".into(), + contact_details: serde_json::json!({ + "mobile": "+85298765432", + }), + bakery_id: Some(1), + }, + vec![] + ), ] ); diff --git a/tests/sql_err_tests.rs b/tests/sql_err_tests.rs index e73b3afe5..e086d730c 100644 --- a/tests/sql_err_tests.rs +++ b/tests/sql_err_tests.rs @@ -64,4 +64,7 @@ pub async fn test_error(db: &DatabaseConnection) { fk_error.sql_err(), Some(SqlErr::ForeignKeyConstraintViolation(_)) )); + + let invalid_error = DbErr::Custom("random error".to_string()); + assert_eq!(invalid_error.sql_err(), None) } diff --git a/tests/value_type_tests.rs b/tests/value_type_tests.rs index 227186606..1898f22a3 100644 --- a/tests/value_type_tests.rs +++ b/tests/value_type_tests.rs @@ -71,6 +71,7 @@ pub fn type_test() { assert_eq!(Integer::array_type(), ArrayType::Int); assert_eq!(Boolbean::column_type(), ColumnType::Boolean); assert_eq!(Boolbean::array_type(), ArrayType::Bool); + // self implied assert_eq!( StringVec::column_type(),