]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Restore object to the identity_map upon delete() unconditionally
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 27 Oct 2016 13:51:50 +0000 (09:51 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 27 Oct 2016 13:51:50 +0000 (09:51 -0400)
Fixed regression caused by :ticket:`2677` whereby calling
:meth:`.Session.delete` on an object that was already flushed as
deleted in that session would fail to set up the object in the
identity map (or reject the object), causing flush errors as the
object were in a state not accommodated by the unit of work.
The pre-1.1 behavior in this case has been restored, which is that
the object is put back into the identity map so that the DELETE
statement will be attempted again, which emits a warning that the number
of expected rows was not matched (unless the row were restored outside
of the session).

Change-Id: I9a8871f82cb1ebe67a7ad54d888d5ee835a9a40a
Fixes: #3839
doc/build/changelog/changelog_11.rst
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/unitofwork.py
test/orm/test_session.py
test/orm/test_unitofworkv2.py

index 70db53d2f12c74fbb0ff52473a93ef2dd832a357..dd8b1b45ec43fca0d7689b6f14ed2208179d309c 100644 (file)
 .. changelog::
     :version: 1.1.3
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 3839
+
+        Fixed regression caused by :ticket:`2677` whereby calling
+        :meth:`.Session.delete` on an object that was already flushed as
+        deleted in that session would fail to set up the object in the
+        identity map (or reject the object), causing flush errors as the
+        object were in a state not accommodated by the unit of work.
+        The pre-1.1 behavior in this case has been restored, which is that
+        the object is put back into the identity map so that the DELETE
+        statement will be attempted again, which emits a warning that the number
+        of expected rows was not matched (unless the row were restored outside
+        of the session).
+
     .. change::
         :tags: bug, postgresql
         :tickets: 3835
index b492cbb7f3df81fbc425171d8134e3af56d1d519..b39ba1465a4af72465e1704cc7a281b73b58e837 100644 (file)
@@ -1726,8 +1726,9 @@ class Session(_SessionClassMethods):
         if state in self._deleted:
             return
 
+        self.identity_map.add(state)
+
         if to_attach:
-            self.identity_map.add(state)
             self._after_attach(state, obj)
 
         if head:
@@ -2196,7 +2197,8 @@ class Session(_SessionClassMethods):
         for state in proc:
             is_orphan = (
                 _state_mapper(state)._is_orphan(state) and state.has_identity)
-            flush_context.register_object(state, isdelete=is_orphan)
+            _reg = flush_context.register_object(state, isdelete=is_orphan)
+            assert _reg, "Failed to add object to the flush context!"
             processed.add(state)
 
         # put all remaining deletes into the flush context.
@@ -2205,7 +2207,8 @@ class Session(_SessionClassMethods):
         else:
             proc = deleted.difference(processed)
         for state in proc:
-            flush_context.register_object(state, isdelete=True)
+            _reg = flush_context.register_object(state, isdelete=True)
+            assert _reg, "Failed to add object to the flush context!"
 
         if not flush_context.has_work:
             return
index f3e39d9b5ac691c2c0d38047f41c0a9821749413..de4842d6fad61112dc3eeca4ba467c4c001d39c5 100644 (file)
@@ -242,6 +242,9 @@ class UOWTransaction(object):
                         listonly=False, cancel_delete=False,
                         operation=None, prop=None):
         if not self.session._contains_state(state):
+            # this condition is normal when objects are registered
+            # as part of a relationship cascade operation.  it should
+            # not occur for the top-level register from Session.flush().
             if not state.deleted and operation is not None:
                 util.warn("Object of type %s not in session, %s operation "
                           "along '%s' will not proceed" %
index caeb08530b28997b28e1c3ab154b997c7463c0ed..24666d084c1bd8fca704e9b9f43daa60efc248f2 100644 (file)
@@ -1,5 +1,5 @@
 from sqlalchemy.testing import eq_, assert_raises, \
-    assert_raises_message
+    assert_raises_message, assertions
 from sqlalchemy.testing.util import gc_collect
 from sqlalchemy.testing import pickleable
 from sqlalchemy.util import pickle
@@ -347,6 +347,37 @@ class SessionStateTest(_fixtures.FixtureTest):
 
         eq_(sess.query(User).count(), 1)
 
+    def test_deleted_adds_to_imap_unconditionally(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+
+        sess = Session()
+        u1 = User(name='u1')
+        sess.add(u1)
+        sess.commit()
+
+        sess.delete(u1)
+        sess.flush()
+
+        # object is not in session
+        assert u1 not in sess
+
+        # but it *is* attached
+        assert u1._sa_instance_state.session_id == sess.hash_key
+
+        # mark as deleted again
+        sess.delete(u1)
+
+        # in the session again
+        assert u1 in sess
+
+        # commit proceeds w/ warning
+        with assertions.expect_warnings(
+                "DELETE statement on table 'users' "
+                r"expected to delete 1 row\(s\); 0 were matched."):
+            sess.commit()
+
     def test_autoflush_expressions(self):
         """test that an expression which is dependent on object state is
         evaluated after the session autoflushes.   This is the lambda
index ae8454f6f3d47ce6ad351e05ef15a13975349f49..b2fefe6167623dc80f7f4ed9d0378d089975f2bc 100644 (file)
@@ -1566,6 +1566,26 @@ class BasicStaleChecksTest(fixtures.MappedTest):
                     sess.flush
                 )
 
+    @testing.requires.sane_multi_rowcount
+    def test_delete_twice(self):
+        Parent, Child = self._fixture()
+        sess = Session()
+        p1 = Parent(id=1, data=2, child=None)
+        sess.add(p1)
+        sess.commit()
+
+        sess.delete(p1)
+        sess.flush()
+
+        sess.delete(p1)
+
+        assert_raises_message(
+            exc.SAWarning,
+            "DELETE statement on table 'parent' expected to "
+            "delete 1 row\(s\); 0 were matched.",
+            sess.commit
+        )
+
     @testing.requires.sane_multi_rowcount
     def test_delete_multi_missing_warning(self):
         Parent, Child = self._fixture()