]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed an obscure bug where the wrong results would be
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 8 Jun 2013 03:04:33 +0000 (23:04 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 8 Jun 2013 03:04:33 +0000 (23:04 -0400)
fetched when joining/joinedloading across a many-to-many
relationship to a single-table-inheriting
subclass with a specific discriminator value, due to "secondary"
rows that would come back.  The "secondary" and right-side
tables are now inner joined inside of parenthesis for all
ORM joins on many-to-many relationships so that the left->right
join can accurately filtered.
[ticket:2369]

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/orm/util.py
lib/sqlalchemy/sql/util.py
test/orm/inheritance/test_relationship.py
test/orm/inheritance/test_single.py
test/orm/test_eager_relations.py
test/orm/test_froms.py

index 1c27d7bf6cc17fe14011a7ba773b4553701c84cb..9d4bbfea1015d1597d2aeb69bb115a2e2d745805 100644 (file)
@@ -6,6 +6,19 @@
 .. changelog::
     :version: 0.9.0
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 2369
+
+        Fixed an obscure bug where the wrong results would be
+        fetched when joining/joinedloading across a many-to-many
+        relationship to a single-table-inheriting
+        subclass with a specific discriminator value, due to "secondary"
+        rows that would come back.  The "secondary" and right-side
+        tables are now inner joined inside of parenthesis for all
+        ORM joins on many-to-many relationships so that the left->right
+        join can accurately filtered.
+
     .. change::
         :tags: bug, mssql
         :tickets: 2747
index 7ac3ac96a4c4e78e279584fa2973b3a4917e3b20..fb6471ac6397a9844f4dea970b7777892c06ed3b 100644 (file)
@@ -918,8 +918,13 @@ class _ORMJoin(expression.Join):
                             of_type=right_info.mapper)
 
             if sj is not None:
-                left = sql.join(left, secondary, pj, isouter)
-                onclause = sj
+                if isouter:
+                    # note this is an inner join from secondary->right
+                    right = sql.join(secondary, right, sj)
+                    onclause = pj
+                else:
+                    left = sql.join(left, secondary, pj, isouter)
+                    onclause = sj
             else:
                 onclause = pj
             self._target_adapter = target_adapter
index 4422705cda80609993a4762d1dbef9d91ee9bc5a..bf3f3397e5a9580106f310a2f10a1589ffbf18b8 100644 (file)
@@ -213,6 +213,8 @@ def surface_selectables(clause):
         yield elem
         if isinstance(elem, expression.Join):
             stack.extend((elem.left, elem.right))
+        elif isinstance(elem, expression.FromGrouping):
+            stack.append(elem.element)
 
 def selectables_overlap(left, right):
     """Return True if left/right have some overlapping selectable"""
index f30a37941a4635d581ad47aed0f707ebd81b7835..20c6de284d7a71517d9c7680430704004e52f93c 100644 (file)
@@ -448,6 +448,7 @@ class M2MFilterTest(fixtures.MappedTest):
             [Organization(name='org1')])
 
 class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL):
+    __dialect__ = "default"
 
     @classmethod
     def define_tables(cls, metadata):
@@ -550,8 +551,7 @@ class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL):
             "(parent AS parent_1 JOIN child1 AS child1_1 ON parent_1.id = child1_1.id) "
             "ON parent_1.id = secondary_2.right_id WHERE "
             "parent_1.id = secondary_1.right_id AND :param_1 = "
-            "secondary_1.left_id",
-            dialect=default.DefaultDialect()
+            "secondary_1.left_id"
         )
 
     def test_eager_join(self):
@@ -571,14 +571,14 @@ class SelfReferentialM2MTest(fixtures.MappedTest, AssertsCompiledSQL):
             "child2_1.id AS child2_1_id, parent_1.id AS "
             "parent_1_id, parent_1.cls AS parent_1_cls FROM "
             "(SELECT child1.id AS child1_id, parent.id AS parent_id, "
