@@ -220,25 +220,24 @@ def __tablename__(cls) -> Optional[str]:
220220 Returns:
221221 Optional[str]: Snake-case table name derived from class name, or None for STI child classes.
222222 """
223- # Check if __tablename__ was explicitly defined on this class (not inherited from parent)
224- explicitly_defined_tablename = cls .__dict__ .get ("__tablename__" )
225- if explicitly_defined_tablename :
226- for parent_cls in cls .__mro__ [1 :]:
227- if not hasattr (parent_cls , "__dict__" ):
228- continue
229- parent_tablename = parent_cls .__dict__ .get ("__tablename__" )
230- if parent_tablename == explicitly_defined_tablename :
231- explicitly_defined_tablename = None
232- break
233-
234- if explicitly_defined_tablename :
235- return explicitly_defined_tablename
223+ # STI pattern detection: only return None if this is truly an STI child
224+ # An STI child has ALL of these characteristics:
225+ # 1. Inherits from a mapped parent (has_inherited_table)
226+ # 2. Parent has polymorphic_on defined (indicating STI hierarchy)
227+ # 3. Not marked as concrete=True (which would be CTI)
228+ # 4. Did NOT explicitly define __tablename__ in its class body
236229
237- # STI pattern: child inherits parent's table (unless concrete=True for CTI)
238230 is_concrete_table_inheritance = getattr (cls , "__mapper_args__" , {}).get ("concrete" , False )
239- if has_inherited_table (cls ) and not is_concrete_table_inheritance :
240- return None
241231
232+ if has_inherited_table (cls ) and not is_concrete_table_inheritance :
233+ # Check if any parent has polymorphic_on to confirm this is truly STI
234+ for parent in cls .__mro__ [1 :]:
235+ parent_mapper_args = getattr (parent , "__mapper_args__" , {})
236+ if "polymorphic_on" in parent_mapper_args :
237+ # This is STI - return None to use parent's table
238+ return None
239+
240+ # Not STI - auto-generate tablename from class name
242241 return table_name_regexp .sub (r"_\1" , cls .__name__ ).lower ()
243242
244243 @classmethod
@@ -270,7 +269,18 @@ def __table_cls__(cls, name: str, metadata: MetaData, *args: Any, **kwargs: Any)
270269 return Table (name , metadata , * args , ** kwargs )
271270
272271 # No own PK - check if this is STI (inheriting from a mapped parent)
273- if has_inherited_table (cls ) and not getattr (cls , "__mapper_args__" , {}).get ("concrete" , False ):
272+ is_concrete = getattr (cls , "__mapper_args__" , {}).get ("concrete" , False )
273+
274+ is_sti_child = False
275+ if has_inherited_table (cls ) and not is_concrete :
276+ # Check if any parent has polymorphic_on to confirm this is truly STI
277+ for parent in cls .__mro__ [1 :]:
278+ parent_mapper_args = getattr (parent , "__mapper_args__" , {})
279+ if "polymorphic_on" in parent_mapper_args :
280+ is_sti_child = True
281+ break
282+
283+ if is_sti_child :
274284 # This is STI - don't create a table, use parent's
275285 # Delete the inherited tablename so SQLAlchemy uses parent table
276286 if hasattr (cls , "__tablename__" ):
0 commit comments