diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index a072827b2..70ad20b09 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -5,22 +5,81 @@ use syn::{ ext::IdentExt, punctuated::Punctuated, token::Comma, Data, DataStruct, Fields, Generics, Meta, }; -pub struct FromQueryResultItem { - pub skip: bool, +enum ItemType { + Normal, + Skipped, + Nested, +} + +struct FromQueryResultItem { + pub typ: ItemType, pub ident: Ident, } -impl ToTokens for FromQueryResultItem { + +/// Initially, we try to obtain the value for each field and check if it is an ordinary DB error +/// (which we return immediatly), or a null error. +/// +/// ### Background +/// +/// Null errors do not necessarily mean that the deserialization as a whole fails, +/// since structs embedding the current one might have wrapped the current one in an `Option`. +/// In this case, we do not want to swallow other errors, which are very likely to actually be +/// programming errors that should be noticed (and fixed). +struct TryFromQueryResultCheck<'a>(&'a FromQueryResultItem); + +impl<'a> ToTokens for TryFromQueryResultCheck<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let FromQueryResultItem { ident, typ } = self.0; + + match typ { + ItemType::Normal => { + let name = ident.unraw().to_string(); + tokens.extend(quote! { + let #ident = match row.try_get_nullable(pre, #name) { + Err(v @ sea_orm::TryGetError::DbErr(_)) => { + return Err(v); + } + v => v, + }; + }); + } + ItemType::Skipped => { + tokens.extend(quote! { + let #ident = std::default::Default::default(); + }); + } + ItemType::Nested => { + let name = ident.unraw().to_string(); + tokens.extend(quote! { + let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, &format!("{pre}{}-", #name)) { + Err(v @ sea_orm::TryGetError::DbErr(_)) => { + return Err(v); + } + v => v, + }; + }); + } + } + } +} + +struct TryFromQueryResultAssignment<'a>(&'a FromQueryResultItem); + +impl<'a> ToTokens for TryFromQueryResultAssignment<'a> { fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { ident, skip } = self; - if *skip { - tokens.extend(quote! { - #ident: std::default::Default::default(), - }); - } else { - let name = ident.unraw().to_string(); - tokens.extend(quote! { - #ident: row.try_get(pre, #name)?, - }); + let FromQueryResultItem { ident, typ, .. } = self.0; + + match typ { + ItemType::Normal | ItemType::Nested => { + tokens.extend(quote! { + #ident: #ident?, + }); + } + ItemType::Skipped => { + tokens.extend(quote! { + #ident, + }); + } } } } @@ -31,7 +90,7 @@ pub fn expand_derive_from_query_result( data: Data, generics: Generics, ) -> syn::Result { - let fields = match data { + let parsed_fields = match data { Data::Struct(DataStruct { fields: Fields::Named(named), .. @@ -42,40 +101,54 @@ pub fn expand_derive_from_query_result( }) } }; - let mut field = Vec::with_capacity(fields.len()); - for parsed_field in fields.into_iter() { - let mut skip = false; + let mut fields = Vec::with_capacity(parsed_fields.len()); + for parsed_field in parsed_fields.into_iter() { + let mut typ = ItemType::Normal; for attr in parsed_field.attrs.iter() { if !attr.path().is_ident("sea_orm") { continue; } if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { for meta in list.iter() { - skip = meta.exists("skip"); + if meta.exists("skip") { + typ = ItemType::Skipped; + } else if meta.exists("nested") { + typ = ItemType::Nested; + } } } } let ident = format_ident!("{}", parsed_field.ident.unwrap().to_string()); - field.push(FromQueryResultItem { skip, ident }); + fields.push(FromQueryResultItem { typ, ident }); } let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let ident_try_init: Vec<_> = fields.iter().map(TryFromQueryResultCheck).collect(); + let ident_try_assign: Vec<_> = fields.iter().map(TryFromQueryResultAssignment).collect(); + Ok(quote!( #[automatically_derived] impl #impl_generics sea_orm::FromQueryResult for #ident #ty_generics #where_clause { - fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { + fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> Result { + Ok(Self::from_query_result_nullable(row, pre)?) + } + + fn from_query_result_nullable(row: &sea_orm::QueryResult, pre: &str) -> Result { + #(#ident_try_init)* + Ok(Self { - #(#field)* + #(#ident_try_assign)* }) } } )) } -mod util { + +pub(super) mod util { use syn::Meta; - pub(super) trait GetMeta { + pub trait GetMeta { fn exists(&self, k: &str) -> bool; } diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 0413fc86b..9d939e568 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -4,21 +4,25 @@ use proc_macro2::TokenStream; use quote::format_ident; use quote::quote; use quote::quote_spanned; +use syn::ext::IdentExt; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::Comma; use syn::Expr; use syn::Meta; +use syn::Type; + +use super::from_query_result::util::GetMeta; use self::util::GetAsKVMeta; #[derive(Debug)] enum Error { InputNotStruct, - EntityNotSpecific, + EntityNotSpecified, NotSupportGeneric(Span), - BothFromColAndFromExpr(Span), + OverlappingAttributes(Span), Syn(syn::Error), } #[derive(Debug, PartialEq, Eq)] @@ -29,6 +33,8 @@ enum ColumnAs { ColAlias { col: syn::Ident, field: String }, /// from an expr Expr { expr: syn::Expr, field_name: String }, + /// nesting another struct + Nested { typ: Type, field_name: String }, } struct DerivePartialModel { @@ -78,6 +84,7 @@ impl DerivePartialModel { let mut from_col = None; let mut from_expr = None; + let mut nested = false; for attr in field.attrs.iter() { if !attr.path().is_ident("sea_orm") { @@ -94,35 +101,37 @@ impl DerivePartialModel { .get_as_kv("from_expr") .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) .transpose()?; + nested = meta.exists("nested"); } } } let field_name = field.ident.unwrap(); - let col_as = match (from_col, from_expr) { - (None, None) => { + let col_as = match (from_col, from_expr, nested) { + (Some(col), None, false) => { if entity.is_none() { - return Err(Error::EntityNotSpecific); + return Err(Error::EntityNotSpecified); } - ColumnAs::Col(format_ident!( - "{}", - field_name.to_string().to_upper_camel_case() - )) + + let field = field_name.to_string(); + ColumnAs::ColAlias { col, field } } - (None, Some(expr)) => ColumnAs::Expr { + (None, Some(expr), false) => ColumnAs::Expr { expr, field_name: field_name.to_string(), }, - (Some(col), None) => { + (None, None, true) => ColumnAs::Nested { + typ: field.ty, + field_name: field_name.unraw().to_string(), + }, + (None, None, false) => { if entity.is_none() { - return Err(Error::EntityNotSpecific); + return Err(Error::EntityNotSpecified); } - - let field = field_name.to_string(); - ColumnAs::ColAlias { col, field } + ColumnAs::Col(field_name) } - (Some(_), Some(_)) => return Err(Error::BothFromColAndFromExpr(field_span)), + (_, _, _) => return Err(Error::OverlappingAttributes(field_span)), }; column_as_list.push(col_as); } @@ -148,23 +157,64 @@ impl DerivePartialModel { let select_col_code_gen = fields.iter().map(|col_as| match col_as { ColumnAs::Col(ident) => { let entity = entity.as_ref().unwrap(); - let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #ident); - quote!(let #select_ident = sea_orm::SelectColumns::select_column(#select_ident, #col_value);) + let uppercase_ident = format_ident!( + "{}", + ident.to_string().to_upper_camel_case() + ); + let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #uppercase_ident); + let ident_stringified = ident.unraw().to_string(); + quote!(let #select_ident = + if let Some(prefix) = pre { + let ident = format!("{prefix}{}", #ident_stringified); + sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, ident) + } else { + sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, #ident_stringified) + }; + ) }, ColumnAs::ColAlias { col, field } => { let entity = entity.as_ref().unwrap(); let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #col); - quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, #field);) + quote!(let #select_ident = + if let Some(prefix) = pre { + let ident = format!("{prefix}{}", #field); + sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, ident) + } else { + sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, #field) + }; + ) }, ColumnAs::Expr { expr, field_name } => { - quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name);) + quote!(let #select_ident = + if let Some(prefix) = pre { + let ident = format!("{prefix}{}", #field_name); + sea_orm::SelectColumns::select_column_as(#select_ident, #expr, ident) + } else { + sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name) + }; + ) + }, + ColumnAs::Nested { typ, field_name } => { + quote!(let #select_ident = + <#typ as sea_orm::PartialModelTrait>::select_cols_nested(#select_ident, + Some(&if let Some(prefix) = pre { + format!("{prefix}{}-", #field_name) } + else { + format!("{}-", #field_name) + } + )); + ) }, }); quote! { #[automatically_derived] impl sea_orm::PartialModelTrait for #ident{ - fn select_cols(#select_ident: S) -> S{ + fn select_cols(#select_ident: S) -> S { + Self::select_cols_nested(#select_ident, None) + } + + fn select_cols_nested(#select_ident: S, pre: Option<&str>) -> S { #(#select_col_code_gen)* #select_ident } @@ -181,10 +231,10 @@ pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result Ok(quote_spanned! { span => compile_error!("you can only derive `DerivePartialModel` on named struct"); }), - Err(Error::BothFromColAndFromExpr(span)) => Ok(quote_spanned! { - span => compile_error!("you can only use one of `from_col` or `from_expr`"); + Err(Error::OverlappingAttributes(span)) => Ok(quote_spanned! { + span => compile_error!("you can only use one of `from_col`, `from_expr`, `nested`"); }), - Err(Error::EntityNotSpecific) => Ok(quote_spanned! { + Err(Error::EntityNotSpecified) => Ok(quote_spanned! { ident_span => compile_error!("you need specific which entity you are using") }), Err(Error::InputNotStruct) => Ok(quote_spanned! { @@ -258,7 +308,7 @@ struct PartialModel{ assert_eq!(middle.fields.len(), 3); assert_eq!( middle.fields[0], - ColumnAs::Col(format_ident!("DefaultField")) + ColumnAs::Col(format_ident!("default_field")) ); assert_eq!( middle.fields[1], diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 0c9d11a00..24290be32 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -762,7 +762,7 @@ pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// If all fields in the partial model is `from_expr`, the `entity` can be ignore. +/// If all fields in the partial model is `from_expr`, the specifying the `entity` can be skipped. /// ``` /// use sea_orm::{entity::prelude::*, sea_query::Expr, DerivePartialModel, FromQueryResult}; /// @@ -773,7 +773,28 @@ pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// A field cannot have attributes `from_col` and `from_expr` at the same time. +/// It is possible to nest structs deriving `FromQueryResult` and `DerivePartialModel`, including +/// optionally, which is useful for specifying columns from tables added via left joins, as well as +/// when building up complicated queries programmatically. +/// ``` +/// use sea_orm::{entity::prelude::*, sea_query::Expr, DerivePartialModel, FromQueryResult}; +/// +/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// struct Inner { +/// #[sea_orm(from_expr = "Expr::val(1).add(1)")] +/// sum: i32, +/// } +/// +/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// struct Outer { +/// #[sea_orm(nested)] +/// inner: Inner, +/// #[sea_orm(nested)] +/// inner_opt: Option, +/// } +/// ``` +/// +/// A field cannot have attributes `from_col`, `from_expr` or `nested` at the same time. /// Or, it will result in a compile error. /// /// ```compile_fail diff --git a/src/entity/model.rs b/src/entity/model.rs index d23e9f541..e85990a34 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -1,7 +1,7 @@ use crate::{ ActiveModelBehavior, ActiveModelTrait, ConnectionTrait, DbErr, DeleteResult, EntityTrait, IntoActiveModel, Linked, QueryFilter, QueryResult, Related, Select, SelectModel, SelectorRaw, - Statement, + Statement, TryGetError, }; use async_trait::async_trait; pub use sea_query::Value; @@ -51,12 +51,36 @@ pub trait ModelTrait: Clone + Send + Debug { /// A Trait for implementing a [QueryResult] pub trait FromQueryResult: Sized { /// Instantiate a Model from a [QueryResult] + /// + /// NOTE: Please also override `from_query_result_nullable` when manually implementing. + /// The future default implementation will be along the lines of: + /// + /// ```rust,ignore + /// fn from_query_result(res: &QueryResult, pre: &str) -> Result { + /// (Self::from_query_result_nullable(res, pre)?) + /// } + /// ``` fn from_query_result(res: &QueryResult, pre: &str) -> Result; /// Transform the error from instantiating a Model from a [QueryResult] /// and converting it to an [Option] fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result, DbErr> { Ok(Self::from_query_result(res, pre).ok()) + + // would really like to do the following, but can't without version bump: + // match Self::from_query_result_nullable(res, pre) { + // Ok(v) => Ok(Some(v)), + // Err(TryGetError::Null(_)) => Ok(None), + // Err(TryGetError::DbErr(err)) => Err(err), + // } + } + + /// Transform the error from instantiating a Model from a [QueryResult] + /// and converting it to an [Option] + /// + /// NOTE: This will most likely stop being a provided method in the next major version! + fn from_query_result_nullable(res: &QueryResult, pre: &str) -> Result { + Self::from_query_result(res, pre).map_err(TryGetError::DbErr) } /// ``` @@ -116,6 +140,28 @@ pub trait FromQueryResult: Sized { } } +impl FromQueryResult for Option { + fn from_query_result(res: &QueryResult, pre: &str) -> Result { + Ok(Self::from_query_result_nullable(res, pre)?) + } + + fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result, DbErr> { + match Self::from_query_result_nullable(res, pre) { + Ok(v) => Ok(Some(v)), + Err(TryGetError::Null(_)) => Ok(None), + Err(TryGetError::DbErr(err)) => Err(err), + } + } + + fn from_query_result_nullable(res: &QueryResult, pre: &str) -> Result { + match T::from_query_result_nullable(res, pre) { + Ok(v) => Ok(Some(v)), + Err(TryGetError::Null(_)) => Ok(None), + Err(err @ TryGetError::DbErr(_)) => Err(err), + } + } +} + /// A Trait for any type that can be converted into an Model pub trait TryIntoModel where diff --git a/src/entity/partial_model.rs b/src/entity/partial_model.rs index 9da4f1d22..edb365ebb 100644 --- a/src/entity/partial_model.rs +++ b/src/entity/partial_model.rs @@ -3,5 +3,27 @@ use crate::{FromQueryResult, SelectColumns}; /// A trait for a part of [Model](super::model::ModelTrait) pub trait PartialModelTrait: FromQueryResult { /// Select specific columns this [PartialModel] needs + /// + /// If you are implementing this by hand, please make sure to read the hints in the + /// documentation for `select_cols_nested` and ensure to implement both methods. fn select_cols(select: S) -> S; + + /// Used when nesting these structs into each other. + /// + /// This will stop being a provided method in a future major release. + /// Please implement this method manually when implementing this trait by hand, + /// and ensure that your `select_cols` implementation is calling it with `_prefix` as `None`. + fn select_cols_nested(select: S, _prefix: Option<&str>) -> S { + Self::select_cols(select) + } +} + +impl PartialModelTrait for Option { + fn select_cols(select: S) -> S { + Self::select_cols_nested(select, None) + } + + fn select_cols_nested(select: S, prefix: Option<&str>) -> S { + T::select_cols_nested(select, prefix) + } } diff --git a/src/executor/query.rs b/src/executor/query.rs index 712e33f58..5d24896f7 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -84,6 +84,15 @@ impl QueryResult { Ok(T::try_get_by(self, index)?) } + /// Get a value from the query result with an ColIdx + pub fn try_get_by_nullable(&self, index: I) -> Result + where + T: TryGetable, + I: ColIdx, + { + T::try_get_by(self, index) + } + /// Get a value from the query result with prefixed column name pub fn try_get(&self, pre: &str, col: &str) -> Result where @@ -92,6 +101,14 @@ impl QueryResult { Ok(T::try_get(self, pre, col)?) } + /// Get a value from the query result with prefixed column name + pub fn try_get_nullable(&self, pre: &str, col: &str) -> Result + where + T: TryGetable, + { + T::try_get(self, pre, col) + } + /// Get a value from the query result based on the order in the select expressions pub fn try_get_by_index(&self, idx: usize) -> Result where @@ -100,6 +117,14 @@ impl QueryResult { Ok(T::try_get_by_index(self, idx)?) } + /// Get a value from the query result based on the order in the select expressions + pub fn try_get_by_index_nullable(&self, idx: usize) -> Result + where + T: TryGetable, + { + T::try_get_by_index(self, idx) + } + /// Get a tuple value from the query result with prefixed column name pub fn try_get_many(&self, pre: &str, cols: &[String]) -> Result where diff --git a/src/executor/select.rs b/src/executor/select.rs index 493f861fe..dde4c92e2 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -182,7 +182,7 @@ where /// .into_partial_model::() /// .into_statement(DbBackend::Sqlite) /// .to_string(), - /// r#"SELECT "cake"."name", UPPER("cake"."name") AS "name_upper" FROM "cake""# + /// r#"SELECT "cake"."name" AS "name", UPPER("cake"."name") AS "name_upper" FROM "cake""# /// ); /// # } /// ``` diff --git a/tests/derive_tests.rs b/tests/derive_tests.rs index bd99a3b46..e48228005 100644 --- a/tests/derive_tests.rs +++ b/tests/derive_tests.rs @@ -63,3 +63,9 @@ struct FromQueryAttributeTests { _foo: i32, _bar: String, } + +#[derive(FromQueryResult)] +struct FromQueryResultNested { + #[sea_orm(nested)] + _test: SimpleTest, +} diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index f59747ba6..863037884 100644 --- a/tests/partial_model_tests.rs +++ b/tests/partial_model_tests.rs @@ -4,8 +4,11 @@ feature = "sqlx-postgres" ))] use entity::{Column, Entity}; -use sea_orm::{ColumnTrait, DerivePartialModel, EntityTrait, FromQueryResult, ModelTrait}; -use sea_query::Expr; +use sea_orm::{prelude::*, DerivePartialModel, FromQueryResult, Set}; + +use crate::common::TestContext; + +mod common; mod entity { use sea_orm::prelude::*; @@ -59,3 +62,167 @@ struct FieldFromExpr { #[sea_orm(from_expr = "Expr::col(Column::Id).equals(Column::Foo)")] _bar: bool, } + +#[derive(FromQueryResult, DerivePartialModel)] +struct Nest { + #[sea_orm(nested)] + _foo: SimpleTest, +} + +#[derive(FromQueryResult, DerivePartialModel)] +struct NestOption { + #[sea_orm(nested)] + _foo: Option, +} + +#[allow(unused)] +mod runtime { + use super::*; + + use common::bakery_chain::*; + + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "bakery::Entity")] + struct Bakery { + id: i32, + #[sea_orm(from_col = "Name")] + title: String, + } + + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "bakery::Entity")] + struct BakeryDetails { + #[sea_orm(nested)] + basics: Bakery, + #[sea_orm(from_expr = "bakery::Column::ProfitMargin")] + profit: f64, + } + + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "cake::Entity")] + struct Cake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, + } + + async fn fill_data(ctx: &TestContext, link: bool) { + bakery::Entity::insert(bakery::ActiveModel { + id: Set(42), + name: Set("cool little bakery".to_string()), + profit_margin: Set(4.1), + }) + .exec(&ctx.db) + .await + .expect("insert succeeds"); + + cake::Entity::insert(cake::ActiveModel { + id: Set(13), + name: Set("Test Cake".to_owned()), + price: Set(Decimal::ZERO), + bakery_id: Set(if link { Some(42) } else { None }), + gluten_free: Set(true), + serial: Set(Uuid::new_v4()), + }) + .exec(&ctx.db) + .await + .expect("insert succeeds"); + } + + #[sea_orm_macros::test] + async fn partial_model_left_join_does_not_exist() { + let ctx = TestContext::new("partial_model_left_join_does_not_exist").await; + create_tables(&ctx.db).await.unwrap(); + + fill_data(&ctx, false).await; + + let cake: Cake = cake::Entity::find() + .left_join(bakery::Entity) + .into_partial_model() + .one(&ctx.db) + .await + .expect("succeeds to get the result") + .expect("exactly one model in DB"); + + assert_eq!(cake.id, 13); + assert!(cake.bakery.is_none()); + + ctx.delete().await; + } + + #[sea_orm_macros::test] + async fn partial_model_left_join_exists() { + let ctx = TestContext::new("partial_model_left_join_exists").await; + create_tables(&ctx.db).await.unwrap(); + + fill_data(&ctx, true).await; + + let cake: Cake = cake::Entity::find() + .left_join(bakery::Entity) + .into_partial_model() + .one(&ctx.db) + .await + .expect("succeeds to get the result") + .expect("exactly one model in DB"); + + assert_eq!(cake.id, 13); + assert!(matches!(cake.bakery, Some(Bakery { id: 42, .. }))); + + ctx.delete().await; + } + + #[sea_orm_macros::test] + async fn partial_model_nested_same_table() { + let ctx = TestContext::new("partial_model_nested_same_table").await; + create_tables(&ctx.db).await.unwrap(); + + fill_data(&ctx, true).await; + + let bakery: BakeryDetails = bakery::Entity::find() + .into_partial_model() + .one(&ctx.db) + .await + .expect("succeeds to get the result") + .expect("exactly one model in DB"); + + assert_eq!(bakery.basics.id, 42); + + ctx.delete().await; + } + + #[derive(Debug, FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "bakery::Entity")] + struct WrongBakery { + id: String, + #[sea_orm(from_col = "Name")] + title: String, + } + + #[derive(Debug, FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "cake::Entity")] + struct WrongCake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, + } + + #[sea_orm_macros::test] + #[ignore = "This currently does not work, as sqlx does not perform type checking when a column is absent.."] + async fn partial_model_optional_field_but_type_error() { + let ctx = TestContext::new("partial_model_nested_same_table").await; + create_tables(&ctx.db).await.unwrap(); + + fill_data(&ctx, false).await; + + let _: DbErr = cake::Entity::find() + .left_join(bakery::Entity) + .into_partial_model::() + .one(&ctx.db) + .await + .expect_err("should error instead of returning an empty Option"); + + ctx.delete().await; + } +}