Skip to content

Commit 1044a33

Browse files
committed
Fix fk issue
1 parent 18f0179 commit 1044a33

13 files changed

Lines changed: 526 additions & 153 deletions

File tree

crates/vespera_macro/src/schema_macro/file_lookup.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,77 @@ pub fn find_struct_from_schema_path(path_str: &str) -> Option<StructMetadata> {
341341
None
342342
}
343343

344+
/// Find the FK column name from the target entity for a HasMany relation with via_rel.
345+
///
346+
/// When a HasMany relation has `via_rel = "TargetUser"`, this function:
347+
/// 1. Looks up the target entity file (e.g., notification.rs from schema path)
348+
/// 2. Finds the field with matching `relation_enum = "TargetUser"`
349+
/// 3. Extracts and returns the `from` attribute value (e.g., "target_user_id")
350+
///
351+
/// Returns None if the target file can't be found or parsed, or if no matching relation exists.
352+
pub fn find_fk_column_from_target_entity(
353+
target_schema_path: &str,
354+
via_rel: &str,
355+
) -> Option<String> {
356+
use crate::schema_macro::seaorm::{extract_belongs_to_from_field, extract_relation_enum};
357+
358+
// Get CARGO_MANIFEST_DIR to locate src folder
359+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
360+
let src_dir = Path::new(&manifest_dir).join("src");
361+
362+
// Parse the schema path to get file path
363+
// e.g., "crate :: models :: notification :: Schema" -> src/models/notification.rs
364+
let segments: Vec<&str> = target_schema_path
365+
.split("::")
366+
.map(|s| s.trim())
367+
.filter(|s| !s.is_empty() && *s != "Schema" && *s != "Entity")
368+
.collect();
369+
370+
let module_segments: Vec<&str> = segments
371+
.iter()
372+
.filter(|s| **s != "crate" && **s != "self" && **s != "super")
373+
.copied()
374+
.collect();
375+
376+
if module_segments.is_empty() {
377+
return None;
378+
}
379+
380+
// Try different file path patterns
381+
let file_paths = vec![
382+
src_dir.join(format!("{}.rs", module_segments.join("/"))),
383+
src_dir.join(format!("{}/mod.rs", module_segments.join("/"))),
384+
];
385+
386+
for file_path in file_paths {
387+
if !file_path.exists() {
388+
continue;
389+
}
390+
391+
let file_ast = try_read_and_parse_file(&file_path)?;
392+
393+
// Look for Model struct in the file
394+
for item in &file_ast.items {
395+
if let syn::Item::Struct(struct_item) = item
396+
&& struct_item.ident == "Model"
397+
{
398+
// Search through fields for the one with matching relation_enum
399+
if let syn::Fields::Named(fields_named) = &struct_item.fields {
400+
for field in &fields_named.named {
401+
let field_relation_enum = extract_relation_enum(&field.attrs);
402+
if field_relation_enum.as_deref() == Some(via_rel) {
403+
// Found the matching field, extract FK column from `from` attribute
404+
return extract_belongs_to_from_field(&field.attrs);
405+
}
406+
}
407+
}
408+
}
409+
}
410+
}
411+
412+
None
413+
}
414+
344415
/// Find the Model definition from a Schema path.
345416
/// Converts "crate::models::user::Schema" -> finds Model in src/models/user.rs
346417
pub fn find_model_from_schema_path(schema_path_str: &str) -> Option<StructMetadata> {

crates/vespera_macro/src/schema_macro/from_model.rs

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,25 @@ use super::{
1111
detect_circular_fields, generate_inline_struct_construction,
1212
generate_inline_type_construction, has_fk_relations, is_circular_relation_required,
1313
},
14-
file_lookup::find_struct_from_schema_path,
14+
file_lookup::{find_fk_column_from_target_entity, find_struct_from_schema_path},
1515
seaorm::RelationFieldInfo,
1616
};
1717
use crate::metadata::StructMetadata;
1818

