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-Tag: rel_2_0_52~18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=06c8bae6ad1156b5748e370953bea55eb0168333;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 (cherry picked from commit 16177b8c73e492e5adaad8f51095f9981831da41) --- 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 d665b1848e..4ea6809448 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -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" diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index bf545d6ad9..0d9aac5639 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -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 diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py index a685274d09..6c06d1b07c 100644 --- a/test/orm/test_utils.py +++ b/test/orm/test_utils.py @@ -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