]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Use coercion rules for aliased() against select/union constructs
authorRens Groothuijsen <l.groothuijsen@alumni.maastrichtuniversity.nl>
Thu, 16 Jul 2026 00:03:25 +0000 (20:03 -0400)
committerMichael Bayer <mike_mp@zzzcomputing.com>
Thu, 16 Jul 2026 02:03:00 +0000 (02:03 +0000)
Calling :func:`_orm.aliased` against a :func:`_sql.select` or
:func:`_sql.union` / :class:`_sql.CompoundSelect` construct, which
previously failed with an obscure ``AttributeError`` regarding a missing
``.mapper`` attribute, now raises when using SQLAlchemy 2.1, and emits a
deprecation warning under SQLAlchemy 2.0 as it coerces the construct into a
subquery instead.  This matches the behavior of other similar implicit
SELECT-to-FROM coercions.  Pull request courtesy Rens Groothuijsen.

Fixes: #6274
Closes: #12433
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12433
Pull-request-sha: 416dde8509ac05b209400603d9e00bc82aa47c79

Change-Id: I63bdec71074b81fb85bf29e6ca0dd81cbe3f8cb3
(cherry picked from commit 16177b8c73e492e5adaad8f51095f9981831da41)

doc/build/changelog/unreleased_20/6274.rst [new file with mode: 0644]
lib/sqlalchemy/orm/util.py
test/orm/test_deprecations.py
test/orm/test_utils.py

diff --git a/doc/build/changelog/unreleased_20/6274.rst b/doc/build/changelog/unreleased_20/6274.rst
new file mode 100644 (file)
index 0000000..20f120b
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 6274
+
+    Calling :func:`_orm.aliased` against a :func:`_sql.select` or
+    :func:`_sql.union` / :class:`_sql.CompoundSelect` construct, which
+    previously failed with an obscure ``AttributeError`` regarding a missing
+    ``.mapper`` attribute, now raises when using SQLAlchemy 2.1, and emits a
+    deprecation warning under SQLAlchemy 2.0 as it coerces the construct into a
+    subquery instead.  This matches the behavior of other similar implicit
+    SELECT-to-FROM coercions.  Pull request courtesy Rens Groothuijsen.
index d665b1848e16479d44e0f83f9d43e786b8347a4a..4ea6809448adad0630afc1ca690ce0c8086eb5cc 100644 (file)
@@ -85,6 +85,7 @@ from ..sql.cache_key import MemoizedHasCacheKey
 from ..sql.elements import ColumnElement
 from ..sql.elements import KeyedColumnElement
 from ..sql.selectable import FromClause
+from ..sql.selectable import GenerativeSelect
 from ..util.langhelpers import MemoizedSlots
 from ..util.typing import de_stringify_annotation as _de_stringify_annotation
 from ..util.typing import eval_name_only as _eval_name_only
@@ -1020,7 +1021,9 @@ class AliasedInsp(
         flat: bool = False,
         adapt_on_names: bool = False,
     ) -> Union[AliasedClass[_O], FromClause]:
-        if isinstance(element, FromClause):
+        if isinstance(element, GenerativeSelect):
+            return coercions.expect(roles.FromClauseRole, element, flat=flat)
+        elif isinstance(element, FromClause):
             if adapt_on_names:
                 raise sa_exc.ArgumentError(
                     "adapt_on_names only applies to ORM elements"
index bf545d6ad99d4ec04ae6fae3553c4ee1206b74fb..0d9aac5639a019f059a6bd8a8baf406a65f7f910 100644 (file)
@@ -18,6 +18,7 @@ from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import testing
 from sqlalchemy import text
+from sqlalchemy import union
 from sqlalchemy.engine import default
 from sqlalchemy.engine import result_tuple
 from sqlalchemy.orm import aliased
@@ -523,6 +524,36 @@ class DeprecatedQueryTest(_fixtures.FixtureTest, AssertsCompiledSQL):
                 "ON users.id = anon_1.user_id",
             )
 
+    def test_aliased_select_deprecated(self):
+        """test #6274"""
+
+        User = self.classes.User
+
+        with self._expect_implicit_subquery():
+            q1 = aliased(select(User.id, User.name))
+
+        self.assert_compile(
+            select(q1),
+            "SELECT anon_1.id, anon_1.name FROM "
+            "(SELECT users.id AS id, users.name AS name FROM users) AS anon_1",
+        )
+
+    def test_aliased_compound_select_deprecated(self):
+        """test #6274"""
+
+        User = self.classes.User
+
+        s1 = select(User.id)
+        s2 = select(User.name)
+        with self._expect_implicit_subquery():
+            q1 = aliased(union(s1, s2))
+
+        self.assert_compile(
+            select(q1),
+            "SELECT anon_1.id FROM (SELECT users.id AS id FROM users "
+            "UNION SELECT users.name AS name FROM users) AS anon_1",
+        )
+
     def test_invalid_column(self):
         User = self.classes.User
 
index a685274d094ae5666c2794b5205586b4b92794bd..6c06d1b07ca781d7c471f5f2770f6d4e0d88d901 100644 (file)
@@ -583,6 +583,22 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL):
             inspect(Point),
         )
 
+    def test_aliased_select_subquery(self):
+        """test for #6274"""
+
+        class Point:
+            pass
+
+        self._fixture(Point)
+
+        subq = select(Point.x).filter(Point.id == 1).subquery()
+        q1 = aliased(subq, name="point_alias")
+        self.assert_compile(
+            select(q1),
+            "SELECT point_alias.x FROM (SELECT point.x AS x "
+            "FROM point WHERE point.id = :id_1) AS point_alias",
+        )
+
 
 class IdentityKeyTest(_fixtures.FixtureTest):
     run_inserts = None