-            "parent.cls AS parent_cls FROM parent JOIN child1 ON parent.id = "
-            "child1.id LIMIT :param_1) AS anon_1 LEFT OUTER JOIN secondary "
-            "AS secondary_1 ON anon_1.parent_id = secondary_1.right_id LEFT "
-            "OUTER JOIN (parent AS parent_1 JOIN child2 AS child2_1 ON "
-            "parent_1.id = child2_1.id) ON parent_1.id = "
-            "secondary_1.left_id",
-            {'param_1':1},
-            dialect=default.DefaultDialect())
+            "parent.cls AS parent_cls "
+            "FROM parent JOIN child1 ON parent.id = child1.id "
+            "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN "
+            "(secondary AS secondary_1 JOIN "
+            "(parent AS parent_1 JOIN child2 AS child2_1 "
+            "ON parent_1.id = child2_1.id) ON parent_1.id = secondary_1.left_id) "
+            "ON anon_1.parent_id = secondary_1.right_id",
+            {'param_1':1})
 
         # another way to check
         assert q.limit(1).with_labels().subquery().count().scalar() == 1
index de6e55e95653247b8546d78ddab4b01b6e1e6747..434642ca14cef15c3ba5f6e23a43219914c01742 100644 (file)
@@ -4,7 +4,7 @@ from sqlalchemy.orm import *
 
 from sqlalchemy import testing
 from test.orm import _fixtures
-from sqlalchemy.testing import fixtures
+from sqlalchemy.testing import fixtures, AssertsCompiledSQL
 from sqlalchemy.testing.schema import Table, Column
 
 
@@ -418,7 +418,8 @@ class RelationshipToSingleTest(testing.AssertsCompiledSQL, fixtures.MappedTest):
 
 
     def test_relationship_to_subclass(self):
