From: Rens Groothuijsen Date: Thu, 16 Jul 2026 00:03:25 +0000 (-0400) Subject: Use coercion rules for aliased() against select/union constructs X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=16177b8c73e492e5adaad8f51095f9981831da41;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Use coercion rules for aliased() against select/union constructs 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 --- diff --git a/doc/build/changelog/unreleased_20/6274.rst b/doc/build/changelog/unreleased_20/6274.rst new file mode 100644 index 0000000000..20f120b980 --- /dev/null +++ b/doc/build/changelog/unreleased_20/6274.rst @@ -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. diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index f333802622..b811a70afd 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -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" diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py index a685274d09..daee82e558 100644 --- a/test/orm/test_utils.py +++ b/test/orm/test_utils.py @@ -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