]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- An instance which is moved to "transient", has
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 30 Jun 2010 20:12:32 +0000 (16:12 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 30 Jun 2010 20:12:32 +0000 (16:12 -0400)
an incomplete or missing set of primary key
attributes, and contains expired attributes, will
raise an InvalidRequestError if an expired attribute
is accessed, instead of getting a recursion overflow.

- make_transient() removes all "loader" callables from
the state being made transient, removing any
"expired" state - all unloaded attributes reset back
to undefined, None/empty on access.

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

diff --git a/CHANGES b/CHANGES
index aba313a7ce7c5c741d616dfcb5028cc56d8c4cab..721cc4d7321750b9db7fed3b376d597332a088e7 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -50,6 +50,20 @@ CHANGES
     those configured on the mapper.  False will make it
     as though order_by() was never called, while
     None is an active setting.
+
+  - An instance which is moved to "transient", has
+    an incomplete or missing set of primary key 
+    attributes, and contains expired attributes, will
+    raise an InvalidRequestError if an expired attribute
+    is accessed, instead of getting a recursion overflow.
+
+  - The make_transient() function is now in the generated
+    documentation.
+    
+  - make_transient() removes all "loader" callables from
+    the state being made transient, removing any
+    "expired" state - all unloaded attributes reset back
+    to undefined, None/empty on access.
     
 - sql
   - The warning emitted by the Unicode and String types
index 935cbc35d79539a417ceb6efdc7081ab1ad6c1f7..b122b495101c033a1e6d3919dd4cf628beef49ec 100644 (file)
@@ -2407,6 +2407,14 @@ def _load_scalar_attributes(state, attribute_names):
         if has_key:
             identity_key = state.key
         else:
+            # this codepath is rare - only valid when inside a flush, and the
+            # object is becoming persistent but hasn't yet been assigned an identity_key.
+            # check here to ensure we have the attrs we need.
+            pk_attrs = [mapper._get_col_to_prop(col).key for col in mapper.primary_key]
+            if state.expired_attributes.intersection(pk_attrs):
+                raise sa_exc.InvalidRequestError("Instance %s cannot be refreshed - it's not "
+                                                " persistent and does not "
+                                                "contain a full primary key." % state_str(state))
             identity_key = mapper._identity_key_from_state(state)
         
         if (_none_set.issubset(identity_key) and \
index af646aa5f538144f93a7cef56db4b2f582707362..ead860ebd3fadc1095983dbe1839dda6bd937b06 100644 (file)
@@ -1592,11 +1592,19 @@ def make_transient(instance):
     such that it's as though the object were newly constructed,
     except retaining its values.
     
+    Attributes which were "expired" or deferred at the
+    instance level are reverted to undefined, and 
+    will not trigger any loads.
+    
     """
     state = attributes.instance_state(instance)
     s = _state_session(state)
     if s:
         s._expunge_state(state)
+
+    # remove expired state and 
+    # deferred callables
+    state.callables.clear()
     del state.key
     
     
index 926ae062998f07de16f2fd08dab2880b0b6a9973..c0c96f8730ed7e1000cdd2e69e2f77be02d82b1e 100644 (file)
@@ -221,7 +221,23 @@ class ExpireTest(_fixtures.FixtureTest):
         assert 'name' not in u.__dict__
         sess.add(u)
         assert u.name == 'jack'
+    
+    @testing.resolve_artifact_names
+    def test_no_instance_key_no_pk(self):
+        # same as test_no_instance_key, but the PK columns
+        # are absent.  ensure an error is raised.
+        mapper(User, users)
+        sess = create_session()
+        u = sess.query(User).get(7)
 
+        sess.expire(u, attribute_names=['name', 'id'])
+        sess.expunge(u)
+        attributes.instance_state(u).key = None
+        assert 'name' not in u.__dict__
+        sess.add(u)
+        assert_raises(sa_exc.InvalidRequestError, getattr, u, 'name')
+        
+        
     @testing.resolve_artifact_names
     def test_expire_preserves_changes(self):
         """test that the expire load operation doesn't revert post-expire changes"""
index fca3bf757e8aed9b00aa40de1d17cf6aec15e135..36709dbcb3e86dd8c5916189278c7087ceecf54c 100644 (file)
@@ -250,6 +250,15 @@ class SessionTest(_fixtures.FixtureTest):
         sess.add(u1)
         assert u1 in sess.new
         
+        # test expired attributes 
+        # get unexpired
+        u1 = sess.query(User).first()
+        sess.expire(u1)
+        make_transient(u1)
+        assert u1.id is None
+        assert u1.name is None
+
+        
     @testing.resolve_artifact_names
     def test_autoflush_expressions(self):
         """test that an expression which is dependent on object state is