diff options
author | mike bayer <mike_mp@zzzcomputing.com> | 2017-12-04 17:37:51 -0500 |
---|---|---|
committer | Gerrit Code Review <gerrit@ci.zzzcomputing.com> | 2017-12-04 17:37:51 -0500 |
commit | fb62b64201ce705c765263e54e0f3bb09efde771 (patch) | |
tree | bd0e8dbfa350496661495b4f43c052325624d83f | |
parent | e6438cf8c3d2200262815840ac597d3f4a22e944 (diff) | |
parent | 8ab652c6cb48ca6e157233aa3a23049e318d9d2b (diff) | |
download | sqlalchemy-fb62b64201ce705c765263e54e0f3bb09efde771.tar.gz |
Merge "Intercept contains_eager() with of_type, set aliased / polymorphic"
-rw-r--r-- | doc/build/changelog/unreleased_12/4130.rst | 15 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 25 | ||||
-rw-r--r-- | test/orm/test_of_type.py | 177 |
3 files changed, 214 insertions, 3 deletions
diff --git a/doc/build/changelog/unreleased_12/4130.rst b/doc/build/changelog/unreleased_12/4130.rst new file mode 100644 index 000000000..192c1e382 --- /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 86a48f3b9..775ed6c97 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 7a81bffa1..c8a042e93 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 + ) + |