From: Mike Bayer Date: Thu, 20 Jul 2023 17:06:22 +0000 (-0400) Subject: have token load flatten aliases unconditionally X-Git-Tag: rel_2_0_20~30^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3ac488ce702ea97369f8a3ca8b6ae6ade2550ec3;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git have token load flatten aliases unconditionally Fixed issue where chaining :func:`_orm.load_only` or other wildcard use of :func:`_orm.defer` from another eager loader using a :func:`_orm.aliased` against a joined inheritance subclass would fail to take effect for columns local to the superclass. Fixes: #10125 Change-Id: I15e4b8be5a6d2ba49d2354fcd1daa099d8c3498f --- diff --git a/doc/build/changelog/unreleased_20/10125.rst b/doc/build/changelog/unreleased_20/10125.rst new file mode 100644 index 0000000000..95336f3b5a --- /dev/null +++ b/doc/build/changelog/unreleased_20/10125.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm + :tickets: 10125 + + Fixed issue where chaining :func:`_orm.load_only` or other wildcard use of + :func:`_orm.defer` from another eager loader using a :func:`_orm.aliased` + against a joined inheritance subclass would fail to take effect for columns + local to the superclass. + diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index aca6fcc088..e7a0d3ebff 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -745,7 +745,6 @@ def _setup_entity_query( for value in poly_properties: if only_load_props and value.key not in only_load_props: continue - value.setup( compile_state, query_entity, diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py index 2269fb26b5..2cd8a1412c 100644 --- a/lib/sqlalchemy/orm/path_registry.py +++ b/lib/sqlalchemy/orm/path_registry.py @@ -449,6 +449,7 @@ class TokenRegistry(PathRegistry): is_token = True def generate_for_superclasses(self) -> Iterator[PathRegistry]: + # NOTE: this method is no longer used. consider removal parent = self.parent if is_root(parent): yield self @@ -474,6 +475,35 @@ class TokenRegistry(PathRegistry): else: yield self + def _generate_natural_for_superclasses( + self, + ) -> Iterator[_PathRepresentation]: + parent = self.parent + if is_root(parent): + yield self.natural_path + return + + if TYPE_CHECKING: + assert isinstance(parent, AbstractEntityRegistry) + for mp_ent in parent.mapper.iterate_to_root(): + yield TokenRegistry(parent.parent[mp_ent], self.token).natural_path + if ( + parent.is_aliased_class + and cast( + "AliasedInsp[Any]", + parent.entity, + )._is_with_polymorphic + ): + yield self.natural_path + for ent in cast( + "AliasedInsp[Any]", parent.entity + )._with_polymorphic_entities: + yield ( + TokenRegistry(parent.parent[ent], self.token).natural_path + ) + else: + yield self.natural_path + def _getitem(self, entity: Any) -> Any: try: return self.path[entity] @@ -589,7 +619,7 @@ class PropRegistry(PathRegistry): self._wildcard_path_loader_key = ( "loader", - parent.path + self.prop._wildcard_token, # type: ignore + parent.natural_path + self.prop._wildcard_token, # type: ignore ) self._default_path_loader_key = self.prop._default_path_loader_key self._loader_key = ("loader", self.natural_path) diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index d93ec0a273..d59fbb7693 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -2129,10 +2129,12 @@ class _TokenStrategyLoad(_LoadElement): # is set. return [ - ("loader", _path.natural_path) - for _path in cast( - TokenRegistry, effective_path - ).generate_for_superclasses() + ("loader", natural_path) + for natural_path in ( + cast( + TokenRegistry, effective_path + )._generate_natural_for_superclasses() + ) ] diff --git a/test/orm/test_deferred.py b/test/orm/test_deferred.py index fa044d033c..66e3104a95 100644 --- a/test/orm/test_deferred.py +++ b/test/orm/test_deferred.py @@ -1735,6 +1735,69 @@ class InheritanceTest(_Polymorphic): "ORDER BY managers.person_id", ) + @testing.variation("load", ["contains_eager", "joinedload"]) + def test_issue_10125(self, load): + s = fixture_session() + + employee_alias = aliased(Manager, flat=True) + company_alias = aliased(Company) + + if load.contains_eager: + q = ( + s.query(company_alias) + .outerjoin( + employee_alias, + company_alias.employees.of_type(employee_alias), + ) + .options( + contains_eager( + company_alias.employees.of_type(employee_alias) + ).load_only( + employee_alias.person_id, + ) + ) + ) + elif load.joinedload: + q = s.query(company_alias).options( + joinedload( + company_alias.employees.of_type(employee_alias) + ).load_only( + employee_alias.person_id, + ) + ) + else: + load.fail() + + if load.contains_eager: + self.assert_compile( + q, + "SELECT people_1.person_id AS people_1_person_id, " + "people_1.type AS people_1_type, " + "managers_1.person_id AS managers_1_person_id, " + "companies_1.company_id AS companies_1_company_id, " + "companies_1.name AS companies_1_name " + "FROM companies AS companies_1 LEFT OUTER JOIN " + "(people AS people_1 JOIN managers AS managers_1 " + "ON people_1.person_id = managers_1.person_id) " + "ON companies_1.company_id = people_1.company_id", + ) + elif load.joinedload: + self.assert_compile( + q, + "SELECT companies_1.company_id AS companies_1_company_id, " + "companies_1.name AS companies_1_name, " + "people_1.person_id AS people_1_person_id, " + "people_1.type AS people_1_type, " + "managers_1.person_id AS managers_1_person_id " + "FROM companies AS companies_1 LEFT OUTER JOIN " + "(people AS people_1 JOIN managers AS managers_1 " + "ON people_1.person_id = managers_1.person_id) " + "ON companies_1.company_id = people_1.company_id " + "ORDER BY people_1.person_id", + ) + else: + load.fail() + def test_load_only_subclass_bound(self): s = fixture_session() q = (