Skip to content

Commit b430244

Browse files
committed
test: add regression tests for issue #530 - relationship annotation preservation
Cover three scenarios: 1. Relationship annotations visible in __init_subclass__ hooks 2. Relationship fields excluded from Pydantic model_fields 3. End-to-end relationship functionality still works after the fix Fixes #530
1 parent 32370cb commit b430244

1 file changed

Lines changed: 125 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Regression test for issue #530:
3+
Relationship type annotations disappear after class definition is evaluated.
4+
5+
Before the fix, `__init_subclass__` hooks could not see Relationship annotations
6+
in `cls.__annotations__`, making it impossible to inspect relationship types
7+
at class creation time.
8+
"""
9+
10+
from typing import Optional
11+
12+
from sqlmodel import Field, Relationship, SQLModel, Session, create_engine, select
13+
14+
15+
# Track what annotations were visible during class creation
16+
_seen_annotations: dict[str, set] = {}
17+
18+
19+
class AnnotationInspector(SQLModel):
20+
"""Mixin that records which annotations are visible in __init_subclass__."""
21+
22+
def __init_subclass__(cls, **kwargs: object) -> None:
23+
super().__init_subclass__(**kwargs)
24+
_seen_annotations[cls.__name__] = set(cls.__annotations__.keys())
25+
26+
27+
def test_relationship_annotations_visible_in_init_subclass() -> None:
28+
"""
29+
Verifies that Relationship fields appear in __annotations__ when
30+
__init_subclass__ is called, fixing issue #530.
31+
"""
32+
_seen_annotations.clear()
33+
34+
class TeamA(AnnotationInspector, SQLModel, table=True):
35+
__tablename__ = "teama_530"
36+
id: Optional[int] = Field(default=None, primary_key=True)
37+
members: list["MemberA"] = Relationship(back_populates="team")
38+
39+
class MemberA(AnnotationInspector, SQLModel, table=True):
40+
__tablename__ = "membera_530"
41+
id: Optional[int] = Field(default=None, primary_key=True)
42+
team_id: Optional[int] = Field(default=None, foreign_key="teama_530.id")
43+
team: Optional[TeamA] = Relationship(back_populates="members")
44+
45+
# The key assertion: relationship fields must be visible in __annotations__
46+
# at the time __init_subclass__ is called.
47+
assert "members" in _seen_annotations["TeamA"], (
48+
"Relationship 'members' was not visible in TeamA.__annotations__ "
49+
"during __init_subclass__ (issue #530)"
50+
)
51+
assert "team" in _seen_annotations["MemberA"], (
52+
"Relationship 'team' was not visible in MemberA.__annotations__ "
53+
"during __init_subclass__ (issue #530)"
54+
)
55+
56+
57+
def test_relationship_annotations_not_in_model_fields() -> None:
58+
"""
59+
Verifies that Relationship fields do NOT appear in model_fields (Pydantic),
60+
which would cause validation overhead and incorrect behavior.
61+
"""
62+
63+
class TeamB(SQLModel, table=True):
64+
__tablename__ = "teamb_530"
65+
id: Optional[int] = Field(default=None, primary_key=True)
66+
members: list["MemberB"] = Relationship(back_populates="team")
67+
68+
class MemberB(SQLModel, table=True):
69+
__tablename__ = "memberb_530"
70+
id: Optional[int] = Field(default=None, primary_key=True)
71+
team_id: Optional[int] = Field(default=None, foreign_key="teamb_530.id")
72+
team: Optional[TeamB] = Relationship(back_populates="members")
73+
74+
# Relationship fields should NOT appear in pydantic model_fields
75+
assert "members" not in TeamB.model_fields, (
76+
"Relationship 'members' incorrectly appeared in TeamB.model_fields"
77+
)
78+
assert "team" not in MemberB.model_fields, (
79+
"Relationship 'team' incorrectly appeared in MemberB.model_fields"
80+
)
81+
82+
# But they should appear in sqlmodel_relationships
83+
assert "members" in TeamB.__sqlmodel_relationships__
84+
assert "team" in MemberB.__sqlmodel_relationships__
85+
86+
87+
def test_relationship_functional_after_fix() -> None:
88+
"""
89+
End-to-end test: Verify that relationships still work correctly after the fix.
90+
"""
91+
92+
class Department(SQLModel, table=True):
93+
__tablename__ = "department_530"
94+
id: Optional[int] = Field(default=None, primary_key=True)
95+
name: str
96+
employees: list["Employee"] = Relationship(back_populates="department")
97+
98+
class Employee(SQLModel, table=True):
99+
__tablename__ = "employee_530"
100+
id: Optional[int] = Field(default=None, primary_key=True)
101+
name: str
102+
department_id: Optional[int] = Field(
103+
default=None, foreign_key="department_530.id"
104+
)
105+
department: Optional[Department] = Relationship(back_populates="employees")
106+
107+
engine = create_engine("sqlite://", echo=False)
108+
SQLModel.metadata.create_all(engine)
109+
110+
with Session(engine) as session:
111+
dept = Department(name="Engineering")
112+
session.add(dept)
113+
session.commit()
114+
session.refresh(dept)
115+
116+
emp = Employee(name="Alice", department_id=dept.id)
117+
session.add(emp)
118+
session.commit()
119+
session.refresh(emp)
120+
121+
# Verify relationship loading
122+
statement = select(Employee).where(Employee.name == "Alice")
123+
loaded_emp = session.exec(statement).first()
124+
assert loaded_emp is not None
125+
assert loaded_emp.department_id == dept.id

0 commit comments

Comments
 (0)