summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/unreleased_12/4454.rst12
-rw-r--r--lib/sqlalchemy/orm/mapper.py2
-rw-r--r--lib/sqlalchemy/orm/query.py2
-rw-r--r--test/orm/inheritance/test_single.py213
4 files changed, 227 insertions, 2 deletions
diff --git a/doc/build/changelog/unreleased_12/4454.rst b/doc/build/changelog/unreleased_12/4454.rst
new file mode 100644
index 000000000..75f698e50
--- /dev/null
+++ b/doc/build/changelog/unreleased_12/4454.rst
@@ -0,0 +1,12 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 4454
+
+ Fixed issue where when using single-table inheritance in conjunction with a
+ joined inheritance hierarchy that uses "with polymorphic" loading, the
+ "single table criteria" for that single-table entity could get confused for
+ that of other entities from the same hierarchy used in the same query.The
+ adaption of the "single table criteria" is made more specific to the target
+ entity to avoid it accidentally getting adapted to other tables in the
+ query.
+
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 56ad965de..a394ec06e 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2103,7 +2103,7 @@ class Mapper(InspectionAttr):
@_memoized_configured_property
def _single_table_criterion(self):
if self.single and self.inherits and self.polymorphic_on is not None:
- return self.polymorphic_on.in_(
+ return self.polymorphic_on._annotate({"parentmapper": self}).in_(
m.polymorphic_identity for m in self.self_and_descendants
)
else:
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 7ddcf10b0..2bd79a2cd 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -364,7 +364,6 @@ class Query(object):
or "_orm_adapt" in elem._annotations
or "parententity" in elem._annotations
):
-
e = adapter(elem)
if e is not None:
return e
@@ -3933,6 +3932,7 @@ class Query(object):
if single_crit is not None:
if adapter:
single_crit = adapter.traverse(single_crit)
+
single_crit = self._adapt_clause(single_crit, False, False)
context.whereclause = sql.and_(
sql.True_._ifnone(context.whereclause), single_crit
diff --git a/test/orm/inheritance/test_single.py b/test/orm/inheritance/test_single.py
index aae9312d4..1b28974b7 100644
--- a/test/orm/inheritance/test_single.py
+++ b/test/orm/inheritance/test_single.py
@@ -4,6 +4,8 @@ from sqlalchemy import inspect
from sqlalchemy import Integer
from sqlalchemy import literal
from sqlalchemy import literal_column
+from sqlalchemy import null
+from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy import testing
from sqlalchemy.orm import aliased
@@ -16,6 +18,7 @@ from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import subqueryload
+from sqlalchemy.orm import with_polymorphic
from sqlalchemy.testing import AssertsCompiledSQL
from sqlalchemy.testing import eq_
from sqlalchemy.testing import fixtures
@@ -1436,6 +1439,216 @@ class SingleOnJoinedTest(fixtures.MappedTest):
self.assert_sql_count(testing.db, go, 1)
+class SingleFromPolySelectableTest(
+ fixtures.DeclarativeMappedTest, AssertsCompiledSQL
+):
+ __dialect__ = "default"
+
+ @classmethod
+ def setup_classes(cls, with_polymorphic=None, include_sub_defaults=False):
+ Base = cls.DeclarativeBasic
+
+ class Employee(Base):
+ __tablename__ = "employee"
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+ type = Column(String(50))
+
+ __mapper_args__ = {
+ "polymorphic_identity": "employee",
+ "polymorphic_on": type,
+ }
+
+ class Engineer(Employee):
+ __tablename__ = "engineer"
+ id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
+ engineer_info = Column(String(50))
+ manager_id = Column(ForeignKey("manager.id"))
+ __mapper_args__ = {"polymorphic_identity": "engineer"}
+
+ class Manager(Employee):
+ __tablename__ = "manager"
+ id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
+ manager_data = Column(String(50))
+ __mapper_args__ = {"polymorphic_identity": "manager"}
+
+ class Boss(Manager):
+ __mapper_args__ = {"polymorphic_identity": "boss"}
+
+ def _with_poly_fixture(self):
+ employee = self.classes.Employee.__table__
+ engineer = self.classes.Engineer.__table__
+ manager = self.classes.Manager.__table__
+
+ poly = (
+ select(
+ [
+ employee.c.id,
+ employee.c.type,
+ employee.c.name,
+ manager.c.manager_data,
+ null().label("engineer_info"),
+ null().label("manager_id"),
+ ]
+ )
+ .select_from(employee.join(manager))
+ .apply_labels()
+ .union_all(
+ select(
+ [
+ employee.c.id,
+ employee.c.type,
+ employee.c.name,
+ null().label("manager_data"),
+ engineer.c.engineer_info,
+ engineer.c.manager_id,
+ ]
+ )
+ .select_from(employee.join(engineer))
+ .apply_labels()
+ )
+ .alias()
+ )
+
+ return poly
+
+ def test_wpoly_single_inh_subclass(self):
+ poly = with_polymorphic(
+ self.classes.Employee,
+ [self.classes.Boss, self.classes.Manager, self.classes.Engineer],
+ self._with_poly_fixture(),
+ )
+ s = Session()
+ q = s.query(poly.Boss)
+ self.assert_compile(
+ q,
+ "SELECT "
+ "anon_1.employee_id AS anon_1_employee_id, "
+ "anon_1.employee_name AS anon_1_employee_name, "
+ "anon_1.employee_type AS anon_1_employee_type, "
+ "anon_1.manager_manager_data AS anon_1_manager_manager_data "
+ "FROM "
+ "(SELECT "
+ "employee.id AS employee_id, employee.type AS employee_type, "
+ "employee.name AS employee_name, "
+ "manager.manager_data AS manager_manager_data, "
+ "NULL AS engineer_info, NULL AS manager_id FROM employee "
+ "JOIN manager ON employee.id = manager.id "
+ "UNION ALL "
+ "SELECT employee.id AS employee_id, "
+ "employee.type AS employee_type, "
+ "employee.name AS employee_name, NULL AS manager_data, "
+ "engineer.engineer_info AS engineer_engineer_info, "
+ "engineer.manager_id AS engineer_manager_id "
+ "FROM employee JOIN engineer ON employee.id = engineer.id) "
+ "AS anon_1 "
+ "WHERE anon_1.employee_type IN (:type_1)",
+ )
+
+ def test_query_wpoly_single_inh_subclass(self):
+ Boss = self.classes.Boss
+
+ poly = self._with_poly_fixture()
+
+ s = Session()
+ q = s.query(Boss).with_polymorphic(Boss, poly)
+ self.assert_compile(
+ q,
+ "SELECT anon_1.employee_id AS anon_1_employee_id, "
+ "anon_1.employee_name AS anon_1_employee_name, "
+ "anon_1.employee_type AS anon_1_employee_type, "
+ "anon_1.manager_manager_data AS anon_1_manager_manager_data "
+ "FROM (SELECT employee.id AS employee_id, employee.type "
+ "AS employee_type, employee.name AS employee_name, "
+ "manager.manager_data AS manager_manager_data, "
+ "NULL AS engineer_info, NULL AS manager_id FROM employee "
+ "JOIN manager ON employee.id = manager.id "
+ "UNION ALL SELECT employee.id AS employee_id, "
+ "employee.type AS employee_type, employee.name AS employee_name, "
+ "NULL AS manager_data, "
+ "engineer.engineer_info AS engineer_engineer_info, "
+ "engineer.manager_id AS engineer_manager_id "
+ "FROM employee JOIN engineer ON employee.id = engineer.id) "
+ "AS anon_1 WHERE anon_1.employee_type IN (:type_1)",
+ )
+
+ def test_single_inh_subclass_join_joined_inh_subclass(self):
+ Boss, Engineer = self.classes("Boss", "Engineer")
+ s = Session()
+
+ q = s.query(Boss).join(Engineer, Engineer.manager_id == Boss.id)
+
+ self.assert_compile(
+ q,
+ "SELECT manager.id AS manager_id, employee.id AS employee_id, "
+ "employee.name AS employee_name, "
+ "employee.type AS employee_type, "
+ "manager.manager_data AS manager_manager_data "
+ "FROM employee JOIN manager ON employee.id = manager.id "
+ "JOIN (employee AS employee_1 JOIN engineer AS engineer_1 "
+ "ON employee_1.id = engineer_1.id) "
+ "ON engineer_1.manager_id = manager.id "
+ "WHERE employee.type IN (:type_1)",
+ )
+
+ def test_single_inh_subclass_join_wpoly_joined_inh_subclass(self):
+ Boss = self.classes.Boss
+ poly = with_polymorphic(
+ self.classes.Employee,
+ [self.classes.Boss, self.classes.Manager, self.classes.Engineer],
+ self._with_poly_fixture(),
+ )
+
+ s = Session()
+
+ q = s.query(Boss).join(
+ poly.Engineer, poly.Engineer.manager_id == Boss.id
+ )
+
+ self.assert_compile(
+ q,
+ "SELECT manager.id AS manager_id, employee.id AS employee_id, "
+ "employee.name AS employee_name, employee.type AS employee_type, "
+ "manager.manager_data AS manager_manager_data "
+ "FROM employee JOIN manager ON employee.id = manager.id "
+ "JOIN (SELECT employee.id AS employee_id, "
+ "employee.type AS employee_type, employee.name AS employee_name, "
+ "manager.manager_data AS manager_manager_data, "
+ "NULL AS engineer_info, NULL AS manager_id "
+ "FROM employee JOIN manager ON employee.id = manager.id "
+ "UNION ALL "
+ "SELECT employee.id AS employee_id, "
+ "employee.type AS employee_type, employee.name AS employee_name, "
+ "NULL AS manager_data, "
+ "engineer.engineer_info AS engineer_engineer_info, "
+ "engineer.manager_id AS engineer_manager_id "
+ "FROM employee "
+ "JOIN engineer ON employee.id = engineer.id) AS anon_1 "
+ "ON anon_1.manager_id = manager.id "
+ "WHERE employee.type IN (:type_1)",
+ )
+
+ def test_joined_inh_subclass_join_single_inh_subclass(self):
+ Engineer = self.classes.Engineer
+ Boss = self.classes.Boss
+ s = Session()
+
+ q = s.query(Engineer).join(Boss, Engineer.manager_id == Boss.id)
+
+ self.assert_compile(
+ q,
+ "SELECT engineer.id AS engineer_id, employee.id AS employee_id, "
+ "employee.name AS employee_name, employee.type AS employee_type, "
+ "engineer.engineer_info AS engineer_engineer_info, "
+ "engineer.manager_id AS engineer_manager_id "
+ "FROM employee JOIN engineer ON employee.id = engineer.id "
+ "JOIN (employee AS employee_1 JOIN manager AS manager_1 "
+ "ON employee_1.id = manager_1.id) "
+ "ON engineer.manager_id = manager_1.id "
+ "AND employee_1.type IN (:type_1)",
+ )
+
+
class EagerDefaultEvalTest(fixtures.DeclarativeMappedTest):
@classmethod
def setup_classes(cls, with_polymorphic=None, include_sub_defaults=False):