]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed bug where :meth:`.Session.expunge` would not fully detach
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 19 Oct 2014 20:53:45 +0000 (16:53 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 19 Oct 2014 20:53:45 +0000 (16:53 -0400)
the given object if the object had been subject to a delete
operation that was flushed, but not committed.  This would also
affect related operations like :func:`.make_transient`.
fixes #3139

doc/build/changelog/changelog_10.rst
doc/build/changelog/migration_10.rst
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/state.py
test/orm/test_session.py

index 4454dd98a17f04ad4441aca5392ca4ab7d6d6d7c..18742f81e0f7781015e420b83d49eef48d6cd1be 100644 (file)
     series as well.  For changes that are specific to 1.0 with an emphasis
     on compatibility concerns, see :doc:`/changelog/migration_10`.
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 3139
+
+        Fixed bug where :meth:`.Session.expunge` would not fully detach
+        the given object if the object had been subject to a delete
+        operation that was flushed, but not committed.  This would also
+        affect related operations like :func:`.make_transient`.
+
+        .. seealso::
+
+            :ref:`bug_3139`
+
     .. change::
         :tags: bug, orm
         :tickets: 3230
index 3591ee0e2444cea9d61dd0753f6bb0e50100bfc3..c025390d2b11eb3acf146851233bb1abe97a4148 100644 (file)
@@ -927,6 +927,39 @@ symbol, and no change to the object's state occurs.
 
 :ticket:`3061`
 
+.. _bug_3139:
+
+session.expunge() will fully detach an object that's been deleted
+-----------------------------------------------------------------
+
+The behavior of :meth:`.Session.expunge` had a bug that caused an
+inconsistency in behavior regarding deleted objects.  The
+:func:`.object_session` function as well as the :attr:`.InstanceState.session`
+attribute would still report object as belonging to the :class:`.Session`
+subsequent to the expunge::
+
+    u1 = sess.query(User).first()
+    sess.delete(u1)
+
+    sess.flush()
+
+    assert u1 not in sess
+    assert inspect(u1).session is sess  # this is normal before commit
+
+    sess.expunge(u1)
+
+    assert u1 not in sess
+    assert inspect(u1).session is None  # would fail
+
+Note that it is normal for ``u1 not in sess`` to be True while
+``inspect(u1).session`` still refers to the session, while the transaction
+is ongoing subsequent to the delete operation and :meth:`.Session.expunge`
+has not been called; the full detachment normally completes once the
+transaction is committed.  This issue would also impact functions
+that rely on :meth:`.Session.expunge` such as :func:`.make_transient`.
+
+:ticket:`3139`
+
 .. _migration_yield_per_eager_loading:
 
 Joined/Subquery eager loading explicitly disallowed with yield_per
index db9d3a51d860241c6ee3291640cbce9615296cc6..f23983cbcd5ed3d4982ea15557fba8302ac61d44 100644 (file)
@@ -292,7 +292,7 @@ class SessionTransaction(object):
             for s in self.session.identity_map.all_states():
                 s._expire(s.dict, self.session.identity_map._modified)
             for s in self._deleted:
-                s.session_id = None
+                s._detach()
             self._deleted.clear()
         elif self.nested:
             self._parent._new.update(self._new)
@@ -1409,6 +1409,7 @@ class Session(_SessionClassMethods):
             state._detach()
         elif self.transaction:
             self.transaction._deleted.pop(state, None)
+            state._detach()
 
     def _register_newly_persistent(self, states):
         for state in states:
@@ -2449,16 +2450,19 @@ def make_transient_to_detached(instance):
 
 
 def object_session(instance):
-    """Return the ``Session`` to which instance belongs.
+    """Return the :class:`.Session` to which the given instance belongs.
 
-    If the instance is not a mapped instance, an error is raised.
+    This is essentially the same as the :attr:`.InstanceState.session`
+    accessor.  See that attribute for details.
 
     """
 
     try:
-        return _state_session(attributes.instance_state(instance))
+        state = attributes.instance_state(instance)
     except exc.NO_STATE:
         raise exc.UnmappedInstanceError(instance)
+    else:
+        return _state_session(state)
 
 
 _new_sessionid = util.counter()
index 4756f17073ca5a0638db91826ed7c34bbad7ca95..560149de598023e65853044d37006d3c35d0f299 100644 (file)
@@ -145,7 +145,16 @@ class InstanceState(interfaces.InspectionAttr):
     @util.dependencies("sqlalchemy.orm.session")
     def session(self, sessionlib):
         """Return the owning :class:`.Session` for this instance,
-        or ``None`` if none available."""
+        or ``None`` if none available.
+
+        Note that the result here can in some cases be *different*
+        from that of ``obj in session``; an object that's been deleted
+        will report as not ``in session``, however if the transaction is
+        still in progress, this attribute will still refer to that session.
+        Only when the transaction is completed does the object become
+        fully detached under normal circumstances.
+
+        """
         return sessionlib._state_session(self)
 
     @property
index b0b00d5ed35e4c262b8f05fe63bde1c4c99403a9..96728612dbe9612c06705c98a11eaf5b3be21041 100644 (file)
@@ -204,6 +204,7 @@ class SessionUtilTest(_fixtures.FixtureTest):
         sess.flush()
         make_transient(u1)
         sess.rollback()
+        assert attributes.instance_state(u1).transient
 
     def test_make_transient_to_detached(self):
         users, User = self.tables.users, self.classes.User
@@ -661,7 +662,7 @@ class SessionStateTest(_fixtures.FixtureTest):
         go()
         eq_(canary, [False])
 
-    def test_deleted_expunged(self):
+    def test_deleted_auto_expunged(self):
         users, User = self.tables.users, self.classes.User
 
         mapper(User, users)
@@ -682,6 +683,53 @@ class SessionStateTest(_fixtures.FixtureTest):
 
         assert object_session(u1) is None
 
+    def test_explicit_expunge_pending(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+        sess = Session()
+        u1 = User(name='x')
+        sess.add(u1)
+
+        sess.flush()
+        sess.expunge(u1)
+
+        assert u1 not in sess
+        assert object_session(u1) is None
+
+        sess.rollback()
+
+        assert u1 not in sess
+        assert object_session(u1) is None
+
+    def test_explicit_expunge_deleted(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+        sess = Session()
+        sess.add(User(name='x'))
+        sess.commit()
+
+        u1 = sess.query(User).first()
+        sess.delete(u1)
+
+        sess.flush()
+
+        assert was_deleted(u1)
+        assert u1 not in sess
+        assert object_session(u1) is sess
+
+        sess.expunge(u1)
+        assert was_deleted(u1)
+        assert u1 not in sess
+        assert object_session(u1) is None
+
+        sess.rollback()
+        assert was_deleted(u1)
+        assert u1 not in sess
+        assert object_session(u1) is None
+
+
 class SessionStateWFixtureTest(_fixtures.FixtureTest):
     __backend__ = True