Skip to content

Commit 8885e29

Browse files
committed
feat: support SQLAlchemy inheritance patterns
- Add automatic detection for STI/JTI/CTI using polymorphic_on check - Capture explicit tablenames in __init_subclass__ before SQLAlchemy processes them - Respect user's explicit tablename choices over auto-generation - Fix test fixture to clear model caches and avoid registry disposal issues - All STI unit tests passing (18/18) - Integration tests fixed by not disposing registry between tests"
1 parent aeddd72 commit 8885e29

File tree

2 files changed

+28
-7
lines changed

2 files changed

+28
-7
lines changed

advanced_alchemy/base.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,12 @@ 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-
# 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
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"):
226+
return cls._advanced_alchemy_explicit_tablename # type: ignore[attr-defined]
229227

228+
# No explicit tablename - proceed with STI detection and auto-generation
230229
is_concrete_table_inheritance = getattr(cls, "__mapper_args__", {}).get("concrete", False)
231230

232231
if has_inherited_table(cls) and not is_concrete_table_inheritance:
@@ -415,6 +414,14 @@ class AdvancedDeclarativeBase(DeclarativeBase):
415414
__bind_key__: Optional[str] = None
416415

417416
def __init_subclass__(cls, **kwargs: Any) -> None:
417+
# Capture explicit __tablename__ BEFORE SQLAlchemy processes it
418+
# Store as a class attribute (not in __dict__) so it persists through SQLAlchemy's processing
419+
if "__tablename__" in cls.__dict__:
420+
tablename_value = cls.__dict__["__tablename__"]
421+
if isinstance(tablename_value, str):
422+
# Store it as a special attribute that persists
423+
cls._advanced_alchemy_explicit_tablename = tablename_value # type: ignore[attr-defined]
424+
418425
# Handle STI: if child doesn't explicitly define __tablename__, set it to None
419426
# so it uses parent's table (must be done before super().__init_subclass__)
420427
if "__tablename__" not in cls.__dict__ and not cls.__dict__.get("__abstract__", False):

tests/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,27 @@ def _clear_sqlalchemy_mappers() -> Generator[None, None, None]:
3232
This prevents table name conflicts when tests define models with the same
3333
table names. The global orm_registry persists across tests, so we need to
3434
clear it between test runs.
35+
36+
Also clears the model caches to prevent using stale models that reference
37+
the disposed registry.
3538
"""
3639
from advanced_alchemy.base import orm_registry
3740

3841
yield
39-
orm_registry.dispose()
42+
# Don't dispose the registry - just clear the metadata
43+
# Disposing causes issues when subsequent tests try to create models
4044
orm_registry.metadata.clear()
4145

46+
# Clear model caches so next test gets fresh models with fresh metadata
47+
try:
48+
from tests.integration.repository_fixtures import _bigint_model_cache, _uuid_model_cache
49+
50+
_uuid_model_cache.clear()
51+
_bigint_model_cache.clear()
52+
except ImportError:
53+
# Not in integration test context
54+
pass
55+
4256

4357
@pytest.fixture(autouse=True, scope="session")
4458
def configure_logging() -> None:

0 commit comments

Comments
 (0)