@@ -887,10 +887,6 @@ fn render_self_ref_link_helpers(
887887 crate_prefix,
888888 ) ;
889889
890- if self_ref_junction. role_columns . len ( ) < 2 {
891- continue ;
892- }
893-
894890 for ( from_idx, from_role) in self_ref_junction. role_relations . iter ( ) . enumerate ( ) {
895891 for ( to_idx, to_role) in self_ref_junction. role_relations . iter ( ) . enumerate ( ) {
896892 if from_idx == to_idx {
@@ -943,10 +939,6 @@ fn render_self_ref_query_helpers(table: &TableDef, schema: &[TableDef]) -> Vec<S
943939 continue ;
944940 } ;
945941
946- if self_ref_junction. role_columns . len ( ) < 2 {
947- continue ;
948- }
949-
950942 for ( from_idx, from_col) in self_ref_junction. role_columns . iter ( ) . enumerate ( ) {
951943 for ( to_idx, to_col) in self_ref_junction. role_columns . iter ( ) . enumerate ( ) {
952944 if from_idx == to_idx {
@@ -1790,6 +1782,77 @@ mod module_path_tests {
17901782 assert_eq ! ( result, "crate::models::estimate::estimate" ) ;
17911783 }
17921784
1785+ #[ test]
1786+ fn resolve_relation_entity_module_path_uses_super_for_same_directory ( ) {
1787+ let mut module_paths = HashMap :: new ( ) ;
1788+ module_paths. insert ( "admin" . into ( ) , vec ! [ "shared" . into( ) , "admin" . into( ) ] ) ;
1789+ module_paths. insert (
1790+ "admin_stamp" . into ( ) ,
1791+ vec ! [ "shared" . into( ) , "admin_stamp" . into( ) ] ,
1792+ ) ;
1793+ let result = resolve_relation_entity_module_path (
1794+ "admin" ,
1795+ "admin_stamp" ,
1796+ & module_paths,
1797+ "crate::models" ,
1798+ ) ;
1799+ assert_eq ! ( result, "super::admin_stamp" ) ;
1800+ }
1801+
1802+ #[ test]
1803+ fn resolve_relation_entity_module_path_fallback_super_when_empty_prefix_cross_directory ( ) {
1804+ let mut module_paths = HashMap :: new ( ) ;
1805+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1806+ module_paths. insert (
1807+ "estimate" . into ( ) ,
1808+ vec ! [ "estimate" . into( ) , "estimate" . into( ) ] ,
1809+ ) ;
1810+ let result = resolve_relation_entity_module_path ( "admin" , "estimate" , & module_paths, "" ) ;
1811+ assert_eq ! ( result, "super::estimate" ) ;
1812+ }
1813+
1814+ #[ test]
1815+ fn resolve_relation_entity_module_path_uses_crate_prefix_when_not_in_module_paths ( ) {
1816+ let module_paths = HashMap :: new ( ) ;
1817+ let result = resolve_relation_entity_module_path (
1818+ "admin" ,
1819+ "estimate" ,
1820+ & module_paths,
1821+ "crate::models" ,
1822+ ) ;
1823+ assert_eq ! ( result, "crate::models::estimate" ) ;
1824+ }
1825+
1826+ #[ test]
1827+ fn resolve_self_ref_link_module_path_uses_super_for_same_directory ( ) {
1828+ let mut module_paths = HashMap :: new ( ) ;
1829+ module_paths. insert ( "admin" . into ( ) , vec ! [ "shared" . into( ) , "admin" . into( ) ] ) ;
1830+ module_paths. insert (
1831+ "admin_friendship" . into ( ) ,
1832+ vec ! [ "shared" . into( ) , "admin_friendship" . into( ) ] ,
1833+ ) ;
1834+ let result = resolve_self_ref_link_module_path (
1835+ "admin" ,
1836+ "admin_friendship" ,
1837+ & module_paths,
1838+ "crate::models" ,
1839+ ) ;
1840+ assert_eq ! ( result, "super::admin_friendship" ) ;
1841+ }
1842+
1843+ #[ test]
1844+ fn resolve_self_ref_link_module_path_absolute_fallback_when_empty_prefix ( ) {
1845+ let mut module_paths = HashMap :: new ( ) ;
1846+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1847+ module_paths. insert (
1848+ "admin_friendship" . into ( ) ,
1849+ vec ! [ "social" . into( ) , "admin_friendship" . into( ) ] ,
1850+ ) ;
1851+ let result =
1852+ resolve_self_ref_link_module_path ( "admin" , "admin_friendship" , & module_paths, "" ) ;
1853+ assert_eq ! ( result, "crate::models::social::admin_friendship" ) ;
1854+ }
1855+
17931856 #[ test]
17941857 fn self_ref_link_helpers_use_crate_path_for_cross_directory_junctions ( ) {
17951858 let admin = TableDef {
@@ -1974,6 +2037,40 @@ mod helper_tests {
19742037 assert_eq ! ( unique_name( "other" , & mut used) , "other_1" ) ;
19752038 }
19762039
2040+ #[ test]
2041+ fn test_unique_relation_enum_name_preferred_available ( ) {
2042+ let used = HashSet :: new ( ) ;
2043+ let result = unique_relation_enum_name ( "User" . into ( ) , "post" , "User" , & used) ;
2044+ assert_eq ! ( result, "User" ) ;
2045+ }
2046+
2047+ #[ test]
2048+ fn test_unique_relation_enum_name_source_prefixed ( ) {
2049+ let mut used = HashSet :: new ( ) ;
2050+ used. insert ( "User" . into ( ) ) ;
2051+ let result = unique_relation_enum_name ( "User" . into ( ) , "post" , "User" , & used) ;
2052+ assert_eq ! ( result, "PostUser" ) ;
2053+ }
2054+
2055+ #[ test]
2056+ fn test_unique_relation_enum_name_numbered_fallback ( ) {
2057+ let mut used = HashSet :: new ( ) ;
2058+ used. insert ( "User" . into ( ) ) ;
2059+ used. insert ( "PostUser" . into ( ) ) ;
2060+ let result = unique_relation_enum_name ( "User" . into ( ) , "post" , "User" , & used) ;
2061+ assert_eq ! ( result, "PostUser2" ) ;
2062+ }
2063+
2064+ #[ test]
2065+ fn test_unique_relation_enum_name_numbered_fallback_skips_taken ( ) {
2066+ let mut used = HashSet :: new ( ) ;
2067+ used. insert ( "User" . into ( ) ) ;
2068+ used. insert ( "PostUser" . into ( ) ) ;
2069+ used. insert ( "PostUser2" . into ( ) ) ;
2070+ let result = unique_relation_enum_name ( "User" . into ( ) , "post" , "User" , & used) ;
2071+ assert_eq ! ( result, "PostUser3" ) ;
2072+ }
2073+
19772074 #[ rstest]
19782075 #[ case( vec![ "creator_user_id" . into( ) ] , "CreatorUser" ) ]
19792076 #[ case( vec![ "used_by_user_id" . into( ) ] , "UsedByUser" ) ]
@@ -4124,6 +4221,142 @@ mod tests {
41244221 assert ! ( result. contains( "#[sea_orm(table_name = \" users\" )]" ) ) ;
41254222 }
41264223
4224+ #[ test]
4225+ fn test_junction_relation_enum_without_via_when_entity_appears_multiple_times ( ) {
4226+ use vespertide_core:: schema:: primary_key:: PrimaryKeySyntax ;
4227+
4228+ // user has a forward FK to user_tag (composite FK), making user_tag appear
4229+ // in both forward and reverse targets => entity_count > 1 for user_tag.
4230+ // The junction table entry from collect_many_to_many_relations has via=None, via_rel=None,
4231+ // so when needs_relation_enum is true, it hits the branch with only relation_enum (no via/via_rel).
4232+ let user = TableDef {
4233+ name : "user" . into ( ) ,
4234+ description : None ,
4235+ columns : vec ! [
4236+ ColumnDef {
4237+ name: "id" . into( ) ,
4238+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
4239+ nullable: false ,
4240+ default : None ,
4241+ comment: None ,
4242+ primary_key: Some ( PrimaryKeySyntax :: Bool ( true ) ) ,
4243+ unique: None ,
4244+ index: None ,
4245+ foreign_key: None ,
4246+ } ,
4247+ ColumnDef {
4248+ name: "pinned_user_id" . into( ) ,
4249+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
4250+ nullable: true ,
4251+ default : None ,
4252+ comment: None ,
4253+ primary_key: None ,
4254+ unique: None ,
4255+ index: None ,
4256+ foreign_key: None ,
4257+ } ,
4258+ ColumnDef {
4259+ name: "pinned_tag_id" . into( ) ,
4260+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
4261+ nullable: true ,
4262+ default : None ,
4263+ comment: None ,
4264+ primary_key: None ,
4265+ unique: None ,
4266+ index: None ,
4267+ foreign_key: None ,
4268+ } ,
4269+ ] ,
4270+ constraints : vec ! [ TableConstraint :: ForeignKey {
4271+ name: None ,
4272+ columns: vec![ "pinned_user_id" . into( ) , "pinned_tag_id" . into( ) ] ,
4273+ ref_table: "user_tag" . into( ) ,
4274+ ref_columns: vec![ "user_id" . into( ) , "tag_id" . into( ) ] ,
4275+ on_delete: None ,
4276+ on_update: None ,
4277+ } ] ,
4278+ } ;
4279+
4280+ let user_tag = TableDef {
4281+ name : "user_tag" . into ( ) ,
4282+ description : None ,
4283+ columns : vec ! [
4284+ ColumnDef {
4285+ name: "user_id" . into( ) ,
4286+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
4287+ nullable: false ,
4288+ default : None ,
4289+ comment: None ,
4290+ primary_key: Some ( PrimaryKeySyntax :: Bool ( true ) ) ,
4291+ unique: None ,
4292+ index: None ,
4293+ foreign_key: None ,
4294+ } ,
4295+ ColumnDef {
4296+ name: "tag_id" . into( ) ,
4297+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
4298+ nullable: false ,
4299+ default : None ,
4300+ comment: None ,
4301+ primary_key: Some ( PrimaryKeySyntax :: Bool ( true ) ) ,
4302+ unique: None ,
4303+ index: None ,
4304+ foreign_key: None ,
4305+ } ,
4306+ ] ,
4307+ constraints : vec ! [
4308+ TableConstraint :: ForeignKey {
4309+ name: None ,
4310+ columns: vec![ "user_id" . into( ) ] ,
4311+ ref_table: "user" . into( ) ,
4312+ ref_columns: vec![ "id" . into( ) ] ,
4313+ on_delete: None ,
4314+ on_update: None ,
4315+ } ,
4316+ TableConstraint :: ForeignKey {
4317+ name: None ,
4318+ columns: vec![ "tag_id" . into( ) ] ,
4319+ ref_table: "tag" . into( ) ,
4320+ ref_columns: vec![ "id" . into( ) ] ,
4321+ on_delete: None ,
4322+ on_update: None ,
4323+ } ,
4324+ ] ,
4325+ } ;
4326+
4327+ let tag = TableDef {
4328+ name : "tag" . into ( ) ,
4329+ description : None ,
4330+ columns : vec ! [ ColumnDef {
4331+ name: "id" . into( ) ,
4332+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
4333+ nullable: false ,
4334+ default : None ,
4335+ comment: None ,
4336+ primary_key: Some ( PrimaryKeySyntax :: Bool ( true ) ) ,
4337+ unique: None ,
4338+ index: None ,
4339+ foreign_key: None ,
4340+ } ] ,
4341+ constraints : vec ! [ ] ,
4342+ } ;
4343+
4344+ let schema = vec ! [ user. clone( ) , user_tag, tag] ;
4345+ let rendered = render_entity_with_schema ( & user, & schema) ;
4346+
4347+ // The junction table "user_tag" appears in both forward (composite FK) and reverse (M2M junction),
4348+ // so it gets relation_enum without via/via_rel
4349+ assert ! ( rendered. contains( "relation_enum" ) ) ;
4350+ // Verify we have a has_many to user_tag with relation_enum but no via
4351+ let has_user_tag_relation_enum_without_via = rendered. lines ( ) . any ( |line| {
4352+ line. contains ( "has_many" ) && line. contains ( "relation_enum" ) && !line. contains ( "via" )
4353+ } ) ;
4354+ assert ! (
4355+ has_user_tag_relation_enum_without_via,
4356+ "Expected has_many with relation_enum but no via for junction table entity, got:\n {rendered}"
4357+ ) ;
4358+ }
4359+
41274360 #[ test]
41284361 fn test_json_default_value_escapes_double_quotes ( ) {
41294362 let table = TableDef {
0 commit comments