]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
consider _ClassStrategyLoad as part of endpoint path
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 14 Sep 2023 14:48:03 +0000 (10:48 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 17 Sep 2023 16:34:30 +0000 (12:34 -0400)
Added new capability to the :func:`_orm.selectin_polymorphic` loader option
which allows other loader options to be bundled as siblings, referring to
one of its subclasses, within the sub-options of parent loader option.
Previously, this pattern was only supported if the
:func:`_orm.selectin_polymorphic` were at the top level of the options for
the query.   See new documentation section for example.

As part of this change, improved the behavior of the
:meth:`_orm.Load.selectin_polymorphic` method / loader strategy so that the
subclass load does not load most already-loaded columns from the parent
table, when the option is used against a class that is already being
relationship-loaded.  Previously, the logic to load only the subclass
columns worked only for a top level class load.

Fixes: #10348
Change-Id: I13a8cad5484e97b2ab51402f100193cb054e2155

doc/build/changelog/unreleased_20/10348.rst [new file with mode: 0644]
doc/build/orm/queryguide/inheritance.rst
lib/sqlalchemy/orm/loading.py
lib/sqlalchemy/orm/path_registry.py
lib/sqlalchemy/orm/strategy_options.py
lib/sqlalchemy/orm/util.py
test/orm/inheritance/test_poly_loading.py
test/orm/inheritance/test_relationship.py
test/orm/test_options.py

diff --git a/doc/build/changelog/unreleased_20/10348.rst b/doc/build/changelog/unreleased_20/10348.rst
new file mode 100644 (file)
index 0000000..b7f0ded
--- /dev/null
@@ -0,0 +1,21 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 10348
+
+    Added new capability to the :func:`_orm.selectin_polymorphic` loader option
+    which allows other loader options to be bundled as siblings, referring to
+    one of its subclasses, within the sub-options of parent loader option.
+    Previously, this pattern was only supported if the
+    :func:`_orm.selectin_polymorphic` were at the top level of the options for
+    the query.   See new documentation section for example.
+
+    As part of this change, improved the behavior of the
+    :meth:`_orm.Load.selectin_polymorphic` method / loader strategy so that the
+    subclass load does not load most already-loaded columns from the parent
+    table, when the option is used against a class that is already being
+    relationship-loaded.  Previously, the logic to load only the subclass
+    columns worked only for a top level class load.
+
+    .. seealso::
+
+        :ref:`polymorphic_selectin_as_loader_option_target_plus_opts`
index 7040128f4057df62ede4d83be55de806a4c6369e..0cb44b8ac729394c7211962508a818a964e41e03 100644 (file)
@@ -27,7 +27,7 @@ Writing SELECT statements for Inheritance Mappings
     :doc:`View the ORM setup for this page <_inheritance_setup>`.
 
 SELECTing from the base class vs. specific sub-classes
---------------------------------------------------------
+------------------------------------------------------
 
 A SELECT statement constructed against a class in a joined inheritance
 hierarchy will query against the table to which the class is mapped, as well as
@@ -177,11 +177,10 @@ objects that were loaded without any additional SQL statements being emitted::
    the :func:`_orm.selectinload` relationship strategy which is more
    sophisticated in this regard and can factor out the JOIN when not needed.
 
+.. _polymorphic_selectin_as_loader_option_target:
 
-.. _polymorphic_selectin_w_loader_options:
-
-Combining additional loader options with selectin_polymorphic() subclass loads
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Applying selectin_polymorphic() to an existing eager load
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 ..  Setup code, not for display
 
@@ -189,17 +188,74 @@ Combining additional loader options with selectin_polymorphic() subclass loads
     >>> session.close()
     ROLLBACK
 
+In addition to :func:`_orm.selectin_polymorphic` being specified as an option
+for a top-level entity loaded by a statement, we may also indicate
+:func:`_orm.selectin_polymorphic` on the target of an existing load.
+As our :doc:`setup <_inheritance_setup>` mapping includes a parent
+``Company`` entity with a ``Company.employees`` :func:`_orm.relationship`
+referring to ``Employee`` entities, we may illustrate a SELECT against
+the ``Company`` entity that eagerly loads all ``Employee`` objects as well as
+all attributes on their subtypes as follows, by applying :meth:`.Load.selectin_polymorphic`
+as a chained loader option; in this form, the first argument is implicit from
+the previous loader option (in this case :func:`_orm.selectinload`), so
+we only indicate the additional target subclasses we wish to load::
+
+    >>> from sqlalchemy.orm import selectinload
+    >>> stmt = select(Company).options(
+    ...     selectinload(Company.employees).selectin_polymorphic([Manager, Engineer])
+    ... )
+    >>> for company in session.scalars(stmt):
+    ...     print(f"company: {company.name}")
+    ...     print(f"employees: {company.employees}")
+    {execsql}BEGIN (implicit)
+    SELECT company.id, company.name
+    FROM company
+    [...] ()
+    SELECT employee.company_id AS employee_company_id, employee.id AS employee_id,
+    employee.name AS employee_name, employee.type AS employee_type
+    FROM employee
+    WHERE employee.company_id IN (?)
+    [...] (1,)
+    SELECT manager.id AS manager_id, employee.id AS employee_id,
+    employee.type AS employee_type,
+    manager.manager_name AS manager_manager_name
+    FROM employee JOIN manager ON employee.id = manager.id
+    WHERE employee.id IN (?) ORDER BY employee.id
+    [...] (1,)
+    SELECT engineer.id AS engineer_id, employee.id AS employee_id,
+    employee.type AS employee_type,
+    engineer.engineer_info AS engineer_engineer_info
+    FROM employee JOIN engineer ON employee.id = engineer.id
+    WHERE employee.id IN (?, ?) ORDER BY employee.id
+    [...] (2, 3)
+    {stop}company: Krusty Krab
+    employees: [Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]
+
+.. seealso::
+
+    :ref:`eagerloading_polymorphic_subtypes` - illustrates the equivalent example
+    as above using :func:`_orm.with_polymorphic` instead
+
+
+.. _polymorphic_selectin_w_loader_options:
+
+Applying loader options to the subclasses loaded by selectin_polymorphic
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 The SELECT statements emitted by :func:`_orm.selectin_polymorphic` are themselves
 ORM statements, so we may also add other loader options (such as those
 documented at :ref:`orm_queryguide_relationship_loaders`) that refer to specific
-subclasses.  For example, if we considered that the ``Manager`` mapper had
+subclasses.   These options should be applied as **siblings** to a
+:func:`_orm.selectin_polymorphic` option, that is, comma separated within
+:meth:`_sql.select.options`.
+
+For example, if we considered that the ``Manager`` mapper had
 a :ref:`one to many <relationship_patterns_o2m>` relationship to an entity
 called ``Paperwork``, we could combine the use of
 :func:`_orm.selectin_polymorphic` and :func:`_orm.selectinload` to eagerly load
 this collection on all ``Manager`` objects, where the sub-attributes of
 ``Manager`` objects were also themselves eagerly loaded::
 
-    >>> from sqlalchemy.orm import selectinload
     >>> from sqlalchemy.orm import selectin_polymorphic
     >>> stmt = (
     ...     select(Employee)
@@ -210,8 +266,7 @@ this collection on all ``Manager`` objects, where the sub-attributes of
     ...     )
     ... )
     >>> objects = session.scalars(stmt).all()
