SQLModel inheritance breaks sqlalchemy mutation tracking #1558
-
First Check
Commit to Help
Example Code#!/usr/bin/env python3
from typing import Optional, Any
import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict, MutableSet
from sqlalchemy.orm import sessionmaker
from sqlalchemy import JSON, Column
from sqlmodel import SQLModel, Field, create_engine
Session = sessionmaker()
Base = declarative_base()
class BadTrackBase(SQLModel):
custom: dict[str, Any] = Field(
sa_column=Column(MutableDict.as_mutable(JSON(none_as_null=True))), default={}
)
class BadTrack(BadTrackBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class Track(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
custom: dict[str, Any] = Field(
sa_column=Column(MutableDict.as_mutable(JSON(none_as_null=True))), default={}
)
engine = create_engine("sqlite:///:memory:")
Session.configure(bind=engine)
SQLModel.metadata.create_all(engine)
session = Session()
good_track = Track(id=1, custom={"test": "good"})
bad_track = BadTrack(id=1, custom={"test": "bad"})
session.add(good_track)
session.add(bad_track)
session.commit()
good_track.custom["test"] = "changed"
bad_track.custom["test"] = "changed"
assert good_track in session.dirty
assert bad_track in session.dirty DescriptionWhen setting a field to one of the fields supported by sqlalchemy mutation tracking, it seems the mutation tracking isn't working as intended. I attached the above code sample to illustrate what I mean. The assertions on the bottom of the script should pass under normal circumstance. Operating SystemLinux Operating System DetailsNo response SQLModel Version0.08 Python Version3.9.13 Additional ContextNo response |
Beta Was this translation helpful? Give feedback.
Replies: 10 comments 1 reply
-
I have the same issue in SQLModel 0.08 and it's kind of a big deal. Is there a workaround for this besides re-assigning the entire dict i.e. |
Beta Was this translation helpful? Give feedback.
-
Any update here, A big blocker before I can migrate from sqlalchemy+pydantic to sqlmodel. |
Beta Was this translation helpful? Give feedback.
-
Also a blocker here, commenting to stay in the loop! |
Beta Was this translation helpful? Give feedback.
-
yep, same here, blocker. |
Beta Was this translation helpful? Give feedback.
-
Blocker here as well. Thanks for your post, saved me from burning any more time on figuring out what was going on with mutation tracking. |
Beta Was this translation helpful? Give feedback.
-
Problem still present with sqlmodel 0.0.22. |
Beta Was this translation helpful? Give feedback.
-
I tested it and that's what I found:
good_track.custom["test"] = "changed first time"
assert good_track in session.dirty # good_track is in dirty
bad_track.custom["test"] = "changed first time"
assert bad_track in session.dirty # bad_track is in dirty
assert good_track not in session.dirty # good_track is NOT in dirty anymore !!! At the same time, mutation is handled correctly - if you commit, the object will be updated. session.commit()
session.flush()
good_track_db = session.get(Track, 1)
bad_track_db = session.get(BadTrack, 1)
assert good_track_db.custom["test"] == "changed first time" # Mutation worked out
assert bad_track_db.custom["test"] == "changed first time" # Mutation worked out Also, it only happens if you add object and commit, then mutate it. good_track_db = session.get(Track, 1)
bad_track_db = session.get(BadTrack, 1)
good_track_db.custom["test"] = "changed"
bad_track_db.custom["test"] = "changed"
assert good_track_db in session.dirty
assert bad_track_db in session.dirty Full code in details: from typing import Any, Optional
from sqlalchemy import JSON, Column
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import sessionmaker
from sqlmodel import Field, SQLModel, create_engine
Session = sessionmaker()
class BadTrackBase(SQLModel):
custom: dict[str, Any] = Field(
sa_column=Column(MutableDict.as_mutable(JSON(none_as_null=True))), default={}
)
class BadTrack(BadTrackBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class Track(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
custom: dict[str, Any] = Field(
sa_column=Column(MutableDict.as_mutable(JSON(none_as_null=True))), default={}
)
engine = create_engine("sqlite:///:memory:")
Session.configure(bind=engine)
SQLModel.metadata.create_all(engine)
session = Session()
# ====================================================================================
good_track = Track(id=1, custom={"test": "good"})
bad_track = BadTrack(id=1, custom={"test": "bad"})
session.add(good_track)
session.add(bad_track)
session.commit()
good_track.custom["test"] = "changed first time"
assert good_track in session.dirty # good_track is in dirty
bad_track.custom["test"] = "changed first time"
assert bad_track in session.dirty # bad_track is in dirty
assert good_track not in session.dirty # good_track is NOT in dirty anymore !!!
session.commit()
session.flush()
good_track_db = session.get(Track, 1)
bad_track_db = session.get(BadTrack, 1)
assert good_track_db.custom["test"] == "changed first time" # Mutation worked out
assert bad_track_db.custom["test"] == "changed first time" # Mutation worked out
# ====================================================================================
# But now session.dirty works fine
good_track_db.custom["test"] = "changed"
assert good_track_db in session.dirty, session.dirty
bad_track_db.custom["test"] = "changed"
assert good_track_db in session.dirty, session.dirty
assert bad_track_db in session.dirty So, I think there is no issue here. The @jtpavlock, do you think we can close this issue now? |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
Why close this issue? The issue documents broken behavior. Things like |
Beta Was this translation helpful? Give feedback.
-
@YuriiMotov came back to say that the "weird" behavior isn't actually broken or weird. I think it's the expected behavior when autoflush is enabled (which it is by default). With |
Beta Was this translation helpful? Give feedback.
I tested it and that's what I found:
session.dirty
behaves a bit strange, but it seems that it's not related to inheritance.If you try mutating only one of
good_track
orbad_track
it will work well (object will be insession.dirty
as expected).But after you mutate second object, first one is not in
session.dirty
anymore... But second object is INsession.dirty
.At the same time, mutation is h…