-        JuniorEngineer, Company, companies, Manager, Employee, employees, Engineer = (self.classes.JuniorEngineer,
+        JuniorEngineer, Company, companies, Manager, \
+                    Employee, employees, Engineer = (self.classes.JuniorEngineer,
                                 self.classes.Company,
                                 self.tables.companies,
                                 self.classes.Manager,
@@ -511,6 +512,125 @@ class RelationshipToSingleTest(testing.AssertsCompiledSQL, fixtures.MappedTest):
             )
         go()
 
+
+class ManyToManyToSingleTest(fixtures.MappedTest, AssertsCompiledSQL):
+    __dialect__ = 'default'
+
+    @classmethod
+    def define_tables(cls, metadata):
+        Table('parent', metadata,
+                Column('id', Integer, primary_key=True,
+                            test_needs_autoincrement=True)
+            )
+        Table('m2m', metadata,
+                Column('parent_id', Integer,
+                            ForeignKey('parent.id'), primary_key=True),
+                Column('child_id', Integer,
+                            ForeignKey('child.id'), primary_key=True),
+            )
+        Table('child', metadata,
+                Column('id', Integer, primary_key=True,
+                            test_needs_autoincrement=True),
+                Column('discriminator', String(20)),
+                Column('name', String(20))
+            )
+
+    @classmethod
+    def setup_classes(cls):
+        class Parent(cls.Comparable):
+            pass
+
+        class Child(cls.Comparable):
+            pass
+
+        class SubChild1(Child):
+            pass
+
+        class SubChild2(Child):
+            pass
+
+    @classmethod
+    def setup_mappers(cls):
+        mapper(cls.classes.Parent, cls.tables.parent, properties={
+                "s1": relationship(cls.classes.SubChild1,
+                        secondary=cls.tables.m2m,
+                        uselist=False),
+                "s2": relationship(cls.classes.SubChild2,
+                        secondary=cls.tables.m2m)
+            })
+        mapper(cls.classes.Child, cls.tables.child,
+                        polymorphic_on=cls.tables.child.c.discriminator)
+        mapper(cls.classes.SubChild1, inherits=cls.classes.Child,
+                    polymorphic_identity='sub1')
+        mapper(cls.classes.SubChild2, inherits=cls.classes.Child,
+                    polymorphic_identity='sub2')
+
+    @classmethod
+    def insert_data(cls):
+        Parent = cls.classes.Parent
+        SubChild1 = cls.classes.SubChild1
+        SubChild2 = cls.classes.SubChild2
+        s = Session()
+        s.add_all([
+                Parent(s1=SubChild1(name='sc1_1'),
+                    s2=[SubChild2(name="sc2_1"), SubChild2(name="sc2_2")]
+                ),
+        ])
+        s.commit()
+
+    def test_eager_join(self):
+        Parent = self.classes.Parent
+        SubChild1 = self.classes.SubChild1
+
+        s = Session()
+
+        p1 = s.query(Parent).options(joinedload(Parent.s1)).all()[0]
+        eq_(p1.__dict__['s1'], SubChild1(name='sc1_1'))
+
+    def test_manual_join(self):
+        Parent = self.classes.Parent
+        Child = self.classes.Child
+        SubChild1 = self.classes.SubChild1
+
+        s = Session()
+
+        p1, c1 = s.query(Parent, Child).outerjoin(Parent.s1).all()[0]
+        eq_(c1, SubChild1(name='sc1_1'))
+
+    def test_assert_join_sql(self):
+        Parent = self.classes.Parent
+        Child = self.classes.Child
+
+        s = Session()
+
+        self.assert_compile(
+            s.query(Parent, Child).outerjoin(Parent.s1),
+            "SELECT parent.id AS parent_id, child.id AS child_id, "
+            "child.discriminator AS child_discriminator, "
+            "child.name AS child_name "
+            "FROM parent LEFT OUTER JOIN (m2m AS m2m_1 "
+            "JOIN child ON child.id = m2m_1.child_id "
+            "AND child.discriminator IN (:discriminator_1)) "
+            "ON parent.id = m2m_1.parent_id"
+        )
+
+    def test_assert_joinedload_sql(self):
+        Parent = self.classes.Parent
+        Child = self.classes.Child
+
+        s = Session()
+
+        self.assert_compile(
+            s.query(Parent).options(joinedload(Parent.s1)),
+            "SELECT parent.id AS parent_id, child_1.id AS child_1_id, "
+            "child_1.discriminator AS child_1_discriminator, "
+            "child_1.name AS child_1_name "
+            "FROM parent LEFT OUTER JOIN "
+            "(m2m AS m2m_1 JOIN child AS child_1 "
+            "ON child_1.id = m2m_1.child_id AND child_1.discriminator "
+            "IN (:discriminator_1)) ON parent.id = m2m_1.parent_id"
+        )
+
 class SingleOnJoinedTest(fixtures.MappedTest):
     @classmethod
     def define_tables(cls, metadata):
@@ -536,7 +656,8 @@ class SingleOnJoinedTest(fixtures.MappedTest):
         class Manager(Employee):
             pass
 
-        mapper(Person, persons_table, polymorphic_on=persons_table.c.type, polymorphic_identity='person')
+        mapper(Person, persons_table, polymorphic_on=persons_table.c.type,
+                    polymorphic_identity='person')
         mapper(Employee, employees_table, inherits=Person,polymorphic_identity='engineer')
         mapper(Manager, inherits=Employee,polymorphic_identity='manager')
 
index 52f45a2d4ca0bf6d86686dc23bfdce76a1b4abcf..e53ff6669529d3dcb306f7aa839d4dd6463ad104 100644 (file)
@@ -23,6 +23,7 @@ import datetime
 class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
     run_inserts = 'once'
     run_deletes = None
+    __dialect__ = 'default'
 
     def test_basic(self):
         users, Address, addresses, User = (self.tables.users,
@@ -787,7 +788,8 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
         """test that the subquery wrapping only occurs with
         limit/offset and m2m or o2m joins present."""
 
-        users, items, order_items, Order, Item, User, Address, orders, addresses = (self.tables.users,
+        users, items, order_items, Order, Item, User, \
+                    Address, orders, addresses = (self.tables.users,
                                 self.tables.items,
                                 self.tables.order_items,
                                 self.classes.Order,
@@ -802,7 +804,8 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             orders=relationship(Order, backref='user')
         ))
         mapper(Order, orders, properties=odict([
-            ('items', relationship(Item, secondary=order_items, backref='orders')),
+            ('items', relationship(Item, secondary=order_items,
+                        backref='orders')),
             ('address', relationship(Address))
             ]))
         mapper(Address, addresses)
@@ -812,63 +815,86 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
 
         self.assert_compile(
             sess.query(User).options(joinedload(User.orders)).limit(10),
-            "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name AS anon_1_users_name, "
-            "orders_1.id AS orders_1_id, orders_1.user_id AS orders_1_user_id, orders_1.address_id AS "
-            "orders_1_address_id, orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen "
+            "SELECT anon_1.users_id AS anon_1_users_id, "
+            "anon_1.users_name AS anon_1_users_name, "
+            "orders_1.id AS orders_1_id, orders_1.user_id AS orders_1_user_id, "
+            "orders_1.address_id AS "
+            "orders_1_address_id, orders_1.description AS orders_1_description, "
+            "orders_1.isopen AS orders_1_isopen "
             "FROM (SELECT users.id AS users_id, users.name AS users_name "
             "FROM users "
-            "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = orders_1.user_id",
-            {'param_1':10},
-            use_default_dialect=True
+            "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS orders_1 "
+            "ON anon_1.users_id = orders_1.user_id",
+            {'param_1':10}
         )
 
         self.assert_compile(
             sess.query(Order).options(joinedload(Order.user)).limit(10),
-            "SELECT orders.id AS orders_id, orders.user_id AS orders_user_id, orders.address_id AS "
-            "orders_address_id, orders.description AS orders_description, orders.isopen AS orders_isopen, "
-            "users_1.id AS users_1_id, users_1.name AS users_1_name FROM orders LEFT OUTER JOIN users AS "
+            "SELECT orders.id AS orders_id, orders.user_id AS orders_user_id, "
+            "orders.address_id AS "
+            "orders_address_id, orders.description AS orders_description, "
+            "orders.isopen AS orders_isopen, "
+            "users_1.id AS users_1_id, users_1.name AS users_1_name "
+            "FROM orders LEFT OUTER JOIN users AS "
             "users_1 ON users_1.id = orders.user_id LIMIT :param_1",
-            {'param_1':10},
-            use_default_dialect=True
+            {'param_1':10}
         )
 
         self.assert_compile(
-            sess.query(Order).options(joinedload(Order.user, innerjoin=True)).limit(10),
-            "SELECT orders.id AS orders_id, orders.user_id AS orders_user_id, orders.address_id AS "
-            "orders_address_id, orders.description AS orders_description, orders.isopen AS orders_isopen, "
-            "users_1.id AS users_1_id, users_1.name AS users_1_name FROM orders JOIN users AS "
+            sess.query(Order).options(
+                        joinedload(Order.user, innerjoin=True)).limit(10),
+            "SELECT orders.id AS orders_id, orders.user_id AS orders_user_id, "
+            "orders.address_id AS "
+            "orders_address_id, orders.description AS orders_description, "
+            "orders.isopen AS orders_isopen, "
+            "users_1.id AS users_1_id, users_1.name AS users_1_name "
+            "FROM orders JOIN users AS "
             "users_1 ON users_1.id = orders.user_id LIMIT :param_1",
-            {'param_1':10},
-            use_default_dialect=True
+            {'param_1':10}
         )
 
         self.assert_compile(
-            sess.query(User).options(joinedload_all("orders.address")).limit(10),
-            "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name AS anon_1_users_name, "
-            "addresses_1.id AS addresses_1_id, addresses_1.user_id AS addresses_1_user_id, "
-            "addresses_1.email_address AS addresses_1_email_address, orders_1.id AS orders_1_id, "
-            "orders_1.user_id AS orders_1_user_id, orders_1.address_id AS orders_1_address_id, "
-            "orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen FROM "
-            "(SELECT users.id AS users_id, users.name AS users_name FROM users LIMIT :param_1) AS anon_1 "
-            "LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = orders_1.user_id LEFT OUTER JOIN "
+            sess.query(User).options(
+                        joinedload_all("orders.address")).limit(10),
+            "SELECT anon_1.users_id AS anon_1_users_id, "
+            "anon_1.users_name AS anon_1_users_name, "
+            "addresses_1.id AS addresses_1_id, "
+            "addresses_1.user_id AS addresses_1_user_id, "
+            "addresses_1.email_address AS addresses_1_email_address, "
+            "orders_1.id AS orders_1_id, "
+            "orders_1.user_id AS orders_1_user_id, "
+            "orders_1.address_id AS orders_1_address_id, "
+            "orders_1.description AS orders_1_description, "
+            "orders_1.isopen AS orders_1_isopen FROM "
+            "(SELECT users.id AS users_id, users.name AS users_name "
+            "FROM users LIMIT :param_1) AS anon_1 "
+            "LEFT OUTER JOIN orders AS orders_1 "
+            "ON anon_1.users_id = orders_1.user_id LEFT OUTER JOIN "
             "addresses AS addresses_1 ON addresses_1.id = orders_1.address_id",
-            {'param_1':10},
-            use_default_dialect=True
+            {'param_1':10}
         )
 
         self.assert_compile(
-            sess.query(User).options(joinedload_all("orders.items"), joinedload("orders.address")),
-            "SELECT users.id AS users_id, users.name AS users_name, items_1.id AS items_1_id, "
-            "items_1.description AS items_1_description, addresses_1.id AS addresses_1_id, "
-            "addresses_1.user_id AS addresses_1_user_id, addresses_1.email_address AS "
-            "addresses_1_email_address, orders_1.id AS orders_1_id, orders_1.user_id AS "
-            "orders_1_user_id, orders_1.address_id AS orders_1_address_id, orders_1.description "
-            "AS orders_1_description, orders_1.isopen AS orders_1_isopen FROM users LEFT OUTER JOIN "
-            "orders AS orders_1 ON users.id = orders_1.user_id LEFT OUTER JOIN order_items AS "
-            "order_items_1 ON orders_1.id = order_items_1.order_id LEFT OUTER JOIN items AS "
-            "items_1 ON items_1.id = order_items_1.item_id LEFT OUTER JOIN addresses AS "
-            "addresses_1 ON addresses_1.id = orders_1.address_id"
-            ,use_default_dialect=True
+            sess.query(User).options(joinedload_all("orders.items"),
+                                joinedload("orders.address")),
+            "SELECT users.id AS users_id, users.name AS users_name, "
+                        "items_1.id AS items_1_id, "
+            "items_1.description AS items_1_description, "
+                        "addresses_1.id AS addresses_1_id, "
+            "addresses_1.user_id AS addresses_1_user_id, "
+            "addresses_1.email_address AS "
+            "addresses_1_email_address, orders_1.id AS orders_1_id, "
+            "orders_1.user_id AS "
+            "orders_1_user_id, orders_1.address_id AS orders_1_address_id, "
+            "orders_1.description "
+            "AS orders_1_description, orders_1.isopen AS orders_1_isopen "
+            "FROM users LEFT OUTER JOIN orders AS orders_1 "
+            "ON users.id = orders_1.user_id "
+            "LEFT OUTER JOIN (order_items AS order_items_1 "
+            "JOIN items AS items_1 ON items_1.id = order_items_1.item_id) "
+            "ON orders_1.id = order_items_1.order_id "
+            "LEFT OUTER JOIN addresses AS addresses_1 "
+            "ON addresses_1.id = orders_1.address_id"
         )
 
         self.assert_compile(
@@ -882,8 +908,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "FROM users "
             "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = "
             "orders_1.user_id LEFT OUTER JOIN addresses AS addresses_1 ON addresses_1.id = orders_1.address_id",
-            {'param_1':10},
-            use_default_dialect=True
+            {'param_1':10}
         )
 
         self.assert_compile(
@@ -898,8 +923,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "FROM users "
             "LIMIT :param_1) AS anon_1 JOIN orders AS orders_1 ON anon_1.users_id = "
             "orders_1.user_id JOIN addresses AS addresses_1 ON addresses_1.id = orders_1.address_id",
-            {'param_1':10},
-            use_default_dialect=True
+            {'param_1':10}
         )
 
     def test_one_to_many_scalar(self):
@@ -1159,8 +1183,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
                 "SELECT users.id AS users_id, users.name AS users_name, "
                 "addresses_1.id AS addresses_1_id, addresses_1.user_id AS addresses_1_user_id, "
                 "addresses_1.email_address AS addresses_1_email_address FROM users JOIN "
-                "addresses AS addresses_1 ON users.id = addresses_1.user_id ORDER BY addresses_1.id"
-        , use_default_dialect=True)
+                "addresses AS addresses_1 ON users.id = addresses_1.user_id ORDER BY addresses_1.id")
 
     def test_inner_join_chaining_options(self):
         users, items, order_items, Order, Item, User, orders = (self.tables.users,
@@ -1191,8 +1214,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "orders_1.isopen AS orders_1_isopen FROM users JOIN orders AS orders_1 ON "
             "users.id = orders_1.user_id JOIN order_items AS order_items_1 ON orders_1.id = "
             "order_items_1.order_id JOIN items AS items_1 ON items_1.id = "
-            "order_items_1.item_id",
-            use_default_dialect=True
+            "order_items_1.item_id"
         )
 
         self.assert_compile(
@@ -1201,11 +1223,12 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "items_1_id, items_1.description AS items_1_description, orders_1.id AS "
             "orders_1_id, orders_1.user_id AS orders_1_user_id, orders_1.address_id AS "
             "orders_1_address_id, orders_1.description AS orders_1_description, "
-            "orders_1.isopen AS orders_1_isopen FROM users LEFT OUTER JOIN orders AS orders_1 ON "
-            "users.id = orders_1.user_id LEFT OUTER JOIN order_items AS order_items_1 ON orders_1.id = "
-            "order_items_1.order_id LEFT OUTER JOIN items AS items_1 ON items_1.id = "
-            "order_items_1.item_id",
-            use_default_dialect=True
+            "orders_1.isopen AS orders_1_isopen "
+            "FROM users LEFT OUTER JOIN orders AS orders_1 "
+            "ON users.id = orders_1.user_id "
+            "LEFT OUTER JOIN (order_items AS order_items_1 "
+            "JOIN items AS items_1 ON items_1.id = order_items_1.item_id) "
+            "ON orders_1.id = order_items_1.order_id"
         )
 
         self.assert_compile(
@@ -1214,11 +1237,70 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "items_1_id, items_1.description AS items_1_description, orders_1.id AS "
             "orders_1_id, orders_1.user_id AS orders_1_user_id, orders_1.address_id AS "
             "orders_1_address_id, orders_1.description AS orders_1_description, "
-            "orders_1.isopen AS orders_1_isopen FROM users JOIN orders AS orders_1 ON "
-            "users.id = orders_1.user_id LEFT OUTER JOIN order_items AS order_items_1 ON orders_1.id = "
-            "order_items_1.order_id LEFT OUTER JOIN items AS items_1 ON items_1.id = "
-            "order_items_1.item_id",
-            use_default_dialect=True
+            "orders_1.isopen AS orders_1_isopen "
+            "FROM users JOIN orders AS orders_1 ON users.id = orders_1.user_id "
+            "LEFT OUTER JOIN (order_items AS order_items_1 "
+            "JOIN items AS items_1 ON items_1.id = order_items_1.item_id) "
+            "ON orders_1.id = order_items_1.order_id"
+
+        )
+
+    def test_catch_the_right_target(self):
+        # test eager join chaining to the "nested" join on the left,
+        # a new feature as of [ticket:2369]
+
+        users, Keyword, orders, items, order_items, Order, Item, \
+                User, keywords, item_keywords = (self.tables.users,
+                                self.classes.Keyword,
+                                self.tables.orders,
+                                self.tables.items,
+                                self.tables.order_items,
+                                self.classes.Order,
+                                self.classes.Item,
+                                self.classes.User,
+                                self.tables.keywords,
+                                self.tables.item_keywords)
+
+        mapper(User, users, properties={
+            'orders':relationship(Order, backref='user'), # o2m, m2o
+        })
+        mapper(Order, orders, properties={
+            'items':relationship(Item, secondary=order_items,
+                                        order_by=items.c.id),  #m2m
+        })
+        mapper(Item, items, properties={
+            'keywords':relationship(Keyword, secondary=item_keywords,
+                                        order_by=keywords.c.id) #m2m
+        })
+        mapper(Keyword, keywords)
+
+        sess = create_session()
+        q = sess.query(User).join(User.orders).join(Order.items).\
+                options(joinedload_all("orders.items.keywords"))
+
+        # here, the eager join for keywords can catch onto
+        # join(Order.items) or the nested (orders LEFT OUTER JOIN items),
+        # it should catch the latter
+        self.assert_compile(
+            q,
+            "SELECT users.id AS users_id, users.name AS users_name, "
+            "keywords_1.id AS keywords_1_id, keywords_1.name AS keywords_1_name, "
+            "items_1.id AS items_1_id, items_1.description AS items_1_description, "
+            "orders_1.id AS orders_1_id, orders_1.user_id AS orders_1_user_id, "
+            "orders_1.address_id AS orders_1_address_id, "
+            "orders_1.description AS orders_1_description, "
+            "orders_1.isopen AS orders_1_isopen "
+            "FROM users JOIN orders ON users.id = orders.user_id "
+            "JOIN order_items AS order_items_1 ON orders.id = order_items_1.order_id "
+            "JOIN items ON items.id = order_items_1.item_id "
+            "LEFT OUTER JOIN orders AS orders_1 ON users.id = orders_1.user_id "
+            "LEFT OUTER JOIN (order_items AS order_items_2 "
+            "JOIN items AS items_1 ON items_1.id = order_items_2.item_id) "
+            "ON orders_1.id = order_items_2.order_id "
+            "LEFT OUTER JOIN (item_keywords AS item_keywords_1 "
+            "JOIN keywords AS keywords_1 ON keywords_1.id = item_keywords_1.keyword_id) "
+            "ON items_1.id = item_keywords_1.item_id "
+            "ORDER BY items_1.id, keywords_1.id"
         )
 
     def test_inner_join_chaining_fixed(self):