-    {execsql}BEGIN (implicit)
-    SELECT employee.id, employee.name, employee.type, employee.company_id
+    {execsql}SELECT employee.id, employee.name, employee.type, employee.company_id
     FROM employee ORDER BY employee.id
     [...] ()
     SELECT manager.id AS manager_id, employee.id AS employee_id, employee.type AS employee_type, manager.manager_name AS manager_manager_name
@@ -231,60 +286,69 @@ this collection on all ``Manager`` objects, where the sub-attributes of
     >>> print(objects[0].paperwork)
     [Paperwork('Secret Recipes'), Paperwork('Krabby Patty Orders')]
 
-.. _polymorphic_selectin_as_loader_option_target:
+.. _polymorphic_selectin_as_loader_option_target_plus_opts:
 
-Applying selectin_polymorphic() to an existing eager load
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Applying loader options when selectin_polymorphic is itself a sub-option
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-In addition to being able to add loader options to the right side of a
-:func:`_orm.selectin_polymorphic` load, we may also indicate
-:func:`_orm.selectin_polymorphic` on the target of an existing load.
-As our :doc:`setup <_inheritance_setup>` mapping includes a parent
-``Company`` entity with a ``Company.employees`` :func:`_orm.relationship`
-referring to ``Employee`` entities, we may illustrate a SELECT against
-the ``Company`` entity that eagerly loads all ``Employee`` objects as well as
-all attributes on their subtypes as follows, by applying :meth:`.Load.selectin_polymorphic`
-as a chained loader option; in this form, the first argument is implicit from
-the previous loader option (in this case :func:`_orm.selectinload`), so
-we only indicate the additional target subclasses we wish to load::
+..  Setup code, not for display
+
+
+    >>> session.close()
+    ROLLBACK
+
+.. versionadded:: 2.0.21
+
+The previous section illustrated
+:func:`_orm.selectin_polymorphic` and :func:`_orm.selectinload` used as siblings
+within a call to :func:`_sql.select.options`.   If the target entity is one that
+is already being loaded from a parent relationship, as in the example at
+:ref:`polymorphic_selectin_as_loader_option_target`, we can apply this
+"sibling" pattern using the :meth:`_orm.Load.options` method that applies
+sub-options to a parent, as illustrated at
+:ref:`orm_queryguide_relationship_sub_options`.  Below we combine the two examples
+to load `Company.employees`, also loading the attributes for the `Manager` and
+`Engineer` classes, as well as eagerly loading the `Manager.paperwork`
+attribute::
 
