summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-10-03 10:52:14 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2012-10-03 10:52:14 -0400
commitd778b2d6fb1533091d7e6c0ae5184eef337dbab4 (patch)
treed4c2d0d961a165158f5e51deb2722d5948e49991
parent0bb5a9eab829f9a4cfda3c37cdf2202d84e55f3f (diff)
downloadsqlalchemy-d778b2d6fb1533091d7e6c0ae5184eef337dbab4.tar.gz
- [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]
-rw-r--r--CHANGES12
-rw-r--r--lib/sqlalchemy/orm/session.py21
-rw-r--r--test/orm/test_transaction.py49
3 files changed, 79 insertions, 3 deletions
diff --git a/CHANGES b/CHANGES
index 470c9f96f..de5368067 100644
--- 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
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 9b0c31e6a..faa9e5a83 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -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):
diff --git a/test/orm/test_transaction.py b/test/orm/test_transaction.py
index d2b8b8b85..49fdd2864 100644
--- a/test/orm/test_transaction.py
+++ b/test/orm/test_transaction.py
@@ -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