]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add BakedQuery.to_query() method
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Aug 2018 15:07:48 +0000 (11:07 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Aug 2018 18:59:08 +0000 (14:59 -0400)
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

doc/build/changelog/unreleased_13/4318.rst [new file with mode: 0644]
doc/build/orm/extensions/baked.rst
lib/sqlalchemy/ext/baked.py
test/ext/test_baked.py

diff --git a/doc/build/changelog/unreleased_13/4318.rst b/doc/build/changelog/unreleased_13/4318.rst
new file mode 100644 (file)
index 0000000..34e3099
--- /dev/null
@@ -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`.
+
index 83faf299e7f4ab876e8100085b2e7bb90791c624..543bff1fadb95e6f35be38c6f4b935f3be999957 100644 (file)
@@ -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
index 72fe92957b21035586ff21a3757029f332d3ef2e..5168791424e7c8df14d1ba3acbed34c5e4ca5ac8 100644 (file)
@@ -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)
 
index c17e81fdd5e0611edcd9919f117270c763097f5c..f6afabd2da721adb0a85c3a8476901a15f9004e9 100644 (file)
@@ -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