]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
enhance double-aliased table logic to handle more cases
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 16 Jan 2022 15:21:45 +0000 (10:21 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Jan 2022 21:07:41 +0000 (16:07 -0500)
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

doc/build/changelog/unreleased_14/7576.rst [new file with mode: 0644]
lib/sqlalchemy/orm/util.py
test/orm/test_froms.py

diff --git a/doc/build/changelog/unreleased_14/7576.rst b/doc/build/changelog/unreleased_14/7576.rst
new file mode 100644 (file)
index 0000000..74d8ac4
--- /dev/null
@@ -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.
index c1ce3b8455676c586297a2b25d66807ca1f017e6..e84517670c8ca1d50a23f63e51ca0a98cff38930 100644 (file)
@@ -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):
index 6e1c94e12f596243afdfc8c968c0347bf2c72d2c..9585da125b41a538821e1e5fdfda301d61971bd6 100644 (file)
@@ -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