+    >>> from sqlalchemy.orm import selectinload
     >>> stmt = select(Company).options(
-    ...     selectinload(Company.employees).selectin_polymorphic([Manager, Engineer])
+    ...     selectinload(Company.employees).options(
+    ...         selectin_polymorphic(Employee, [Manager, Engineer]),
+    ...         selectinload(Manager.paperwork),
+    ...     )
     ... )
     >>> for company in session.scalars(stmt):
     ...     print(f"company: {company.name}")
-    ...     print(f"employees: {company.employees}")
-    {execsql}SELECT company.id, company.name
+    ...     for employee in company.employees:
+    ...         if isinstance(employee, Manager):
+    ...             print(f"manager: {employee.name} paperwork: {employee.paperwork}")
+    BEGIN (implicit)
+    SELECT company.id, company.name
     FROM company
     [...] ()
-    SELECT employee.company_id AS employee_company_id, employee.id AS employee_id,
-    employee.name AS employee_name, employee.type AS employee_type
+    SELECT employee.company_id AS employee_company_id, employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type
     FROM employee
     WHERE employee.company_id IN (?)
     [...] (1,)
-    SELECT manager.id AS manager_id, employee.id AS employee_id, employee.name AS employee_name,
-    employee.type AS employee_type, employee.company_id AS employee_company_id,
-    manager.manager_name AS manager_manager_name
+    SELECT manager.id AS manager_id, employee.id AS employee_id, employee.type AS employee_type, manager.manager_name AS manager_manager_name
     FROM employee JOIN manager ON employee.id = manager.id
     WHERE employee.id IN (?) ORDER BY employee.id
     [...] (1,)
-    SELECT engineer.id AS engineer_id, employee.id AS employee_id, employee.name AS employee_name,
-    employee.type AS employee_type, employee.company_id AS employee_company_id,
-    engineer.engineer_info AS engineer_engineer_info
+    SELECT paperwork.manager_id AS paperwork_manager_id, paperwork.id AS paperwork_id, paperwork.document_name AS paperwork_document_name
+    FROM paperwork
+    WHERE paperwork.manager_id IN (?)
+    [...] (1,)
+    SELECT engineer.id AS engineer_id, employee.id AS employee_id, employee.type AS employee_type, engineer.engineer_info AS engineer_engineer_info
     FROM employee JOIN engineer ON employee.id = engineer.id
     WHERE employee.id IN (?, ?) ORDER BY employee.id
     [...] (2, 3)
     {stop}company: Krusty Krab
-    employees: [Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]
-
-.. seealso::
-
-    :ref:`eagerloading_polymorphic_subtypes` - illustrates the equivalent example
-    as above using :func:`_orm.with_polymorphic` instead
+    manager: Mr. Krabs paperwork: [Paperwork('Secret Recipes'), Paperwork('Krabby Patty Orders')]
 
 
 Configuring selectin_polymorphic() on mappers
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The behavior of :func:`_orm.selectin_polymorphic` may be configured on specific
 mappers so that it takes place by default, by using the
