@@ -99,10 +99,10 @@ pub fn generate_schema_type_code(
9999 find_struct_from_path ( & input. source_type , schema_name_hint)
100100 {
101101 struct_def_owned = found;
102- // Use the module path from the file lookup if the extracted one is empty
103- if source_module_path . is_empty ( ) {
104- source_module_path = module_path ;
105- }
102+ // Always use the module path from file lookup for qualified paths
103+ // The file lookup derives module path from actual file location, which is more accurate
104+ // for resolving relative paths like `super::user::Entity`
105+ source_module_path = module_path ;
106106 & struct_def_owned
107107 } else if let Some ( found) = schema_storage. iter ( ) . find ( |s| s. name == source_type_name) {
108108 found
@@ -637,6 +637,7 @@ pub fn generate_schema_type_code(
637637#[ cfg( test) ]
638638mod tests {
639639 use super :: * ;
640+ use serial_test:: serial;
640641
641642 fn create_test_struct_metadata ( name : & str , definition : & str ) -> StructMetadata {
642643 StructMetadata :: new ( name. to_string ( ) , definition. to_string ( ) )
@@ -1450,6 +1451,7 @@ mod tests {
14501451 }
14511452
14521453 #[ test]
1454+ #[ serial]
14531455 fn test_generate_schema_type_code_qualified_path_file_lookup_success ( ) {
14541456 // Coverage for lines 76, 78-79, 81
14551457 // Tests: qualified path found via file lookup, module_path used when source is empty
@@ -1502,6 +1504,7 @@ pub struct Model {
15021504 }
15031505
15041506 #[ test]
1507+ #[ serial]
15051508 fn test_generate_schema_type_code_simple_name_file_lookup_fallback ( ) {
15061509 // Coverage for lines 100, 103-104
15071510 // Tests: simple name (not in storage) found via file lookup with schema_name hint
@@ -1560,6 +1563,7 @@ pub struct Model {
15601563 // ============================================================
15611564
15621565 #[ test]
1566+ #[ serial]
15631567 fn test_generate_schema_type_code_has_many_explicit_pick_inline_type ( ) {
15641568 // Coverage for lines 258-260, 262-263, 265, 267-268
15651569 // Tests: HasMany is explicitly picked, inline type is generated
@@ -1625,6 +1629,7 @@ pub struct Model {
16251629 }
16261630
16271631 #[ test]
1632+ #[ serial]
16281633 fn test_generate_schema_type_code_has_many_explicit_pick_file_not_found ( ) {
16291634 // Coverage for line 270
16301635 // Tests: HasMany is explicitly picked but target file not found - should skip field
@@ -1684,6 +1689,7 @@ pub struct Model {
16841689 // ============================================================
16851690
16861691 #[ test]
1692+ #[ serial]
16871693 fn test_generate_schema_type_code_belongs_to_circular_inline_optional ( ) {
16881694 // Coverage for lines 277-278, 281-282, 285, 288-289, 294
16891695 // Tests: BelongsTo with circular reference, optional field (is_optional = true)
@@ -1750,6 +1756,7 @@ pub struct Model {
17501756 }
17511757
17521758 #[ test]
1759+ #[ serial]
17531760 fn test_generate_schema_type_code_has_one_circular_inline_required ( ) {
17541761 // Coverage for lines 277-278, 281-282, 285, 291, 294
17551762 // Tests: HasOne with circular reference, required field (is_optional = false)
@@ -1818,6 +1825,191 @@ pub struct Model {
18181825 }
18191826
18201827 #[ test]
1828+ #[ serial]
1829+ fn test_generate_schema_type_code_belongs_to_circular_inline_required_file ( ) {
1830+ // Coverage for line 291 specifically
1831+ // Tests: BelongsTo with circular reference AND required FK (is_optional = false)
1832+ // This requires file-based lookup with:
1833+ // 1. #[sea_orm(from = "required_fk")] where required_fk is NOT Option<T>
1834+ // 2. Circular reference between two models
1835+ use tempfile:: TempDir ;
1836+
1837+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1838+ let src_dir = temp_dir. path ( ) . join ( "src" ) ;
1839+ let models_dir = src_dir. join ( "models" ) ;
1840+ std:: fs:: create_dir_all ( & models_dir) . unwrap ( ) ;
1841+
1842+ // Create user.rs with Model that references memo (circular)
1843+ let user_model = r#"
1844+ #[sea_orm(table_name = "users")]
1845+ pub struct Model {
1846+ pub id: i32,
1847+ pub name: String,
1848+ pub memo_id: i32,
1849+ #[sea_orm(belongs_to, from = "memo_id", to = "id")]
1850+ pub memo: BelongsTo<super::memo::Entity>,
1851+ }
1852+ "# ;
1853+ std:: fs:: write ( models_dir. join ( "user.rs" ) , user_model) . unwrap ( ) ;
1854+
1855+ // Create memo.rs with Model that references user (completing the circle)
1856+ // Note: using flag-style `belongs_to` with `from = "user_id"`
1857+ let memo_model = r#"
1858+ #[sea_orm(table_name = "memos")]
1859+ pub struct Model {
1860+ pub id: i32,
1861+ pub title: String,
1862+ pub user_id: i32,
1863+ #[sea_orm(belongs_to, from = "user_id", to = "id")]
1864+ pub user: BelongsTo<super::user::Entity>,
1865+ }
1866+ "# ;
1867+ std:: fs:: write ( models_dir. join ( "memo.rs" ) , memo_model) . unwrap ( ) ;
1868+
1869+ // Save original CARGO_MANIFEST_DIR
1870+ let original_manifest_dir = std:: env:: var ( "CARGO_MANIFEST_DIR" ) . ok ( ) ;
1871+ // SAFETY: This is a test that runs single-threaded
1872+ unsafe { std:: env:: set_var ( "CARGO_MANIFEST_DIR" , temp_dir. path ( ) ) } ;
1873+
1874+ // Generate schema from memo - has BelongsTo user which has circular ref back
1875+ // The user_id field is required (not Option), so is_optional = false
1876+ // This should generate Box<...> instead of Option<Box<...>>
1877+ let tokens = quote ! ( MemoSchema from crate :: models:: memo:: Model ) ;
1878+ let input: SchemaTypeInput = syn:: parse2 ( tokens) . unwrap ( ) ;
1879+ let storage: Vec < StructMetadata > = vec ! [ ] ;
1880+
1881+ let result = generate_schema_type_code ( & input, & storage) ;
1882+
1883+ // Restore CARGO_MANIFEST_DIR
1884+ // SAFETY: This is a test that runs single-threaded
1885+ unsafe {
1886+ if let Some ( dir) = original_manifest_dir {
1887+ std:: env:: set_var ( "CARGO_MANIFEST_DIR" , dir) ;
1888+ } else {
1889+ std:: env:: remove_var ( "CARGO_MANIFEST_DIR" ) ;
1890+ }
1891+ }
1892+
1893+ assert ! ( result. is_ok( ) , "Should generate schema: {:?}" , result. err( ) ) ;
1894+ let ( tokens, _metadata) = result. unwrap ( ) ;
1895+ let output = tokens. to_string ( ) ;
1896+ // Should have inline type definition for circular relation
1897+ assert ! (
1898+ output. contains( "MemoSchema" ) ,
1899+ "Should contain MemoSchema: {}" ,
1900+ output
1901+ ) ;
1902+ assert ! (
1903+ output. contains( "user" ) ,
1904+ "Should contain user field: {}" ,
1905+ output
1906+ ) ;
1907+ // BelongsTo with required FK (user_id: i32) should generate Box<...> not Option<Box<...>>
1908+ // This hits line 291: quote! { Box<#inline_type_name> }
1909+ assert ! (
1910+ output. contains( "pub user : Box <" ) ,
1911+ "BelongsTo with required FK should generate Box<>, not Option<Box<>>. Output: {}" ,
1912+ output
1913+ ) ;
1914+ }
1915+
1916+ #[ test]
1917+ fn test_seaorm_relation_required_fk_directly ( ) {
1918+ // Test the convert_relation_type_to_schema_with_info function directly
1919+ // to verify is_optional = false when FK is required
1920+ use crate :: schema_macro:: seaorm:: {
1921+ convert_relation_type_to_schema_with_info, extract_belongs_to_from_field,
1922+ is_field_optional_in_struct,
1923+ } ;
1924+
1925+ // Use the same attribute format that works in seaorm tests: belongs_to (flag), not belongs_to = "..."
1926+ let struct_def = r#"
1927+ #[sea_orm(table_name = "memos")]
1928+ pub struct Model {
1929+ pub id: i32,
1930+ pub user_id: i32,
1931+ #[sea_orm(belongs_to, from = "user_id", to = "id")]
1932+ pub user: BelongsTo<super::user::Entity>,
1933+ }
1934+ "# ;
1935+ let parsed_struct: syn:: ItemStruct = syn:: parse_str ( struct_def) . unwrap ( ) ;
1936+
1937+ // Get the user field
1938+ let fields_named = match & parsed_struct. fields {
1939+ syn:: Fields :: Named ( f) => f,
1940+ _ => panic ! ( "Expected named fields" ) ,
1941+ } ;
1942+
1943+ let user_field = fields_named
1944+ . named
1945+ . iter ( )
1946+ . find ( |f| f. ident . as_ref ( ) . map ( |i| i == "user" ) . unwrap_or ( false ) )
1947+ . expect ( "user field not found" ) ;
1948+
1949+ // Debug: Check if extract_belongs_to_from_field works
1950+ let fk_field = extract_belongs_to_from_field ( & user_field. attrs ) ;
1951+ assert_eq ! (
1952+ fk_field,
1953+ Some ( "user_id" . to_string( ) ) ,
1954+ "Should extract FK field from attribute"
1955+ ) ;
1956+
1957+ // Debug: Check if is_field_optional_in_struct works
1958+ let is_fk_optional = is_field_optional_in_struct ( & parsed_struct, "user_id" ) ;
1959+ assert ! ( !is_fk_optional, "user_id: i32 should not be optional" ) ;
1960+
1961+ let result = convert_relation_type_to_schema_with_info (
1962+ & user_field. ty ,
1963+ & user_field. attrs ,
1964+ & parsed_struct,
1965+ & [
1966+ "crate" . to_string ( ) ,
1967+ "models" . to_string ( ) ,
1968+ "memo" . to_string ( ) ,
1969+ ] ,
1970+ user_field. ident . clone ( ) . unwrap ( ) ,
1971+ ) ;
1972+
1973+ assert ! ( result. is_some( ) , "Should convert BelongsTo relation" ) ;
1974+ let ( _, rel_info) = result. unwrap ( ) ;
1975+ assert_eq ! ( rel_info. relation_type, "BelongsTo" ) ;
1976+ // The FK field user_id is i32 (not Option), so is_optional should be false
1977+ assert ! (
1978+ !rel_info. is_optional,
1979+ "BelongsTo with required FK (user_id: i32) should have is_optional = false"
1980+ ) ;
1981+ }
1982+
1983+ #[ test]
1984+ fn test_extract_belongs_to_from_field_with_equals_value ( ) {
1985+ // Test that extract_belongs_to_from_field works with belongs_to = "..." format
1986+ use crate :: schema_macro:: seaorm:: extract_belongs_to_from_field;
1987+
1988+ // Format 1: belongs_to (flag style) - known to work
1989+ let attrs1: Vec < syn:: Attribute > = vec ! [ syn:: parse_quote!(
1990+ #[ sea_orm( belongs_to, from = "user_id" , to = "id" ) ]
1991+ ) ] ;
1992+ let result1 = extract_belongs_to_from_field ( & attrs1) ;
1993+ assert_eq ! (
1994+ result1,
1995+ Some ( "user_id" . to_string( ) ) ,
1996+ "Flag style should work"
1997+ ) ;
1998+
1999+ // Format 2: belongs_to = "..." (value style) - testing this
2000+ let attrs2: Vec < syn:: Attribute > = vec ! [ syn:: parse_quote!(
2001+ #[ sea_orm( belongs_to = "super::user::Entity" , from = "user_id" , to = "id" ) ]
2002+ ) ] ;
2003+ let result2 = extract_belongs_to_from_field ( & attrs2) ;
2004+ assert_eq ! (
2005+ result2,
2006+ Some ( "user_id" . to_string( ) ) ,
2007+ "Value style should also work"
2008+ ) ;
2009+ }
2010+
2011+ #[ test]
2012+ #[ serial]
18212013 fn test_generate_schema_type_code_qualified_path_with_nonempty_module_path ( ) {
18222014 // Coverage for line 78 (the else branch where source_module_path is NOT empty)
18232015 // Tests: qualified path with explicit module segments that are not empty
0 commit comments