diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-06-07 23:04:33 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-06-07 23:04:33 -0400 |
commit | d5363fca5400f6c4969c2756fcfcdae6b9703091 (patch) | |
tree | 2893e5faa72e8606f4664ab7efee9d346586e37f | |
parent | 0d9ec9fe840eb71935c2a55c3063620a028e59aa (diff) | |
download | sqlalchemy-d5363fca5400f6c4969c2756fcfcdae6b9703091.tar.gz |
- 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.
[ticket:2369]
-rw-r--r-- | doc/build/changelog/changelog_09.rst | 13 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 9 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/util.py | 2 | ||||
-rw-r--r-- | test/orm/inheritance/test_relationship.py | 20 | ||||
-rw-r--r-- | test/orm/inheritance/test_single.py | 127 | ||||
-rw-r--r-- | test/orm/test_eager_relations.py | 268 | ||||
-rw-r--r-- | test/orm/test_froms.py | 33 |
7 files changed, 375 insertions, 97 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst index 1c27d7bf6..9d4bbfea1 100644 --- a/doc/build/changelog/changelog_09.rst +++ b/doc/build/changelog/changelog_09.rst @@ -7,6 +7,19 @@ :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 diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 7ac3ac96a..fb6471ac6 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -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 diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 4422705cd..bf3f3397e 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -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""" diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py index f30a37941..20c6de284 100644 --- a/test/orm/inheritance/test_relationship.py +++ b/test/orm/inheritance/test_relationship.py @@ -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 diff --git a/test/orm/inheritance/test_single.py b/test/orm/inheritance/test_single.py index de6e55e95..434642ca1 100644 --- a/test/orm/inheritance/test_single.py +++ b/test/orm/inheritance/test_single.py @@ -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') diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index 52f45a2d4..e53ff6669 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -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" ) diff --git a/test/orm/test_froms.py b/test/orm/test_froms.py index 1a972d965..2403f4aae 100644 --- a/test/orm/test_froms.py +++ b/test/orm/test_froms.py @@ -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). |