]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Compare entities also on chop_path
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 Apr 2017 17:25:50 +0000 (13:25 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 Apr 2017 18:36:16 +0000 (14:36 -0400)
When comparing query._current_path to options, the path chop
was not taking into account that the query or the options are
against aliased classes that don't match the mapper.

The issue does not seem to take place for the Load() version
of _chop_path.

Fixed bug to improve upon the specificity of loader options that
take effect subsequent to the lazy load of a related entity, so
that the loader options will match to an aliased or non-aliased
entity more specifically if those options include entity information.

Fixes: #3963
Change-Id: Ifdff37d579042fcc62bdeabce9e2413e9a03fbba

doc/build/changelog/changelog_12.rst
lib/sqlalchemy/orm/strategy_options.py
test/orm/test_eager_relations.py
test/orm/test_options.py

index 2070b92c03e8391fd2537efdb8a6224ba9bb76f9..a64a5d38030c5df35411f6c8f920b7bfae9666fa 100644 (file)
 .. changelog::
     :version: 1.2.0b1
 
+    .. change:: 3963
+        :tags: bug, orm
+        :tickets: 3963
+
+        Fixed bug to improve upon the specificity of loader options that
+        take effect subsequent to the lazy load of a related entity, so
+        that the loader options will match to an aliased or non-aliased
+        entity more specifically if those options include entity information.
+
     .. change:: 3740
         :tags: bug, sql
         :tickets: 3740
index bae2b73e26c93710093452ff8949e639b2bc1145..60ed2be3040ac32be3efb0ab363c977c6ff0eebf 100644 (file)
@@ -376,7 +376,8 @@ class _UnboundLoad(Load):
                         _WILDCARD_TOKEN,) and c_token != p_prop.key:
                     return None
             elif isinstance(c_token, PropComparator):
-                if c_token.property is not p_prop:
+                if c_token.property is not p_prop or \
+                        c_token._parententity is not p_mapper:
                     return None
         else:
             i += 1
index 0d324b3ffbd2efb01ec99d80dabab2cf12f48acc..3c669d90d60ef863f55fac95de6055d2a4549ade 100644 (file)
@@ -4362,6 +4362,165 @@ class EntityViaMultiplePathTestTwo(fixtures.DeclarativeMappedTest):
         )
 
 
