.. changelog::
:version: 1.1.3
+ .. change::
+ :tags: bug, orm
+ :tickets: 3839
+
+ Fixed regression caused by :ticket:`2677` whereby calling
+ :meth:`.Session.delete` on an object that was already flushed as
+ deleted in that session would fail to set up the object in the
+ identity map (or reject the object), causing flush errors as the
+ object were in a state not accommodated by the unit of work.
+ The pre-1.1 behavior in this case has been restored, which is that
+ the object is put back into the identity map so that the DELETE
+ statement will be attempted again, which emits a warning that the number
+ of expected rows was not matched (unless the row were restored outside
+ of the session).
+
.. change::
:tags: bug, postgresql
:tickets: 3835
if state in self._deleted:
return
+ self.identity_map.add(state)
+
if to_attach:
- self.identity_map.add(state)
self._after_attach(state, obj)
if head:
for state in proc:
is_orphan = (
_state_mapper(state)._is_orphan(state) and state.has_identity)
- flush_context.register_object(state, isdelete=is_orphan)
+ _reg = flush_context.register_object(state, isdelete=is_orphan)
+ assert _reg, "Failed to add object to the flush context!"
processed.add(state)
# put all remaining deletes into the flush context.
else:
proc = deleted.difference(processed)
for state in proc:
- flush_context.register_object(state, isdelete=True)
+ _reg = flush_context.register_object(state, isdelete=True)
+ assert _reg, "Failed to add object to the flush context!"
if not flush_context.has_work:
return
listonly=False, cancel_delete=False,
operation=None, prop=None):
if not self.session._contains_state(state):
+ # this condition is normal when objects are registered
+ # as part of a relationship cascade operation. it should
+ # not occur for the top-level register from Session.flush().
if not state.deleted and operation is not None:
util.warn("Object of type %s not in session, %s operation "
"along '%s' will not proceed" %
from sqlalchemy.testing import eq_, assert_raises, \
- assert_raises_message
+ assert_raises_message, assertions
from sqlalchemy.testing.util import gc_collect
from sqlalchemy.testing import pickleable
from sqlalchemy.util import pickle
eq_(sess.query(User).count(), 1)
+ def test_deleted_adds_to_imap_unconditionally(self):
+ users, User = self.tables.users, self.classes.User
+
+ mapper(User, users)
+
+ sess = Session()
+ u1 = User(name='u1')
+ sess.add(u1)
+ sess.commit()
+
+ sess.delete(u1)
+ sess.flush()
+
+ # object is not in session
+ assert u1 not in sess
+
+ # but it *is* attached
+ assert u1._sa_instance_state.session_id == sess.hash_key
+
+ # mark as deleted again
+ sess.delete(u1)
+
+ # in the session again
+ assert u1 in sess
+
+ # commit proceeds w/ warning
+ with assertions.expect_warnings(
+ "DELETE statement on table 'users' "
+ r"expected to delete 1 row\(s\); 0 were matched."):
+ sess.commit()
+
def test_autoflush_expressions(self):
"""test that an expression which is dependent on object state is
evaluated after the session autoflushes. This is the lambda
sess.flush
)
+ @testing.requires.sane_multi_rowcount
+ def test_delete_twice(self):
+ Parent, Child = self._fixture()
+ sess = Session()
+ p1 = Parent(id=1, data=2, child=None)
+ sess.add(p1)
+ sess.commit()
+
+ sess.delete(p1)
+ sess.flush()
+
+ sess.delete(p1)
+
+ assert_raises_message(
+ exc.SAWarning,
+ "DELETE statement on table 'parent' expected to "
+ "delete 1 row\(s\); 0 were matched.",
+ sess.commit
+ )
+
@testing.requires.sane_multi_rowcount
def test_delete_multi_missing_warning(self):
Parent, Child = self._fixture()