Mike Bayer avatar Mike Bayer committed 32ea0c2

- [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]

Comments (0)

Files changed (3)

 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

lib/sqlalchemy/orm/session.py

             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:
         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
             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
 
                     # 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)
             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
             )
         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):

test/orm/test_transaction.py

 
         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
+
+
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.