]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [bug] Fixed Session accounting bug whereby replacing
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 3 Oct 2012 14:52:14 +0000 (10:52 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 3 Oct 2012 14:52:14 +0000 (10:52 -0400)
    a deleted object in the identity map with another
    object of the same primary key would raise a
   "conflicting state" error on rollback(),
    if the replaced primary key were established either
    via non-unitofwork-established INSERT statement
    or by primary key switch of another instance.
    [ticket:2583]

CHANGES
lib/sqlalchemy/orm/session.py
test/orm/test_transaction.py

diff --git a/CHANGES b/CHANGES
index 470c9f96f2fd2fde66ce8f9a7356abc1a75e1118..de5368067b9de3a4a37852f55da24be33389c793 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -773,6 +773,18 @@ All relevant bug fixes
 and features listed below from version 0.7.7 on
 are also present in 0.8.
 
+0.7.10
+======
+- orm
+  - [bug] Fixed Session accounting bug whereby replacing
+    a deleted object in the identity map with another
+    object of the same primary key would raise a
+    "conflicting state" error on rollback(),
+    if the replaced primary key were established either
+    via non-unitofwork-established INSERT statement
+    or by primary key switch of another instance.
+    [ticket:2583]
+
 0.7.9
 =====
 - orm
index 9b0c31e6a91d1792160b0aed9f426088e3c33a64..faa9e5a83c737ad77c9eae11b98d5159e1027004 100644 (file)
@@ -209,6 +209,7 @@ class SessionTransaction(object):
             self._new = self._parent._new
             self._deleted = self._parent._deleted
             self._dirty = self._parent._dirty
+            self._key_switches = self._parent._key_switches
             return
 
         if not self.session._flushing:
@@ -217,6 +218,7 @@ class SessionTransaction(object):
         self._new = weakref.WeakKeyDictionary()
         self._deleted = weakref.WeakKeyDictionary()
         self._dirty = weakref.WeakKeyDictionary()
+        self._key_switches = weakref.WeakKeyDictionary()
 
     def _restore_snapshot(self, dirty_only=False):
         assert self._is_transaction_boundary
@@ -226,11 +228,16 @@ class SessionTransaction(object):
             if s.key:
                 del s.key
 
+        for s, (oldkey, newkey) in self._key_switches.items():
+            self.session.identity_map.discard(s)
+            s.key = oldkey
+            self.session.identity_map.replace(s)
+
         for s in set(self._deleted).union(self.session._deleted):
             if s.deleted:
                 #assert s in self._deleted
                 del s.deleted
-            self.session._update_impl(s)
+            self.session._update_impl(s, discard_existing=True)
 
         assert not self.session._deleted
 
@@ -1280,6 +1287,11 @@ class Session(_SessionClassMethods):
                     # state has already replaced this one in the identity
                     # map (see test/orm/test_naturalpks.py ReversePKsTest)
                     self.identity_map.discard(state)
+                    if state in self.transaction._key_switches:
+                        orig_key = self.transaction._key_switches[0]
+                    else:
+                        orig_key = state.key
+                    self.transaction._key_switches[state] = (orig_key, instance_key)
                     state.key = instance_key
 
                 self.identity_map.replace(state)
@@ -1558,7 +1570,7 @@ class Session(_SessionClassMethods):
             state.insert_order = len(self._new)
         self._attach(state)
 
-    def _update_impl(self, state):
+    def _update_impl(self, state, discard_existing=False):
         if (self.identity_map.contains_state(state) and
             state not in self._deleted):
             return
@@ -1576,7 +1588,10 @@ class Session(_SessionClassMethods):
             )
         self._before_attach(state)
         self._deleted.pop(state, None)
-        self.identity_map.add(state)
+        if discard_existing:
+            self.identity_map.replace(state)
+        else:
+            self.identity_map.add(state)
         self._attach(state)
 
     def _save_or_update_impl(self, state):
index d2b8b8b8597fcdb776bfe6bf7757abe6cc4b1301..49fdd2864915f8753857a14098575f6623c8fbf7 100644 (file)
@@ -1157,7 +1157,56 @@ class NaturalPKRollbackTest(fixtures.MappedTest):
 
         session.rollback()
 
+    def test_key_replaced_by_update(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+
+        u1 = User(name='u1')
+        u2 = User(name='u2')
+
+        s = Session()
+        s.add_all([u1, u2])
+        s.commit()
+
+        s.delete(u1)
+        s.flush()
+
+        u2.name = 'u1'
+        s.flush()
+
+        assert u1 not in s
+        s.rollback()
+
+        assert u1 in s
+        assert u2 in s
 
+        assert s.identity_map[(User, ('u1',))] is u1
+        assert s.identity_map[(User, ('u2',))] is u2
+
+    def test_key_replaced_by_oob_insert(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+
+        u1 = User(name='u1')
+
+        s = Session()
+        s.add(u1)
+        s.commit()
+
+        s.delete(u1)
+        s.flush()
+
+        s.execute(users.insert().values(name='u1'))
+        u2 = s.query(User).get('u1')
+
+        assert u1 not in s
+        s.rollback()
+
+        assert u1 in s
+        assert u2 not in s
 
+        assert s.identity_map[(User, ('u1',))] is u1