@@ -3,8 +3,8 @@ use std::collections::{HashMap, HashSet};
33use crate :: orm:: OrmExporter ;
44use vespertide_config:: SeaOrmConfig ;
55use vespertide_core:: {
6- ColumnDef , ColumnType , ComplexColumnType , EnumValues , NumValue , StringOrBool , TableConstraint ,
7- TableDef ,
6+ ColumnDef , ColumnType , ComplexColumnType , EnumValues , NumValue , SimpleColumnType , StringOrBool ,
7+ TableConstraint , TableDef ,
88} ;
99
1010/// Build an absolute `crate::` module path for the target table.
@@ -23,18 +23,30 @@ fn absolute_module_path(crate_prefix: &str, to_module: &[String]) -> String {
2323}
2424
2525/// Look up the module path for a table name from the module_paths map.
26- /// Uses `crate ::` absolute paths when crate_prefix and module_paths are available.
27- /// Falls back to `super::{table_name}` when no mapping exists .
26+ /// Uses `super ::` for sibling modules in the same folder, `crate::` absolute paths for
27+ /// cross-directory relations when mappings are available, and falls back to `super::{table_name}`.
2828fn resolve_entity_module_path (
29+ current_table : & str ,
2930 target_table : & str ,
3031 module_paths : & HashMap < String , Vec < String > > ,
3132 crate_prefix : & str ,
3233) -> String {
33- if !crate_prefix. is_empty ( )
34- && let Some ( to) = module_paths. get ( target_table)
35- {
36- return absolute_module_path ( crate_prefix, to) ;
34+ if let ( Some ( current) , Some ( target) ) = (
35+ module_paths. get ( current_table) ,
36+ module_paths. get ( target_table) ,
37+ ) {
38+ let current_parent = current. split_last ( ) . map_or ( & [ ] [ ..] , |( _, parent) | parent) ;
39+ let target_parent = target. split_last ( ) . map_or ( & [ ] [ ..] , |( _, parent) | parent) ;
40+
41+ if current_parent == target_parent {
42+ return format ! ( "super::{target_table}" ) ;
43+ }
44+
45+ if !crate_prefix. is_empty ( ) {
46+ return absolute_module_path ( crate_prefix, target) ;
47+ }
3748 }
49+
3850 format ! ( "super::{target_table}" )
3951}
4052
@@ -176,8 +188,13 @@ pub fn render_entity_with_config_and_paths(
176188 }
177189 }
178190
179- // Build model derive line with optional extra derives
180- let mut model_derives = vec ! [ "Clone" , "Debug" , "PartialEq" , "Eq" , "DeriveEntityModel" ] ;
191+ // Build model derive line with optional extra derives.
192+ // Float-backed fields (f32/f64) cannot implement Eq, so omit it when present.
193+ let mut model_derives = vec ! [ "Clone" , "Debug" , "PartialEq" ] ;
194+ if table. columns . iter ( ) . all ( column_supports_eq) {
195+ model_derives. push ( "Eq" ) ;
196+ }
197+ model_derives. push ( "DeriveEntityModel" ) ;
181198 let extra_model_derives: Vec < & str > = config
182199 . extra_model_derives ( )
183200 . iter ( )
@@ -416,7 +433,6 @@ fn format_default_value(value: &StringOrBool, column_type: &ColumnType) -> Strin
416433
417434/// Check if the simple column type is numeric.
418435fn is_numeric_simple_type ( simple : & vespertide_core:: SimpleColumnType ) -> bool {
419- use vespertide_core:: SimpleColumnType ;
420436 matches ! (
421437 simple,
422438 SimpleColumnType :: SmallInt
@@ -427,6 +443,17 @@ fn is_numeric_simple_type(simple: &vespertide_core::SimpleColumnType) -> bool {
427443 )
428444}
429445
446+ fn column_supports_eq ( column : & ColumnDef ) -> bool {
447+ column_type_supports_eq ( & column. r#type )
448+ }
449+
450+ fn column_type_supports_eq ( column_type : & ColumnType ) -> bool {
451+ match column_type {
452+ ColumnType :: Simple ( SimpleColumnType :: Real | SimpleColumnType :: DoublePrecision ) => false ,
453+ ColumnType :: Simple ( _) | ColumnType :: Complex ( _) => true ,
454+ }
455+ }
456+
430457fn primary_key_columns ( table : & TableDef ) -> HashSet < String > {
431458 use vespertide_core:: schema:: primary_key:: PrimaryKeySyntax ;
432459 let mut keys = HashSet :: new ( ) ;
@@ -620,7 +647,7 @@ fn relation_field_defs_with_schema(
620647
621648 out. push ( attr) ;
622649 let entity_path =
623- resolve_entity_module_path ( resolved_table, module_paths, crate_prefix) ;
650+ resolve_entity_module_path ( & table . name , resolved_table, module_paths, crate_prefix) ;
624651 out. push ( format ! (
625652 " pub {field_name}: HasOne<{entity_path}::Entity>,"
626653 ) ) ;
@@ -673,6 +700,7 @@ fn generate_relation_enum_name(columns: &[String]) -> String {
673700/// - FK column: "org_id", table: "user", to: "id" -> "org"
674701fn infer_field_name_from_fk_column ( fk_column : & str , table_name : & str , to : & str ) -> String {
675702 let table_lower = table_name. to_lowercase ( ) ;
703+ let to_lower = to. to_lowercase ( ) ;
676704
677705 // Remove the "to" suffix from FK column (e.g., "user_id" for to="id", "user_idx" for to="idx").
678706 // If FK column still uses common suffixes like "*_id"/"*_idx", strip them as fallbacks.
@@ -686,6 +714,13 @@ fn infer_field_name_from_fk_column(fk_column: &str, table_name: &str, to: &str)
686714 let sanitized = sanitize_field_name ( without_suffix) ;
687715 let sanitized_lower = sanitized. to_lowercase ( ) ;
688716
717+ // If the FK column exactly matches the referenced column name, treat it as a natural-key
718+ // relation and expose the target entity name instead of the raw column name.
719+ // Also handle compact forms like `username` for `user.name`.
720+ if sanitized_lower == to_lower || sanitized_lower == format ! ( "{table_lower}{to_lower}" ) {
721+ return sanitize_field_name ( table_name) ;
722+ }
723+
689724 // If the sanitized name is exactly the table name (e.g., "user_id" -> "user" for table "user"),
690725 // we need to fall back to the table name for proper disambiguation
691726 if sanitized_lower == table_lower {
@@ -978,7 +1013,7 @@ fn reverse_relation_field_defs(
9781013
9791014 out. push ( attr) ;
9801015 let entity_path =
981- resolve_entity_module_path ( & rel. target_entity , module_paths, crate_prefix) ;
1016+ resolve_entity_module_path ( & table . name , & rel. target_entity , module_paths, crate_prefix) ;
9821017 out. push ( format ! (
9831018 " pub {field_name}: {rust_type}<{entity_path}::Entity>,"
9841019 ) ) ;
@@ -1346,23 +1381,42 @@ mod module_path_tests {
13461381 #[ test]
13471382 fn resolve_entity_module_path_with_crate_prefix ( ) {
13481383 let mut module_paths = HashMap :: new ( ) ;
1384+ module_paths. insert (
1385+ "estimate" . into ( ) ,
1386+ vec ! [ "estimate" . into( ) , "estimate" . into( ) ] ,
1387+ ) ;
13491388 module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1350- let result = resolve_entity_module_path ( "admin" , & module_paths, "crate::models" ) ;
1389+ let result =
1390+ resolve_entity_module_path ( "estimate" , "admin" , & module_paths, "crate::models" ) ;
13511391 assert_eq ! ( result, "crate::models::admin::admin" ) ;
13521392 }
13531393
1394+ #[ test]
1395+ fn resolve_entity_module_path_prefers_super_for_siblings ( ) {
1396+ let mut module_paths = HashMap :: new ( ) ;
1397+ module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1398+ module_paths. insert (
1399+ "admin_stamp" . into ( ) ,
1400+ vec ! [ "admin" . into( ) , "admin_stamp" . into( ) ] ,
1401+ ) ;
1402+
1403+ let result =
1404+ resolve_entity_module_path ( "admin_stamp" , "admin" , & module_paths, "crate::models" ) ;
1405+ assert_eq ! ( result, "super::admin" ) ;
1406+ }
1407+
13541408 #[ test]
13551409 fn resolve_entity_module_path_fallback_when_no_mapping ( ) {
13561410 let module_paths = HashMap :: new ( ) ;
1357- let result = resolve_entity_module_path ( "user" , & module_paths, "crate::models" ) ;
1411+ let result = resolve_entity_module_path ( "post" , " user", & module_paths, "crate::models" ) ;
13581412 assert_eq ! ( result, "super::user" ) ;
13591413 }
13601414
13611415 #[ test]
13621416 fn resolve_entity_module_path_fallback_when_empty_prefix ( ) {
13631417 let mut module_paths = HashMap :: new ( ) ;
13641418 module_paths. insert ( "admin" . into ( ) , vec ! [ "admin" . into( ) , "admin" . into( ) ] ) ;
1365- let result = resolve_entity_module_path ( "admin" , & module_paths, "" ) ;
1419+ let result = resolve_entity_module_path ( "user" , " admin", & module_paths, "" ) ;
13661420 assert_eq ! ( result, "super::admin" ) ;
13671421 }
13681422}
@@ -1520,6 +1574,8 @@ mod helper_tests {
15201574 // FK column WITHOUT _id suffix (coverage for line 450)
15211575 #[ case( "creator_user" , "user" , "id" , "creator_user" ) ]
15221576 #[ case( "user" , "user" , "id" , "user" ) ]
1577+ #[ case( "username" , "user" , "name" , "user" ) ]
1578+ #[ case( "username" , "admin" , "username" , "admin" ) ]
15231579 // FK column exactly matches table name with _id (coverage for line 464)
15241580 #[ case( "customer_id" , "customer" , "id" , "customer" ) ]
15251581 #[ case( "product_id" , "product" , "id" , "product" ) ]
@@ -1545,6 +1601,28 @@ mod helper_tests {
15451601 ) ;
15461602 }
15471603
1604+ #[ test]
1605+ fn test_column_type_supports_eq ( ) {
1606+ assert ! ( column_type_supports_eq( & ColumnType :: Simple (
1607+ SimpleColumnType :: Integer
1608+ ) ) ) ;
1609+ assert ! ( column_type_supports_eq( & ColumnType :: Simple (
1610+ SimpleColumnType :: Text
1611+ ) ) ) ;
1612+ assert ! ( !column_type_supports_eq( & ColumnType :: Simple (
1613+ SimpleColumnType :: Real
1614+ ) ) ) ;
1615+ assert ! ( !column_type_supports_eq( & ColumnType :: Simple (
1616+ SimpleColumnType :: DoublePrecision
1617+ ) ) ) ;
1618+ assert ! ( column_type_supports_eq( & ColumnType :: Complex (
1619+ ComplexColumnType :: Numeric {
1620+ precision: 10 ,
1621+ scale: 2 ,
1622+ }
1623+ ) ) ) ;
1624+ }
1625+
15481626 #[ rstest]
15491627 #[ case( "hello_world" , "HelloWorld" ) ]
15501628 #[ case( "order_status" , "OrderStatus" ) ]
@@ -2730,6 +2808,7 @@ mod tests {
27302808 #[ case( "not_junction_fk_not_in_pk_other" ) ]
27312809 #[ case( "not_junction_fk_not_in_pk_another" ) ]
27322810 #[ case( "multiple_fk_same_table" ) ]
2811+ #[ case( "username_fk" ) ]
27332812 #[ case( "multiple_reverse_relations" ) ]
27342813 #[ case( "multiple_has_one_relations" ) ]
27352814 fn render_entity_with_schema_snapshots ( #[ case] name : & str ) {
@@ -2978,6 +3057,23 @@ mod tests {
29783057 ) ;
29793058 ( post. clone ( ) , vec ! [ user, post] )
29803059 }
3060+ "username_fk" => {
3061+ let user = table_with_pk (
3062+ "user" ,
3063+ vec ! [ col( "username" , ColumnType :: Simple ( Text ) ) ] ,
3064+ vec ! [ "username" ] ,
3065+ ) ;
3066+ let session = table_with_pk_and_fk (
3067+ "session" ,
3068+ vec ! [
3069+ col( "id" , ColumnType :: Simple ( Uuid ) ) ,
3070+ col( "username" , ColumnType :: Simple ( Text ) ) ,
3071+ ] ,
3072+ vec ! [ "id" ] ,
3073+ vec ! [ ( vec![ "username" ] , "user" , vec![ "username" ] ) ] ,
3074+ ) ;
3075+ ( session. clone ( ) , vec ! [ user, session] )
3076+ }
29813077 "multiple_reverse_relations" => {
29823078 // Test case where user has multiple has_one relations from profile
29833079 let user = table_with_pk (
@@ -3077,6 +3173,45 @@ mod tests {
30773173 assert ! ( rendered. contains( "default_value = 0.00" ) ) ;
30783174 }
30793175
3176+ #[ test]
3177+ fn render_entity_omits_eq_for_float_models ( ) {
3178+ use vespertide_core:: schema:: primary_key:: PrimaryKeySyntax ;
3179+
3180+ let table = TableDef {
3181+ name : "measurements" . into ( ) ,
3182+ description : None ,
3183+ columns : vec ! [
3184+ ColumnDef {
3185+ name: "id" . into( ) ,
3186+ r#type: ColumnType :: Simple ( SimpleColumnType :: Integer ) ,
3187+ nullable: false ,
3188+ default : None ,
3189+ comment: None ,
3190+ primary_key: Some ( PrimaryKeySyntax :: Bool ( true ) ) ,
3191+ unique: None ,
3192+ index: None ,
3193+ foreign_key: None ,
3194+ } ,
3195+ ColumnDef {
3196+ name: "score" . into( ) ,
3197+ r#type: ColumnType :: Simple ( SimpleColumnType :: DoublePrecision ) ,
3198+ nullable: false ,
3199+ default : None ,
3200+ comment: None ,
3201+ primary_key: None ,
3202+ unique: None ,
3203+ index: None ,
3204+ foreign_key: None ,
3205+ } ,
3206+ ] ,
3207+ constraints : vec ! [ ] ,
3208+ } ;
3209+
3210+ let rendered = render_entity ( & table) ;
3211+ assert ! ( rendered. contains( "#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]" ) ) ;
3212+ assert ! ( !rendered. contains( "#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]" ) ) ;
3213+ }
3214+
30803215 #[ test]
30813216 fn test_orm_exporter_trait ( ) {
30823217 use crate :: orm:: OrmExporter ;
0 commit comments