@@ -1249,10 +1331,10 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "orders_1_id, orders_1.user_id AS orders_1_user_id, orders_1.address_id AS "
             "orders_1_address_id, orders_1.description AS orders_1_description, "
             "orders_1.isopen AS orders_1_isopen FROM users LEFT OUTER JOIN orders AS orders_1 ON "
-            "users.id = orders_1.user_id LEFT OUTER JOIN order_items AS order_items_1 ON orders_1.id = "
-            "order_items_1.order_id LEFT OUTER JOIN items AS items_1 ON items_1.id = "
-            "order_items_1.item_id",
-            use_default_dialect=True
+            "users.id = orders_1.user_id LEFT OUTER JOIN "
+            "(order_items AS order_items_1 JOIN items AS items_1 ON items_1.id = "
+            "order_items_1.item_id) ON orders_1.id = "
+            "order_items_1.order_id"
         )
 
         # joining just from Order, innerjoin=True can be respected
@@ -1264,8 +1346,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "AS items_1_id, items_1.description AS items_1_description FROM "
             "orders JOIN order_items AS order_items_1 ON orders.id = "
             "order_items_1.order_id JOIN items AS items_1 ON items_1.id = "
-            "order_items_1.item_id",
-            use_default_dialect=True
+            "order_items_1.item_id"
         )
 
 
