--- /dev/null
+.. change::
+ :tags: feature, ext
+ :tickets: 4318
+
+ Added new feature :meth:`.BakedQuery.to_query`, which allows for a
+ clean way of using one :class:`.BakedQuery` as a subquery inside of another
+ :class:`.BakedQuery` without needing to refer explicitly to a
+ :class:`.Session`.
+
When using :class:`.Query` objects, it is often needed that one :class:`.Query`
object is used to generate a subquery within another. In the case where the
:class:`.Query` is currently in baked form, an interim method may be used to
-retrieve the :class:`.Query` object, using the semi-private ``_as_query()``
-method. This method requires that a :class:`.Session` is passed, which should
-be relative to the :class:`.Session` that is in the lambda callable, e.g.
-``q.session`` or ``s``::
+retrieve the :class:`.Query` object, using the :meth:`.BakedQuery.to_query`
+method. This method is passed the :class:`.Session` or :class:`.Query` that is
+the argument to the lambda callable used to generate a particular step
+of the baked query::
bakery = baked.bakery()
+ # a baked query that will end up being used as a subquery
my_subq = bakery(lambda s: s.query(User.id))
my_subq += lambda q: q.filter(User.id == Address.user_id)
- # select a correlated subquery in the top columns list
+ # select a correlated subquery in the top columns list,
+ # we have the "session" argument, pass that
my_q = bakery(
- lambda s: s.query(Address.id, my_subq._as_query(s).as_scalar()))
+ lambda s: s.query(Address.id, my_subq.to_query(s).as_scalar()))
- # use a correlated subquery in some of the criteria
- my_q += lambda q: q.filter(my_subq._as_query(q.session).exists())
+ # use a correlated subquery in some of the criteria, we have
+ # the "query" argument, pass that.
+ my_q += lambda q: q.filter(my_subq.to_query(q).exists())
-A future feature will provide a public method for the above use case.
+.. versionadded:: 1.3
Disabling Baked Queries Session-wide
from ..orm.query import Query
from ..orm import strategy_options
+from ..orm.session import Session
from ..sql import util as sql_util, func, literal_column
from ..orm import exc as orm_exc
from .. import exc as sa_exc
self._bakery[self._effective_key(session)] = context
return context
+ def to_query(self, query_or_session):
+ """Return the :class:`.Query` object for use as a subquery.
+
+ This method should be used within the lambda callable being used
+ to generate a step of an enclosing :class:`.BakedQuery`. The
+ parameter should normally be the :class:`.Query` object that
+ is passed to the lambda::
+
+ sub_bq = self.bakery(lambda s: s.query(User.name))
+ sub_bq += lambda q: q.filter(
+ User.id == Address.user_id).correlate(Address)
+
+ main_bq = self.bakery(lambda s: s.query(Address))
+ main_bq += lambda q: q.filter(
+ sub_bq.to_query(q).exists())
+
+ In the case where the subquery is used in the first callable against
+ a :class:`.Session`, the :class:`.Session` is also accepted::
+
+ sub_bq = self.bakery(lambda s: s.query(User.name))
+ sub_bq += lambda q: q.filter(
+ User.id == Address.user_id).correlate(Address)
+
+ main_bq = self.bakery(
+ lambda s: s.query(Address.id, sub_bq.to_query(q).as_scalar())
+ )
+
+ :param query_or_session: a :class:`.Query` object or a class
+ :class:`.Session` object, that is assumed to be within the context
+ of an enclosing :class:`.BakedQuery` callable.
+
+
+ .. versionadded:: 1.3
+
+
+ """
+
+ if isinstance(query_or_session, Session):
+ session = query_or_session
+ elif isinstance(query_or_session, Query):
+ session = query_or_session.session
+ if session is None:
+ raise sa_exc.ArgumentError(
+ "Given Query needs to be associated with a Session")
+ else:
+ raise TypeError(
+ "Query or Session object expected, got %r." %
+ type(query_or_session))
+ return self._as_query(session)
+
def _as_query(self, session):
query = self.steps[0](session)
from sqlalchemy.ext import baked
from sqlalchemy import bindparam, func
from sqlalchemy.orm import exc as orm_exc
+from sqlalchemy.orm.query import Query
import itertools
from sqlalchemy.testing import mock
from sqlalchemy.testing.assertsql import CompiledSQL
import contextlib
+from sqlalchemy import exc as sa_exc
class BakedTest(_fixtures.FixtureTest):
sess.close()
+ def test_to_query_query(self):
+ User = self.classes.User
+ Address = self.classes.Address
+
+ sub_bq = self.bakery(
+ lambda s: s.query(User.name)
+ )
+ sub_bq += lambda q: q.filter(
+ User.id == Address.user_id).filter(User.name == 'ed').\
+ correlate(Address)
+
+ main_bq = self.bakery(lambda s: s.query(Address.id))
+ main_bq += lambda q: q.filter(
+ sub_bq.to_query(q).exists())
+ main_bq += lambda q: q.order_by(Address.id)
+
+ sess = Session()
+ result = main_bq(sess).all()
+ eq_(result, [(2,), (3,), (4,)])
+
+ def test_to_query_session(self):
+ User = self.classes.User
+ Address = self.classes.Address
+
+ sub_bq = self.bakery(
+ lambda s: s.query(User.name)
+ )
+ sub_bq += lambda q: q.filter(
+ User.id == Address.user_id).correlate(Address)
+
+ main_bq = self.bakery(
+ lambda s: s.query(Address.id, sub_bq.to_query(s).as_scalar()))
+ main_bq += lambda q: q.filter(sub_bq.to_query(q).as_scalar() == 'ed')
+ main_bq += lambda q: q.order_by(Address.id)
+
+ sess = Session()
+ result = main_bq(sess).all()
+ eq_(result, [(2, 'ed'), (3, 'ed'), (4, 'ed')])
+
+ def test_to_query_args(self):
+ User = self.classes.User
+ sub_bq = self.bakery(
+ lambda s: s.query(User.name)
+ )
+
+ q = Query([], None)
+ assert_raises_message(
+ sa_exc.ArgumentError,
+ "Given Query needs to be associated with a Session",
+ sub_bq.to_query, q
+ )
+
+ assert_raises_message(
+ TypeError,
+ "Query or Session object expected, got .*'int'.*",
+ sub_bq.to_query, 5
+ )
+
def test_subquery_eagerloading(self):
User = self.classes.User
Address = self.classes.Address