diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 4e80cdc374..6a6012ee91 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -341,7 +341,7 @@ def sqlmodel_validate( for key in new_obj.__sqlmodel_relationships__: value = getattr(use_obj, key, Undefined) if value is not Undefined: - setattr(new_obj, key, value) + new_obj.__dict__[key] = value return new_obj def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: diff --git a/tests/test_validation.py b/tests/test_validation.py index 3265922070..92f57c5d5e 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import List, Optional import pytest from pydantic.error_wrappers import ValidationError -from sqlmodel import SQLModel +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.main import Field, Relationship from .conftest import needs_pydanticv1, needs_pydanticv2 @@ -63,3 +64,73 @@ def reject_none(cls, v): with pytest.raises(ValidationError): Hero.model_validate({"name": None, "age": 25}) + + +@needs_pydanticv1 +def test_validation_related_object_not_in_session_pydantic_v1(clear_sqlmodel): + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + heroes: List["Hero"] = Relationship(back_populates="team") + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + team = Team(name="team") + hero = Hero(name="hero", team=team) + with Session(engine) as session: + session.add(team) + session.add(hero) + session.commit() + + with Session(engine) as session: + hero = session.get(Hero, 1) + assert session._is_clean() + + new_hero = Hero.validate(hero) + + assert session._is_clean() + # The new hero is a different instance, but the team is the same + assert id(new_hero) != id(hero) + assert id(new_hero.team) == id(hero.team) + + +@needs_pydanticv2 +def test_validation_related_object_not_in_session_pydantic_v2(clear_sqlmodel): + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + heroes: List["Hero"] = Relationship(back_populates="team") + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + team = Team(name="team") + hero = Hero(name="hero", team=team) + with Session(engine) as session: + session.add(team) + session.add(hero) + session.commit() + + with Session(engine) as session: + hero = session.get(Hero, 1) + assert session._is_clean() + + new_hero = Hero.model_validate(hero) + + assert session._is_clean() + # The new hero is a different instance, but the team is the same + assert id(new_hero) != id(hero) + assert id(new_hero.team) == id(hero.team)