From f16402b9b878c0b2bd54b1c25621f150e12033c5 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 31 May 2023 22:36:00 +0200 Subject: [PATCH] Guard against mapped classes in map_imperatively. Added a check to prevent invocation of meth:`_orm.registry.map_imperatively` using a mapped class as paramref:`_orm.registry.map_imperatively.local_table`. Fixes: #9869 Change-Id: Id404f75bf0af69eea942ad71662593bdafbd92ce --- doc/build/changelog/unreleased_20/9869.rst | 7 ++ lib/sqlalchemy/orm/mapper.py | 5 +- lib/sqlalchemy/sql/coercions.py | 3 +- test/orm/declarative/test_basic.py | 89 ++++++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 doc/build/changelog/unreleased_20/9869.rst diff --git a/doc/build/changelog/unreleased_20/9869.rst b/doc/build/changelog/unreleased_20/9869.rst new file mode 100644 index 0000000000..8dd258a833 --- /dev/null +++ b/doc/build/changelog/unreleased_20/9869.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: bug, orm + :tickets: 9869 + + Added a check to prevent invocation of + :meth:`_orm.registry.map_imperatively` using a mapped class as + :paramref:`_orm.registry.map_imperatively.local_table`. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 7bc5de449d..a54c9e9707 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -754,7 +754,10 @@ class Mapper( if local_table is not None: self.local_table = coercions.expect( - roles.StrictFromClauseRole, local_table + roles.StrictFromClauseRole, + local_table, + disable_inspection=True, + argname="local_table", ) elif self.inherits: # note this is a new flow as of 2.0 so that diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index ead564ea30..c4d340713b 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -335,6 +335,7 @@ def expect( apply_propagate_attrs: Optional[ClauseElement] = None, argname: Optional[str] = None, post_inspect: bool = False, + disable_inspection: bool = False, **kw: Any, ) -> Any: if ( @@ -398,7 +399,7 @@ def expect( break if not is_clause_element: - if impl._use_inspection: + if impl._use_inspection and not disable_inspection: insp = inspection.inspect(element, raiseerr=False) if insp is not None: if post_inspect: diff --git a/test/orm/declarative/test_basic.py b/test/orm/declarative/test_basic.py index a76e375b22..5ba1034543 100644 --- a/test/orm/declarative/test_basic.py +++ b/test/orm/declarative/test_basic.py @@ -3227,3 +3227,92 @@ class NamedAttrOrderingTest(fixtures.TestBase): stmt = select(new_cls) eq_(stmt.selected_columns.keys(), col_names_only) + + @testing.variation( + "mapping_style", + [ + "decl_base_fn", + "decl_base_base", + "decl_base_no_meta", + "map_declaratively", + "decorator", + "mapped_as_dataclass", + ], + ) + def test_no_imperative_with_declarative_table(self, mapping_style): + if mapping_style.decl_base_fn: + Base = declarative_base() + + class DecModel(Base): + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + elif mapping_style.decl_base_base: + + class Base(DeclarativeBase): + pass + + class DecModel(Base): + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + elif mapping_style.decl_base_no_meta: + + class Base(DeclarativeBaseNoMeta): + pass + + class DecModel(Base): + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + elif mapping_style.decorator: + r = registry() + + @r.mapped + class DecModel: + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + elif mapping_style.map_declaratively: + + class DecModel: + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + registry().map_declaratively(DecModel) + elif mapping_style.decorator: + r = registry() + + @r.mapped + class DecModel: + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + elif mapping_style.mapped_as_dataclass: + r = registry() + + @r.mapped_as_dataclass + class DecModel: + __tablename__ = "foo" + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] + + else: + assert False + + class ImpModel: + id: int + data: str + + with expect_raises_message( + exc.ArgumentError, + "FROM expression, such as a Table or alias.. object expected " + "for argument 'local_table'; got", + ): + registry().map_imperatively(ImpModel, DecModel) -- 2.47.2