From c35921b79e1683d3738a69f8ff6a892c3c2846e2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 10 Oct 2022 14:03:04 -0400 Subject: [PATCH] warn for no polymorphic identity w/ poly hierarchy A warning is emitted when attempting to configure a mapped class within an inheritance hierarchy where the mapper is not given any polymorphic identity, however there is a polymorphic discriminator column assigned. Such classes should be abstract if they never intend to load directly. Fixes: #7545 Change-Id: I94f04e59736c73e3f39d883a75d763e3f06ecc3d --- doc/build/changelog/unreleased_14/7545.rst | 9 +++++++ lib/sqlalchemy/orm/mapper.py | 23 ++++++++++++++--- test/orm/declarative/test_dc_transforms.py | 2 ++ test/orm/declarative/test_inheritance.py | 10 ++++++++ test/orm/declarative/test_typed_mapping.py | 2 ++ test/orm/inheritance/test_basic.py | 30 +++++++++++++++++++++- test/orm/test_events.py | 1 + 7 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/7545.rst diff --git a/doc/build/changelog/unreleased_14/7545.rst b/doc/build/changelog/unreleased_14/7545.rst new file mode 100644 index 0000000000..ea31d1a3c3 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7545.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm + :tickets: 7545 + + A warning is emitted when attempting to configure a mapped class within an + inheritance hierarchy where the mapper is not given any polymorphic + identity, however there is a polymorphic discriminator column assigned. + Such classes should be abstract if they never intend to load directly. + diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 36b97cf17e..5d784498a0 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1224,10 +1224,27 @@ class Mapper( else: self.persist_selectable = self.local_table - if self.polymorphic_identity is not None and not self.concrete: - self._identity_class = self.inherits._identity_class - else: + if self.polymorphic_identity is None: + self._identity_class = self.class_ + + if self.inherits.base_mapper.polymorphic_on is not None: + util.warn( + "Mapper %s does not indicate a polymorphic_identity, " + "yet is part of an inheritance hierarchy that has a " + "polymorphic_on column of '%s'. Objects of this type " + "cannot be loaded polymorphically which can lead to " + "degraded or incorrect loading behavior in some " + "scenarios. Please establish a polmorphic_identity " + "for this class, or leave it un-mapped. " + "To omit mapping an intermediary class when using " + "declarative, set the '__abstract__ = True' " + "attribute on that class." + % (self, self.inherits.base_mapper.polymorphic_on) + ) + elif self.concrete: self._identity_class = self.class_ + else: + self._identity_class = self.inherits._identity_class if self.version_id_col is None: self.version_id_col = self.inherits.version_id_col diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py index ca656a8682..d6f1532ee5 100644 --- a/test/orm/declarative/test_dc_transforms.py +++ b/test/orm/declarative/test_dc_transforms.py @@ -303,6 +303,7 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): status: Mapped[str] = mapped_column(String(30)) engineer_name: Mapped[str] primary_language: Mapped[str] + __mapper_args__ = {"polymorphic_identity": "engineer"} e1 = Engineer("nm", "st", "en", "pl") eq_(e1.name, "nm") @@ -451,6 +452,7 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): status: Mapped[str] = mapped_column(String(30)) engineer_name: Mapped[str] primary_language: Mapped[str] + __mapper_args__ = {"polymorphic_identity": "engineer"} e1 = Engineer("st", "en", "pl") eq_(e1.status, "st") diff --git a/test/orm/declarative/test_inheritance.py b/test/orm/declarative/test_inheritance.py index fb27c910ff..dcb0a5e749 100644 --- a/test/orm/declarative/test_inheritance.py +++ b/test/orm/declarative/test_inheritance.py @@ -40,6 +40,7 @@ class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults): class DeclarativeInheritanceTest(DeclarativeTestBase): + @testing.emits_warning(r".*does not indicate a polymorphic_identity") def test_we_must_copy_mapper_args(self): class Person(Base): @@ -694,6 +695,9 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): __tablename__ = "employee" id = Column(Integer, ForeignKey(Person.id), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "employee", + } class Engineer(Employee): __mapper_args__ = {"polymorphic_identity": "engineer"} @@ -1028,9 +1032,15 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): __mapper_args__ = {"polymorphic_identity": "manager"} id = Column(Integer, ForeignKey("people.id"), primary_key=True) golf_swing = Column(String(50)) + __mapper_args__ = { + "polymorphic_identity": "manager", + } class Boss(Manager): boss_name = Column(String(50)) + __mapper_args__ = { + "polymorphic_identity": "boss", + } is_( Boss.__mapper__.column_attrs["boss_name"].columns[0], diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index 5c5b481db1..6ad2f20972 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -1822,6 +1822,7 @@ class AllYourFavoriteHitsTest(fixtures.TestBase, testing.AssertsCompiledSQL): person_id: Mapped[int] = mapped_column( ForeignKey("person.person_id"), primary_key=True ) + __mapper_args__ = {"polymorphic_identity": "engineer"} status: Mapped[str] = mapped_column(String(30)) engineer_name: Mapped[opt_str50] @@ -1835,6 +1836,7 @@ class AllYourFavoriteHitsTest(fixtures.TestBase, testing.AssertsCompiledSQL): ) status: Mapped[str] = mapped_column(String(30)) manager_name: Mapped[str50] + __mapper_args__ = {"polymorphic_identity": "manager"} is_(Person.__mapper__.polymorphic_on, Person.__table__.c.type) diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py index 87c61d3952..9d7b73e1cf 100644 --- a/test/orm/inheritance/test_basic.py +++ b/test/orm/inheritance/test_basic.py @@ -32,6 +32,7 @@ from sqlalchemy.sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from sqlalchemy.testing import assert_raises from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import eq_ +from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ from sqlalchemy.testing import mock @@ -3531,7 +3532,12 @@ class NoPolyIdentInMiddleTest(fixtures.MappedTest): cls.mapper_registry.map_imperatively( A, base, polymorphic_on=base.c.type ) - cls.mapper_registry.map_imperatively(B, inherits=A) + + with expect_warnings( + r"Mapper Mapper\[B\(base\)\] does not indicate a " + "polymorphic_identity," + ): + cls.mapper_registry.map_imperatively(B, inherits=A) cls.mapper_registry.map_imperatively( C, inherits=B, polymorphic_identity="c" ) @@ -3541,6 +3547,28 @@ class NoPolyIdentInMiddleTest(fixtures.MappedTest): cls.mapper_registry.map_imperatively( E, inherits=A, polymorphic_identity="e" ) + cls.mapper_registry.configure() + + def test_warning(self, decl_base): + """test #7545""" + + class A(decl_base): + __tablename__ = "a" + id = Column(Integer, primary_key=True) + type = Column(String) + + __mapper_args__ = {"polymorphic_on": type} + + class B(A): + __mapper_args__ = {"polymorphic_identity": "b"} + + with expect_warnings( + r"Mapper Mapper\[C\(a\)\] does not indicate a " + "polymorphic_identity," + ): + + class C(A): + __mapper_args__ = {} def test_load_from_middle(self): C, B = self.classes.C, self.classes.B diff --git a/test/orm/test_events.py b/test/orm/test_events.py index 75955afb5a..47609daa06 100644 --- a/test/orm/test_events.py +++ b/test/orm/test_events.py @@ -1335,6 +1335,7 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest): # not been loaded yet (Employer), and therefore cannot be configured: class Mammal(Animal): nonexistent = relationship("Nonexistent") + __mapper_args__ = {"polymorphic_identity": "mammal"} # These new classes should not be configured at this point: unconfigured = list(mapperlib._unconfigured_mappers()) -- 2.47.2