summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_12.rst9
-rw-r--r--lib/sqlalchemy/orm/persistence.py12
-rw-r--r--test/orm/test_unitofwork.py53
3 files changed, 70 insertions, 4 deletions
diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst
index b53ed4530..24c525f85 100644
--- a/doc/build/changelog/changelog_12.rst
+++ b/doc/build/changelog/changelog_12.rst
@@ -13,6 +13,15 @@
.. changelog::
:version: 1.2.0b1
+ .. change:: 3984
+ :tags: bug, orm
+ :tickets: 3984
+
+ Fixed bug where a :func:`.column_property` that is also marked as
+ "deferred" would be marked as "expired" during a flush, causing it
+ to be loaded along with the unexpiry of regular attributes even
+ though this attribute was never accessed.
+
.. change:: 3873
:tags: bug, sql
:tickets: 3873
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index 5dc5a90b1..588a1d696 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -982,8 +982,16 @@ def _finalize_insert_update_commands(base_mapper, uowtransaction, states):
if mapper._readonly_props:
readonly = state.unmodified_intersection(
- [p.key for p in mapper._readonly_props
- if p.expire_on_flush or p.key not in state.dict]
+ [
+ p.key for p in mapper._readonly_props
+ if (
+ p.expire_on_flush and
+ (not p.deferred or p.key in state.dict)
+ ) or (
+ not p.expire_on_flush and
+ not p.deferred and p.key not in state.dict
+ )
+ ]
)
if readonly:
state._expire_attributes(state.dict, readonly)
diff --git a/test/orm/test_unitofwork.py b/test/orm/test_unitofwork.py
index ea24d4bf0..74014781b 100644
--- a/test/orm/test_unitofwork.py
+++ b/test/orm/test_unitofwork.py
@@ -1078,7 +1078,7 @@ class ColumnPropertyTest(fixtures.MappedTest):
})
self._test(True)
- def test_no_refresh(self):
+ def test_no_refresh_ro_column_property_no_expire_on_flush(self):
Data, data = self.classes.Data, self.tables.data
mapper(Data, data, properties={
@@ -1088,6 +1088,36 @@ class ColumnPropertyTest(fixtures.MappedTest):
})
self._test(False)
+ def test_no_refresh_ro_column_property_expire_on_flush(self):
+ Data, data = self.classes.Data, self.tables.data
+
+ mapper(Data, data, properties={
+ 'aplusb': column_property(
+ data.c.a + literal_column("' '") + data.c.b,
+ expire_on_flush=True)
+ })
+ self._test(True)
+
+ def test_no_refresh_ro_deferred_no_expire_on_flush(self):
+ Data, data = self.classes.Data, self.tables.data
+
+ mapper(Data, data, properties={
+ 'aplusb': column_property(
+ data.c.a + literal_column("' '") + data.c.b,
+ expire_on_flush=False, deferred=True)
+ })
+ self._test(False, expect_deferred_load=True)
+
+ def test_no_refresh_ro_deferred_expire_on_flush(self):
+ Data, data = self.classes.Data, self.tables.data
+
+ mapper(Data, data, properties={
+ 'aplusb': column_property(
+ data.c.a + literal_column("' '") + data.c.b,
+ expire_on_flush=True, deferred=True)
+ })
+ self._test(True, expect_deferred_load=True)
+
def test_refreshes_post_init(self):
Data, data = self.classes.Data, self.tables.data
@@ -1115,7 +1145,7 @@ class ColumnPropertyTest(fixtures.MappedTest):
sess.flush()
eq_(sd1.aplusb, "hello there")
- def _test(self, expect_expiry):
+ def _test(self, expect_expiry, expect_deferred_load=False):
Data = self.classes.Data
sess = create_session()
@@ -1138,6 +1168,25 @@ class ColumnPropertyTest(fixtures.MappedTest):
sess.flush()
eq_(d1.aplusb, "im setting this explicitly")
+ # test issue #3984.
+ # NOTE: if we only expire_all() here rather than start with brand new
+ # 'd1', d1.aplusb since it was loaded moves into "expired" and stays
+ # "undeferred". this is questionable but not as severe as the never-
+ # loaded attribute being loaded during an unexpire.
+
+ sess.close()
+ d1 = sess.query(Data).first()
+
+ d1.b = 'so long'
+ sess.flush()
+ sess.expire_all()
+ eq_(d1.b, 'so long')
+ if expect_deferred_load:
+ eq_('aplusb' in d1.__dict__, False)
+ else:
+ eq_('aplusb' in d1.__dict__, True)
+ eq_(d1.aplusb, "hello so long")
+
class OneToManyTest(_fixtures.FixtureTest):
run_inserts = None