@@ -1291,8 +1372,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "SELECT users.id AS users_id, users.name AS users_name, orders_1.id AS orders_1_id, "
             "orders_1.user_id AS orders_1_user_id, orders_1.address_id AS orders_1_address_id, "
             "orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen "
-            "FROM users JOIN orders AS orders_1 ON users.id = orders_1.user_id ORDER BY orders_1.id"
-        , use_default_dialect=True)
+            "FROM users JOIN orders AS orders_1 ON users.id = orders_1.user_id ORDER BY orders_1.id")
 
         self.assert_compile(sess.query(User).options(joinedload_all(User.orders, Order.items, innerjoin=True)),
             "SELECT users.id AS users_id, users.name AS users_name, items_1.id AS items_1_id, "
@@ -1301,8 +1381,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen "
             "FROM users JOIN orders AS orders_1 ON users.id = orders_1.user_id JOIN order_items AS "
             "order_items_1 ON orders_1.id = order_items_1.order_id JOIN items AS items_1 ON "
-            "items_1.id = order_items_1.item_id ORDER BY orders_1.id, items_1.id"
-        , use_default_dialect=True)
+            "items_1.id = order_items_1.item_id ORDER BY orders_1.id, items_1.id")
 
         def go():
             eq_(
@@ -1330,8 +1409,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "orders_address_id, orders.description AS orders_description, orders.isopen AS "
             "orders_isopen, users_1.id AS users_1_id, users_1.name AS users_1_name "
             "FROM orders JOIN users AS users_1 ON users_1.id = orders.user_id "
-            "WHERE orders.description = :description_1",
-            use_default_dialect=True
+            "WHERE orders.description = :description_1"
         )
 
 
