Skip to content

Commit 27bd3f5

Browse files
fix(base): remove misleading DataclassProtocol annotation mapping (#771)
## Summary Closes #477. `create_registry()` currently seeds `type_annotation_map` with `DataclassProtocol: JsonB`. That entry does not provide structural dataclass support: - SQLAlchemy resolves `type_annotation_map` through exact and `__mro__` dictionary lookups; it does not call `isinstance()` or otherwise perform runtime Protocol validation. - A concrete dataclass used as `Mapped[SomeDataclass]` does not inherit `DataclassProtocol`, so it does not match that entry and raises `MappedAnnotationError`. - An exact `Mapped[DataclassProtocol]` annotation can select `JsonB`, but it does not validate assigned values against the Protocol or preserve the concrete dataclass type for reconstruction. The default entry is therefore misleading: it appears to support arbitrary concrete dataclasses, but concrete dataclass annotations must be registered explicitly. ## Changes - Remove the default `DataclassProtocol: JsonB` entry and its now-unused import from `advanced_alchemy/base.py`. - Add regression coverage for the default registry map. - Document the supported path: register each concrete dataclass with `custom_annotation_map={MyDataclass: JsonB}` so SQLAlchemy can resolve the exact annotation. ## Compatibility This changes the narrow case where a model is annotated literally as `Mapped[DataclassProtocol]`; that exact annotation previously selected `JsonB`. It did not validate Protocol conformance or provide concrete dataclass reconstruction. Concrete annotations such as `Mapped[SomeDataclass]` did not resolve through this entry before this change and continue to require an explicit concrete mapping. ## Supported replacement ```python from dataclasses import dataclass from advanced_alchemy.base import create_registry from advanced_alchemy.types import JsonB @DataClass class ExtraData: before: int registry = create_registry(custom_annotation_map={ExtraData: JsonB}) ```
1 parent c5ea0c9 commit 27bd3f5

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

advanced_alchemy/base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
UUIDv7PrimaryKey,
3535
)
3636
from advanced_alchemy.types import GUID, DateTimeUTC, FileObject, FileObjectList, JsonB, StoredObject
37-
from advanced_alchemy.utils.dataclass import DataclassProtocol
3837

3938
if TYPE_CHECKING:
4039
from sqlalchemy.sql import FromClause
@@ -405,7 +404,6 @@ def create_registry(
405404
dict: JsonB,
406405
dict[str, Any]: JsonB,
407406
dict[str, str]: JsonB,
408-
DataclassProtocol: JsonB,
409407
FileObject: StoredObject,
410408
FileObjectList: StoredObject,
411409
}

tests/unit/test_base.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,40 @@ class SQLiteIdentityAuditBaseModel(IdentityAuditBase):
282282

283283
# Should not raise any errors
284284
assert True # If we get here, it worked
285+
286+
287+
def test_registry_type_annotation_map_has_no_protocol_keys() -> None:
288+
"""Regression test for https://github.com/litestar-org/advanced-alchemy/issues/477.
289+
290+
``type_annotation_map`` is resolved by SQLAlchemy through an exact/``__mro__``
291+
dictionary lookup that rejects supertype matches. A ``Protocol`` (such as the
292+
now-removed ``DataclassProtocol`` entry) is never present in the ``__mro__`` of
293+
a class that structurally satisfies it, so a Protocol key can never match and
294+
is misleading dead configuration. Guard against reintroducing one.
295+
"""
296+
from advanced_alchemy.base import orm_registry
297+
298+
protocol_keys = [key for key in orm_registry.type_annotation_map if getattr(key, "_is_protocol", False)]
299+
assert protocol_keys == []
300+
301+
302+
def test_dataclass_column_resolves_via_custom_annotation_map() -> None:
303+
"""A concrete dataclass resolves only when registered explicitly.
304+
305+
This is the supported replacement for the removed ``DataclassProtocol`` entry:
306+
users map their own concrete dataclass type, which lands in the registry as an
307+
exact key and therefore resolves.
308+
"""
309+
from dataclasses import dataclass
310+
311+
from advanced_alchemy.base import create_registry
312+
from advanced_alchemy.types import JsonB
313+
314+
@dataclass
315+
class ExtraData:
316+
before: int
317+
318+
registry = create_registry(custom_annotation_map={ExtraData: JsonB})
319+
320+
resolved = registry._resolve_type(ExtraData) # pyright: ignore[reportPrivateUsage]
321+
assert resolved is not None

0 commit comments

Comments
 (0)