19+
/// Convert snake_case to PascalCase for Column enum names.
20+
/// e.g., "target_user_id" -> "TargetUserId"
21+
fn snake_to_pascal_case(s: &str) -> String {
22+
s.split('_')
23+
.map(|part| {
24+
let mut chars = part.chars();
25+
match chars.next() {
26+
None => String::new(),
27+
Some(first) => first.to_uppercase().chain(chars).collect(),
28+
}
29+
})
30+
.collect()
31+
}
32+
1933
/// Build Entity path from Schema path.
2034
/// e.g., `crate::models::user::Schema` -> `crate::models::user::Entity`
2135
pub fn build_entity_path_from_schema_path(
@@ -89,15 +103,143 @@ pub fn generate_from_model_with_relations(
89103

90104
match rel.relation_type.as_str() {
91105
"HasOne" | "BelongsTo" => {
92-
// Load single related entity
93-
quote! {
94-
let #field_name = model.find_related(#entity_path).one(db).await?;
106+
// When relation_enum is specified, use the specific Relation variant
107+
// This handles cases where multiple relations point to the same Entity type
108+
if let Some(ref relation_enum_name) = rel.relation_enum {
109+
let relation_variant =
110+
syn::Ident::new(relation_enum_name, proc_macro2::Span::call_site());
111+
112+
if rel.is_optional {
113+
// Optional FK: load only if FK value exists
114+
if let Some(ref fk_col) = rel.fk_column {
115+
let fk_ident =
116+
syn::Ident::new(fk_col, proc_macro2::Span::call_site());
117+
quote! {
118+
let #field_name = match &model.#fk_ident {
119+
Some(fk_value) => #entity_path::find_by_id(fk_value.clone()).one(db).await?,
120+
None => None,
121+
};
122+
}
123+
} else {
124+
// Fallback: use find_related with Relation enum
125+
quote! {
126+
let #field_name = Entity::find_related(Relation::#relation_variant)
127+
.filter(<Entity as sea_orm::EntityTrait>::PrimaryKey::eq(&model))
128+
.one(db)
129+
.await?;
130+
}
131+
}
132+
} else {
133+
// Required FK: directly query by FK value
134+
if let Some(ref fk_col) = rel.fk_column {
135+
let fk_ident =
136+
syn::Ident::new(fk_col, proc_macro2::Span::call_site());
137+
quote! {
138+
let #field_name = #entity_path::find_by_id(model.#fk_ident.clone()).one(db).await?;
139+
}
140+
} else {
141+
// Fallback: use find_related with Relation enum
142+
quote! {
143+
let #field_name = Entity::find_related(Relation::#relation_variant)
144+
.filter(<Entity as sea_orm::EntityTrait>::PrimaryKey::eq(&model))
145+
.one(db)
146+
.await?;
147+
}
148+
}
149+
}
150+
} else {
151+
// Standard case: single relation to target entity, use find_related
152+
quote! {
153+
let #field_name = model.find_related(#entity_path).one(db).await?;
154+
}
95155
}
96156
}
97157
"HasMany" => {
98-
// Load multiple related entities
99-
quote! {
100-
let #field_name = model.find_related(#entity_path).all(db).await?;
158+
// HasMany with relation_enum: use FK-based query on target entity
159+
// HasMany without relation_enum: use standard find_related
160+
if let Some(ref via_rel_value) = rel.via_rel {
161+
// Look up the FK column from the target entity
162+
let schema_path_str = rel.schema_path.to_string().replace(' ', "");
163+
if let Some(fk_col_name) =
164+
find_fk_column_from_target_entity(&schema_path_str, via_rel_value)
165+
{
166+
// Convert snake_case FK column to PascalCase for Column enum
167+
let fk_col_pascal = snake_to_pascal_case(&fk_col_name);
168+
let fk_col_ident =
169+
syn::Ident::new(&fk_col_pascal, proc_macro2::Span::call_site());
170+
171+
// Build the Column path: entity_path without ::Entity, then ::Column::FkCol
172+
// e.g., crate::models::notification::Entity -> crate::models::notification::Column::TargetUserId
173+
let entity_path_str = entity_path.to_string().replace(' ', "");
174+
let column_path_str =
175+
entity_path_str.replace(":: Entity", ":: Column");
176+
let column_path_idents: Vec<syn::Ident> = column_path_str
177+
.split("::")
178+
.map(|s| s.trim())
179+
.filter(|s| !s.is_empty())
180+
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
181+
.collect();
182+
183+
quote! {
184+
let #field_name = #(#column_path_idents)::*::#fk_col_ident
185+
.into_column()
186+
.eq(model.id.clone())
187+
.into_condition();
188+
let #field_name = #entity_path::find()
189+
.filter(#field_name)
190+
.all(db)
191+
.await?;
192+
}
193+
} else {
194+
// FK column not found - fall back to empty vec with warning comment
195+
quote! {
196+
// WARNING: Could not find FK column for relation_enum, using empty vec
197+
let #field_name: Vec<_> = vec![];
198+
}
199+
}
200+
} else if rel.relation_enum.is_some() {
201+
// Has relation_enum but no via_rel - try using relation_enum as via_rel
202+
let via_rel_value = rel.relation_enum.as_ref().unwrap();
203+
let schema_path_str = rel.schema_path.to_string().replace(' ', "");
204+
if let Some(fk_col_name) =
205+
find_fk_column_from_target_entity(&schema_path_str, via_rel_value)
206+
{
207+
let fk_col_pascal = snake_to_pascal_case(&fk_col_name);
208+
let fk_col_ident =
209+
syn::Ident::new(&fk_col_pascal, proc_macro2::Span::call_site());
210+
211+
let entity_path_str = entity_path.to_string().replace(' ', "");
212+
let column_path_str =
213+
entity_path_str.replace(":: Entity", ":: Column");
214+
let column_path_idents: Vec<syn::Ident> = column_path_str
215+
.split("::")
216+
.map(|s| s.trim())
217+
.filter(|s| !s.is_empty())
218+
.map(|s| syn::Ident::new(s, proc_macro2::Span::call_site()))
219+
.collect();
220+
221+
quote! {
222+
let #field_name = #(#column_path_idents)::*::#fk_col_ident
223+
.into_column()
224+
.eq(model.id.clone())
225+
.into_condition();
226+
let #field_name = #entity_path::find()
227+
.filter(#field_name)
228+
.all(db)
229+
.await?;
230+
}
231+
} else {
232+
// FK column not found - fall back to empty vec
233+
quote! {
234+
// WARNING: Could not find FK column for relation_enum, using empty vec
235+
let #field_name: Vec<_> = vec![];
236+
}
237+
}
238+
} else {
239+
// Standard HasMany - use find_related
240+
quote! {
241+
let #field_name = model.find_related(#entity_path).all(db).await?;
242+
}
101243
}
102244
}
103245
_ => quote! {},
@@ -432,6 +574,9 @@ mod tests {
432574
schema_path,
433575
is_optional,
434576
inline_type_info: None,
577+
relation_enum: None,
578+
fk_column: None,
579+
via_rel: None,
435580
}
436581
}
437582

0 commit comments

Comments
 (0)