Skip to content

Commit 2f9d3d9

Browse files
committed
fix: handle nested Option<Model> in FromQueryResult missing struct for test
1 parent 5b37411 commit 2f9d3d9

File tree

3 files changed

+99
-10
lines changed

3 files changed

+99
-10
lines changed

sea-orm-macros/src/derives/from_query_result.rs

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub(super) struct FromQueryResultItem {
2727
pub typ: ItemType,
2828
pub ident: Ident,
2929
pub alias: Option<String>,
30+
pub field_type: syn::Type,
31+
pub prefix: bool,
3032
}
3133

3234
/// Initially, we try to obtain the value for each field and check if it is an ordinary DB error
@@ -42,15 +44,27 @@ struct TryFromQueryResultCheck<'a>(bool, &'a FromQueryResultItem);
4244

4345
impl ToTokens for TryFromQueryResultCheck<'_> {
4446
fn to_tokens(&self, tokens: &mut TokenStream) {
45-
let FromQueryResultItem { ident, typ, alias } = self.1;
47+
let FromQueryResultItem {
48+
ident,
49+
typ,
50+
alias,
51+
field_type,
52+
..
53+
} = self.1;
4654

4755
match typ {
4856
ItemType::Flat => {
4957
let name = alias
5058
.to_owned()
5159
.unwrap_or_else(|| ident.unraw().to_string());
60+
let prefix_to_use = if alias.is_some() {
61+
// If there's an explicit alias, use it as-is without prefix
62+
quote! { "" }
63+
} else {
64+
quote! { pre }
65+
};
5266
tokens.extend(quote! {
53-
let #ident = match row.try_get_nullable(pre, #name) {
67+
let #ident = match row.try_get_nullable(#prefix_to_use, #name) {
5468
Err(v @ sea_orm::TryGetError::DbErr(_)) => {
5569
return Err(v);
5670
}
@@ -64,14 +78,35 @@ impl ToTokens for TryFromQueryResultCheck<'_> {
6478
});
6579
}
6680
ItemType::Nested => {
67-
let prefix = if self.0 {
81+
let prefix_str = if self.1.prefix {
82+
// Use field name as prefix if #[sea_orm(prefix)] is specified
6883
let name = ident.unraw().to_string();
6984
quote! { &format!("{pre}{}_", #name) }
85+
} else if self.0 {
86+
// For PartialModel (when self.0 is true), check if field is Option<T>
87+
// and add automatic prefix for optional nested fields
88+
let is_option = if let syn::Type::Path(type_path) = field_type {
89+
type_path
90+
.path
91+
.segments
92+
.last()
93+
.map(|seg| seg.ident == "Option")
94+
.unwrap_or(false)
95+
} else {
96+
false
97+
};
98+
99+
if is_option {
100+
let name = ident.unraw().to_string();
101+
quote! { &format!("{pre}{}_", #name) }
102+
} else {
103+
quote! { pre }
104+
}
70105
} else {
71106
quote! { pre }
72107
};
73108
tokens.extend(quote! {
74-
let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, #prefix) {
109+
let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, #prefix_str) {
75110
Err(v @ sea_orm::TryGetError::DbErr(_)) => {
76111
return Err(v);
77112
}
@@ -87,14 +122,45 @@ struct TryFromQueryResultAssignment<'a>(&'a FromQueryResultItem);
87122

88123
impl ToTokens for TryFromQueryResultAssignment<'_> {
89124
fn to_tokens(&self, tokens: &mut TokenStream) {
90-
let FromQueryResultItem { ident, typ, .. } = self.0;
125+
let FromQueryResultItem {
126+
ident,
127+
typ,
128+
field_type,
129+
..
130+
} = self.0;
91131

92132
match typ {
93-
ItemType::Flat | ItemType::Nested => {
133+
ItemType::Flat => {
94134
tokens.extend(quote! {
95135
#ident: #ident?,
96136
});
97137
}
138+
ItemType::Nested => {
139+
let is_option = if let syn::Type::Path(type_path) = field_type {
140+
type_path
141+
.path
142+
.segments
143+
.last()
144+
.map(|seg| seg.ident == "Option")
145+
.unwrap_or(false)
146+
} else {
147+
false
148+
};
149+
150+
if is_option {
151+
tokens.extend(quote! {
152+
#ident: match #ident {
153+
Ok(v) => Some(v),
154+
Err(sea_orm::TryGetError::Null(_)) => None,
155+
Err(e) => return Err(e),
156+
},
157+
});
158+
} else {
159+
tokens.extend(quote! {
160+
#ident: #ident?,
161+
});
162+
}
163+
}
98164
ItemType::Skip => {
99165
tokens.extend(quote! {
100166
#ident,
@@ -125,6 +191,7 @@ impl DeriveFromQueryResult {
125191
for parsed_field in parsed_fields {
126192
let mut typ = ItemType::Flat;
127193
let mut alias = None;
194+
let mut prefix = false;
128195
for attr in parsed_field.attrs.iter() {
129196
if !attr.path().is_ident("sea_orm") {
130197
continue;
@@ -136,6 +203,8 @@ impl DeriveFromQueryResult {
136203
typ = ItemType::Skip;
137204
} else if meta.exists("nested") {
138205
typ = ItemType::Nested;
206+
} else if meta.exists("prefix") {
207+
prefix = true;
139208
} else if let Some(alias_) = meta.get_as_kv("from_alias") {
140209
alias = Some(alias_);
141210
} else {
@@ -145,7 +214,14 @@ impl DeriveFromQueryResult {
145214
}
146215
}
147216
let ident = format_ident!("{}", parsed_field.ident.unwrap().to_string());
148-
fields.push(FromQueryResultItem { typ, ident, alias });
217+
let field_type = parsed_field.ty;
218+
fields.push(FromQueryResultItem {
219+
typ,
220+
ident,
221+
alias,
222+
field_type,
223+
prefix,
224+
});
149225
}
150226

151227
Ok(Self {

sea-orm-macros/src/derives/partial_model.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ impl DerivePartialModel {
213213
}
214214
.to_owned(),
215215
alias: None,
216+
field_type: match col_as {
217+
ColumnAs::Nested { typ, .. } => typ.clone(),
218+
_ => syn::parse_quote!(()),
219+
},
220+
prefix: false,
216221
})
217222
.collect(),
218223
}

tests/from_query_result_tests.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ struct BakeryFlat {
4949
profit: f64,
5050
}
5151

52+
#[derive(FromQueryResult)]
53+
struct CakeWithOptionalBakeryModel {
54+
id: i32,
55+
name: String,
56+
#[sea_orm(nested, prefix)]
57+
bakery: Option<bakery::Model>,
58+
}
59+
5260
#[sea_orm_macros::test]
5361
async fn from_query_result_left_join_does_not_exist() {
5462
let ctx = TestContext::new("from_query_result_left_join_does_not_exist").await;
@@ -89,9 +97,9 @@ async fn from_query_result_left_join_with_optional_model_does_not_exist() {
8997
.select_only()
9098
.column(cake::Column::Id)
9199
.column(cake::Column::Name)
92-
.column(bakery::Column::Id)
93-
.column(bakery::Column::Name)
94-
.column(bakery::Column::ProfitMargin)
100+
.column_as(bakery::Column::Id, "bakery_id")
101+
.column_as(bakery::Column::Name, "bakery_name")
102+
.column_as(bakery::Column::ProfitMargin, "bakery_profit_margin")
95103
.left_join(bakery::Entity)
96104
.order_by_asc(cake::Column::Id)
97105
.into_model()

0 commit comments

Comments
 (0)