From: Mike Bayer Date: Fri, 3 Nov 2017 15:11:48 +0000 (-0400) Subject: Intercept contains_eager() with of_type, set aliased / polymorphic X-Git-Tag: rel_1_2_0~18^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=8ab652c6cb48ca6e157233aa3a23049e318d9d2b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Intercept contains_eager() with of_type, set aliased / polymorphic Fixed bug in :func:`.contains_eager` query option where making use of a path that used :meth:`.PropComparator.of_type` to refer to a subclass across more than one level of joins would also require that the "alias" argument were provided with the same subtype in order to avoid adding unwanted FROM clauses to the query; additionally, using :func:`.contains_eager` across subclasses that use :func:`.aliased` objects of subclasses as the :meth:`.PropComparator.of_type` argument will also render correctly. Change-Id: Ie1c10924faa45251aab1a076a3ba7ef9fb1bdeee Fixes: #4130 --- diff --git a/doc/build/changelog/unreleased_12/4130.rst b/doc/build/changelog/unreleased_12/4130.rst new file mode 100644 index 0000000000..192c1e3823 --- /dev/null +++ b/doc/build/changelog/unreleased_12/4130.rst @@ -0,0 +1,15 @@ +.. change:: + :tags: bug, orm + :tickets: 4130 + + Fixed bug in :func:`.contains_eager` query option where making use of a + path that used :meth:`.PropComparator.of_type` to refer to a subclass + across more than one level of joins would also require that the "alias" + argument were provided with the same subtype in order to avoid adding + unwanted FROM clauses to the query; additionally, using + :func:`.contains_eager` across subclasses that use :func:`.aliased` objects + of subclasses as the :meth:`.PropComparator.of_type` argument will also + render correctly. + + + diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 86a48f3b9a..775ed6c97d 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -211,7 +211,7 @@ class Load(Generative, MapperOption): if getattr(attr, '_of_type', None): ac = attr._of_type - ext_info = inspect(ac) + ext_info = of_type_info = inspect(ac) existing = path.entity_path[prop].get( self.context, "path_with_polymorphic") @@ -221,8 +221,23 @@ class Load(Generative, MapperOption): ext_info.mapper, aliased=True, _use_mapper_path=True, _existing_alias=existing) + ext_info = inspect(ac) + elif not ext_info.with_polymorphic_mappers: + ext_info = orm_util.AliasedInsp( + ext_info.entity, + ext_info.mapper.base_mapper, + ext_info.selectable, + ext_info.name, + ext_info.with_polymorphic_mappers or [ext_info.mapper], + ext_info.polymorphic_on, + ext_info._base_alias, + ext_info._use_mapper_path, + ext_info._adapt_on_names, + ext_info.represents_outer_join + ) + path.entity_path[prop].set( - self.context, "path_with_polymorphic", inspect(ac)) + self.context, "path_with_polymorphic", ext_info) # the path here will go into the context dictionary and # needs to match up to how the class graph is traversed. @@ -235,7 +250,7 @@ class Load(Generative, MapperOption): # it might be better for "path" to really represent, # "the path", but trying to keep the impact of the cache # key feature localized for now - self._of_type = ext_info + self._of_type = of_type_info else: path = path[prop] @@ -787,6 +802,10 @@ def contains_eager(loadopt, attr, alias=None): info = inspect(alias) alias = info.selectable + elif getattr(attr, '_of_type', None): + ot = inspect(attr._of_type) + alias = ot.selectable + cloned = loadopt.set_relationship_strategy( attr, {"lazy": "joined"}, diff --git a/test/orm/test_of_type.py b/test/orm/test_of_type.py index 7a81bffa1e..c8a042e93a 100644 --- a/test/orm/test_of_type.py +++ b/test/orm/test_of_type.py @@ -909,3 +909,180 @@ class SubclassRelationshipTest2( {} ) ) + + +class SubclassRelationshipTest3( + testing.AssertsCompiledSQL, fixtures.DeclarativeMappedTest): + + run_setup_classes = 'once' + run_setup_mappers = 'once' + run_inserts = 'once' + run_deletes = None + __dialect__ = 'default' + + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class _A(Base): + __tablename__ = 'a' + id = Column(Integer, primary_key=True) + type = Column(String(50), nullable=False) + b = relationship('_B', back_populates='a') + __mapper_args__ = {"polymorphic_on": type} + + class _B(Base): + __tablename__ = 'b' + id = Column(Integer, primary_key=True) + type = Column(String(50), nullable=False) + a_id = Column(Integer, ForeignKey(_A.id)) + a = relationship(_A, back_populates='b') + __mapper_args__ = {"polymorphic_on": type} + + class _C(Base): + __tablename__ = 'c' + id = Column(Integer, primary_key=True) + type = Column(String(50), nullable=False) + b_id = Column(Integer, ForeignKey(_B.id)) + __mapper_args__ = {"polymorphic_on": type} + + class A1(_A): + __mapper_args__ = {'polymorphic_identity': 'A1'} + + class B1(_B): + __mapper_args__ = {'polymorphic_identity': 'B1'} + + class C1(_C): + __mapper_args__ = {'polymorphic_identity': 'C1'} + b1 = relationship(B1, backref='c1') + + _query1 = ( + "SELECT b.id AS b_id, b.type AS b_type, b.a_id AS b_a_id, " + "c.id AS c_id, c.type AS c_type, c.b_id AS c_b_id, a.id AS a_id, " + "a.type AS a_type " + "FROM a LEFT OUTER JOIN b ON " + "a.id = b.a_id AND b.type IN (:type_1) " + "LEFT OUTER JOIN c ON " + "b.id = c.b_id AND c.type IN (:type_2) WHERE a.type IN (:type_3)" + ) + + _query2 = ( + "SELECT bbb.id AS bbb_id, bbb.type AS bbb_type, bbb.a_id AS bbb_a_id, " + "ccc.id AS ccc_id, ccc.type AS ccc_type, ccc.b_id AS ccc_b_id, " + "aaa.id AS aaa_id, aaa.type AS aaa_type " + "FROM a AS aaa LEFT OUTER JOIN b AS bbb " + "ON aaa.id = bbb.a_id AND bbb.type IN (:type_1) " + "LEFT OUTER JOIN c AS ccc ON " + "bbb.id = ccc.b_id AND ccc.type IN (:type_2) " + "WHERE aaa.type IN (:type_3)" + ) + + _query3 = ( + "SELECT bbb.id AS bbb_id, bbb.type AS bbb_type, bbb.a_id AS bbb_a_id, " + "c.id AS c_id, c.type AS c_type, c.b_id AS c_b_id, " + "aaa.id AS aaa_id, aaa.type AS aaa_type " + "FROM a AS aaa LEFT OUTER JOIN b AS bbb " + "ON aaa.id = bbb.a_id AND bbb.type IN (:type_1) " + "LEFT OUTER JOIN c ON " + "bbb.id = c.b_id AND c.type IN (:type_2) " + "WHERE aaa.type IN (:type_3)" + ) + + def _test(self, join_of_type, of_type_for_c1, aliased_): + A1, B1, C1 = self.classes('A1', 'B1', 'C1') + + if aliased_: + A1 = aliased(A1, name='aaa') + B1 = aliased(B1, name='bbb') + C1 = aliased(C1, name='ccc') + + sess = Session() + abc = sess.query(A1) + + if join_of_type: + abc = abc.outerjoin(A1.b.of_type(B1)).\ + options(contains_eager(A1.b.of_type(B1))) + + if of_type_for_c1: + abc = abc.outerjoin(B1.c1.of_type(C1)).\ + options( + contains_eager(A1.b.of_type(B1), B1.c1.of_type(C1))) + else: + abc = abc.outerjoin(B1.c1).\ + options(contains_eager(A1.b.of_type(B1), B1.c1)) + else: + abc = abc.outerjoin(B1, A1.b).\ + options(contains_eager(A1.b.of_type(B1))) + + if of_type_for_c1: + abc = abc.outerjoin(C1, B1.c1).\ + options( + contains_eager(A1.b.of_type(B1), B1.c1.of_type(C1))) + else: + abc = abc.outerjoin(B1.c1).\ + options(contains_eager(A1.b.of_type(B1), B1.c1)) + + if aliased_: + if of_type_for_c1: + self.assert_compile(abc, self._query2) + else: + self.assert_compile(abc, self._query3) + else: + self.assert_compile(abc, self._query1) + + def test_join_of_type_contains_eager_of_type_b1_c1(self): + self._test( + join_of_type=True, + of_type_for_c1=True, + aliased_=False + ) + + def test_join_flat_contains_eager_of_type_b1_c1(self): + self._test( + join_of_type=False, + of_type_for_c1=True, + aliased_=False + ) + + def test_join_of_type_contains_eager_of_type_b1(self): + self._test( + join_of_type=True, + of_type_for_c1=False, + aliased_=False + ) + + def test_join_flat_contains_eager_of_type_b1(self): + self._test( + join_of_type=False, + of_type_for_c1=False, + aliased_=False + ) + + def test_aliased_join_of_type_contains_eager_of_type_b1_c1(self): + self._test( + join_of_type=True, + of_type_for_c1=True, + aliased_=True + ) + + def test_aliased_join_flat_contains_eager_of_type_b1_c1(self): + self._test( + join_of_type=False, + of_type_for_c1=True, + aliased_=True + ) + + def test_aliased_join_of_type_contains_eager_of_type_b1(self): + self._test( + join_of_type=True, + of_type_for_c1=False, + aliased_=True + ) + + def test_aliased_join_flat_contains_eager_of_type_b1(self): + self._test( + join_of_type=False, + of_type_for_c1=False, + aliased_=True + ) +