+class LazyLoadOptSpecificityTest(fixtures.DeclarativeMappedTest):
+    """test for [ticket:3963]"""
+
+    @classmethod
+    def setup_classes(cls):
+        Base = cls.DeclarativeBasic
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            bs = relationship("B")
+
+        class B(Base):
+            __tablename__ = 'b'
+            id = Column(Integer, primary_key=True)
+            a_id = Column(ForeignKey('a.id'))
+            cs = relationship("C")
+
+        class C(Base):
+            __tablename__ = 'c'
+            id = Column(Integer, primary_key=True)
+            b_id = Column(ForeignKey('b.id'))
+
+    @classmethod
+    def insert_data(cls):
+        A, B, C = cls.classes("A", "B", "C")
+        s = Session()
+        s.add(A(id=1, bs=[B(cs=[C()])]))
+        s.add(A(id=2))
+        s.commit()
+
+    def _run_tests(self, query, expected):
+        def go():
+            for a, _ in query:
+                for b in a.bs:
+                    b.cs
+        self.assert_sql_count(testing.db, go, expected)
+
+    def test_string_options_aliased_whatever(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(
+            joinedload("bs").joinedload("cs"))
+        self._run_tests(q, 1)
+
+    def test_string_options_unaliased_whatever(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(A, aa).filter(
+            aa.id == 2).filter(A.id == 1).options(
+            joinedload("bs").joinedload("cs"))
+        self._run_tests(q, 1)
+
+    def test_lazyload_aliased_abs_bcs_one(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(
+            joinedload(A.bs).joinedload(B.cs))
+        self._run_tests(q, 3)
+
+    def test_lazyload_aliased_abs_bcs_two(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(
+            defaultload(A.bs).joinedload(B.cs))
+        self._run_tests(q, 3)
+
+    def test_pathed_lazyload_aliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        opt = Load(A).joinedload(A.bs).joinedload(B.cs)
+
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(opt)
+        self._run_tests(q, 3)
+
+    def test_pathed_lazyload_plus_joined_aliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        opt = Load(aa).defaultload(aa.bs).joinedload(B.cs)
+
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(opt)
+        self._run_tests(q, 2)
+
+    def test_pathed_joinedload_aliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        opt = Load(aa).joinedload(aa.bs).joinedload(B.cs)
+
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(opt)
+        self._run_tests(q, 1)
+
+    def test_lazyload_plus_joined_aliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(
+            defaultload(aa.bs).joinedload(B.cs))
+        self._run_tests(q, 2)
+
+    def test_joinedload_aliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(aa, A).filter(
+            aa.id == 1).filter(A.id == 2).options(
+            joinedload(aa.bs).joinedload(B.cs))
+        self._run_tests(q, 1)
+
+    def test_lazyload_unaliased_abs_bcs_one(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(A, aa).filter(
+            aa.id == 2).filter(A.id == 1).options(
+            joinedload(aa.bs).joinedload(B.cs))
+        self._run_tests(q, 3)
+
+    def test_lazyload_unaliased_abs_bcs_two(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(A, aa).filter(
+            aa.id == 2).filter(A.id == 1).options(
+            defaultload(aa.bs).joinedload(B.cs))
+        self._run_tests(q, 3)
+
+    def test_lazyload_plus_joined_unaliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(A, aa).filter(
+            aa.id == 2).filter(A.id == 1).options(
+            defaultload(A.bs).joinedload(B.cs))
+        self._run_tests(q, 2)
+
+    def test_joinedload_unaliased_abs_bcs(self):
+        A, B, C = self.classes("A", "B", "C")
+        s = Session()
+        aa = aliased(A)
+        q = s.query(A, aa).filter(
+            aa.id == 2).filter(A.id == 1).options(
+            joinedload(A.bs).joinedload(B.cs))
+        self._run_tests(q, 1)
+
+
 class EntityViaMultiplePathTestThree(fixtures.DeclarativeMappedTest):
     """test for [ticket:3811] continuing on [ticket:3431]"""
 
index 556f73c4b8b3900757b3d6638fe2db701e5c6efd..b7c574e2a5ae8f7dbc19d9055e6ae00a2d5e3d2e 100644 (file)
@@ -306,6 +306,54 @@ class OptionsTest(PathTest, QueryTest):
         opt = self._option_fixture(Order.items, Item.keywords)
         self._assert_path_result(opt, q, [])
 
+    def test_with_current_nonmatching_entity(self):
+        Item, User, Order = (self.classes.Item,
+                             self.classes.User,
+                             self.classes.Order)
+
+        sess = Session()
+        q = sess.query(Item)._with_current_path(
+            self._make_path_registry(
+                [inspect(aliased(User)), 'orders', Order, 'items'])
+        )
+
+        opt = self._option_fixture(User.orders)
+        self._assert_path_result(opt, q, [])
+
+        opt = self._option_fixture(User.orders, Order.items, Item.keywords)
+        self._assert_path_result(opt, q, [])
+
+        q = sess.query(Item)._with_current_path(
+            self._make_path_registry(
+                [User, 'orders', Order, 'items'])
+        )
+
+        ac = aliased(User)
+
+        opt = self._option_fixture(ac.orders)
+        self._assert_path_result(opt, q, [])
+
+        opt = self._option_fixture(ac.orders, Order.items, Item.keywords)
+        self._assert_path_result(opt, q, [])
+
+    def test_with_current_match_aliased_classes(self):
+        Item, User, Order = (self.classes.Item,
+                             self.classes.User,
+                             self.classes.Order)
+
+        ac = aliased(User)
+        sess = Session()
+        q = sess.query(Item)._with_current_path(
+            self._make_path_registry(
+                [inspect(ac), 'orders', Order, 'items'])
+        )
+
+        opt = self._option_fixture(ac.orders, Order.items, Item.keywords)
+        self._assert_path_result(opt, q, [(Item, "keywords")])
+
+        opt = self._option_fixture(ac.orders, Order.items)
+        self._assert_path_result(opt, q, [])
+
     def test_from_base_to_subclass_attr(self):
         Dingaling, Address = self.classes.Dingaling, self.classes.Address