diff --git a/sea-orm-sync/src/query/select.rs b/sea-orm-sync/src/query/select.rs index 67d2aea963..76e5307180 100644 --- a/sea-orm-sync/src/query/select.rs +++ b/sea-orm-sync/src/query/select.rs @@ -1,6 +1,6 @@ use crate::{ - ColumnTrait, EntityTrait, Iterable, Order, PrimaryKeyToColumn, QueryFilter, QueryOrder, - QuerySelect, QueryTrait, + ColumnTrait, EntityTrait, IdenStatic, Iterable, Order, PrimaryKeyToColumn, QueryFilter, + QueryOrder, QuerySelect, QueryTrait, }; use core::fmt::Debug; use core::marker::PhantomData; @@ -317,6 +317,26 @@ where } self } + + /// Select all columns of this entity except the given ones. + /// + /// Clears any prior selection (`clear_selects`) and then rebuilds the query, + /// this overrides earlier [`QuerySelect::select_only`] / [`QuerySelect::column`] calls + /// and is not composable with them + /// + /// Rows still hydrate into the full `Model`. Only `Option<_>` columns can be excluded, + /// excluding a non-nullable column results in a runtime error, + /// `Missing value for column`. + pub fn select_except(mut self, except: impl IntoIterator) -> Self { + let except: Vec<&str> = except.into_iter().map(|col| col.as_str()).collect(); + self.query.clear_selects(); + for col in E::Column::iter() { + if !except.contains(&col.as_str()) { + self.query.expr(col.select_as(col.into_expr())); + } + } + self + } } impl QueryTrait for Select diff --git a/sea-orm-sync/tests/query_tests.rs b/sea-orm-sync/tests/query_tests.rs index e176076626..a24ec1c757 100644 --- a/sea-orm-sync/tests/query_tests.rs +++ b/sea-orm-sync/tests/query_tests.rs @@ -239,3 +239,97 @@ pub fn select_only_exclude_option_fields() { ctx.delete(); } + +#[sea_orm_macros::test] +pub fn select_except_excludes_option_fields() { + let ctx = TestContext::new("select_except_excludes_option_fields"); + create_bakery_table(&ctx.db).unwrap(); + create_baker_table(&ctx.db).unwrap(); + + let bakery = bakery::ActiveModel { + name: Set("Sweet Treats".to_owned()), + profit_margin: Set(0.5), + ..Default::default() + } + .save(&ctx.db) + .expect("could not insert bakery"); + + let _ = baker::ActiveModel { + name: Set("Alice".to_owned()), + contact_details: Set(serde_json::json!({ + "phone": "555-1234", + "email": "alice@example.com" + })), + bakery_id: Set(Some(bakery.id.clone().unwrap())), + ..Default::default() + } + .save(&ctx.db) + .expect("could not insert baker"); + + let bakers = Baker::find() + .select_except([baker::Column::BakeryId]) + .all(&ctx.db) + .unwrap(); + + assert_eq!(bakers.len(), 1); + assert_eq!(bakers[0].name, "Alice"); + assert_eq!( + bakers[0].contact_details, + serde_json::json!({ + "phone": "555-1234", + "email": "alice@example.com" + }) + ); + assert_eq!(bakers[0].bakery_id, None); + + let bakery_id: Option = Baker::find_by_id(bakers[0].id) + .select_only() + .column(baker::Column::BakeryId) + .into_tuple() + .one(&ctx.db) + .unwrap() + .unwrap(); + + assert_eq!(bakery_id, Some(bakery.id.unwrap())); + + ctx.delete(); +} + +#[sea_orm_macros::test] +pub fn select_except_non_nullable_column_fails() { + let ctx = TestContext::new("select_except_non_nullable_column_fails"); + create_bakery_table(&ctx.db).unwrap(); + create_baker_table(&ctx.db).unwrap(); + + let bakery = bakery::ActiveModel { + name: Set("Sweet Treats".to_owned()), + profit_margin: Set(0.5), + ..Default::default() + } + .save(&ctx.db) + .expect("could not insert bakery"); + + let _ = baker::ActiveModel { + name: Set("Alice".to_owned()), + contact_details: Set(serde_json::json!({ + "phone": "555-1234", + "email": "alice@example.com" + })), + bakery_id: Set(Some(bakery.id.clone().unwrap())), + ..Default::default() + } + .save(&ctx.db) + .expect("could not insert baker"); + + let err = Baker::find() + .select_except([baker::Column::Name]) + .all(&ctx.db) + .unwrap_err(); + + assert!(matches!( + err, + sea_orm::DbErr::Type(msg) if msg == "Missing value for column 'name'" + )); + + ctx.delete(); +} diff --git a/src/query/select.rs b/src/query/select.rs index 67d2aea963..76e5307180 100644 --- a/src/query/select.rs +++ b/src/query/select.rs @@ -1,6 +1,6 @@ use crate::{ - ColumnTrait, EntityTrait, Iterable, Order, PrimaryKeyToColumn, QueryFilter, QueryOrder, - QuerySelect, QueryTrait, + ColumnTrait, EntityTrait, IdenStatic, Iterable, Order, PrimaryKeyToColumn, QueryFilter, + QueryOrder, QuerySelect, QueryTrait, }; use core::fmt::Debug; use core::marker::PhantomData; @@ -317,6 +317,26 @@ where } self } + + /// Select all columns of this entity except the given ones. + /// + /// Clears any prior selection (`clear_selects`) and then rebuilds the query, + /// this overrides earlier [`QuerySelect::select_only`] / [`QuerySelect::column`] calls + /// and is not composable with them + /// + /// Rows still hydrate into the full `Model`. Only `Option<_>` columns can be excluded, + /// excluding a non-nullable column results in a runtime error, + /// `Missing value for column`. + pub fn select_except(mut self, except: impl IntoIterator) -> Self { + let except: Vec<&str> = except.into_iter().map(|col| col.as_str()).collect(); + self.query.clear_selects(); + for col in E::Column::iter() { + if !except.contains(&col.as_str()) { + self.query.expr(col.select_as(col.into_expr())); + } + } + self + } } impl QueryTrait for Select diff --git a/tests/query_tests.rs b/tests/query_tests.rs index ff181f7202..4c93631bde 100644 --- a/tests/query_tests.rs +++ b/tests/query_tests.rs @@ -256,3 +256,104 @@ pub async fn select_only_exclude_option_fields() { ctx.delete().await; } + +#[sea_orm_macros::test] +pub async fn select_except_excludes_option_fields() { + let ctx = TestContext::new("select_except_excludes_option_fields").await; + create_bakery_table(&ctx.db).await.unwrap(); + create_baker_table(&ctx.db).await.unwrap(); + + let bakery = bakery::ActiveModel { + name: Set("Sweet Treats".to_owned()), + profit_margin: Set(0.5), + ..Default::default() + } + .save(&ctx.db) + .await + .expect("could not insert bakery"); + + let _ = baker::ActiveModel { + name: Set("Alice".to_owned()), + contact_details: Set(serde_json::json!({ + "phone": "555-1234", + "email": "alice@example.com" + })), + bakery_id: Set(Some(bakery.id.clone().unwrap())), + ..Default::default() + } + .save(&ctx.db) + .await + .expect("could not insert baker"); + + let bakers = Baker::find() + .select_except([baker::Column::BakeryId]) + .all(&ctx.db) + .await + .unwrap(); + + assert_eq!(bakers.len(), 1); + assert_eq!(bakers[0].name, "Alice"); + assert_eq!( + bakers[0].contact_details, + serde_json::json!({ + "phone": "555-1234", + "email": "alice@example.com" + }) + ); + assert_eq!(bakers[0].bakery_id, None); + + let bakery_id: Option = Baker::find_by_id(bakers[0].id) + .select_only() + .column(baker::Column::BakeryId) + .into_tuple() + .one(&ctx.db) + .await + .unwrap() + .unwrap(); + + assert_eq!(bakery_id, Some(bakery.id.unwrap())); + + ctx.delete().await; +} + +#[sea_orm_macros::test] +pub async fn select_except_non_nullable_column_fails() { + let ctx = TestContext::new("select_except_non_nullable_column_fails").await; + create_bakery_table(&ctx.db).await.unwrap(); + create_baker_table(&ctx.db).await.unwrap(); + + let bakery = bakery::ActiveModel { + name: Set("Sweet Treats".to_owned()), + profit_margin: Set(0.5), + ..Default::default() + } + .save(&ctx.db) + .await + .expect("could not insert bakery"); + + let _ = baker::ActiveModel { + name: Set("Alice".to_owned()), + contact_details: Set(serde_json::json!({ + "phone": "555-1234", + "email": "alice@example.com" + })), + bakery_id: Set(Some(bakery.id.clone().unwrap())), + ..Default::default() + } + .save(&ctx.db) + .await + .expect("could not insert baker"); + + let err = Baker::find() + .select_except([baker::Column::Name]) + .all(&ctx.db) + .await + .unwrap_err(); + + assert!(matches!( + err, + sea_orm::DbErr::Type(msg) if msg == "Missing value for column 'name'" + )); + + ctx.delete().await; +}