|
13 | 13 | DeclarativeBase, |
14 | 14 | Mapper, |
15 | 15 | declared_attr, |
16 | | - has_inherited_table, |
17 | 16 | ) |
18 | 17 | from sqlalchemy.orm import ( |
19 | 18 | registry as SQLAlchemyRegistry, # noqa: N812 |
@@ -220,75 +219,16 @@ def __tablename__(cls) -> Optional[str]: |
220 | 219 | Returns: |
221 | 220 | Optional[str]: Snake-case table name derived from class name, or None for STI child classes. |
222 | 221 | """ |
223 | | - # Check if class explicitly defined __tablename__ (captured in __init_subclass__) |
224 | | - # This must come FIRST to respect user's explicit choice |
225 | | - if hasattr(cls, "_advanced_alchemy_explicit_tablename"): |
| 222 | + # Check if THIS class explicitly defined __tablename__ |
| 223 | + if "_advanced_alchemy_explicit_tablename" in cls.__dict__: |
226 | 224 | return cls._advanced_alchemy_explicit_tablename # type: ignore[attr-defined] |
227 | 225 |
|
228 | | - # No explicit tablename - proceed with STI detection and auto-generation |
229 | | - is_concrete_table_inheritance = getattr(cls, "__mapper_args__", {}).get("concrete", False) |
230 | | - |
231 | | - if has_inherited_table(cls) and not is_concrete_table_inheritance: |
232 | | - # Check if any parent has polymorphic_on to confirm this is truly STI |
233 | | - for parent in cls.__mro__[1:]: |
234 | | - parent_mapper_args = getattr(parent, "__mapper_args__", {}) |
235 | | - if "polymorphic_on" in parent_mapper_args: |
236 | | - # This is STI - return None to use parent's table |
237 | | - return None |
238 | | - |
239 | | - # Not STI - auto-generate tablename from class name |
240 | | - return table_name_regexp.sub(r"_\1", cls.__name__).lower() |
241 | | - |
242 | | - @classmethod |
243 | | - def __table_cls__(cls, name: str, metadata: MetaData, *args: Any, **kwargs: Any) -> Optional[Any]: |
244 | | - """Control table creation to support inheritance patterns. |
245 | | -
|
246 | | - This hook is called by SQLAlchemy when constructing a Table. It allows |
247 | | - us to inspect the columns and determine if this is truly a new table |
248 | | - (JTI/CTI) or if it should use the parent's table (STI). |
249 | | -
|
250 | | - For STI, when a child class inherits __tablename__ from its parent, |
251 | | - we prevent table creation by returning None. |
252 | | - """ |
253 | | - from sqlalchemy import Column, PrimaryKeyConstraint, Table |
254 | | - |
255 | | - # Check if this class has its own primary key |
256 | | - # JTI/CTI classes define their own PK, STI children do not |
257 | | - has_own_pk = False |
258 | | - for arg in args: |
259 | | - if isinstance(arg, Column) and arg.primary_key: |
260 | | - has_own_pk = True |
261 | | - break |
262 | | - if isinstance(arg, PrimaryKeyConstraint): |
263 | | - has_own_pk = True |
264 | | - break |
265 | | - |
266 | | - if has_own_pk: |
267 | | - # Has its own PK - this is JTI or CTI, create the table |
268 | | - return Table(name, metadata, *args, **kwargs) |
269 | | - |
270 | | - # No own PK - check if this is STI (inheriting from a mapped parent) |
271 | | - is_concrete = getattr(cls, "__mapper_args__", {}).get("concrete", False) |
272 | | - |
273 | | - is_sti_child = False |
274 | | - if has_inherited_table(cls) and not is_concrete: |
275 | | - # Check if any parent has polymorphic_on to confirm this is truly STI |
276 | | - for parent in cls.__mro__[1:]: |
277 | | - parent_mapper_args = getattr(parent, "__mapper_args__", {}) |
278 | | - if "polymorphic_on" in parent_mapper_args: |
279 | | - is_sti_child = True |
280 | | - break |
281 | | - |
282 | | - if is_sti_child: |
283 | | - # This is STI - don't create a table, use parent's |
284 | | - # Delete the inherited tablename so SQLAlchemy uses parent table |
285 | | - if hasattr(cls, "__tablename__"): |
286 | | - with contextlib.suppress(AttributeError, TypeError): |
287 | | - del cls.__tablename__ |
| 226 | + # Check if __init_subclass__ set __tablename__ to None (STI child) |
| 227 | + if "__tablename__" in cls.__dict__ and cls.__dict__["__tablename__"] is None: |
288 | 228 | return None |
289 | 229 |
|
290 | | - # Default: create the table |
291 | | - return Table(name, metadata, *args, **kwargs) |
| 230 | + # Auto-generate tablename from class name |
| 231 | + return table_name_regexp.sub(r"_\1", cls.__name__).lower() |
292 | 232 |
|
293 | 233 |
|
294 | 234 | def create_registry( |
@@ -415,45 +355,34 @@ class AdvancedDeclarativeBase(DeclarativeBase): |
415 | 355 |
|
416 | 356 | def __init_subclass__(cls, **kwargs: Any) -> None: |
417 | 357 | # Capture explicit __tablename__ BEFORE SQLAlchemy processes it |
418 | | - # Store as a class attribute (not in __dict__) so it persists through SQLAlchemy's processing |
419 | 358 | if "__tablename__" in cls.__dict__: |
420 | 359 | tablename_value = cls.__dict__["__tablename__"] |
421 | 360 | if isinstance(tablename_value, str): |
422 | | - # Store it as a special attribute that persists |
423 | 361 | cls._advanced_alchemy_explicit_tablename = tablename_value # type: ignore[attr-defined] |
424 | 362 |
|
425 | | - # Handle STI: if child doesn't explicitly define __tablename__, set it to None |
426 | | - # so it uses parent's table (must be done before super().__init_subclass__) |
| 363 | + # Detect STI child and set __tablename__ = None BEFORE super().__init_subclass__ |
427 | 364 | if "__tablename__" not in cls.__dict__ and not cls.__dict__.get("__abstract__", False): |
428 | | - # Child didn't explicitly set __tablename__ |
429 | | - # Check if it would inherit one from a CONCRETE mapped parent (not an abstract base) |
430 | | - has_mapped_parent = False |
431 | | - for base in cls.__mro__[1:]: |
432 | | - # Skip abstract classes |
433 | | - if base.__dict__.get("__abstract__", False): |
434 | | - continue |
435 | | - # Check if this base has an EXPLICIT tablename set in its __dict__ |
436 | | - # (not a @declared_attr, which would be a function/property) |
437 | | - if "__tablename__" in base.__dict__ and isinstance(base.__dict__["__tablename__"], str): |
438 | | - # This base has an explicit tablename - likely a concrete mapped class |
439 | | - # Check this isn't JTI or CTI by looking for concrete=True |
440 | | - mapper_args = getattr(cls, "__mapper_args__", {}) |
441 | | - is_concrete = mapper_args.get("concrete", False) |
442 | | - |
443 | | - if not is_concrete: |
444 | | - # Likely STI - set tablename to None |
445 | | - # (JTI must set explicit __tablename__, so they won't hit this path) |
446 | | - has_mapped_parent = True |
447 | | - break |
448 | | - |
449 | | - if has_mapped_parent: |
450 | | - cls.__tablename__ = None # type: ignore[misc] |
| 365 | + # Child didn't explicitly set __tablename__ - check for STI pattern |
| 366 | + is_concrete = getattr(cls, "__mapper_args__", {}).get("concrete", False) |
| 367 | + if not is_concrete: |
| 368 | + for parent in cls.__mro__[1:]: |
| 369 | + # Skip parents that explicitly set __abstract__ = True in THEIR __dict__ |
| 370 | + if parent.__dict__.get("__abstract__", False): |
| 371 | + continue |
| 372 | + if "_advanced_alchemy_explicit_tablename" in parent.__dict__: |
| 373 | + parent_mapper_args = getattr(parent, "__mapper_args__", {}) |
| 374 | + if "polymorphic_on" in parent_mapper_args: |
| 375 | + # STI child - set __tablename__ to None BEFORE SQLAlchemy processes |
| 376 | + cls.__tablename__ = None # type: ignore[misc] |
| 377 | + break |
451 | 378 |
|
452 | 379 | bind_key = getattr(cls, "__bind_key__", None) |
453 | 380 | if bind_key is not None: |
454 | 381 | cls.metadata = cls.__metadata_registry__.get(bind_key) |
455 | 382 | elif None not in cls.__metadata_registry__ and getattr(cls, "metadata", None) is not None: |
456 | 383 | cls.__metadata_registry__[None] = cls.metadata |
| 384 | + |
| 385 | + # Call super() AFTER setting __tablename__ |
457 | 386 | super().__init_subclass__(**kwargs) |
458 | 387 |
|
459 | 388 |
|
|
0 commit comments