@@ -2141,6 +2219,7 @@ class MixedEntitiesTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
     run_setup_mappers = 'once'
     run_inserts = 'once'
     run_deletes = None
+    __dialect__ = 'default'
 
     @classmethod
     def setup_mappers(cls):
@@ -2249,7 +2328,7 @@ class MixedEntitiesTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
 
 
 
-    def test_aliased_entity(self):
+    def test_aliased_entity_one(self):
         Item, Order, User, Address = (self.classes.Item,
                                 self.classes.Order,
                                 self.classes.User,
@@ -2272,6 +2351,17 @@ class MixedEntitiesTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             )
         self.assert_sql_count(testing.db, go, 1)
 
+    def test_aliased_entity_two(self):
+        Item, Order, User, Address = (self.classes.Item,
+                                self.classes.Order,
+                                self.classes.User,
+                                self.classes.Address)
+
+        sess = create_session()
+
+        oalias = sa.orm.aliased(Order)
+
+
         # one FROM clause
         def go():
             eq_(
@@ -2287,19 +2377,35 @@ class MixedEntitiesTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             )
         self.assert_sql_count(testing.db, go, 1)
 
-        from sqlalchemy.engine.default import DefaultDialect
+
+    def test_aliased_entity_three(self):
+        Item, Order, User, Address = (self.classes.Item,
+                                self.classes.Order,
+                                self.classes.User,
+                                self.classes.Address)
+
+        sess = create_session()
+
+        oalias = sa.orm.aliased(Order)
+
 
         # improper setup: oalias in the columns clause but join to usual
         # orders alias.  this should create two FROM clauses even though the
         # query has a from_clause set up via the join
