]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
use the options from the cached statement for propagate_options
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 12 Dec 2021 18:37:21 +0000 (13:37 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 12 Dec 2021 19:23:03 +0000 (14:23 -0500)
Fixed caching-related issue where the use of a loader option of the form
``lazyload(aliased(A).bs).joinedload(B.cs)`` would fail to result in the
joinedload being invoked for runs subsequent to the query being cached, due
to a mismatch for the options / object path applied to the objects loaded
for a query with a lead entity that used ``aliased()``.

Fixes: #7447
Change-Id: I4e9c34654b7d3668cd8878decbd688afe2af5f81

doc/build/changelog/unreleased_14/7447.rst [new file with mode: 0644]
lib/sqlalchemy/orm/context.py
test/orm/test_eager_relations.py

diff --git a/doc/build/changelog/unreleased_14/7447.rst b/doc/build/changelog/unreleased_14/7447.rst
new file mode 100644 (file)
index 0000000..3f954fa
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, orm, regression
+    :tickets: 7447
+
+    Fixed caching-related issue where the use of a loader option of the form
+    ``lazyload(aliased(A).bs).joinedload(B.cs)`` would fail to result in the
+    joinedload being invoked for runs subsequent to the query being cached, due
+    to a mismatch for the options / object path applied to the objects loaded
+    for a query with a lead entity that used ``aliased()``.
+
index 8e3dc3134bc7e0b0d83b2d6d98f23fd1d295e851..61b95728057b4b5b02cf1f94d6fe6efed063b371 100644 (file)
@@ -106,7 +106,27 @@ class QueryContext:
         self.params = params
 
         self.propagated_loader_options = {
-            o for o in statement._with_options if o.propagate_to_loaders
+            # issue 7447.
+            # propagated loader options will be present on loaded InstanceState
+            # objects under state.load_options and are typically used by
+            # LazyLoader to apply options to the SELECT statement it emits.
+            # For compile state options (i.e. loader strategy options), these
+            # need to line up with the ".load_path" attribute which in
+            # loader.py is pulled from context.compile_state.current_path.
+            # so, this means these options have to be the ones from the
+            # *cached* statement that's travelling with compile_state, not the
+            # *current* statement which won't match up for an ad-hoc
+            # AliasedClass
+            cached_o
+            for cached_o in compile_state.select_statement._with_options
+            if cached_o.propagate_to_loaders and cached_o._is_compile_state
+        } | {
+            # for user defined loader options that are not "compile state",
+            # those just need to be present as they are
+            uncached_o
+            for uncached_o in statement._with_options
+            if uncached_o.propagate_to_loaders
+            and not uncached_o._is_compile_state
         }
 
         self.attributes = dict(compile_state.attributes)
index 9cefb51cbf898e06136ad3d462e7beccd19630dd..4bd0f14171de70e30e15475fb16029d8e4d99328 100644 (file)
@@ -6068,153 +6068,179 @@ class LazyLoadOptSpecificityTest(fixtures.DeclarativeMappedTest):
 
     def test_lazyload_aliased_abs_bcs_one(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(joinedload(A.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 3)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(joinedload(A.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 3)
 
     def test_lazyload_aliased_abs_bcs_two(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(defaultload(A.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 3)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(defaultload(A.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 3)
 
     def test_pathed_lazyload_aliased_abs_bcs(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        opt = Load(A).joinedload(A.bs).joinedload(B.cs)
 
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(opt)
-        )
-        self._run_tests(q, 3)
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            opt = Load(A).joinedload(A.bs).joinedload(B.cs)
+
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(opt)
+            )
+            self._run_tests(q, 3)
 
     def test_pathed_lazyload_plus_joined_aliased_abs_bcs(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        opt = Load(aa).defaultload(aa.bs).joinedload(B.cs)
 
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(opt)
-        )
-        self._run_tests(q, 2)
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            opt = Load(aa).defaultload(aa.bs).joinedload(B.cs)
+
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(opt)
+            )
+            self._run_tests(q, 2)
 
     def test_pathed_joinedload_aliased_abs_bcs(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        opt = Load(aa).joinedload(aa.bs).joinedload(B.cs)
 
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(opt)
-        )
-        self._run_tests(q, 1)
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            opt = Load(aa).joinedload(aa.bs).joinedload(B.cs)
+
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(opt)
+            )
+            self._run_tests(q, 1)
 
     def test_lazyload_plus_joined_aliased_abs_bcs(self):
+        """by running the test twice, this test includes a test
+        for #7447 to ensure cached queries apply the cached option objects
+        to the InstanceState which line up with the cached current_path."""
+
         A, B, C = self.classes("A", "B", "C")
 
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(defaultload(aa.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 2)
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(defaultload(aa.bs).joinedload(B.cs))
+            )
+
+            self._run_tests(q, 2)
 
     def test_joinedload_aliased_abs_bcs(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(aa, A)
-            .filter(aa.id == 1)
-            .filter(A.id == 2)
-            .filter(aa.id != A.id)
-            .options(joinedload(aa.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 1)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(aa, A)
+                .filter(aa.id == 1)
+                .filter(A.id == 2)
+                .filter(aa.id != A.id)
+                .options(joinedload(aa.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 1)
 
     def test_lazyload_unaliased_abs_bcs_one(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(A, aa)
-            .filter(aa.id == 2)
-            .filter(A.id == 1)
-            .filter(aa.id != A.id)
-            .options(joinedload(aa.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 3)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(A, aa)
+                .filter(aa.id == 2)
+                .filter(A.id == 1)
+                .filter(aa.id != A.id)
+                .options(joinedload(aa.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 3)
 
     def test_lazyload_unaliased_abs_bcs_two(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(A, aa)
-            .filter(aa.id == 2)
-            .filter(A.id == 1)
-            .filter(aa.id != A.id)
-            .options(defaultload(aa.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 3)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(A, aa)
+                .filter(aa.id == 2)
+                .filter(A.id == 1)
+                .filter(aa.id != A.id)
+                .options(defaultload(aa.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 3)
 
     def test_lazyload_plus_joined_unaliased_abs_bcs(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(A, aa)
-            .filter(aa.id == 2)
-            .filter(A.id == 1)
-            .filter(aa.id != A.id)
-            .options(defaultload(A.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 2)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(A, aa)
+                .filter(aa.id == 2)
+                .filter(A.id == 1)
+                .filter(aa.id != A.id)
+                .options(defaultload(A.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 2)
 
     def test_joinedload_unaliased_abs_bcs(self):
         A, B, C = self.classes("A", "B", "C")
-        s = fixture_session()
-        aa = aliased(A)
-        q = (
-            s.query(A, aa)
-            .filter(aa.id == 2)
-            .filter(A.id == 1)
-            .filter(aa.id != A.id)
-            .options(joinedload(A.bs).joinedload(B.cs))
-        )
-        self._run_tests(q, 1)
+
+        for i in range(2):
+            s = fixture_session()
+            aa = aliased(A)
+            q = (
+                s.query(A, aa)
+                .filter(aa.id == 2)
+                .filter(A.id == 1)
+                .filter(aa.id != A.id)
+                .options(joinedload(A.bs).joinedload(B.cs))
+            )
+            self._run_tests(q, 1)
 
 
 class EntityViaMultiplePathTestThree(fixtures.DeclarativeMappedTest):