From: Mike Bayer Date: Thu, 14 Sep 2023 14:48:03 +0000 (-0400) Subject: consider _ClassStrategyLoad as part of endpoint path X-Git-Tag: rel_2_0_21~5^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=825172a47de9a02cd2a7aafd8ae1fbccc263a7fd;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git consider _ClassStrategyLoad as part of endpoint path 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 --- diff --git a/doc/build/changelog/unreleased_20/10348.rst b/doc/build/changelog/unreleased_20/10348.rst new file mode 100644 index 0000000000..b7f0ded84c --- /dev/null +++ b/doc/build/changelog/unreleased_20/10348.rst @@ -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` diff --git a/doc/build/orm/queryguide/inheritance.rst b/doc/build/orm/queryguide/inheritance.rst index 7040128f40..0cb44b8ac7 100644 --- a/doc/build/orm/queryguide/inheritance.rst +++ b/doc/build/orm/queryguide/inheritance.rst @@ -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 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 diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index e7a0d3ebff..fb62c2f46e 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -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 diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py index 2cd8a1412c..c0f6e9cbcc 100644 --- a/lib/sqlalchemy/orm/path_registry.py +++ b/lib/sqlalchemy/orm/path_registry.py @@ -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: diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index d59fbb7693..0d64be5a09 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -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( diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 4371e6116f..46761fab81 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -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: diff --git a/test/orm/inheritance/test_poly_loading.py b/test/orm/inheritance/test_poly_loading.py index 8790848288..df286f0d35 100644 --- a/test/orm/inheritance/test_poly_loading.py +++ b/test/orm/inheritance/test_poly_loading.py @@ -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 " diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py index b86e9f9f79..daaf937b91 100644 --- a/test/orm/inheritance/test_relationship.py +++ b/test/orm/inheritance/test_relationship.py @@ -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) diff --git a/test/orm/test_options.py b/test/orm/test_options.py index 049d861fd6..7c96539583 100644 --- a/test/orm/test_options.py +++ b/test/orm/test_options.py @@ -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(