From 7208d244f22e355565db101d583a57a3d1c10e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 16:46:03 +0900 Subject: [PATCH 01/21] feat: implement optional from query result in derive --- .../src/derives/from_query_result.rs | 70 +++++++++++++++++-- src/executor/query.rs | 25 +++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index a072827b2..ff95512eb 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -5,10 +5,11 @@ use syn::{ ext::IdentExt, punctuated::Punctuated, token::Comma, Data, DataStruct, Fields, Generics, Meta, }; -pub struct FromQueryResultItem { +struct FromQueryResultItem { pub skip: bool, pub ident: Ident, } + impl ToTokens for FromQueryResultItem { fn to_tokens(&self, tokens: &mut TokenStream) { let Self { ident, skip } = self; @@ -25,13 +26,59 @@ impl ToTokens for FromQueryResultItem { } } +struct TryFromQueryResultCheck<'a>(&'a FromQueryResultItem); + +impl<'a> ToTokens for TryFromQueryResultCheck<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let FromQueryResultItem { ident, skip } = self.0; + if *skip { + tokens.extend(quote! { + let #ident = std::default::Default::default(); + }); + } else { + let name = ident.unraw().to_string(); + tokens.extend(quote! { + let #ident = match row.try_get_nullable(pre, #name) { + std::result::Result::Err(sea_orm::TryGetError::DbErr(err)) => { + return Err(err); + } + std::result::Result::Err(sea_orm::TryGetError::Null(_)) => std::option::Option::None, + std::result::Result::Ok(v) => std::option::Option::Some(v), + }; + }); + } + } +} + +struct TryFromQueryResultAssignment<'a>(&'a FromQueryResultItem); + +impl<'a> ToTokens for TryFromQueryResultAssignment<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let FromQueryResultItem { ident, skip } = self.0; + if *skip { + tokens.extend(quote! { + #ident, + }); + } else { + tokens.extend(quote! { + #ident: match #ident { + std::option::Option::Some(v) => v, + std::option::Option::None => { + return std::result::Result::Ok(std::option::Option::None); + } + }, + }); + } + } +} + /// Method to derive a [QueryResult](sea_orm::QueryResult) pub fn expand_derive_from_query_result( ident: Ident, data: Data, generics: Generics, ) -> syn::Result { - let fields = match data { + let parsed_fields = match data { Data::Struct(DataStruct { fields: Fields::Named(named), .. @@ -42,9 +89,9 @@ pub fn expand_derive_from_query_result( }) } }; - let mut field = Vec::with_capacity(fields.len()); - for parsed_field in fields.into_iter() { + let mut fields = Vec::with_capacity(parsed_fields.len()); + for parsed_field in parsed_fields.into_iter() { let mut skip = false; for attr in parsed_field.attrs.iter() { if !attr.path().is_ident("sea_orm") { @@ -57,18 +104,29 @@ pub fn expand_derive_from_query_result( } } let ident = format_ident!("{}", parsed_field.ident.unwrap().to_string()); - field.push(FromQueryResultItem { skip, ident }); + fields.push(FromQueryResultItem { skip, 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 { Ok(Self { - #(#field)* + #(#fields)* }) } + + fn from_query_result_optional(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result, sea_orm::DbErr> { + #(#ident_try_init)* + + std::result::Result::Ok(std::option::Option::Some(Self { + #(#ident_try_assign)* + })) + } } )) } 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 From 521113a64851235c940a02a2f190e6bdf1c723b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 19:05:26 +0900 Subject: [PATCH 02/21] feat: allow for recursive usage of FromQueryResult derive --- .../src/derives/from_query_result.rs | 120 +++++++++--------- src/entity/model.rs | 25 +++- 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index ff95512eb..aa71e046d 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -3,49 +3,53 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ ext::IdentExt, punctuated::Punctuated, token::Comma, Data, DataStruct, Fields, Generics, Meta, + Type, }; -struct FromQueryResultItem { - pub skip: bool, - pub ident: Ident, +enum ItemType { + Normal, + Skipped, + Nested, } -impl ToTokens for FromQueryResultItem { - 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)?, - }); - } - } +struct FromQueryResultItem { + pub typ: ItemType, + pub ident: Ident, } struct TryFromQueryResultCheck<'a>(&'a FromQueryResultItem); impl<'a> ToTokens for TryFromQueryResultCheck<'a> { fn to_tokens(&self, tokens: &mut TokenStream) { - let FromQueryResultItem { ident, skip } = self.0; - if *skip { - tokens.extend(quote! { - let #ident = std::default::Default::default(); - }); - } else { - let name = ident.unraw().to_string(); - tokens.extend(quote! { - let #ident = match row.try_get_nullable(pre, #name) { - std::result::Result::Err(sea_orm::TryGetError::DbErr(err)) => { - return Err(err); - } - std::result::Result::Err(sea_orm::TryGetError::Null(_)) => std::option::Option::None, - std::result::Result::Ok(v) => std::option::Option::Some(v), - }; - }); + 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 => { + tokens.extend(quote! { + let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, pre) { + Err(v @ sea_orm::TryGetError::DbErr(_)) => { + return Err(v); + } + v => v, + }; + }); + } } } } @@ -54,20 +58,19 @@ struct TryFromQueryResultAssignment<'a>(&'a FromQueryResultItem); impl<'a> ToTokens for TryFromQueryResultAssignment<'a> { fn to_tokens(&self, tokens: &mut TokenStream) { - let FromQueryResultItem { ident, skip } = self.0; - if *skip { - tokens.extend(quote! { - #ident, - }); - } else { - tokens.extend(quote! { - #ident: match #ident { - std::option::Option::Some(v) => v, - std::option::Option::None => { - return std::result::Result::Ok(std::option::Option::None); - } - }, - }); + let FromQueryResultItem { ident, typ, .. } = self.0; + + match typ { + ItemType::Normal | ItemType::Nested => { + tokens.extend(quote! { + #ident: #ident?, + }); + } + ItemType::Skipped => { + tokens.extend(quote! { + #ident, + }); + } } } } @@ -92,19 +95,23 @@ pub fn expand_derive_from_query_result( let mut fields = Vec::with_capacity(parsed_fields.len()); for parsed_field in parsed_fields.into_iter() { - let mut skip = false; + 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()); - fields.push(FromQueryResultItem { skip, ident }); + fields.push(FromQueryResultItem { typ, ident }); } let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); @@ -114,22 +121,21 @@ pub fn expand_derive_from_query_result( 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 { - Ok(Self { - #(#fields)* - }) + fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> Result { + Ok(Self::from_query_result_nullable(row, pre)?) } - fn from_query_result_optional(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result, sea_orm::DbErr> { + fn from_query_result_nullable(row: &sea_orm::QueryResult, pre: &str) -> Result { #(#ident_try_init)* - std::result::Result::Ok(std::option::Option::Some(Self { + Ok(Self { #(#ident_try_assign)* - })) + }) } } )) } + mod util { use syn::Meta; diff --git a/src/entity/model.rs b/src/entity/model.rs index d23e9f541..801bbd14a 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; @@ -56,7 +56,19 @@ pub trait FromQueryResult: Sized { /// 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()) + 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 +128,15 @@ pub trait FromQueryResult: Sized { } } +impl FromQueryResult for Option { + fn from_query_result(res: &QueryResult, pre: &str) -> Result { + match T::from_query_result_optional(res, pre) { + Ok(v) => Ok(v), + Err(error) => Err(error), + } + } +} + /// A Trait for any type that can be converted into an Model pub trait TryIntoModel where From 5b62e75cdc64e099776245bdc88560f23c78d3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 19:34:03 +0900 Subject: [PATCH 03/21] fix: fix tests to maintain API (even though probably should not..) --- src/entity/model.rs | 24 +++++++++++++++++++----- tests/derive_tests.rs | 6 ++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/entity/model.rs b/src/entity/model.rs index 801bbd14a..726f043bb 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -51,16 +51,30 @@ 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,no_compile + /// fn from_query_result(res: &QueryResult, pre: &str) -> Result { + /// (Self::from_query_result_nullable(res, pre)?) + /// } + /// + /// ``` + /// Internal note: Should be implemented as 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> { - match Self::from_query_result_nullable(res, pre) { - Ok(v) => Ok(Some(v)), - Err(TryGetError::Null(_)) => Ok(None), - Err(TryGetError::DbErr(err)) => Err(err), - } + 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] 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, +} From 8f9a13fbedeea3b4cbe4e3e6ce63e9c61ddf013c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 19:48:32 +0900 Subject: [PATCH 04/21] fix: do not compile informative code snippet --- src/entity/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entity/model.rs b/src/entity/model.rs index 726f043bb..fec9fc265 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -55,7 +55,7 @@ pub trait FromQueryResult: Sized { /// NOTE: Please also override `from_query_result_nullable` when manually implementing. /// The future default implementation will be along the lines of: /// - /// ```rust,no_compile + /// ```rust,ignore /// fn from_query_result(res: &QueryResult, pre: &str) -> Result { /// (Self::from_query_result_nullable(res, pre)?) /// } From 310f4f6365d53a46e86d3d4506751b29ba202ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 19:48:53 +0900 Subject: [PATCH 05/21] feat: allow nested attribute in DerivePartialModel --- sea-orm-macros/src/derives/partial_model.rs | 49 ++++++++++++--------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 0413fc86b..d3465d733 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -10,15 +10,16 @@ use syn::token::Comma; use syn::Expr; use syn::Meta; +use syn::Type; 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 +30,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 }, } struct DerivePartialModel { @@ -78,6 +81,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 +98,37 @@ impl DerivePartialModel { .get_as_kv("from_expr") .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) .transpose()?; + nested = meta.get_as_kv("nested").is_some(); } } } 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 }, + (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(format_ident!( + "{}", + field_name.to_string().to_upper_camel_case() + )) } - (Some(_), Some(_)) => return Err(Error::BothFromColAndFromExpr(field_span)), + (_, _, _) => return Err(Error::OverlappingAttributes(field_span)), }; column_as_list.push(col_as); } @@ -154,10 +160,13 @@ impl DerivePartialModel { 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 = 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 = sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name);) + }, + ColumnAs::Nested { typ } => { + quote!(let #select_ident = <#typ as PartialModelTrait>::select_cols(#select_ident);) }, }); @@ -181,10 +190,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! { + Err(Error::OverlappingAttributes(span)) => Ok(quote_spanned! { span => compile_error!("you can only use one of `from_col` or `from_expr`"); }), - 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! { From 8c73560a396d60d9eedd9e295dd34d8db018ec0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 19:58:08 +0900 Subject: [PATCH 06/21] feat: test cases and fixes for DerivePartialModel --- sea-orm-macros/src/derives/from_query_result.rs | 4 ++-- sea-orm-macros/src/derives/partial_model.rs | 6 ++++-- tests/partial_model_tests.rs | 6 ++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index aa71e046d..11c6e2d53 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -136,10 +136,10 @@ pub fn expand_derive_from_query_result( )) } -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 d3465d733..d4d62b321 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -12,6 +12,8 @@ use syn::Expr; use syn::Meta; use syn::Type; +use super::from_query_result::util::GetMeta; + use self::util::GetAsKVMeta; #[derive(Debug)] @@ -98,7 +100,7 @@ impl DerivePartialModel { .get_as_kv("from_expr") .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) .transpose()?; - nested = meta.get_as_kv("nested").is_some(); + nested = meta.exists("nested"); } } } @@ -166,7 +168,7 @@ impl DerivePartialModel { quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name);) }, ColumnAs::Nested { typ } => { - quote!(let #select_ident = <#typ as PartialModelTrait>::select_cols(#select_ident);) + quote!(let #select_ident = <#typ as sea_orm::PartialModelTrait>::select_cols(#select_ident);) }, }); diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index f59747ba6..6f8501c35 100644 --- a/tests/partial_model_tests.rs +++ b/tests/partial_model_tests.rs @@ -59,3 +59,9 @@ 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, +} From f7ebc9b9ce25011a1ba1bda62e666b0a21821262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 20:01:06 +0900 Subject: [PATCH 07/21] feat: enable DerivePartialModel for Option --- src/entity/partial_model.rs | 6 ++++++ tests/partial_model_tests.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/entity/partial_model.rs b/src/entity/partial_model.rs index 9da4f1d22..1ca3421d8 100644 --- a/src/entity/partial_model.rs +++ b/src/entity/partial_model.rs @@ -5,3 +5,9 @@ pub trait PartialModelTrait: FromQueryResult { /// Select specific columns this [PartialModel] needs fn select_cols(select: S) -> S; } + +impl PartialModelTrait for Option { + fn select_cols(select: S) -> S { + T::select_cols(select) + } +} diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index 6f8501c35..747bc8ed5 100644 --- a/tests/partial_model_tests.rs +++ b/tests/partial_model_tests.rs @@ -65,3 +65,9 @@ struct Nest { #[sea_orm(nested)] _foo: SimpleTest, } + +#[derive(FromQueryResult, DerivePartialModel)] +struct NestOption { + #[sea_orm(nested)] + _foo: Option, +} From be0c2435de67be6293433e8e3ab6c75a675d926a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 20:03:23 +0900 Subject: [PATCH 08/21] fix: fix typos --- sea-orm-macros/src/derives/partial_model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index d4d62b321..43a33e13a 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -193,7 +193,7 @@ pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result compile_error!("you can only derive `DerivePartialModel` on named struct"); }), Err(Error::OverlappingAttributes(span)) => Ok(quote_spanned! { - span => compile_error!("you can only use one of `from_col` or `from_expr`"); + span => compile_error!("you can only use one of `from_col`, `from_expr`, `nested`"); }), Err(Error::EntityNotSpecified) => Ok(quote_spanned! { ident_span => compile_error!("you need specific which entity you are using") From 7d9032312d37368814b28c242f9f455ac62b5618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Fri, 29 Mar 2024 20:09:08 +0900 Subject: [PATCH 09/21] feat: update documentation --- sea-orm-macros/src/lib.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) 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 From f0587e25074401c5b3f0dbc16609dda21b8579c7 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 11:11:47 +0900 Subject: [PATCH 10/21] fix: fix comment --- src/entity/model.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/entity/model.rs b/src/entity/model.rs index fec9fc265..e30ff4793 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -61,7 +61,6 @@ pub trait FromQueryResult: Sized { /// } /// /// ``` - /// Internal note: Should be implemented as fn from_query_result(res: &QueryResult, pre: &str) -> Result; /// Transform the error from instantiating a Model from a [QueryResult] From d838ae1dda14b4765d8d52c29093baaecba7682c Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 11:12:24 +0900 Subject: [PATCH 11/21] fix: remove unused import --- sea-orm-macros/src/derives/from_query_result.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index 11c6e2d53..1208734a7 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -3,7 +3,6 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ ext::IdentExt, punctuated::Punctuated, token::Comma, Data, DataStruct, Fields, Generics, Meta, - Type, }; enum ItemType { From 4c5ce6695e9a71a8f43c75a900dcf3ca8dfb3d91 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 14:50:09 +0900 Subject: [PATCH 12/21] feat: add test and some fixes regarding option --- sea-orm-macros/src/derives/partial_model.rs | 2 +- src/entity/model.rs | 20 ++++++-- tests/partial_model_tests.rs | 55 ++++++++++++++++++++- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 43a33e13a..33174b9ed 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -157,7 +157,7 @@ impl DerivePartialModel { 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);) + quote!(let #select_ident = sea_orm::SelectColumns::select_column(#select_ident, #col_value);) }, ColumnAs::ColAlias { col, field } => { let entity = entity.as_ref().unwrap(); diff --git a/src/entity/model.rs b/src/entity/model.rs index e30ff4793..e85990a34 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -59,7 +59,6 @@ pub trait FromQueryResult: Sized { /// 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; @@ -143,9 +142,22 @@ pub trait FromQueryResult: Sized { impl FromQueryResult for Option { fn from_query_result(res: &QueryResult, pre: &str) -> Result { - match T::from_query_result_optional(res, pre) { - Ok(v) => Ok(v), - Err(error) => Err(error), + 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), } } } diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index 747bc8ed5..f9dedd41c 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::*; @@ -71,3 +74,51 @@ struct NestOption { #[sea_orm(nested)] _foo: Option, } + +#[sea_orm_macros::test] +async fn partial_model_left_join_does_not_exist() { + use common::bakery_chain::*; + + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "bakery::Entity")] + struct Bakery { + id: i32, + name: String, + } + + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "cake::Entity")] + struct Cake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, + } + + let ctx = TestContext::new("find_one_with_result").await; + create_tables(&ctx.db).await.unwrap(); + + cake::Entity::insert(cake::ActiveModel { + name: Set("Test Cake".to_owned()), + price: Set(Decimal::ZERO), + bakery_id: Set(None), + gluten_free: Set(true), + serial: Set(Uuid::new_v4()), + ..Default::default() + }) + .exec(&ctx.db) + .await + .expect("insert succeeds"); + + let data: 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!(data.bakery.is_none()); + + ctx.delete().await; +} From d692dedbb90aae23a2ee1799888fd47f5df11db9 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 17:12:58 +0900 Subject: [PATCH 13/21] feat: properly differentiate same-name columns via explicit AS --- .../src/derives/from_query_result.rs | 3 +- sea-orm-macros/src/derives/partial_model.rs | 68 +++++++++++++++---- src/entity/partial_model.rs | 18 ++++- src/executor/select.rs | 2 +- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index 1208734a7..4fbf31fa3 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -40,8 +40,9 @@ impl<'a> ToTokens for TryFromQueryResultCheck<'a> { }); } ItemType::Nested => { + let name = ident.unraw().to_string(); tokens.extend(quote! { - let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, pre) { + let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, &format!("{pre}{}_", #name)) { Err(v @ sea_orm::TryGetError::DbErr(_)) => { return Err(v); } diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 33174b9ed..061ee76bc 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -4,6 +4,7 @@ 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; @@ -33,7 +34,7 @@ enum ColumnAs { /// from an expr Expr { expr: syn::Expr, field_name: String }, /// nesting another struct - Nested { typ: Type }, + Nested { typ: Type, field_name: String }, } struct DerivePartialModel { @@ -120,15 +121,15 @@ impl DerivePartialModel { expr, field_name: field_name.to_string(), }, - (None, None, true) => ColumnAs::Nested { typ: field.ty }, + (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::EntityNotSpecified); } - ColumnAs::Col(format_ident!( - "{}", - field_name.to_string().to_upper_camel_case() - )) + ColumnAs::Col(field_name) } (_, _, _) => return Err(Error::OverlappingAttributes(field_span)), }; @@ -156,26 +157,65 @@ 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); + eprintln!("{ident}"); + 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); + eprintln!("{ident}"); + 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 } => { - quote!(let #select_ident = <#typ as sea_orm::PartialModelTrait>::select_cols(#select_ident);) + 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 } @@ -269,7 +309,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/src/entity/partial_model.rs b/src/entity/partial_model.rs index 1ca3421d8..edb365ebb 100644 --- a/src/entity/partial_model.rs +++ b/src/entity/partial_model.rs @@ -3,11 +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 { - T::select_cols(select) + 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/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""# /// ); /// # } /// ``` From 3bd1d34672ecfc6ff6f643bd8289688a2df4ae76 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 17:15:37 +0900 Subject: [PATCH 14/21] fix: cargo fmt --- sea-orm-macros/src/derives/partial_model.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 061ee76bc..f6623db6f 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -163,7 +163,7 @@ impl DerivePartialModel { ); let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #uppercase_ident); let ident_stringified = ident.unraw().to_string(); - quote!(let #select_ident = + quote!(let #select_ident = if let Some(prefix) = pre { let ident = format!("{prefix}{}", #ident_stringified); eprintln!("{ident}"); @@ -176,7 +176,7 @@ impl DerivePartialModel { 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 = + 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) @@ -186,7 +186,7 @@ impl DerivePartialModel { ) }, ColumnAs::Expr { expr, field_name } => { - quote!(let #select_ident = + quote!(let #select_ident = if let Some(prefix) = pre { let ident = format!("{prefix}{}", #field_name); eprintln!("{ident}"); @@ -197,9 +197,9 @@ impl DerivePartialModel { ) }, 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 { + 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) From ea7329a90f9d79e7281fb6619ce982a9c7b51bb0 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 17:16:54 +0900 Subject: [PATCH 15/21] fix: more formatting --- sea-orm-macros/src/derives/partial_model.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index f6623db6f..e594a840f 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -199,11 +199,12 @@ impl DerivePartialModel { 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) - })); + Some(&if let Some(prefix) = pre { + format!("{prefix}{}_", #field_name) } + else { + format!("{}_", #field_name) + } + )); ) }, }); From 9ad93270131a84add25c1e447297e13b0dfc9046 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 17:35:48 +0900 Subject: [PATCH 16/21] feat: more tests --- tests/partial_model_tests.rs | 109 +++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index f9dedd41c..c1ff7c9e6 100644 --- a/tests/partial_model_tests.rs +++ b/tests/partial_model_tests.rs @@ -75,42 +75,64 @@ struct NestOption { _foo: Option, } -#[sea_orm_macros::test] -async fn partial_model_left_join_does_not_exist() { - use common::bakery_chain::*; +use common::bakery_chain::*; - #[derive(FromQueryResult, DerivePartialModel)] - #[sea_orm(entity = "bakery::Entity")] - struct Bakery { - id: i32, - name: String, - } +#[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 = "cake::Entity")] - struct Cake { - id: i32, - name: String, - #[sea_orm(nested)] - bakery: Option, - } +#[derive(FromQueryResult, DerivePartialModel)] +#[sea_orm(entity = "bakery::Entity")] +struct BakeryDetails { + #[sea_orm(nested)] + _basics: Bakery, + _profit_margin: f64, +} - let ctx = TestContext::new("find_one_with_result").await; - create_tables(&ctx.db).await.unwrap(); +#[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(None), + bakery_id: Set(if link { Some(42) } else { None }), gluten_free: Set(true), serial: Set(Uuid::new_v4()), - ..Default::default() }) .exec(&ctx.db) .await .expect("insert succeeds"); +} - let data: Cake = cake::Entity::find() +#[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) @@ -118,7 +140,48 @@ async fn partial_model_left_join_does_not_exist() { .expect("succeeds to get the result") .expect("exactly one model in DB"); - assert!(data.bakery.is_none()); + 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; } From 1dcab162c4ef5f4b1994b1cee654801a3345256e Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 17:49:16 +0900 Subject: [PATCH 17/21] fix: reorganize tests --- tests/partial_model_tests.rs | 186 ++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 90 deletions(-) diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index c1ff7c9e6..6e6f33d03 100644 --- a/tests/partial_model_tests.rs +++ b/tests/partial_model_tests.rs @@ -75,113 +75,119 @@ struct NestOption { _foo: Option, } -use common::bakery_chain::*; +#[allow(unused)] +mod runtime { + use super::*; -#[derive(FromQueryResult, DerivePartialModel)] -#[sea_orm(entity = "bakery::Entity")] -struct Bakery { - _id: i32, - #[sea_orm(from_col = "Name")] - _title: String, -} + use common::bakery_chain::*; -#[derive(FromQueryResult, DerivePartialModel)] -#[sea_orm(entity = "bakery::Entity")] -struct BakeryDetails { - #[sea_orm(nested)] - _basics: Bakery, - _profit_margin: f64, -} + #[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 = "cake::Entity")] -struct Cake { - _id: i32, - _name: String, - #[sea_orm(nested)] - _bakery: Option, -} + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "bakery::Entity")] + struct BakeryDetails { + #[sea_orm(nested)] + basics: Bakery, + #[sea_orm(from_expr = "bakery::Column::ProfitMargin")] + profit: f64, + } -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"); -} + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "cake::Entity")] + struct Cake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, + } -#[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(); + 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"); + } - fill_data(&ctx, false).await; + #[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(); - 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"); + fill_data(&ctx, false).await; - assert_eq!(cake._id, 13); - assert!(cake._bakery.is_none()); + 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"); - ctx.delete().await; -} + assert_eq!(cake.id, 13); + assert!(cake.bakery.is_none()); -#[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(); + ctx.delete().await; + } - fill_data(&ctx, true).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(); - 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"); + fill_data(&ctx, true).await; - assert_eq!(cake._id, 13); - assert!(matches!(cake._bakery, Some(Bakery { _id: 42, .. }))); + 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"); - ctx.delete().await; -} + 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(); + #[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; + 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"); + 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); + assert_eq!(bakery.basics.id, 42); - ctx.delete().await; + ctx.delete().await; + } } From 57a95349c9e0989bb0340b663a93e58e1c893424 Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 21:43:42 +0900 Subject: [PATCH 18/21] fix: use - instead of _ to separate identifiers to avoid mixups --- sea-orm-macros/src/derives/from_query_result.rs | 2 +- sea-orm-macros/src/derives/partial_model.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index 4fbf31fa3..2507dc496 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -42,7 +42,7 @@ impl<'a> ToTokens for TryFromQueryResultCheck<'a> { 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)) { + let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, &format!("{pre}{}-", #name)) { Err(v @ sea_orm::TryGetError::DbErr(_)) => { return Err(v); } diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index e594a840f..33f615bbf 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -166,7 +166,6 @@ impl DerivePartialModel { quote!(let #select_ident = if let Some(prefix) = pre { let ident = format!("{prefix}{}", #ident_stringified); - eprintln!("{ident}"); sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, ident) } else { sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, #ident_stringified) @@ -200,9 +199,9 @@ impl DerivePartialModel { quote!(let #select_ident = <#typ as sea_orm::PartialModelTrait>::select_cols_nested(#select_ident, Some(&if let Some(prefix) = pre { - format!("{prefix}{}_", #field_name) } + format!("{prefix}{}-", #field_name) } else { - format!("{}_", #field_name) + format!("{}-", #field_name) } )); ) From 673b8f1e6fecee193fcaf84b607666dbc628638f Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sat, 30 Mar 2024 21:48:03 +0900 Subject: [PATCH 19/21] fix: add comment explaining error handling in FromQueryResult derive --- sea-orm-macros/src/derives/from_query_result.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index 2507dc496..70ad20b09 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -16,6 +16,15 @@ struct FromQueryResultItem { pub ident: Ident, } +/// 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> { From e3c6ef3a4666b77c2338ac54afde3fa5941320ba Mon Sep 17 00:00:00 2001 From: Janosch Reppnow Date: Sun, 7 Apr 2024 11:38:46 +0900 Subject: [PATCH 20/21] feat: add aspirational test --- tests/partial_model_tests.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/partial_model_tests.rs b/tests/partial_model_tests.rs index 6e6f33d03..863037884 100644 --- a/tests/partial_model_tests.rs +++ b/tests/partial_model_tests.rs @@ -190,4 +190,39 @@ mod runtime { 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; + } } From 984827a6de82f965b41a1d7eb36852702eac8755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=A4=E3=83=BC=E3=83=8E=E3=82=B7=E3=83=A5?= Date: Thu, 11 Apr 2024 19:10:13 +0900 Subject: [PATCH 21/21] fix: remove eprintln --- sea-orm-macros/src/derives/partial_model.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 33f615bbf..9d939e568 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -188,7 +188,6 @@ impl DerivePartialModel { quote!(let #select_ident = if let Some(prefix) = pre { let ident = format!("{prefix}{}", #field_name); - eprintln!("{ident}"); sea_orm::SelectColumns::select_column_as(#select_ident, #expr, ident) } else { sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name)