Skip to content

Commit 1db2a9a

Browse files
authored
Merge pull request #1 from claythearc/claude/fix-dict-relationship-recursion-011CUbiwV1LogX5vM5VcjpbW
Fix RecursionError with Dict relationships in get_relationship_to()
2 parents 2bfcad1 + b1b6d0e commit 1db2a9a

File tree

2 files changed

+72
-0
lines changed

2 files changed

+72
-0
lines changed

sqlmodel/_compat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ def get_relationship_to(
179179
elif origin is list:
180180
use_annotation = get_args(annotation)[0]
181181

182+
# If a dict or Mapping, get the value type (second type argument)
183+
elif origin is dict or origin is Mapping:
184+
args = get_args(annotation)
185+
if len(args) >= 2:
186+
# For Dict[K, V] or Mapping[K, V], we want the value type (V)
187+
use_annotation = args[1]
188+
else:
189+
raise ValueError(
190+
f"Dict/Mapping relationship field '{name}' must have both key "
191+
"and value type arguments (e.g., Dict[str, Model])"
192+
)
193+
182194
return get_relationship_to(
183195
name=name, rel_info=rel_info, annotation=use_annotation
184196
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Test for Dict relationship recursion bug fix."""
2+
from typing import Dict
3+
4+
import pytest
5+
from sqlalchemy.orm.collections import attribute_mapped_collection
6+
from sqlmodel import Field, Relationship, SQLModel
7+
8+
9+
def test_dict_relationship_pattern():
10+
"""Test that Dict relationships with attribute_mapped_collection work."""
11+
12+
# Create a minimal reproduction of the pattern
13+
# This should not raise a RecursionError
14+
15+
class TestChild(SQLModel, table=True):
16+
__tablename__ = "test_child"
17+
id: int = Field(primary_key=True)
18+
key: str = Field(nullable=False)
19+
parent_id: int = Field(foreign_key="test_parent.id")
20+
parent: "TestParent" = Relationship(back_populates="children")
21+
22+
class TestParent(SQLModel, table=True):
23+
__tablename__ = "test_parent"
24+
id: int = Field(primary_key=True)
25+
children: Dict[str, "TestChild"] = Relationship(
26+
back_populates="parent",
27+
sa_relationship_kwargs={
28+
"collection_class": attribute_mapped_collection("key")
29+
},
30+
)
31+
32+
# If we got here without RecursionError, the bug is fixed
33+
assert TestParent.__tablename__ == "test_parent"
34+
assert TestChild.__tablename__ == "test_child"
35+
36+
37+
def test_dict_relationship_with_optional():
38+
"""Test that Optional[Dict[...]] relationships also work."""
39+
from typing import Optional
40+
41+
class Child(SQLModel, table=True):
42+
__tablename__ = "child"
43+
id: int = Field(primary_key=True)
44+
key: str = Field(nullable=False)
45+
parent_id: int = Field(foreign_key="parent.id")
46+
parent: Optional["Parent"] = Relationship(back_populates="children")
47+
48+
class Parent(SQLModel, table=True):
49+
__tablename__ = "parent"
50+
id: int = Field(primary_key=True)
51+
children: Optional[Dict[str, "Child"]] = Relationship(
52+
back_populates="parent",
53+
sa_relationship_kwargs={
54+
"collection_class": attribute_mapped_collection("key")
55+
},
56+
)
57+
58+
# If we got here without RecursionError, the bug is fixed
59+
assert Parent.__tablename__ == "parent"
60+
assert Child.__tablename__ == "child"

0 commit comments

Comments
 (0)