From 7b8b23b42700598adf2ee79083aa13dbca5e75e7 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 21 Sep 2010 18:09:29 -0400 Subject: [PATCH] - Query.select_from() has been beefed up to help ensure that a subsequent call to query.join() will use the select_from() entity, assuming it's a mapped entity and not a plain selectable, as the default "left" side, not the first entity in the Query object's list of entities. --- CHANGES | 7 +++++++ lib/sqlalchemy/orm/query.py | 32 +++++++++++++++++++------------- test/orm/test_query.py | 24 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/CHANGES b/CHANGES index 3362efb22e..a59b295707 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,13 @@ CHANGES - Patched a case where query.join() would adapt the right side to the right side of the left's join inappropriately [ticket:1925] + + - Query.select_from() has been beefed up to help + ensure that a subsequent call to query.join() + will use the select_from() entity, assuming it's + a mapped entity and not a plain selectable, + as the default "left" side, not the first entity + in the Query object's list of entities. - The exception raised by Session when it is used subsequent to a subtransaction rollback (which is what diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 0ce84435ff..cba3e7619c 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -90,6 +90,7 @@ class Query(object): _only_load_props = None _refresh_state = None _from_obj = () + _select_from_entity = None _filter_aliases = None _from_obj_alias = None _joinpath = _joinpoint = util.frozendict() @@ -266,7 +267,8 @@ class Query(object): return self._entities[0] def _mapper_zero(self): - return self._entity_zero().entity_zero + return self._select_from_entity or \ + self._entity_zero().entity_zero def _extension_zero(self): ent = self._entity_zero() @@ -283,8 +285,9 @@ class Query(object): def _joinpoint_zero(self): return self._joinpoint.get( - '_joinpoint_entity', - self._entity_zero().entity_zero) + '_joinpoint_entity', + self._mapper_zero() + ) def _mapper_zero_or_none(self): if not getattr(self._entities[0], 'primary_entity', False): @@ -1169,7 +1172,7 @@ class Query(object): arg1, arg2 = arg1 else: arg2 = None - + # determine onclause/right_entity. there # is a little bit of legacy behavior still at work here # which means they might be in either order. may possibly @@ -1417,20 +1420,23 @@ class Query(object): @_generative(_no_clauseelement_condition) def select_from(self, *from_obj): - """Set the `from_obj` parameter of the query and return the newly - resulting ``Query``. This replaces the table which this Query selects - from with the given table. + """Set the FROM clause of this :class:`.Query` explicitly. - ``select_from()`` also accepts class arguments. Though usually not - necessary, can ensure that the full selectable of the given mapper is - applied, e.g. for joined-table mappers. - - """ + Sending a mapped class or entity here effectively replaces the + "left edge" of any calls to :meth:`.Query.join`, when no + joinpoint is otherwise established - usually, the default "join + point" is the leftmost entity in the :class:`.Query` object's + list of entities to be selected. + Mapped entities or plain :class:`.Table` or other selectables + can be sent here which will form the default FROM clause. + + """ obj = [] for fo in from_obj: if _is_mapped_class(fo): mapper, selectable, is_aliased_class = _entity_info(fo) + self._select_from_entity = fo obj.append(selectable) elif not isinstance(fo, expression.FromClause): raise sa_exc.ArgumentError( @@ -1439,7 +1445,7 @@ class Query(object): obj.append(fo) self._set_select_from(*obj) - + def __getitem__(self, item): if isinstance(item, slice): start, stop, step = util.decode_slice(item) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index b52716198c..3ba1f0fa09 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -2543,6 +2543,30 @@ class JoinTest(QueryTest, AssertsCompiledSQL): "Could not find a FROM", sess.query(users.c.id).select_from(users).join, User ) + + def test_select_from(self): + """Test that the left edge of the join can be set reliably with select_from().""" + + sess = create_session() + self.assert_compile( + sess.query(Item.id).select_from(User).join(User.orders).join(Order.items), + "SELECT items.id AS items_id 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", + use_default_dialect=True + ) + + # here, the join really wants to add a second FROM clause + # for "Item". but select_from disallows that + self.assert_compile( + sess.query(Item.id).select_from(User).join((Item, User.id==Item.id)), + "SELECT items.id AS items_id FROM users JOIN items ON users.id = items.id", + use_default_dialect=True + ) + + + def test_from_self_resets_joinpaths(self): """test a join from from_self() doesn't confuse joins inside the subquery -- 2.47.2