-        self.assert_compile(sess.query(User, oalias).join(User.orders).options(joinedload(oalias.items)).with_labels().statement,
-        "SELECT users.id AS users_id, users.name AS users_name, orders_1.id AS orders_1_id, "\
-        "orders_1.user_id AS orders_1_user_id, orders_1.address_id AS orders_1_address_id, "\
-        "orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen, items_1.id AS items_1_id, "\
-        "items_1.description AS items_1_description FROM users JOIN orders ON users.id = orders.user_id, "\
-        "orders AS orders_1 LEFT OUTER JOIN order_items AS order_items_1 ON orders_1.id = order_items_1.order_id "\
-        "LEFT OUTER JOIN items AS items_1 ON items_1.id = order_items_1.item_id ORDER BY items_1.id",
-        dialect=DefaultDialect()
+        self.assert_compile(
+                sess.query(User, oalias).join(User.orders).
+                    options(joinedload(oalias.items)).with_labels().statement,
+        "SELECT users.id AS users_id, users.name AS users_name, "
+        "orders_1.id AS orders_1_id, "
+        "orders_1.user_id AS orders_1_user_id, "
+        "orders_1.address_id AS orders_1_address_id, "
+        "orders_1.description AS orders_1_description, "
+        "orders_1.isopen AS orders_1_isopen, items_1.id AS items_1_id, "
+        "items_1.description AS items_1_description FROM users "
+        "JOIN orders ON users.id = orders.user_id, "
+        "orders AS orders_1 LEFT OUTER JOIN (order_items AS order_items_1 "
+        "JOIN items AS items_1 ON items_1.id = order_items_1.item_id) "
+        "ON orders_1.id = order_items_1.order_id ORDER BY items_1.id"
         )
 
 
index 1a972d965660726f847ae25f952ac3b4c3600664..2403f4aae726ea00c5f979dae12c27c33a8f0685 100644 (file)
@@ -1929,7 +1929,8 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
 
 
     def test_more_joins(self):
-        users, Keyword, orders, items, order_items, Order, Item, User, keywords, item_keywords = (self.tables.users,
+        users, Keyword, orders, items, order_items, Order, Item, \
+                User, keywords, item_keywords = (self.tables.users,
                                 self.classes.Keyword,
                                 self.tables.orders,
                                 self.tables.items,
@@ -1972,6 +1973,36 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
             User(name='jack',id=7)
         ])
 
+    def test_very_nested_joins_with_joinedload(self):
+        users, Keyword, orders, items, order_items, Order, Item, \
+                User, keywords, item_keywords = (self.tables.users,
+                                self.classes.Keyword,
+                                self.tables.orders,
+                                self.tables.items,
+                                self.tables.order_items,
+                                self.classes.Order,
+                                self.classes.Item,
+                                self.classes.User,
+                                self.tables.keywords,
+                                self.tables.item_keywords)
+
+        mapper(User, users, properties={
+            'orders':relationship(Order, backref='user'), # o2m, m2o
+        })
+        mapper(Order, orders, properties={
+            'items':relationship(Item, secondary=order_items,
+                                        order_by=items.c.id),  #m2m
+        })
+        mapper(Item, items, properties={
+            'keywords':relationship(Keyword, secondary=item_keywords,
+                                        order_by=keywords.c.id) #m2m
+        })
+        mapper(Keyword, keywords)
+
+        sess = create_session()
+
+        sel = users.select(users.c.id.in_([7, 8]))
+
         def go():
             eq_(
                 sess.query(User).select_entity_from(sel).