@@ -25,6 +25,7 @@ fn absolute_module_path(crate_prefix: &str, to_module: &[String]) -> String {
2525/// Look up the module path for a table name from the module_paths map.
2626/// Uses `super::` for sibling modules in the same folder, `crate::` absolute paths for
2727/// cross-directory relations when mappings are available, and falls back to `super::{table_name}`.
28+ #[ cfg( test) ]
2829fn resolve_entity_module_path (
2930 current_table : & str ,
3031 target_table : & str ,
@@ -50,6 +51,44 @@ fn resolve_entity_module_path(
5051 format ! ( "super::{target_table}" )
5152}
5253
54+ /// Resolve relation field entity paths for SeaORM model macros.
55+ ///
56+ /// Rule:
57+ /// - same folder → `super::{table}`
58+ /// - different folder → absolute `crate::...` path
59+ ///
60+ /// This avoids generating brittle `super::super::...` paths for cross-folder relations.
61+ fn resolve_relation_entity_module_path (
62+ current_table : & str ,
63+ target_table : & str ,
64+ module_paths : & HashMap < String , Vec < String > > ,
65+ crate_prefix : & str ,
66+ ) -> String {
67+ if let ( Some ( current) , Some ( target) ) = (
68+ module_paths. get ( current_table) ,
69+ module_paths. get ( target_table) ,
70+ ) {
71+ let current_parent = current. split_last ( ) . map_or ( & [ ] [ ..] , |( _, parent) | parent) ;
72+ let target_parent = target. split_last ( ) . map_or ( & [ ] [ ..] , |( _, parent) | parent) ;
73+
74+ if current_parent == target_parent {
75+ return format ! ( "super::{target_table}" ) ;
76+ }
77+
78+ if !crate_prefix. is_empty ( ) {
79+ return absolute_module_path ( crate_prefix, target) ;
80+ }
81+
82+ return format ! ( "super::{target_table}" ) ;
83+ }
84+
85+ if !crate_prefix. is_empty ( ) {
86+ return format ! ( "{crate_prefix}::{target_table}" ) ;
87+ }
88+
89+ format ! ( "super::{target_table}" )
90+ }
91+
5392pub struct SeaOrmExporter ;
5493
5594/// SeaORM exporter with configuration support.
@@ -248,7 +287,7 @@ pub fn render_entity_with_config_and_paths(
248287
249288 lines. push ( "impl ActiveModelBehavior for ActiveModel {}" . into ( ) ) ;
250289
251- let self_ref_links = render_self_ref_link_helpers ( table, schema) ;
290+ let self_ref_links = render_self_ref_link_helpers ( table, schema, module_paths , crate_prefix ) ;
252291 if !self_ref_links. is_empty ( ) {
253292 lines. push ( String :: new ( ) ) ;
254293 lines. extend ( self_ref_links) ;
@@ -658,8 +697,12 @@ fn relation_field_defs_with_schema(
658697 } ;
659698
660699 out. push ( attr) ;
661- let entity_path =
662- resolve_entity_module_path ( & table. name , resolved_table, module_paths, crate_prefix) ;
700+ let entity_path = resolve_relation_entity_module_path (
701+ & table. name ,
702+ resolved_table,
703+ module_paths,
704+ crate_prefix,
705+ ) ;
663706 out. push ( format ! (
664707 " pub {field_name}: HasOne<{entity_path}::Entity>,"
665708 ) ) ;
@@ -791,7 +834,39 @@ fn self_ref_link_name(
791834 )
792835}
793836
794- fn render_self_ref_link_helpers ( table : & TableDef , schema : & [ TableDef ] ) -> Vec < String > {
837+ fn resolve_self_ref_link_module_path (
838+ current_table : & str ,
839+ junction_table : & str ,
840+ module_paths : & HashMap < String , Vec < String > > ,
841+ crate_prefix : & str ,
842+ ) -> String {
843+ if let ( Some ( current) , Some ( target) ) = (
844+ module_paths. get ( current_table) ,
845+ module_paths. get ( junction_table) ,
846+ ) {
847+ let current_parent = current. split_last ( ) . map_or ( & [ ] [ ..] , |( _, parent) | parent) ;
848+ let target_parent = target. split_last ( ) . map_or ( & [ ] [ ..] , |( _, parent) | parent) ;
849+
850+ if current_parent == target_parent {
851+ return format ! ( "super::{junction_table}" ) ;
852+ }
853+
854+ if !crate_prefix. is_empty ( ) {
855+ return absolute_module_path ( crate_prefix, target) ;
856+ }
857+
858+ return absolute_module_path ( "crate::models" , target) ;
859+ }
860+
861+ format ! ( "super::{junction_table}" )
862+ }
863+
864+ fn render_self_ref_link_helpers (
865+ table : & TableDef ,
866+ schema : & [ TableDef ] ,
867+ module_paths : & HashMap < String , Vec < String > > ,
868+ crate_prefix : & str ,
869+ ) -> Vec < String > {
795870 let mut out = Vec :: new ( ) ;
796871
797872 for other_table in schema {
@@ -805,6 +880,13 @@ fn render_self_ref_link_helpers(table: &TableDef, schema: &[TableDef]) -> Vec<St
805880 continue ;
806881 } ;
807882
883+ let junction_entity_path = resolve_self_ref_link_module_path (
884+ & table. name ,
885+ & self_ref_junction. junction_table ,
886+ module_paths,
887+ crate_prefix,
888+ ) ;
889+
808890 if self_ref_junction. role_columns . len ( ) < 2 {
809891 continue ;
810892 }
@@ -824,12 +906,12 @@ fn render_self_ref_link_helpers(table: &TableDef, schema: &[TableDef]) -> Vec<St
824906 out. push ( " fn link(&self) -> Vec<RelationDef> {" . into ( ) ) ;
825907 out. push ( " vec![" . into ( ) ) ;
826908 out. push ( format ! (
827- " super::{ }::Relation::{}.def().rev()," ,
828- self_ref_junction . junction_table , from_role
909+ " {junction_entity_path }::Relation::{}.def().rev()," ,
910+ from_role
829911 ) ) ;
830912 out. push ( format ! (
831- " super::{ }::Relation::{}.def()," ,
832- self_ref_junction . junction_table , to_role
913+ " {junction_entity_path }::Relation::{}.def()," ,
914+ to_role
833915 ) ) ;
834916 out. push ( " ]" . into ( ) ) ;
835917 out. push ( " }" . into ( ) ) ;
@@ -1252,8 +1334,12 @@ fn reverse_relation_field_defs(
12521334 } ;
12531335
12541336 out. push ( attr) ;
1255- let entity_path =
1256- resolve_entity_module_path ( & table. name , & rel. target_entity , module_paths, crate_prefix) ;
1337+ let entity_path = resolve_relation_entity_module_path (
1338+ & table. name ,
1339+ & rel. target_entity ,
1340+ module_paths,
1341+ crate_prefix,
1342+ ) ;
12571343 out. push ( format ! (
12581344 " pub {field_name}: {rust_type}<{entity_path}::Entity>,"
12591345 ) ) ;
@@ -1607,6 +1693,21 @@ fn to_snake_case(s: &str) -> String {
16071693#[ cfg( test) ]
16081694mod module_path_tests {
16091695 use super :: * ;
1696+ use vespertide_core:: { ColumnType , SimpleColumnType } ;
1697+
1698+ fn test_pk_column ( name : & str ) -> ColumnDef {
1699+ ColumnDef {
1700+ name : name. into ( ) ,
1701+ r#type : ColumnType :: Simple ( SimpleColumnType :: Text ) ,
1702+ nullable : false ,
1703+ default : None ,
1704+ comment : None ,
1705+ primary_key : Some ( vespertide_core:: schema:: primary_key:: PrimaryKeySyntax :: Bool ( true ) ) ,
1706+ unique : None ,
1707+ index : None ,
1708+ foreign_key : None ,
1709+ }
1710+ }
16101711
16111712 #[ test]
16121713 fn absolute_module_path_builds_correct_path ( ) {
@@ -1670,6 +1771,85 @@ mod module_path_tests {
16701771 let result = resolve_entity_module_path ( "user" , "admin" , & module_paths, "" ) ;
16711772 assert_eq ! ( result, "super::admin" ) ;
16721773 }
1774+
1775+ #[ test]
1776+ fn resolve_relation_entity_module_path_uses_crate_for_cross_directory_nested_models ( ) {
1777+ let mut module_paths = HashMap :: new ( ) ;
1778+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1779+ module_paths. insert (
1780+ "estimate" . into ( ) ,
1781+ vec ! [ "estimate" . into( ) , "estimate" . into( ) ] ,
1782+ ) ;
1783+
1784+ let result = resolve_relation_entity_module_path (
1785+ "admin" ,
1786+ "estimate" ,
1787+ & module_paths,
1788+ "crate::models" ,
1789+ ) ;
1790+ assert_eq ! ( result, "crate::models::estimate::estimate" ) ;
1791+ }
1792+
1793+ #[ test]
1794+ fn self_ref_link_helpers_use_crate_path_for_cross_directory_junctions ( ) {
1795+ let admin = TableDef {
1796+ name : "admin" . into ( ) ,
1797+ description : None ,
1798+ columns : vec ! [ test_pk_column( "username" ) ] ,
1799+ constraints : vec ! [ ] ,
1800+ } ;
1801+
1802+ let estimate_user_checker_setting = TableDef {
1803+ name : "estimate_user_checker_setting" . into ( ) ,
1804+ description : None ,
1805+ columns : vec ! [
1806+ test_pk_column( "username" ) ,
1807+ test_pk_column( "checker_username" ) ,
1808+ ] ,
1809+ constraints : vec ! [
1810+ TableConstraint :: ForeignKey {
1811+ name: None ,
1812+ columns: vec![ "username" . into( ) ] ,
1813+ ref_table: "admin" . into( ) ,
1814+ ref_columns: vec![ "username" . into( ) ] ,
1815+ on_delete: None ,
1816+ on_update: None ,
1817+ } ,
1818+ TableConstraint :: ForeignKey {
1819+ name: None ,
1820+ columns: vec![ "checker_username" . into( ) ] ,
1821+ ref_table: "admin" . into( ) ,
1822+ ref_columns: vec![ "username" . into( ) ] ,
1823+ on_delete: None ,
1824+ on_update: None ,
1825+ } ,
1826+ ] ,
1827+ } ;
1828+
1829+ let schema = vec ! [ admin. clone( ) , estimate_user_checker_setting] ;
1830+ let mut module_paths = HashMap :: new ( ) ;
1831+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1832+ module_paths. insert (
1833+ "estimate_user_checker_setting" . into ( ) ,
1834+ vec ! [ "estimate" . into( ) , "estimate_user_checker_setting" . into( ) ] ,
1835+ ) ;
1836+
1837+ let rendered = render_entity_with_config_and_paths (
1838+ & admin,
1839+ & schema,
1840+ & SeaOrmConfig :: default ( ) ,
1841+ "" ,
1842+ & module_paths,
1843+ "crate::models" ,
1844+ ) ;
1845+
1846+ assert ! ( rendered. contains(
1847+ "crate::models::estimate::estimate_user_checker_setting::Relation::Username.def().rev()"
1848+ ) ) ;
1849+ assert ! ( rendered. contains(
1850+ "crate::models::estimate::estimate_user_checker_setting::Relation::CheckerUsername.def()"
1851+ ) ) ;
1852+ }
16731853}
16741854
16751855#[ cfg( test) ]
0 commit comments