From 04421c8bed9e93a625b7164e99eb1ee0395bebfe Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 12 Dec 2021 13:37:21 -0500 Subject: [PATCH] use the options from the cached statement for propagate_options 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 | 10 + lib/sqlalchemy/orm/context.py | 22 +- test/orm/test_eager_relations.py | 252 ++++++++++++--------- 3 files changed, 170 insertions(+), 114 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/7447.rst diff --git a/doc/build/changelog/unreleased_14/7447.rst b/doc/build/changelog/unreleased_14/7447.rst new file mode 100644 index 0000000000..3f954faba4 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7447.rst @@ -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()``. + diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 8e3dc3134b..61b9572805 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -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) diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index 9cefb51cbf..4bd0f14171 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -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): -- 2.47.2