]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Don't expire "deferred" attributes in make_transient_to_detached
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 26 Sep 2017 19:33:04 +0000 (15:33 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 26 Sep 2017 19:33:04 +0000 (15:33 -0400)
Fixed issue where the :func:`.make_transient_to_detached` function
would expire all attributes on the target object, including "deferred"
attributes, which has the effect of the attribute being undeferred
for the next refesh, causing an unexpected load of the attribute.

Change-Id: I82a385e3033e3f3c31569b1e908efb5f258d0f27
Fixes: #4084
doc/build/changelog/unreleased_12/4084.rst [new file with mode: 0644]
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/state.py
test/orm/test_expire.py

diff --git a/doc/build/changelog/unreleased_12/4084.rst b/doc/build/changelog/unreleased_12/4084.rst
new file mode 100644 (file)
index 0000000..b7b924e
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 4084
+
+    Fixed issue where the :func:`.make_transient_to_detached` function
+    would expire all attributes on the target object, including "deferred"
+    attributes, which has the effect of the attribute being undeferred
+    for the next refesh, causing an unexpected load of the attribute.
index 359370ab54be96d2ec8b5fc6e0ac42bb6a0d73c0..0287f1cfb7294057087726c8ff0523de13cf76af 100644 (file)
@@ -3037,7 +3037,7 @@ def make_transient_to_detached(instance):
     if state._deleted:
         del state._deleted
     state._commit_all(state.dict)
-    state._expire_attributes(state.dict, state.unloaded)
+    state._expire_attributes(state.dict, state.unloaded_expirable)
 
 
 def object_session(instance):
index 2e53fe9e34c9b3339a16189492e789f0f59ae7c0..4964c22e6594aa307c2857e343dee8e0ee50cc88 100644 (file)
@@ -610,6 +610,7 @@ class InstanceState(interfaces.InspectionAttr):
     def unmodified_intersection(self, keys):
         """Return self.unmodified.intersection(keys)."""
 
+
         return set(keys).intersection(self.manager).\
             difference(self.committed_state)
 
@@ -625,6 +626,18 @@ class InstanceState(interfaces.InspectionAttr):
             difference(self.committed_state).\
             difference(self.dict)
 
+    @property
+    def unloaded_expirable(self):
+        """Return the set of keys which do not have a loaded value.
+
+        This includes expired attributes and any other attribute that
+        was never populated or modified.
+
+        """
+        return self.unloaded.intersection(
+            attr for attr in self.manager
+            if self.manager[attr].impl.expire_missing)
+
     @property
     def _unloaded_non_object(self):
         return self.unloaded.intersection(
index c6ecd1f35f9262e9f25f248ec3e486bc83bb7106..e7ef20e7b56eac2123761082122ddc5d3f819486 100644 (file)
@@ -13,6 +13,7 @@ from sqlalchemy.orm import mapper, relationship, create_session, \
 from sqlalchemy.testing import fixtures
 from test.orm import _fixtures
 from sqlalchemy.sql import select
+from sqlalchemy.orm import make_transient_to_detached
 
 
 class ExpireTest(_fixtures.FixtureTest):
@@ -1035,6 +1036,46 @@ class ExpireTest(_fixtures.FixtureTest):
             .expired_attributes
         assert 'addresses' not in attributes.instance_state(u1).callables
 
+    def test_deferred_expire_w_transient_to_detached(self):
+        orders, Order = self.tables.orders, self.classes.Order
+        mapper(Order, orders, properties={
+            "description": deferred(orders.c.description)
+        })
+
+        s = Session()
+        item = Order(id=1)
+
+        make_transient_to_detached(item)
+        s.add(item)
+        item.isopen
+        assert 'description' not in item.__dict__
+
+    def test_deferred_expire_normally(self):
+        orders, Order = self.tables.orders, self.classes.Order
+        mapper(Order, orders, properties={
+            "description": deferred(orders.c.description)
+        })
+
+        s = Session()
+
+        item = s.query(Order).first()
+        s.expire(item)
+        item.isopen
+        assert 'description' not in item.__dict__
+
+    def test_deferred_expire_explicit_attrs(self):
+        orders, Order = self.tables.orders, self.classes.Order
+        mapper(Order, orders, properties={
+            "description": deferred(orders.c.description)
+        })
+
+        s = Session()
+
+        item = s.query(Order).first()
+        s.expire(item, ['isopen', 'description'])
+        item.isopen
+        assert 'description' in item.__dict__
+
 
 class PolymorphicExpireTest(fixtures.MappedTest):
     run_inserts = 'once'