Skip to content

Commit a355654

Browse files
authored
fix: handle relationship data in model_from_dict for service.create() (#512)
Fixed regression where service.create() method stopped handling relationship data correctly when passed SQLAlchemy model instances. Changed model_from_dict() in _util.py to use `__mapper__.attrs.keys()` instead of `__mapper__.columns.keys()` to include relationship attributes alongside column attributes. - Use `attrs.keys()` to include both columns and relationships - Add comprehensive tests for relationship handling in model_from_dict - Verify unknown attributes are still ignored
1 parent 3315826 commit a355654

File tree

2 files changed

+78
-4
lines changed

2 files changed

+78
-4
lines changed

advanced_alchemy/repository/_util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ def model_from_dict(model: type[ModelT], **kwargs: Any) -> ModelT:
9797
ModelT: A new instance of the model populated with the provided values.
9898
"""
9999
data = {
100-
column_name: kwargs[column_name]
101-
for column_name in model.__mapper__.columns.keys() # noqa: SIM118 # pyright: ignore[reportUnknownMemberType]
102-
if column_name in kwargs
100+
attr_name: kwargs[attr_name]
101+
for attr_name in model.__mapper__.attrs.keys() # noqa: SIM118 # pyright: ignore[reportUnknownMemberType]
102+
if attr_name in kwargs
103103
}
104104
return model(**data)
105105

tests/unit/test_repository.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
SQLAlchemyAsyncRepository,
3333
SQLAlchemySyncRepository,
3434
)
35-
from advanced_alchemy.repository._util import column_has_defaults
35+
from advanced_alchemy.repository._util import column_has_defaults, model_from_dict
3636
from advanced_alchemy.service.typing import (
3737
is_msgspec_struct,
3838
is_pydantic_model,
@@ -1318,3 +1318,77 @@ def test_column_with_empty_string_default() -> None:
13181318
mock_column.server_onupdate = None
13191319

13201320
assert column_has_defaults(mock_column) is True
1321+
1322+
1323+
def test_model_from_dict_includes_relationship_attributes() -> None:
1324+
"""Test that model_from_dict includes relationship attributes from __mapper__.attrs.keys()."""
1325+
from tests.fixtures.uuid.models import UUIDAuthor
1326+
1327+
# Verify that attrs.keys() includes relationships while columns.keys() doesn't
1328+
columns_keys = list(UUIDAuthor.__mapper__.columns.keys())
1329+
attrs_keys = list(UUIDAuthor.__mapper__.attrs.keys())
1330+
1331+
assert "books" not in columns_keys, "books relationship should NOT be in columns.keys()"
1332+
assert "books" in attrs_keys, "books relationship should be in attrs.keys()"
1333+
1334+
1335+
def test_model_from_dict_with_relationship_data() -> None:
1336+
"""Test that model_from_dict can handle relationship data."""
1337+
from tests.fixtures.uuid.models import UUIDAuthor, UUIDBook
1338+
1339+
# Create test data with relationship
1340+
book1 = UUIDBook(title="Test Book 1", author_id="dummy-uuid")
1341+
book2 = UUIDBook(title="Test Book 2", author_id="dummy-uuid")
1342+
1343+
author_data = {"name": "Test Author", "string_field": "test value", "books": [book1, book2]}
1344+
1345+
# model_from_dict should handle the relationship attribute
1346+
author = model_from_dict(UUIDAuthor, **author_data)
1347+
1348+
assert author.name == "Test Author"
1349+
assert author.string_field == "test value"
1350+
assert hasattr(author, "books"), "Author should have books attribute"
1351+
assert len(author.books) == 2
1352+
assert author.books[0].title == "Test Book 1"
1353+
assert author.books[1].title == "Test Book 2"
1354+
1355+
1356+
def test_model_from_dict_backward_compatibility() -> None:
1357+
"""Test that model_from_dict maintains backward compatibility with column-only data."""
1358+
from tests.fixtures.uuid.models import UUIDAuthor
1359+
1360+
author_data = {"name": "Compatible Author", "string_field": "compatibility test"}
1361+
1362+
author = model_from_dict(UUIDAuthor, **author_data)
1363+
1364+
assert author.name == "Compatible Author"
1365+
assert author.string_field == "compatibility test"
1366+
1367+
1368+
def test_model_from_dict_ignores_unknown_attributes() -> None:
1369+
"""Test that model_from_dict still ignores unknown attributes."""
1370+
from tests.fixtures.uuid.models import UUIDAuthor
1371+
1372+
author_data = {"name": "Test Author", "unknown_attribute": "should be ignored", "another_unknown": 12345}
1373+
1374+
author = model_from_dict(UUIDAuthor, **author_data)
1375+
1376+
assert author.name == "Test Author"
1377+
assert not hasattr(author, "unknown_attribute")
1378+
assert not hasattr(author, "another_unknown")
1379+
1380+
1381+
def test_model_from_dict_empty_relationship() -> None:
1382+
"""Test that model_from_dict handles empty relationship lists."""
1383+
from tests.fixtures.uuid.models import UUIDAuthor
1384+
1385+
author_data = {
1386+
"name": "Author Without Books",
1387+
"books": [], # Empty relationship
1388+
}
1389+
1390+
author = model_from_dict(UUIDAuthor, **author_data)
1391+
1392+
assert author.name == "Author Without Books"
1393+
assert hasattr(author, "books")
1394+
assert author.books == []

0 commit comments

Comments
 (0)