@@ -551,7 +615,7 @@ using alternative polymorphic selectables in general.
 .. _with_polymorphic_mapper_config:
 
 Configuring with_polymorphic() on mappers
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 As is the case with :func:`_orm.selectin_polymorphic`, the
 :func:`_orm.with_polymorphic` construct also supports a mapper-configured
@@ -764,7 +828,7 @@ point of view.
 .. _eagerloading_polymorphic_subtypes:
 
 Eager Loading of Polymorphic Subtypes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The use of :meth:`_orm.PropComparator.of_type` illustrated with the
 :meth:`.Select.join` method in the previous section may also be applied
@@ -868,7 +932,7 @@ the ``Engineer`` entity is performed::
 
 
 Optimizing Attribute Loads for Single Inheritance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 ..  Setup code, not for display
 
index e7a0d3ebffdcc92a514da76076013288f5f8eb53..fb62c2f46edcc652e0056914bc225cae6d9af9b4 100644 (file)
@@ -1246,7 +1246,16 @@ def _load_subclass_via_in(
 
         orig_query = context.query
 
-        options = (enable_opt,) + orig_query._with_options + (disable_opt,)
+        if path.parent:
+            enable_opt_lcl = enable_opt._prepend_path(path)
+            disable_opt_lcl = disable_opt._prepend_path(path)
+        else:
+            enable_opt_lcl = enable_opt
+            disable_opt_lcl = disable_opt
+        options = (
+            (enable_opt_lcl,) + orig_query._with_options + (disable_opt_lcl,)
+        )
+
         q2 = q.options(*options)
 
         q2._compile_options = context.compile_state.default_compile_options
index 2cd8a1412c4d2e1b91b913970088b8e839b0e702..c0f6e9cbcc2aaf64117d50e70fe2d83b9bc024e7 100644 (file)
@@ -70,6 +70,8 @@ _PathElementType = Union[
 # chopped at odd intervals as well so this is less flexible
 _PathRepresentation = Tuple[_PathElementType, ...]
 
+# NOTE: these names are weird since the array is 0-indexed,
+# the "_Odd" entries are at 0, 2, 4, etc
 _OddPathRepresentation = Sequence["_InternalEntityType[Any]"]
 _EvenPathRepresentation = Sequence[Union["MapperProperty[Any]", str]]
 
@@ -154,6 +156,9 @@ class PathRegistry(HasCacheKey):
     def _path_for_compare(self) -> Optional[_PathRepresentation]:
         return self.path
 
+    def odd_element(self, index: int) -> _InternalEntityType[Any]:
+        return self.path[index]  # type: ignore
+
     def set(self, attributes: Dict[Any, Any], key: Any, value: Any) -> None:
         log.debug("set '%s' on path '%s' to '%s'", key, self, value)
         attributes[(key, self.natural_path)] = value
@@ -721,7 +726,7 @@ class AbstractEntityRegistry(CreatesToken):
 
     @property
     def root_entity(self) -> _InternalEntityType[Any]:
-        return cast("_InternalEntityType[Any]", self.path[0])
+        return self.odd_element(0)
 
     @property
     def entity_path(self) -> PathRegistry:
index d59fbb7693f534f1c688dcb9657926b084c4e517..0d64be5a09a2917c03d8492ab1b49abca0f08570 100644 (file)
@@ -977,6 +977,7 @@ class Load(_AbstractLoad):
     __slots__ = (
         "path",
         "context",
+        "additional_source_entities",
     )
 
     _traverse_internals = [
@@ -986,11 +987,16 @@ class Load(_AbstractLoad):
             visitors.InternalTraversal.dp_has_cache_key_list,
         ),
         ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean),
+        (
+            "additional_source_entities",
+            visitors.InternalTraversal.dp_has_cache_key_list,
+        ),
     ]
     _cache_key_traversal = None
 
     path: PathRegistry
     context: Tuple[_LoadElement, ...]
+    additional_source_entities: Tuple[_InternalEntityType[Any], ...]
 
     def __init__(self, entity: _EntityType[Any]):
         insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity))
