-
-
Notifications
You must be signed in to change notification settings - Fork 826
Description
Description
When sqlmodel_update() receives a BaseModel instance, it iterates over every field and calls setattr for each one — including fields the caller never set. There is no exclude_unset parameter and no awareness of __pydantic_fields_set__.
Reproduction
class HeroUpdate(SQLModel):
name: str | None = None
age: int | None = None
@app.patch("/heroes/{hero_id}")
def update_hero(hero_id: int, hero_update: HeroUpdate, session: Session = Depends(get_session)):
db_hero = session.get(Hero, hero_id)
db_hero.sqlmodel_update(hero_update) # Overwrites age=None even if client only sent name
session.add(db_hero)
session.commit()Client sends {"name": "New Name"}. hero_update.age defaults to None. The method overwrites the hero's existing age with None, silently destroying data.
Current Workaround
db_hero.sqlmodel_update(hero_update.model_dump(exclude_unset=True))This works but is non-obvious. The method's type signature explicitly accepts BaseModel, implying it handles this case correctly.
Proposed Fix
Add an exclude_unset: bool = False parameter to sqlmodel_update(). When True and obj is a BaseModel, filter to only obj.__pydantic_fields_set__ before applying:
def sqlmodel_update(self, obj, *, exclude_unset: bool = False):
if isinstance(obj, BaseModel):
if exclude_unset:
data = obj.model_dump(exclude_unset=True)
else:
data = {field: getattr(obj, field) for field in get_model_fields(obj)}
# ...This is the single most common FastAPI + SQLModel pattern and the silent data corruption is a significant footgun.