From 5abb036e9b9eba0f61cf9617dea2d879c2d5b09c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 26 Sep 2017 15:33:04 -0400 Subject: [PATCH] Don't expire "deferred" attributes in make_transient_to_detached 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 | 8 +++++ lib/sqlalchemy/orm/session.py | 2 +- lib/sqlalchemy/orm/state.py | 13 +++++++ test/orm/test_expire.py | 41 ++++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 doc/build/changelog/unreleased_12/4084.rst diff --git a/doc/build/changelog/unreleased_12/4084.rst b/doc/build/changelog/unreleased_12/4084.rst new file mode 100644 index 0000000000..b7b924e005 --- /dev/null +++ b/doc/build/changelog/unreleased_12/4084.rst @@ -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. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 359370ab54..0287f1cfb7 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -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): diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 2e53fe9e34..4964c22e65 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -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( diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py index c6ecd1f35f..e7ef20e7b5 100644 --- a/test/orm/test_expire.py +++ b/test/orm/test_expire.py @@ -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' -- 2.47.3