=======
CHANGES
=======
+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
if not self._is_transaction_boundary:
self._new = self._parent._new
self._deleted = self._parent._deleted
+ self._key_switches = self._parent._key_switches
return
if not self.session._flushing:
self._new = weakref.WeakKeyDictionary()
self._deleted = weakref.WeakKeyDictionary()
+ self._key_switches = weakref.WeakKeyDictionary()
def _restore_snapshot(self):
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)
self._new[state] = state.obj()
state.insert_order = len(self._new)
- 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
"function to send this object back to the transient state." %
mapperutil.state_str(state)
)
+ if discard_existing:
+ existing = self.identity_map.get(state.key)
+ if existing is not None:
+ self.identity_map.discard(attributes.instance_state(existing))
self._attach(state)
self._deleted.pop(state, None)
self.identity_map.add(state)
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