@@ -999,16 +1005,20 @@ class Load(_AbstractLoad):
         self.path = insp._path_registry
         self.context = ()
         self.propagate_to_loaders = False
+        self.additional_source_entities = ()
 
     def __str__(self) -> str:
         return f"Load({self.path[0]})"
 
     @classmethod
-    def _construct_for_existing_path(cls, path: PathRegistry) -> Load:
+    def _construct_for_existing_path(
+        cls, path: AbstractEntityRegistry
+    ) -> Load:
         load = cls.__new__(cls)
         load.path = path
         load.context = ()
         load.propagate_to_loaders = False
+        load.additional_source_entities = ()
         return load
 
     def _adapt_cached_option_to_uncached_option(
@@ -1016,6 +1026,13 @@ class Load(_AbstractLoad):
     ) -> ORMOption:
         return self._adjust_for_extra_criteria(context)
 
+    def _prepend_path(self, path: PathRegistry) -> Load:
+        cloned = self._clone()
+        cloned.context = tuple(
+            element._prepend_path(path) for element in self.context
+        )
+        return cloned
+
     def _adjust_for_extra_criteria(self, context: QueryContext) -> Load:
         """Apply the current bound parameters in a QueryContext to all
         occurrences "extra_criteria" stored within this ``Load`` object,
@@ -1116,9 +1133,12 @@ class Load(_AbstractLoad):
 
         assert cloned.propagate_to_loaders == self.propagate_to_loaders
 
-        if not orm_util._entity_corresponds_to_use_path_impl(
-            cast("_InternalEntityType[Any]", parent.path[-1]),
-            cast("_InternalEntityType[Any]", cloned.path[0]),
+        if not any(
+            orm_util._entity_corresponds_to_use_path_impl(
+                elem, cloned.path.odd_element(0)
+            )
+            for elem in (parent.path.odd_element(-1),)
+            + parent.additional_source_entities
         ):
             if len(cloned.path) > 1:
                 attrname = cloned.path[1]
@@ -1137,6 +1157,9 @@ class Load(_AbstractLoad):
 
         if cloned.context:
             parent.context += cloned.context
+            parent.additional_source_entities += (
+                cloned.additional_source_entities
+            )
 
     @_generative
     def options(self, *opts: _AbstractLoad) -> Self:
@@ -1227,6 +1250,10 @@ class Load(_AbstractLoad):
             )
             if load_element:
                 self.context += (load_element,)
+                assert opts is not None
+                self.additional_source_entities += cast(
+                    "Tuple[_InternalEntityType[Any]]", opts["entities"]
+                )
 
         else:
             for attr in attrs:
@@ -1714,9 +1741,7 @@ class _LoadElement(
 
         return cloned
 
-    def _prepend_path_from(
-        self, parent: Union[Load, _LoadElement]
-    ) -> _LoadElement:
+    def _prepend_path_from(self, parent: Load) -> _LoadElement:
         """adjust the path of this :class:`._LoadElement` to be
         a subpath of that of the given parent :class:`_orm.Load` object's
         path.
