]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Demote innerjoin to outerjoin coming from with_polymorphic
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 12 May 2017 16:01:53 +0000 (12:01 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 12 May 2017 19:18:45 +0000 (15:18 -0400)
a with_polymorphic, regardless of inheritance type, represents
multiple classes.  A subclass that wants to joinedload with innerjoin=True
needs to be demoted to an outerjoin because the parent entity rows
might not be of that type.   Looks more intuitive with a joined
inheritance load, but applies just as well to single or concrete.

Change-Id: I4d3d76106ae20032269f8848aad70a8e2f9422f9
Fixes: #3988
doc/build/changelog/changelog_12.rst
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/util.py
test/orm/inheritance/test_relationship.py

index 24c525f8508d896ee62b7eeaacc2535f8db8f749..3d0b7fa0884be2dc5389b4f0b381b7e31fb21565 100644 (file)
 .. changelog::
     :version: 1.2.0b1
 
+    .. change:: 3988
+        :tags: bug, orm
+        :tickets: 3988
+
+        Fixed bug where combining a "with_polymorphic" load in conjunction
+        with subclass-linked relationships that specify joinedload with
+        innerjoin=True, would fail to demote those "innerjoins" to
+        "outerjoins" to suit the other polymorphic classes that don't
+        support that relationship.   This applies to both a single and a
+        joined inheritance polymorphic load.
+
     .. change:: 3984
         :tags: bug, orm
         :tickets: 3984
index 3fdba44825ecfdf05135f897e463a84c9b25a615..25aef3af79131587882a21c7f776baa73aae74d1 100644 (file)
@@ -687,6 +687,8 @@ class Mapper(InspectionAttr):
     is_mapper = True
     """Part of the inspection API."""
 
+    represents_outer_join = False
+
     @property
     def mapper(self):
         """Part of the inspection API.
index a2f83818c86553e4f840fe761439380d672a7abb..f1734194a448765d097c7f8a127efbfe46d47523 100644 (file)
@@ -3729,7 +3729,8 @@ class _MapperEntity(_QueryEntity):
             self.path, adapter, context.primary_columns,
             with_polymorphic=self._with_polymorphic,
             only_load_props=query._only_load_props,
-            polymorphic_discriminator=self._polymorphic_discriminator)
+            polymorphic_discriminator=self._polymorphic_discriminator
+        )
 
     def __str__(self):
         return str(self.mapper)
index 00aabed0bb0fb2d41af9a6414c944b0cc87d6857..05bb55d58d4e5997b0f2b551334b3a2c4ee042f4 100644 (file)
@@ -1502,7 +1502,9 @@ class JoinedLoader(AbstractRelationshipLoader):
 
         attach_on_outside = (
             not chained_from_outerjoin or
-            not innerjoin or innerjoin == 'unnested')
+            not innerjoin or innerjoin == 'unnested' or
+            entity.entity_zero.represents_outer_join
+        )
 
         if attach_on_outside:
             # this is the "classic" eager join case.
@@ -1510,7 +1512,9 @@ class JoinedLoader(AbstractRelationshipLoader):
                 towrap,
                 clauses.aliased_class,
                 onclause,
-                isouter=not innerjoin or (
+                isouter=not innerjoin or
+                entity.entity_zero.represents_outer_join or
+                (
                     chained_from_outerjoin and isinstance(towrap, sql.Join)
                 ), _left_memo=self.parent, _right_memo=self.mapper
             )
index 5bcb28f319aaf6f77dd95dfa0809d29ca034d7d2..eebe1883789b58aba8c8f6390ae7d2349ff1737a 100644 (file)
@@ -376,7 +376,8 @@ class AliasedClass(object):
                  with_polymorphic_mappers=(),
                  with_polymorphic_discriminator=None,
                  base_alias=None,
-                 use_mapper_path=False):
+                 use_mapper_path=False,
+                 represents_outer_join=False):
         mapper = _class_to_mapper(cls)
         if alias is None:
             alias = mapper._with_polymorphic_selectable.alias(
@@ -395,7 +396,8 @@ class AliasedClass(object):
             else mapper.polymorphic_on,
             base_alias,
             use_mapper_path,
-            adapt_on_names
+            adapt_on_names,
+            represents_outer_join
         )
 
         self.__name__ = 'AliasedClass_%s' % mapper.class_.__name__
@@ -478,7 +480,8 @@ class AliasedInsp(InspectionAttr):
 
     def __init__(self, entity, mapper, selectable, name,
                  with_polymorphic_mappers, polymorphic_on,
-                 _base_alias, _use_mapper_path, adapt_on_names):
+                 _base_alias, _use_mapper_path, adapt_on_names,
+                 represents_outer_join):
         self.entity = entity
         self.mapper = mapper
         self.selectable = selectable
@@ -487,6 +490,7 @@ class AliasedInsp(InspectionAttr):
         self.polymorphic_on = polymorphic_on
         self._base_alias = _base_alias or self
         self._use_mapper_path = _use_mapper_path
+        self.represents_outer_join = represents_outer_join
 
         self._adapter = sql_util.ColumnAdapter(
             selectable, equivalents=mapper._equivalent_columns,
@@ -530,7 +534,8 @@ class AliasedInsp(InspectionAttr):
             'with_polymorphic_discriminator':
                 self.polymorphic_on,
             'base_alias': self._base_alias,
-            'use_mapper_path': self._use_mapper_path
+            'use_mapper_path': self._use_mapper_path,
+            'represents_outer_join': self.represents_outer_join
         }
 
     def __setstate__(self, state):
@@ -543,7 +548,8 @@ class AliasedInsp(InspectionAttr):
             state['with_polymorphic_discriminator'],
             state['base_alias'],
             state['use_mapper_path'],
-            state['adapt_on_names']
+            state['adapt_on_names'],
+            state['represents_outer_join']
         )
 
     def _adapt_element(self, elem):
@@ -772,7 +778,8 @@ def with_polymorphic(base, classes, selectable=False,
                         selectable,
                         with_polymorphic_mappers=mappers,
                         with_polymorphic_discriminator=polymorphic_on,
-                        use_mapper_path=_use_mapper_path)
+                        use_mapper_path=_use_mapper_path,
+                        represents_outer_join=not innerjoin)
 
 
 def _orm_annotate(element, exclude=None):
index 74956930cba244bd61040e9c2cba83ace7aa6f13..246cf214b8ae7322289bbfa0cbd083beb7817511 100644 (file)
@@ -1662,6 +1662,58 @@ class JoinedloadOverWPolyAliased(
             "LEFT OUTER JOIN link AS link_2 ON parent_1.id = link_2.parent_id"
         )
 
+    def test_local_wpoly_innerjoins(self):
+        # test for issue #3988
+        Sub1 = self._fixture_from_subclass()
+        Parent = self.classes.Parent
+        Link = self.classes.Link
+
+        poly = with_polymorphic(Parent, [Sub1])
+
+        session = Session()
+        q = session.query(poly).options(
+            joinedload(poly.Sub1.links, innerjoin=True).
+            joinedload(Link.child.of_type(Sub1), innerjoin=True).
+            joinedload(poly.Sub1.links, innerjoin=True)
+        )
+        self.assert_compile(
+            q,
+            "SELECT parent.id AS parent_id, parent.type AS parent_type, "
+            "link_1.parent_id AS link_1_parent_id, "
+            "link_1.child_id AS link_1_child_id, "
+            "parent_1.id AS parent_1_id, parent_1.type AS parent_1_type, "
+            "link_2.parent_id AS link_2_parent_id, "
+            "link_2.child_id AS link_2_child_id FROM parent "
+            "LEFT OUTER JOIN link AS link_1 ON parent.id = link_1.parent_id "
+            "LEFT OUTER JOIN parent AS parent_1 "
+            "ON link_1.child_id = parent_1.id "
+            "LEFT OUTER JOIN link AS link_2 ON parent_1.id = link_2.parent_id"
+        )
+
+    def test_local_wpoly_innerjoins_roundtrip(self):
+        # test for issue #3988
+        Sub1 = self._fixture_from_subclass()
+        Parent = self.classes.Parent
+        Link = self.classes.Link
+
+        session = Session()
+        session.add_all([
+            Parent(),
+            Parent()
+        ])
+
+        # represents "Parent" and "Sub1" rows
+        poly = with_polymorphic(Parent, [Sub1])
+
+        # innerjoin for Sub1 only, but this needs
+        # to be cancelled because the Parent rows
+        # would be omitted
+        q = session.query(poly).options(
+            joinedload(poly.Sub1.links, innerjoin=True).
+            joinedload(Link.child.of_type(Sub1), innerjoin=True)
+        )
+        eq_(len(q.all()), 2)
+
 
 class JoinAcrossJoinedInhMultiPath(fixtures.DeclarativeMappedTest,
                                    testing.AssertsCompiledSQL):