]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Check for object was expunged before restoring after pk switch + rollback
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 4 Jan 2018 19:09:32 +0000 (14:09 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 4 Jan 2018 19:11:00 +0000 (14:11 -0500)
Fixed bug where an object that is expunged during a rollback of
a nested or subtransaction which also had its primary key mutated
would not be correctly removed from the session, causing subsequent
issues in using the session.

Change-Id: I57e2888902015d67ee11857e44382818f1d2f8bc
Fixes: #4151
(cherry picked from commit 5811276bb7515af3418a6d20f5213d658e320121)

doc/build/changelog/unreleased_11/4151.rst [new file with mode: 0644]
lib/sqlalchemy/orm/session.py
test/orm/test_transaction.py

diff --git a/doc/build/changelog/unreleased_11/4151.rst b/doc/build/changelog/unreleased_11/4151.rst
new file mode 100644 (file)
index 0000000..f498959
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 4151
+    :versions: 1.2.1
+
+    Fixed bug where an object that is expunged during a rollback of
+    a nested or subtransaction which also had its primary key mutated
+    would not be correctly removed from the session, causing subsequent
+    issues in using the session.
index 96944f47896d84bbaba9af5f7a16e6d9f981b87c..c74850bf6691e45697de339cb8645c621add4655 100644 (file)
@@ -339,14 +339,20 @@ class SessionTransaction(object):
         """
         assert self._is_transaction_boundary
 
-        self.session._expunge_states(
-            set(self._new).union(self.session._new),
-            to_transient=True)
+        to_expunge = set(self._new).union(self.session._new)
+        self.session._expunge_states(to_expunge, to_transient=True)
 
         for s, (oldkey, newkey) in self._key_switches.items():
+            # we probably can do this conditionally based on
+            # if we expunged or not, but safe_discard does that anyway
             self.session.identity_map.safe_discard(s)
+
+            # restore the old key
             s.key = oldkey
-            self.session.identity_map.replace(s)
+
+            # now restore the object, but only if we didn't expunge
+            if s not in to_expunge:
+                self.session.identity_map.replace(s)
 
         for s in set(self._deleted).union(self.session._deleted):
             self.session._update_impl(s, revert_deletion=True)
index c4f6a1eaf0c04ba97e9d47d31009405ce1e027c5..2b623db01322fa2bbbe32e8fa312d25187a5d2a3 100644 (file)
@@ -1600,6 +1600,34 @@ class NaturalPKRollbackTest(fixtures.MappedTest):
         assert s.identity_map[(User, ('u1',))] is u1
         assert s.identity_map[(User, ('u2',))] is u2
 
+    @testing.requires.savepoints
+    def test_key_replaced_by_update_nested(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+
+        u1 = User(name='u1')
+
+        s = Session()
+        s.add(u1)
+        s.commit()
+
+        with s.begin_nested():
+            u2 = User(name='u2')
+            s.add(u2)
+            s.flush()
+
+            u2.name = 'u3'
+
+        s.rollback()
+
+        assert u1 in s
+        assert u2 not in s
+
+        u1.name = 'u5'
+
+        s.commit()
+
     def test_multiple_key_replaced_by_update(self):
         users, User = self.tables.users, self.classes.User