summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2017-12-04 17:37:51 -0500
committerGerrit Code Review <gerrit@ci.zzzcomputing.com>2017-12-04 17:37:51 -0500
commitfb62b64201ce705c765263e54e0f3bb09efde771 (patch)
treebd0e8dbfa350496661495b4f43c052325624d83f
parente6438cf8c3d2200262815840ac597d3f4a22e944 (diff)
parent8ab652c6cb48ca6e157233aa3a23049e318d9d2b (diff)
downloadsqlalchemy-fb62b64201ce705c765263e54e0f3bb09efde771.tar.gz
Merge "Intercept contains_eager() with of_type, set aliased / polymorphic"
-rw-r--r--doc/build/changelog/unreleased_12/4130.rst15
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py25
-rw-r--r--test/orm/test_of_type.py177
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
+ )
+