From: Mike Bayer Date: Sun, 16 Jan 2022 15:21:45 +0000 (-0500) Subject: enhance double-aliased table logic to handle more cases X-Git-Tag: rel_2_0_0b1~536^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bb11d5b7c2256861fdfe64f5cded94ce15266132;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git enhance double-aliased table logic to handle more cases Fixed ORM regression where calling the :func:`_orm.aliased` function against an existing :func:`_orm.aliased` construct would fail to produce correct SQL if the existing construct were against a fixed table. The fix allows that the original :func:`_orm.aliased` construct is disregarded if it were only against a table that's now being replaced. It also allows for correct behavior when constructing a :func:`_orm.aliased` without a selectable argument against a :func:`_orm.aliased` that's against a subuquery, to create an alias of that subquery (i.e. to change its name). The nesting behavior of :func:`_orm.aliased` remains in place for the case where the outer :func:`_orm.aliased` object is against a subquery which in turn refers to the inner :func:`_orm.aliased` object. This is a relatively new 1.4 feature that helps to suit use cases that were previously served by the deprecated ``Query.from_self()`` method. Fixes: #7576 Change-Id: Ia9ca606f6300e38b6040eb6fc7facfe97c8cf057 --- diff --git a/doc/build/changelog/unreleased_14/7576.rst b/doc/build/changelog/unreleased_14/7576.rst new file mode 100644 index 0000000000..74d8ac4942 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7576.rst @@ -0,0 +1,18 @@ +.. change:: + :tags: bug, orm, regression + :tickets: 7576 + + Fixed ORM regression where calling the :func:`_orm.aliased` function + against an existing :func:`_orm.aliased` construct would fail to produce + correct SQL if the existing construct were against a fixed table. The fix + allows that the original :func:`_orm.aliased` construct is disregarded if + it were only against a table that's now being replaced. It also allows for + correct behavior when constructing a :func:`_orm.aliased` without a + selectable argument against a :func:`_orm.aliased` that's against a + subuquery, to create an alias of that subquery (i.e. to change its name). + + The nesting behavior of :func:`_orm.aliased` remains in place for the case + where the outer :func:`_orm.aliased` object is against a subquery which in + turn refers to the inner :func:`_orm.aliased` object. This is a relatively + new 1.4 feature that helps to suit use cases that were previously served by + the deprecated ``Query.from_self()`` method. diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index c1ce3b8455..e84517670c 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -495,11 +495,20 @@ class AliasedClass: insp = inspection.inspect(mapped_class_or_ac) mapper = insp.mapper + nest_adapters = False + if alias is None: - alias = mapper._with_polymorphic_selectable._anonymous_fromclause( - name=name, - flat=flat, - ) + if insp.is_aliased_class and insp.selectable._is_subquery: + alias = insp.selectable.alias() + else: + alias = ( + mapper._with_polymorphic_selectable._anonymous_fromclause( + name=name, + flat=flat, + ) + ) + elif insp.is_aliased_class: + nest_adapters = True self._aliased_insp = AliasedInsp( self, @@ -516,6 +525,7 @@ class AliasedClass: use_mapper_path, adapt_on_names, represents_outer_join, + nest_adapters, ) self.__name__ = f"aliased({mapper.class_.__name__})" @@ -652,6 +662,7 @@ class AliasedInsp( _use_mapper_path, adapt_on_names, represents_outer_join, + nest_adapters, ): mapped_class_or_ac = inspected.entity @@ -667,6 +678,7 @@ class AliasedInsp( self._base_alias = weakref.ref(_base_alias or self) self._use_mapper_path = _use_mapper_path self.represents_outer_join = represents_outer_join + self._nest_adapters = nest_adapters if with_polymorphic_mappers: self._is_with_polymorphic = True @@ -702,7 +714,7 @@ class AliasedInsp( ], ) - if inspected.is_aliased_class: + if nest_adapters: self._adapter = inspected._adapter.wrap(self._adapter) self._adapt_on_names = adapt_on_names @@ -773,6 +785,7 @@ class AliasedInsp( "base_alias": self._base_alias(), "use_mapper_path": self._use_mapper_path, "represents_outer_join": self.represents_outer_join, + "nest_adapters": self._nest_adapters, } def __setstate__(self, state): @@ -787,6 +800,7 @@ class AliasedInsp( state["use_mapper_path"], state["adapt_on_names"], state["represents_outer_join"], + state["nest_adapters"], ) def _merge_with(self, other): diff --git a/test/orm/test_froms.py b/test/orm/test_froms.py index 6e1c94e12f..9585da125b 100644 --- a/test/orm/test_froms.py +++ b/test/orm/test_froms.py @@ -573,6 +573,16 @@ class EntityFromSubqueryTest(QueryTest, AssertsCompiledSQL): q = s.query(uq1.name, uq2.name).order_by(uq1.name, uq2.name) + self.assert_compile( + q, + "SELECT anon_1.name AS anon_1_name, anon_1.name_1 AS " + "anon_1_name_1 FROM " + "(SELECT users.id AS id, users.name AS name, users_1.id AS id_1, " + "users_1.name AS name_1 FROM users, users AS users_1 " + "WHERE users.id > users_1.id) AS anon_1 " + "ORDER BY anon_1.name, anon_1.name_1", + ) + eq_( q.all(), [ @@ -613,6 +623,158 @@ class EntityFromSubqueryTest(QueryTest, AssertsCompiledSQL): ], ) + def test_nested_aliases_none_to_none(self): + """test #7576""" + + User = self.classes.User + + u1 = aliased(User) + u2 = aliased(u1) + + self.assert_compile( + select(u2), "SELECT users_1.id, users_1.name FROM users AS users_1" + ) + + def test_nested_alias_none_to_subquery(self): + """test #7576""" + + User = self.classes.User + + subq = select(User.id, User.name).subquery() + + u1 = aliased(User, subq) + + self.assert_compile( + select(u1), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1", + ) + + u2 = aliased(u1) + + self.assert_compile( + select(u2), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1", + ) + + def test_nested_alias_subquery_to_subquery_w_replace(self): + """test #7576""" + + User = self.classes.User + + subq = select(User.id, User.name).subquery() + + u1 = aliased(User, subq) + + self.assert_compile( + select(u1), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1", + ) + + u2 = aliased(u1, subq) + + self.assert_compile( + select(u2), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1", + ) + + def test_nested_alias_subquery_to_subquery_w_adaption(self): + """test #7576""" + + User = self.classes.User + + inner_subq = select(User.id, User.name).subquery() + + u1 = aliased(User, inner_subq) + + self.assert_compile( + select(u1), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1", + ) + + outer_subq = select(u1.id, u1.name).subquery() + + u2 = aliased(u1, outer_subq) + + self.assert_compile( + select(u2), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT anon_2.id AS id, anon_2.name AS name FROM " + "(SELECT users.id AS id, users.name AS name FROM users) " + "AS anon_2) AS anon_1", + ) + + outer_subq = ( + select(u1.id, u1.name, User.id, User.name) + .where(u1.id > User.id) + .subquery() + ) + u2 = aliased(u1, outer_subq) + + # query here is: + # SELECT derived_from_inner_subq.id, derived_from_inner_subq.name + # FROM ( + # SELECT ... FROM inner_subq, users WHERE inner_subq.id > users.id + # ) as outer_subq + self.assert_compile( + select(u2), + "SELECT anon_1.id, anon_1.name FROM " + "(SELECT anon_2.id AS id, anon_2.name AS name, users.id AS id_1, " + "users.name AS name_1 FROM " + "(SELECT users.id AS id, users.name AS name FROM users) " + "AS anon_2, users " + "WHERE anon_2.id > users.id) AS anon_1", + ) + + def test_nested_alias_subquery_w_alias_to_none(self): + """test #7576""" + + User = self.classes.User + + u1 = aliased(User) + + self.assert_compile( + select(u1), "SELECT users_1.id, users_1.name FROM users AS users_1" + ) + + subq = ( + select(User.id, User.name, u1.id, u1.name) + .where(User.id > u1.id) + .subquery() + ) + + # aliased against aliased w/ subquery means, look for u1 inside the + # given subquery. adapt that. + u2 = aliased(u1, subq) + + self.assert_compile( + select(u2), + "SELECT anon_1.id_1, anon_1.name_1 FROM " + "(SELECT users.id AS id, users.name AS name, " + "users_1.id AS id_1, users_1.name AS name_1 " + "FROM users, users AS users_1 " + "WHERE users.id > users_1.id) AS anon_1", + ) + + subq = select(User.id, User.name).subquery() + u2 = aliased(u1, subq) + + # given that, it makes sense that if we remove "u1" from the subquery, + # we get a second FROM element like below. + # this is actually a form of the "wrong" query that was + # reported in #7576, but this is the case where we have a subquery, + # so yes, we need to adapt the "inner" alias to it. + + self.assert_compile( + select(u2), + "SELECT users_1.id, users_1.name FROM users AS users_1, " + "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1", + ) + def test_multiple_entities(self): User, Address = self.classes.User, self.classes.Address