From: Mike Bayer Date: Mon, 27 Aug 2018 15:07:48 +0000 (-0400) Subject: Add BakedQuery.to_query() method X-Git-Tag: rel_1_3_0b1~89 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b601051b217c435934cd41011c8a47de4c783e09;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add BakedQuery.to_query() method 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`. Fixes: #4318 Change-Id: I528056c7d140036c27b95500d7a60dcd14784016 --- diff --git a/doc/build/changelog/unreleased_13/4318.rst b/doc/build/changelog/unreleased_13/4318.rst new file mode 100644 index 0000000000..34e3099ce9 --- /dev/null +++ b/doc/build/changelog/unreleased_13/4318.rst @@ -0,0 +1,9 @@ +.. 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`. + diff --git a/doc/build/orm/extensions/baked.rst b/doc/build/orm/extensions/baked.rst index 83faf299e7..543bff1fad 100644 --- a/doc/build/orm/extensions/baked.rst +++ b/doc/build/orm/extensions/baked.rst @@ -372,24 +372,27 @@ Using Subqueries 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 diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index 72fe92957b..5168791424 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -15,6 +15,7 @@ compiled result to be fully cached. 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 @@ -232,6 +233,56 @@ class BakedQuery(object): 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) diff --git a/test/ext/test_baked.py b/test/ext/test_baked.py index c17e81fdd5..f6afabd2da 100644 --- a/test/ext/test_baked.py +++ b/test/ext/test_baked.py @@ -7,10 +7,12 @@ from test.orm import _fixtures 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): @@ -741,6 +743,64 @@ class ResultTest(BakedTest): 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