]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- An object that's been deleted now gets a flag
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 26 Aug 2010 15:32:50 +0000 (11:32 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 26 Aug 2010 15:32:50 +0000 (11:32 -0400)
'deleted', which prohibits the object from
being re-add()ed to the session, as previously
the object would live in the identity map
silently until its attributes were accessed.
The make_transient() function now resets this
flag along with the "key" flag.

- make_transient() can be safely called on an
already transient instance.

CHANGES
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/state.py
test/orm/test_expire.py
test/orm/test_session.py

diff --git a/CHANGES b/CHANGES
index 8fe4c49f5635629c47f4e6d99e3e046d3a91dc8c..6ac7acd74c0d16fe54c92db604aac51603425dd3 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -15,6 +15,17 @@ CHANGES
     ConcurrentModificationError in an "except:"
     clause.
     
+  - An object that's been deleted now gets a flag
+    'deleted', which prohibits the object from
+    being re-add()ed to the session, as previously
+    the object would live in the identity map
+    silently until its attributes were accessed.
+    The make_transient() function now resets this
+    flag along with the "key" flag.
+
+  - make_transient() can be safely called on an
+    already transient instance.
+    
   - a warning is emitted in mapper() if the polymorphic_on
     column is not present either in direct or derived
     form in the mapped selectable or in the 
index 86d5dd7730d5769f246c550d2bd95fe6a8dabff4..06d5b89a1457299d2345cb4193312fefda4f7096 100644 (file)
@@ -278,8 +278,11 @@ class SessionTransaction(object):
 
         for s in set(self._new).union(self.session._new):
             self.session._expunge_state(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)
 
         assert not self.session._deleted
@@ -1102,6 +1105,7 @@ class Session(object):
 
         self.identity_map.discard(state)
         self._deleted.pop(state, None)
+        state.deleted = True
 
     def _save_without_cascade(self, instance):
         """Used by scoping.py to save on init without cascade."""
@@ -1309,7 +1313,13 @@ class Session(object):
             raise sa_exc.InvalidRequestError(
                 "Instance '%s' is not persisted" %
                 mapperutil.state_str(state))
-
+        
+        if state.deleted:
+            raise sa_exc.InvalidRequestError(
+                "Instance '%s' has been deleted.  Use the make_transient() "
+                "function to send this object back to the transient state." %
+                mapperutil.state_str(state)
+            )
         self._attach(state)
         self._deleted.pop(state, None)
         self.identity_map.add(state)
@@ -1655,7 +1665,9 @@ def make_transient(instance):
     This will remove its association with any 
     session and additionally will remove its "identity key",
     such that it's as though the object were newly constructed,
-    except retaining its values.
+    except retaining its values.   It also resets the
+    "deleted" flag on the state if this object
+    had been explicitly deleted by its session.
     
     Attributes which were "expired" or deferred at the
     instance level are reverted to undefined, and 
@@ -1670,8 +1682,10 @@ def make_transient(instance):
     # remove expired state and 
     # deferred callables
     state.callables.clear()
-    del state.key
-    
+    if state.key:
+        del state.key
+    if state.deleted:
+        del state.deleted
     
 def object_session(instance):
     """Return the ``Session`` to which instance belongs.
index 82e7e91301d1a92366e0555b422148515514a49f..f6828f5a9a5e0db4bfa68da8565588a357d1ea04 100644 (file)
@@ -22,6 +22,7 @@ class InstanceState(object):
     _strong_obj = None
     modified = False
     expired = False
+    deleted = False
     
     def __init__(self, obj, manager):
         self.class_ = obj.__class__
index c0c96f8730ed7e1000cdd2e69e2f77be02d82b1e..c8bdf1719e5f9d2b9b660cd1bb60228f3a391d69 100644 (file)
@@ -89,6 +89,10 @@ class ExpireTest(_fixtures.FixtureTest):
         assert s.query(User).get(10) is None
         assert u not in s # and expunges
 
+        # trick the "deleted" flag so we can re-add for the sake
+        # of this test
+        del attributes.instance_state(u).deleted
+        
         # add it back
         s.add(u)
         # nope, raises ObjectDeletedError
index 779db304e2b485c9300ed8811e059324a8625800..4976db1311ad617b72d1f42d9ab01822b2462c96 100644 (file)
@@ -280,6 +280,52 @@ class SessionTest(_fixtures.FixtureTest):
         assert u1.id is None
         assert u1.name is None
 
+        # works twice
+        make_transient(u1)
+        
+        sess.close()
+        
+        u1.name = 'test2'
+        sess.add(u1)
+        sess.flush()
+        assert u1 in sess
+        sess.delete(u1)
+        sess.flush()
+        assert u1 not in sess
+        
+        assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+        make_transient(u1)
+        sess.add(u1)
+        sess.flush()
+        assert u1 in sess
+
+    @testing.resolve_artifact_names
+    def test_deleted_flag(self):
+        mapper(User, users)
+        
+        sess = sessionmaker()()
+        
+        u1 = User(name='u1')
+        sess.add(u1)
+        sess.commit()
+        
+        sess.delete(u1)
+        sess.flush()
+        assert u1 not in sess
+        assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+        sess.rollback()
+        assert u1 in sess
+        
+        sess.delete(u1)
+        sess.commit()
+        assert u1 not in sess
+        assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
+        
+        make_transient(u1)
+        sess.add(u1)
+        sess.commit()
+        
+        eq_(sess.query(User).count(), 1)
         
     @testing.resolve_artifact_names
     def test_autoflush_expressions(self):