@@ -969,6 +969,7 @@ class DeclarativeGenerator(TablesGenerator):
969969 "nojoined" ,
970970 "nobidi" ,
971971 "noidsuffix" ,
972+ "nofknames" ,
972973 }
973974
974975 def __init__ (
@@ -1290,43 +1291,149 @@ def generate_relationship_name(
12901291 global_names : set [str ],
12911292 local_names : set [str ],
12921293 ) -> None :
1293- # Self referential reverse relationships
1294- preferred_name : str
1295- if (
1296- relationship .type
1297- in (RelationshipType .ONE_TO_MANY , RelationshipType .ONE_TO_ONE )
1298- and relationship .source is relationship .target
1299- and relationship .backref
1300- and relationship .backref .name
1301- ):
1302- preferred_name = relationship .backref .name + "_reverse"
1303- else :
1304- preferred_name = relationship .target .table .name
1294+ def strip_id_suffix (name : str ) -> str :
1295+ # Strip _id only if at the end or followed by underscore (e.g., "course_id" -> "course", "course_id_1" -> "course_1")
1296+ # But don't strip from "parent_id1" (where id is followed by a digit without underscore)
1297+ return re .sub (r"_id(?=_|$)" , "" , name )
1298+
1299+ def get_m2m_qualified_name (default_name : str ) -> str :
1300+ """Generate qualified name for many-to-many relationship when multiple junction tables exist."""
1301+ # Check if there are multiple M2M relationships to the same target
1302+ target_m2m_relationships = [
1303+ r
1304+ for r in relationship .source .relationships
1305+ if r .target is relationship .target
1306+ and r .type == RelationshipType .MANY_TO_MANY
1307+ ]
1308+
1309+ # Only use junction-based naming when there are multiple M2M to same target
1310+ if len (target_m2m_relationships ) > 1 :
1311+ if relationship .source is relationship .target :
1312+ # Self-referential: use FK column name from junction table
1313+ # (e.g., "parent_id" -> "parent", "child_id" -> "child")
1314+ if relationship .constraint :
1315+ column_names = [c .name for c in relationship .constraint .columns ]
1316+ if len (column_names ) == 1 :
1317+ fk_qualifier = strip_id_suffix (column_names [0 ])
1318+ else :
1319+ fk_qualifier = "_" .join (
1320+ strip_id_suffix (col_name ) for col_name in column_names
1321+ )
1322+ return fk_qualifier
1323+ elif relationship .association_table :
1324+ # Normal: use junction table name as qualifier
1325+ junction_name = relationship .association_table .table .name
1326+ fk_qualifier = strip_id_suffix (junction_name )
1327+ return f"{ relationship .target .table .name } _{ fk_qualifier } "
1328+ else :
1329+ # Single M2M: use simple name from junction table FK column
1330+ # (e.g., "right_id" -> "right" instead of "right_table")
1331+ if relationship .constraint and "noidsuffix" not in self .options :
1332+ column_names = [c .name for c in relationship .constraint .columns ]
1333+ if len (column_names ) == 1 :
1334+ stripped_name = strip_id_suffix (column_names [0 ])
1335+ if stripped_name != column_names [0 ]:
1336+ return stripped_name
1337+
1338+ return default_name
1339+
1340+ def get_fk_qualified_name (constraint : ForeignKeyConstraint ) -> str :
1341+ """Generate qualified name for one-to-many/one-to-one relationship using FK column names."""
1342+ column_names = [c .name for c in constraint .columns ]
1343+
1344+ if len (column_names ) == 1 :
1345+ # Single column FK: strip _id suffix if present
1346+ fk_qualifier = strip_id_suffix (column_names [0 ])
1347+ else :
1348+ # Multi-column FK: concatenate all column names (strip _id from each)
1349+ fk_qualifier = "_" .join (
1350+ strip_id_suffix (col_name ) for col_name in column_names
1351+ )
13051352
1306- # If there's a constraint with a single column that ends with "_id", use the
1307- # preceding part as the relationship name
1308- if relationship .constraint and "noidsuffix" not in self .options :
1353+ # For self-referential relationships, don't prepend the table name
1354+ if relationship .source is relationship .target :
1355+ return fk_qualifier
1356+ else :
1357+ return f"{ relationship .target .table .name } _{ fk_qualifier } "
1358+
1359+ def resolve_preferred_name () -> str :
1360+ resolved_name = relationship .target .table .name
1361+
1362+ # For reverse relationships with multiple FKs to the same table, use the FK
1363+ # column name to create a more descriptive relationship name
1364+ # For M2M relationships with multiple junction tables, use the junction table name
1365+ use_fk_based_naming = "nofknames" not in self .options and (
1366+ (
1367+ relationship .constraint
1368+ and relationship .type
1369+ in (RelationshipType .ONE_TO_MANY , RelationshipType .ONE_TO_ONE )
1370+ and relationship .foreign_keys
1371+ )
1372+ or (
1373+ relationship .type == RelationshipType .MANY_TO_MANY
1374+ and relationship .association_table
1375+ )
1376+ )
1377+
1378+ if use_fk_based_naming :
1379+ if relationship .type == RelationshipType .MANY_TO_MANY :
1380+ resolved_name = get_m2m_qualified_name (resolved_name )
1381+ elif relationship .constraint :
1382+ resolved_name = get_fk_qualified_name (relationship .constraint )
1383+
1384+ # If there's a constraint with a single column that contains "_id", use the
1385+ # stripped version as the relationship name
1386+ elif relationship .constraint and "noidsuffix" not in self .options :
13091387 is_source = relationship .source .table is relationship .constraint .table
13101388 if is_source or relationship .type not in (
13111389 RelationshipType .ONE_TO_ONE ,
13121390 RelationshipType .ONE_TO_MANY ,
13131391 ):
13141392 column_names = [c .name for c in relationship .constraint .columns ]
1315- if len (column_names ) == 1 and column_names [0 ].endswith ("_id" ):
1316- preferred_name = column_names [0 ][:- 3 ]
1393+ if len (column_names ) == 1 :
1394+ stripped_name = strip_id_suffix (column_names [0 ])
1395+ # Only use the stripped name if it actually changed (had _id in it)
1396+ if stripped_name != column_names [0 ]:
1397+ resolved_name = stripped_name
1398+ else :
1399+ # For composite FKs, check if there are multiple FKs to the same target
1400+ target_relationships = [
1401+ r
1402+ for r in relationship .source .relationships
1403+ if r .target is relationship .target
1404+ and r .type == relationship .type
1405+ ]
1406+ if len (target_relationships ) > 1 :
1407+ # Multiple FKs to same table - use concatenated column names
1408+ resolved_name = "_" .join (
1409+ strip_id_suffix (col_name ) for col_name in column_names
1410+ )
13171411
13181412 if "use_inflect" in self .options :
13191413 inflected_name : str | Literal [False ]
13201414 if relationship .type in (
13211415 RelationshipType .ONE_TO_MANY ,
13221416 RelationshipType .MANY_TO_MANY ,
13231417 ):
1324- if not self .inflect_engine .singular_noun (preferred_name ):
1325- preferred_name = self .inflect_engine .plural_noun (preferred_name )
1418+ if not self .inflect_engine .singular_noun (resolved_name ):
1419+ resolved_name = self .inflect_engine .plural_noun (resolved_name )
13261420 else :
1327- inflected_name = self .inflect_engine .singular_noun (preferred_name )
1421+ inflected_name = self .inflect_engine .singular_noun (resolved_name )
13281422 if inflected_name :
1329- preferred_name = inflected_name
1423+ resolved_name = inflected_name
1424+
1425+ return resolved_name
1426+
1427+ if (
1428+ relationship .type
1429+ in (RelationshipType .ONE_TO_MANY , RelationshipType .ONE_TO_ONE )
1430+ and relationship .source is relationship .target
1431+ and relationship .backref
1432+ and relationship .backref .name
1433+ ):
1434+ preferred_name = relationship .backref .name + "_reverse"
1435+ else :
1436+ preferred_name = resolve_preferred_name ()
13301437
13311438 relationship .name = self .find_free_name (
13321439 preferred_name , global_names , local_names
0 commit comments