]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Restore detached object logic for dynamic, but warn
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 4 May 2021 15:19:00 +0000 (11:19 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 4 May 2021 15:25:12 +0000 (11:25 -0400)
Fixed regression involving ``lazy='dynamic'`` loader in conjunction with a
detached object. The previous behavior was that the dynamic loader upon
calling methods like ``.all()`` returns empty lists for detached objects
without error, this has been restored; however a warning is now emitted as
this is not the correct result. Other dynamic loader scenarios correctly
raise ``DetachedInstanceError``.

Fixes: #6426
Change-Id: Id7ad204bef947491fa7e462c5acda2055fada910

doc/build/changelog/unreleased_14/6426.rst [new file with mode: 0644]
lib/sqlalchemy/engine/result.py
lib/sqlalchemy/orm/dynamic.py
test/orm/test_dynamic.py

diff --git a/doc/build/changelog/unreleased_14/6426.rst b/doc/build/changelog/unreleased_14/6426.rst
new file mode 100644 (file)
index 0000000..d0a3cc2
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, regression, orm
+    :tickets: 6426
+
+    Fixed regression involving ``lazy='dynamic'`` loader in conjunction with a
+    detached object. The previous behavior was that the dynamic loader upon
+    calling methods like ``.all()`` returns empty lists for detached objects
+    without error, this has been restored; however a warning is now emitted as
+    this is not the correct result. Other dynamic loader scenarios correctly
+    raise ``DetachedInstanceError``.
index f02ceff156be90b1c569cfd1983b9f5c0a14691f..a8039594191e394f280ca77809c3529be72715d2 100644 (file)
@@ -1593,10 +1593,17 @@ class IteratorResult(Result):
 
     """
 
-    def __init__(self, cursor_metadata, iterator, raw=None):
+    def __init__(
+        self,
+        cursor_metadata,
+        iterator,
+        raw=None,
+        _source_supports_scalars=False,
+    ):
         self._metadata = cursor_metadata
         self.iterator = iterator
         self.raw = raw
+        self._source_supports_scalars = _source_supports_scalars
 
     def _soft_close(self, **kw):
         self.iterator = iter([])
index a4b5f58c72ca984da2f985d098c48df260ff6948..ac7eba03b8bfde0f9575730cf3b3d8617e58d8af 100644 (file)
@@ -24,6 +24,7 @@ from .query import Query
 from .. import exc
 from .. import log
 from .. import util
+from ..engine import result
 
 
 @log.class_logger
@@ -324,17 +325,28 @@ class AppenderMixin(object):
 
     session = property(session, lambda s, x: None)
 
-    def __iter__(self):
+    def _iter(self):
         sess = self.session
         if sess is None:
-            return iter(
+            state = attributes.instance_state(self.instance)
+            if state.detached:
+                util.warn(
+                    "Instance %s is detached, dynamic relationship cannot "
+                    "return a correct result.   This warning will become "
+                    "a DetachedInstanceError in a future release."
+                    % (orm_util.state_str(state))
+                )
+
+            return result.IteratorResult(
+                result.SimpleResultMetaData([self.attr.class_.__name__]),
                 self.attr._get_collection_history(
                     attributes.instance_state(self.instance),
                     attributes.PASSIVE_NO_INITIALIZE,
-                ).added_items
-            )
+                ).added_items,
+                _source_supports_scalars=True,
+            ).scalars()
         else:
-            return iter(self._generate(sess))
+            return self._generate(sess)._iter()
 
     def __getitem__(self, index):
         sess = self.session
index 8ea04c2689941e1310a0628d5405c0ad9af5d6eb..e87b5a363611d2c82ef31346234f9851d53094ce 100644 (file)
@@ -233,6 +233,8 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL):
         )
 
     def test_detached_raise(self):
+        """so filtering on a detached dynamic list raises an error..."""
+
         User, Address = self._user_address_fixture()
         sess = fixture_session()
         u = sess.query(User).get(8)
@@ -243,6 +245,35 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL):
             email_address="e",
         )
 
+    def test_detached_all_empty_list(self):
+        """test #6426 - but you can call .all() on it and you get an empty
+        list.   This is legacy stuff, as this should be raising
+        DetachedInstanceError.
+
+        """
+
+        User, Address = self._user_address_fixture()
+        sess = fixture_session()
+        u = sess.query(User).get(8)
+        sess.expunge(u)
+
+        with testing.expect_warnings(
+            r"Instance <User .*> is detached, dynamic relationship"
+        ):
+            eq_(u.addresses.all(), [])
+
+        with testing.expect_warnings(
+            r"Instance <User .*> is detached, dynamic relationship"
+        ):
+            eq_(list(u.addresses), [])
+
+    def test_transient_all_empty_list(self):
+        User, Address = self._user_address_fixture()
+        u1 = User()
+        eq_(u1.addresses.all(), [])
+
+        eq_(list(u1.addresses), [])
+
     def test_no_uselist_false(self):
         User, Address = self._user_address_fixture(
             addresses_args={"uselist": False}