@@ -1725,22 +1750,30 @@ class _LoadElement(
         which is in turn part of the :meth:`_orm.Load.options` method.
 
         """
-        cloned = self._clone()
 
-        assert cloned.strategy == self.strategy
-        assert cloned.local_opts == self.local_opts
-        assert cloned.is_class_strategy == self.is_class_strategy
-
-        if not orm_util._entity_corresponds_to_use_path_impl(
-            cast("_InternalEntityType[Any]", parent.path[-1]),
-            cast("_InternalEntityType[Any]", cloned.path[0]),
+        if not any(
+            orm_util._entity_corresponds_to_use_path_impl(
+                elem,
+                self.path.odd_element(0),
+            )
+            for elem in (parent.path.odd_element(-1),)
+            + parent.additional_source_entities
         ):
             raise sa_exc.ArgumentError(
-                f'Attribute "{cloned.path[1]}" does not link '
+                f'Attribute "{self.path[1]}" does not link '
                 f'from element "{parent.path[-1]}".'
             )
 
-        cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:])
+        return self._prepend_path(parent.path)
+
+    def _prepend_path(self, path: PathRegistry) -> _LoadElement:
+        cloned = self._clone()
+
+        assert cloned.strategy == self.strategy
+        assert cloned.local_opts == self.local_opts
+        assert cloned.is_class_strategy == self.is_class_strategy
+
+        cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:])
 
         return cloned
 
@@ -2447,7 +2480,9 @@ def _raise_for_does_not_link(path, attrname, parent_entity):
                 (
                     "  Did you mean to use "
                     f'"{path[-2]}'
-                    f'.of_type({parent_entity_str})"?'
+                    f'.of_type({parent_entity_str})" or "loadopt.options('
+                    f"selectin_polymorphic({path[-2].mapper.class_.__name__}, "
+                    f'[{parent_entity_str}]), ...)" ?'
                     if not path_is_of_type
                     and not path[-1].is_aliased_class
                     and orm_util._entity_corresponds_to(
index 4371e6116f8fa9df0541fbed0e4679aaddc07551..46761fab81d0b143a40ceee98685ccd1e5cf9110 100644 (file)
@@ -100,6 +100,7 @@ if typing.TYPE_CHECKING:
     from .context import _MapperEntity
     from .context import ORMCompileState
     from .mapper import Mapper
+    from .path_registry import AbstractEntityRegistry
     from .query import Query
     from .relationships import RelationshipProperty
     from ..engine import Row
@@ -1124,7 +1125,7 @@ class AliasedInsp(
         return self.mapper.class_
 
     @property
-    def _path_registry(self) -> PathRegistry:
+    def _path_registry(self) -> AbstractEntityRegistry:
         if self._use_mapper_path:
             return self.mapper._path_registry
         else:
index 87908482886512fc8bf35593d8920753d11792d9..df286f0d35c817d40f7a14f99404d99b17af2982 100644 (file)
@@ -281,8 +281,7 @@ class FixtureLoadTest(_Polymorphic, testing.AssertsExecutionResults):
                 CompiledSQL(
                     "SELECT managers.person_id AS managers_person_id, "
                     "people.person_id AS people_person_id, "
-                    "people.company_id AS people_company_id, "
-                    "people.name AS people_name, people.type AS people_type, "
+                    "people.type AS people_type, "
                     "managers.status AS managers_status, "
                     "managers.manager_name AS managers_manager_name "
                     "FROM people JOIN managers "
@@ -294,8 +293,7 @@ class FixtureLoadTest(_Polymorphic, testing.AssertsExecutionResults):
                 CompiledSQL(
                     "SELECT engineers.person_id AS engineers_person_id, "
                     "people.person_id AS people_person_id, "
-                    "people.company_id AS people_company_id, "
-                    "people.name AS people_name, people.type AS people_type, "
+                    "people.type AS people_type, "
                     "engineers.status AS engineers_status, "
                     "engineers.engineer_name AS engineers_engineer_name, "
                     "engineers.primary_language AS engineers_primary_language "
@@ -309,6 +307,80 @@ class FixtureLoadTest(_Polymorphic, testing.AssertsExecutionResults):
         )
         eq_(result, [self.c1, self.c2])
 
+    def test_load_company_plus_employees_w_paperwork(self):
+        s = fixture_session()
+        q = (
+            s.query(Company)
+            .options(
+                selectinload(Company.employees).options(
+                    selectin_polymorphic(Person, [Engineer, Manager]),
+                    selectinload(Engineer.machines),
+                    # NOTE: if this is selectinload(Person.paperwork),
+                    # we get duplicate loads from the subclasses which is
+                    # not ideal
+                )
+            )
+            .order_by(Company.company_id)
+        )
+
+        result = self.assert_sql_execution(
+            testing.db,
+            q.all,
+            CompiledSQL(
+                "SELECT companies.company_id AS companies_company_id, "
+                "companies.name AS companies_name FROM companies "
+                "ORDER BY companies.company_id",
+                {},
+            ),
+            CompiledSQL(
+                "SELECT people.company_id AS people_company_id, "
+                "people.person_id AS people_person_id, "
+                "people.name AS people_name, people.type AS people_type "
+                "FROM people WHERE people.company_id "
+                "IN (__[POSTCOMPILE_primary_keys]) "
+                "ORDER BY people.person_id",
+                {"primary_keys": [1, 2]},
+            ),
+            AllOf(
+                CompiledSQL(
+                    "SELECT managers.person_id AS managers_person_id, "
+                    "people.person_id AS people_person_id, "
+                    "people.type AS people_type, "
+                    "managers.status AS managers_status, "
+                    "managers.manager_name AS managers_manager_name "
+                    "FROM people JOIN managers "
+                    "ON people.person_id = managers.person_id "
+                    "WHERE people.person_id IN (__[POSTCOMPILE_primary_keys]) "
+                    "ORDER BY people.person_id",
+                    {"primary_keys": [3, 4]},
+                ),
+                CompiledSQL(
+                    "SELECT engineers.person_id AS engineers_person_id, "
+                    "people.person_id AS people_person_id, "
+                    "people.type AS people_type, "
+                    "engineers.status AS engineers_status, "
+                    "engineers.engineer_name AS engineers_engineer_name, "
+                    "engineers.primary_language AS engineers_primary_language "
+                    "FROM people JOIN engineers "
+                    "ON people.person_id = engineers.person_id "
+                    "WHERE people.person_id IN (__[POSTCOMPILE_primary_keys]) "
+                    "ORDER BY people.person_id",
+                    {"primary_keys": [1, 2, 5]},
+                ),
+                CompiledSQL(
+                    "SELECT machines.engineer_id AS machines_engineer_id, "
+                    "machines.machine_id AS machines_machine_id, "
+                    "machines.name AS machines_name "
+                    "FROM machines "
+                    "WHERE machines.engineer_id "
+                    "IN (__[POSTCOMPILE_primary_keys]) "
+                    "ORDER BY machines.machine_id",
+                    {"primary_keys": [1, 2, 5]},
+                ),
+            ),
+        )
+        eq_(result, [self.c1, self.c2])
+
 
 class TestGeometries(GeometryFixtureBase):
     def test_threelevel_selectin_to_inline_mapped(self):
@@ -908,7 +980,6 @@ class LoaderOptionsTest(
                 CompiledSQL(
                     "SELECT child_subclass1.id AS child_subclass1_id, "
                     "child.id AS child_id, "
-                    "child.parent_id AS child_parent_id, "
                     "child.type AS child_type "
                     "FROM child JOIN child_subclass1 "
                     "ON child.id = child_subclass1.id "
@@ -955,7 +1026,7 @@ class LoaderOptionsTest(
             ),
             CompiledSQL(
                 "SELECT child_subclass1.id AS child_subclass1_id, "
-                "child.id AS child_id, child.parent_id AS child_parent_id, "
+                "child.id AS child_id, "
                 "child.type AS child_type, other_1.id AS other_1_id, "
                 "other_1.child_subclass_id AS other_1_child_subclass_id "
                 "FROM child JOIN child_subclass1 "
index b86e9f9f798868b2128312229b0292ddc890f195..daaf937b91237cb6e61084a33b7d0978a612b089 100644 (file)
@@ -2335,7 +2335,9 @@ class JoinedloadOverWPolyAliased(
                 exc.ArgumentError,
                 r'ORM mapped entity or attribute "Sub1.links" does not '
                 r'link from relationship "Link.child".  Did you mean to use '
-                r'"Link.child.of_type\(Sub1\)"\?',
+                r'"Link.child.of_type\(Sub1\)"\ or '
+                r'"loadopt.options'
+                r'\(selectin_polymorphic\(Parent, \[Sub1\]\), ...\)" \?',
             ):
                 session.query(cls).options(
                     joinedload(cls.links)
index 049d861fd6509e3e86773a59cd5ce272e2ce2a57..7c96539583fa727d953529012e5a1960e41875f3 100644 (file)
@@ -1234,7 +1234,9 @@ class OptionsNoPropTestInh(_Polymorphic):
             r"ORM mapped entity or attribute "
             r'(?:"Mapper\[Engineer\(engineers\)\]"|"Engineer.engineer_name") '
             r'does not link from relationship "Company.employees".  Did you '
-            r'mean to use "Company.employees.of_type\(Engineer\)"\?',
+            r'mean to use "Company.employees.of_type\(Engineer\)" '
+            r'or "loadopt.options'
+            r'\(selectin_polymorphic\(Person, \[Engineer\]\), ...\)" \?',
         ):
             if use_options:
                 s.query(Company).options(