]> 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)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 16 Jul 2026 01:52:41 +0000 (21:52 -0400)
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

doc/build/changelog/unreleased_20/6274.rst [new file with mode: 0644]
lib/sqlalchemy/orm/util.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 f3338026228d387d344e08394333d3f731833aa8..b811a70afd7020f4db1780a7e9c520651e367aca 100644 (file)
@@ -88,6 +88,7 @@ from ..sql.elements import ColumnElement
 from ..sql.elements import KeyedColumnElement
 from ..sql.schema import MetaData
 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
@@ -1025,7 +1026,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 a685274d094ae5666c2794b5205586b4b92794bd..daee82e5580d3b5a707aab055881ebd275359fa5 100644 (file)
@@ -9,7 +9,9 @@ from sqlalchemy import MetaData
 from sqlalchemy import select
 from sqlalchemy import Table
 from sqlalchemy import testing
+from sqlalchemy import union
 from sqlalchemy.engine import result
+from sqlalchemy.exc import ArgumentError
 from sqlalchemy.ext.hybrid import hybrid_method
 from sqlalchemy.ext.hybrid import hybrid_property
 from sqlalchemy.orm import aliased
@@ -28,6 +30,7 @@ from sqlalchemy.testing import assert_raises
 from sqlalchemy.testing import AssertsCompiledSQL
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import expect_raises
+from sqlalchemy.testing import expect_raises_message
 from sqlalchemy.testing import expect_warnings
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import is_
@@ -583,6 +586,50 @@ class AliasedClassTest(fixtures.MappedTest, AssertsCompiledSQL):
             inspect(Point),
         )
 
+    def test_aliased_select_raises(self):
+        """test for #6274"""
+
+        class Point:
+            pass
+
+        self._fixture(Point)
+
+        with expect_raises_message(
+            ArgumentError, r"use the \.subquery\(\) method"
+        ):
+            aliased(select(Point.x).filter(Point.id == 1))
+
+    def test_aliased_compound_select_raises(self):
+        """test for #6274"""
+
+        class Point:
+            pass
+
+        self._fixture(Point)
+
+        q1 = select(Point.x).filter(Point.id == 1)
+        q2 = select(Point.y).filter(Point.id == 2)
+        with expect_raises_message(
+            ArgumentError, r"use the \.subquery\(\) method"
+        ):
+            aliased(union(q1, q2))
+
+    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