make_transient_to_detached expires deferred attrs making them load on refresh

Issue #4084 new
Mike Bayer repo owner created an issue
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import Session, deferred
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.session import make_transient_to_detached


Base = declarative_base()


class MyTable(Base):
    __tablename__ = 'my_table'

    id = Column(Integer, primary_key=True)
    undeferred = Column(String)
    deferred_column = deferred(Column(String))

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
e.execute(
    "insert into my_table (id, undeferred, "
    "deferred_column) values (1, 'foo', 'bar')")

s = Session(e)


def expire_via_detached():
    item = MyTable(id=1)

    make_transient_to_detached(item)
    s.add(item)
    item.undeferred
    assert 'deferred_column' not in item.__dict__
    s.close()


def expire_normally():
    item = s.query(MyTable).first()
    s.expire(item)
    item.undeferred
    assert 'deferred_column' not in item.__dict__
    s.close()


def expire_explicit_attrs():
    item = s.query(MyTable).first()
    s.expire(item, ['undeferred', 'deferred_column'])
    item.undeferred
    assert 'deferred_column' in item.__dict__
    s.close()

expire_normally()
expire_explicit_attrs()
expire_via_detached()

this is due to state._expire(state, state.unloaded) in make_transient_to_pending(). When a deferred attribute is explicitly expired, it becomes part of the next full load.

Comments (3)

  1. Mike Bayer reporter
    diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
    index 359370ab5..0287f1cfb 100644
    --- a/lib/sqlalchemy/orm/session.py
    +++ b/lib/sqlalchemy/orm/session.py
    @@ -3037,7 +3037,7 @@ def make_transient_to_detached(instance):
         if state._deleted:
             del state._deleted
         state._commit_all(state.dict)
    -    state._expire_attributes(state.dict, state.unloaded)
    +    state._expire_attributes(state.dict, state.unloaded_expirable)
    
    
     def object_session(instance):
    diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py
    index 2e53fe9e3..4964c22e6 100644
    --- a/lib/sqlalchemy/orm/state.py
    +++ b/lib/sqlalchemy/orm/state.py
    @@ -610,6 +610,7 @@ class InstanceState(interfaces.InspectionAttr):
         def unmodified_intersection(self, keys):
             """Return self.unmodified.intersection(keys)."""
    
    +
             return set(keys).intersection(self.manager).\
                 difference(self.committed_state)
    
    @@ -626,6 +627,18 @@ class InstanceState(interfaces.InspectionAttr):
                 difference(self.dict)
    
         @property
    +    def unloaded_expirable(self):
    +        """Return the set of keys which do not have a loaded value.
    +
    +        This includes expired attributes and any other attribute that
    +        was never populated or modified.
    +
    +        """
    +        return self.unloaded.intersection(
    +            attr for attr in self.manager
    +            if self.manager[attr].impl.expire_missing)
    +
    +    @property
         def _unloaded_non_object(self):
             return self.unloaded.intersection(
                 attr for attr in self.manager
    
  2. Log in to comment