From b1b6d0e3a761b331f01c25c8ece728f7c635ef04 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Oct 2025 15:32:15 +0000 Subject: [PATCH 1/2] Fix RecursionError with Dict relationships in get_relationship_to() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed a bug where using Dict or Mapping type hints with Relationship() would cause infinite recursion. The get_relationship_to() function now properly handles dict/Mapping types by extracting the value type (second type argument), similar to how it handles List types. Changes: - Added handling for dict and Mapping origins in get_relationship_to() - Extracts the value type from Dict[K, V] or Mapping[K, V] annotations - Added comprehensive tests for Dict relationships with attribute_mapped_collection This resolves the RecursionError that occurred when defining relationships like: children: Dict[str, Child] = Relationship(...) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- sqlmodel/_compat.py | 12 +++++ tests/test_dict_relationship_recursion.py | 60 +++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/test_dict_relationship_recursion.py diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 230f8cc362..e204b1778a 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -179,6 +179,18 @@ def get_relationship_to( elif origin is list: use_annotation = get_args(annotation)[0] + # If a dict or Mapping, get the value type (second type argument) + elif origin is dict or origin is Mapping: + args = get_args(annotation) + if len(args) >= 2: + # For Dict[K, V] or Mapping[K, V], we want the value type (V) + use_annotation = args[1] + else: + raise ValueError( + f"Dict/Mapping relationship field '{name}' must have both key " + "and value type arguments (e.g., Dict[str, Model])" + ) + return get_relationship_to( name=name, rel_info=rel_info, annotation=use_annotation ) diff --git a/tests/test_dict_relationship_recursion.py b/tests/test_dict_relationship_recursion.py new file mode 100644 index 0000000000..37d8a6aca9 --- /dev/null +++ b/tests/test_dict_relationship_recursion.py @@ -0,0 +1,60 @@ +"""Test for Dict relationship recursion bug fix.""" +from typing import Dict + +import pytest +from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlmodel import Field, Relationship, SQLModel + + +def test_dict_relationship_pattern(): + """Test that Dict relationships with attribute_mapped_collection work.""" + + # Create a minimal reproduction of the pattern + # This should not raise a RecursionError + + class TestChild(SQLModel, table=True): + __tablename__ = "test_child" + id: int = Field(primary_key=True) + key: str = Field(nullable=False) + parent_id: int = Field(foreign_key="test_parent.id") + parent: "TestParent" = Relationship(back_populates="children") + + class TestParent(SQLModel, table=True): + __tablename__ = "test_parent" + id: int = Field(primary_key=True) + children: Dict[str, "TestChild"] = Relationship( + back_populates="parent", + sa_relationship_kwargs={ + "collection_class": attribute_mapped_collection("key") + }, + ) + + # If we got here without RecursionError, the bug is fixed + assert TestParent.__tablename__ == "test_parent" + assert TestChild.__tablename__ == "test_child" + + +def test_dict_relationship_with_optional(): + """Test that Optional[Dict[...]] relationships also work.""" + from typing import Optional + + class Child(SQLModel, table=True): + __tablename__ = "child" + id: int = Field(primary_key=True) + key: str = Field(nullable=False) + parent_id: int = Field(foreign_key="parent.id") + parent: Optional["Parent"] = Relationship(back_populates="children") + + class Parent(SQLModel, table=True): + __tablename__ = "parent" + id: int = Field(primary_key=True) + children: Optional[Dict[str, "Child"]] = Relationship( + back_populates="parent", + sa_relationship_kwargs={ + "collection_class": attribute_mapped_collection("key") + }, + ) + + # If we got here without RecursionError, the bug is fixed + assert Parent.__tablename__ == "parent" + assert Child.__tablename__ == "child" From 8c2e4c4a9cb10247d216a24a778136bf79a09fa5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:10:47 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_dict_relationship_recursion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dict_relationship_recursion.py b/tests/test_dict_relationship_recursion.py index 37d8a6aca9..9993b6843d 100644 --- a/tests/test_dict_relationship_recursion.py +++ b/tests/test_dict_relationship_recursion.py @@ -1,7 +1,7 @@ """Test for Dict relationship recursion bug fix.""" + from typing import Dict -import pytest from sqlalchemy.orm.collections import attribute_mapped_collection from sqlmodel import Field, Relationship, SQLModel