diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-03-11 20:17:08 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-03-11 20:17:08 -0400 |
commit | 944fa00211db55c051dffc14bbf94169b8919a75 (patch) | |
tree | a8d7a733bfd04c72d2b24f6548040f0fceae6373 | |
parent | 5ccda3f2d95d7fbf7713df7e4eab61059a6683cd (diff) | |
download | sqlalchemy-ticket_3054.tar.gz |
- API cleanupticket_3054
- docs
- lets go
-rw-r--r-- | doc/build/index.rst | 3 | ||||
-rw-r--r-- | doc/build/orm/extensions/baked.rst | 207 | ||||
-rw-r--r-- | doc/build/orm/extensions/index.rst | 1 | ||||
-rw-r--r-- | examples/performance/__init__.py | 2 | ||||
-rw-r--r-- | examples/performance/short_selects.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/baked.py | 137 | ||||
-rw-r--r-- | test/ext/test_baked.py | 117 |
7 files changed, 422 insertions, 51 deletions
diff --git a/doc/build/index.rst b/doc/build/index.rst index 55dba45fe..1990df8e2 100644 --- a/doc/build/index.rst +++ b/doc/build/index.rst @@ -42,7 +42,8 @@ of Python objects, proceed first to the tutorial. * **ORM Usage:** :doc:`Session Usage and Guidelines <orm/session>` | - :doc:`Loading Objects <orm/loading_objects>` + :doc:`Loading Objects <orm/loading_objects>` | + :doc:`Cached Query Extension <orm/extensions/baked>` * **Extending the ORM:** :doc:`ORM Events and Internals <orm/extending>` diff --git a/doc/build/orm/extensions/baked.rst b/doc/build/orm/extensions/baked.rst new file mode 100644 index 000000000..2fd930c3d --- /dev/null +++ b/doc/build/orm/extensions/baked.rst @@ -0,0 +1,207 @@ +.. _baked_toplevel: + +Baked Queries +============= + +.. module:: sqlalchemy.ext.baked + +``baked`` provides an alternative creational pattern for +:class:`~.query.Query` objects, which allows for caching of the object's +construction and string-compilation steps. This means that for a +particular :class:`~.query.Query` building scenario that is used more than +once, all of the Python function invocation involved in building the query +from its initial construction up through generating a SQL string will only +occur **once**, rather than for each time that query is built up and executed. + +The rationale for this system is to greatly reduce Python interpreter +overhead for everything that occurs **before the SQL is emitted**. +The caching of the "baked" system does **not** in any way reduce SQL calls or +cache the **return results** from the database. A technique that demonstates +the caching of the SQL calls and result sets themselves is available in +:ref:`examples_caching`. + + +.. versionadded:: 1.0.0 + +.. note:: + + The :mod:`sqlalchemy.ext.baked` extension should be considered + **experimental** as of 1.0.0. It provides a dramatically different system + of producing queries which has yet to be proven at scale. + +Synopsis +-------- + +Usage of the baked system starts by producing a so-called "bakery", which +represents storage for a particular series of query objects:: + + from sqlalchemy.ext import baked + + bakery = baked.bakery() + +The above "bakery" will store cached data in an LRU cache that defaults +to 200 elements, noting that an ORM query will typically contain one entry +for the ORM query as invoked, as well as one entry per database dialect for +the SQL string. + +The bakery allows us to build up a :class:`~.query.Query` object by specifying +its construction as a series of Python callables, which are typically lambdas. +For succinct usage, it overrides the ``+=`` operator so that a typical +query build-up looks like the following:: + + from sqlalchemy import bindparam + + def search_for_user(session, username, email=None): + + baked_query = bakery(lambda session: session.query(User)) + baked_query += lambda q: q.filter(User.name == bindparam('username')) + + baked_query += lambda q: q.order_by(User.id) + + if email: + baked_query += lambda q: q.filter(User.email == bindparam('email')) + + result = baked_query(session).params(username=username, email=email).all() + + return result + +Following are some observations about the above code: + +1. The ``baked_query`` object is an instance of :class:`.BakedQuery`. This + object is essentially the "builder" for a real orm :class:`~.query.Query` + object, but it is not itself the *actual* :class:`~.query.Query` + object. + +2. The actual :class:`~.query.Query` object is not built at all, until the + very end of the function when :meth:`.Result.all` is called. + +3. The steps that are added to the ``baked_query`` object are all expressed + as Python functions, typically lambdas. The first lambda given + to the :func:`.bakery` function receives a :class:`.Session` as its + argument. The remaining lambdas each receive a :class:`~.query.Query` + as their argument. + +4. In the above code, even though our application may call upon + ``search_for_user()`` many times, and even though within each invocation + we build up an entirely new :class:`.BakedQuery` object, + *all of the lambdas are only called once*. Each lambda is **never** called + a second time for as long as this query is cached in the bakery. + +5. The caching is achieved by storing references to the **lambda objects + themselves** in order to formulate a cache key; that is, the fact that the + Python interpreter assigns an in-Python identity to these functions is + what determines how to identify the query on successive runs. For + those invocations of ``search_for_user()`` where the ``email`` parameter + is specified, the callable ``lambda q: q.filter(User.email == bindparam('email'))`` + will be part of the cache key that's retrieved; when ``email`` is + ``None``, this callable is not part of the cache key. + +6. Because the lambdas are all called only once, it is essential that no + variables which may change across calls are referenced **within** the + lambdas; instead, assuming these are values to be bound into the + SQL string, we use :func:`.bindparam` to construct named parameters, + where we apply their actual values later using :meth:`.Result.params`. + +Performance +----------- + +The baked query probably looks a little odd, a little bit awkward and +a little bit verbose. However, the savings in +Python performance for a query which is invoked lots of times in an +application are very dramatic. The example suite ``short_selects`` +demonstrated in :ref:`examples_performance` illustrates a comparison +of queries which each return only one row, such as the following regular +query:: + + session = Session(bind=engine) + for id_ in random.sample(ids, n): + session.query(Customer).filter(Customer.id == id_).one() + +compared to the equivalent "baked" query:: + + bakery = baked.bakery() + s = Session(bind=engine) + for id_ in random.sample(ids, n): + q = bakery(lambda s: s.query(Customer)) + q += lambda q: q.filter(Customer.id == bindparam('id')) + q(s).params(id=id_).one() + +The difference in Python function call count for an iteration of 10000 +calls to each block are:: + + test_baked_query : test a baked query of the full entity. + (10000 iterations); total fn calls 1951294 + + test_orm_query : test a straight ORM query of the full entity. + (10000 iterations); total fn calls 7900535 + +In terms of number of seconds on a powerful laptop, this comes out as:: + + test_baked_query : test a baked query of the full entity. + (10000 iterations); total time 2.174126 sec + + test_orm_query : test a straight ORM query of the full entity. + (10000 iterations); total time 7.958516 sec + +Note that this test very intentionally features queries that only return one row. +For queries that return many rows, the performance advantage of the baked query will have +less and less of an impact, proportional to the time spent fetching rows. +It is critical to keep in mind that the **baked query feature only applies to +building the query itself, not the fetching of results**. Using the +baked feature is by no means a guarantee to a much faster application; it is +only a potentially useful feature for those applications that have been measured +as being impacted by this particular form of overhead. + +.. topic:: Measure twice, cut once + + For background on how to profile a SQLAlchemy application, please see + the section :ref:`faq_performance`. It is essential that performance + measurement techniques are used when attempting to improve the performance + of an application. + + +Lazy Loading Integration +------------------------ + +The baked query can be integrated with SQLAlchemy's lazy loader feature +transparently. A future release of SQLAlchemy may enable this by default, +as its use within lazy loading is completely transparent. For now, +to enable baked lazyloading for all lazyloaders systemwide, call upon +the :func:`.bake_lazy_loaders` function. This will impact all relationships +that use the ``lazy='select'`` strategy as well as all use of the :func:`.lazyload` +per-query strategy. + +"Baked" lazy loading may be enabled on a per-:func:`.relationship` basis +using the ``baked_select`` loader strategy:: + + class MyClass(Base): + # ... + + widgets = relationship("Widget", lazy="baked_select") + +The ``baked_select`` strategy is available once any part of the application +has imported the ``sqlalchemy.ext.baked`` module. The "bakery" used by +this feature is local to the mapper for ``MyClass``. + +For per-query use, the :func:`.baked_lazyload` strategy may be used, +which works like any other loader option. + + +API Documentation +----------------- + +.. autofunction:: bakery + +.. autoclass:: BakedQuery + :members: + +.. autoclass:: Result + :members: + +.. autofunction:: bake_lazy_loaders + +.. autofunction:: unbake_lazy_loaders + +.. autofunction:: baked_lazyload + +.. autofunction:: baked_lazyload_all diff --git a/doc/build/orm/extensions/index.rst b/doc/build/orm/extensions/index.rst index f7f58e381..091ceb40a 100644 --- a/doc/build/orm/extensions/index.rst +++ b/doc/build/orm/extensions/index.rst @@ -17,6 +17,7 @@ behavior. In particular the "Horizontal Sharding", "Hybrid Attributes", and associationproxy automap + baked declarative/index mutable orderinglist diff --git a/examples/performance/__init__.py b/examples/performance/__init__.py index 88ae9b7dc..6264ae9f7 100644 --- a/examples/performance/__init__.py +++ b/examples/performance/__init__.py @@ -6,7 +6,7 @@ profile and associated implications: * bulk inserts * individual inserts, with or without transactions * fetching large numbers of rows -* running lots of small queries (TODO) +* running lots of short queries All suites include a variety of use patterns illustrating both Core and ORM use, and are generally sorted in order of performance from worst diff --git a/examples/performance/short_selects.py b/examples/performance/short_selects.py index 27120fe6a..ef1fcff4a 100644 --- a/examples/performance/short_selects.py +++ b/examples/performance/short_selects.py @@ -73,9 +73,10 @@ def test_orm_query_cols_only(n): @Profiler.profile def test_baked_query(n): """test a baked query of the full entity.""" + bakery = baked.bakery() s = Session(bind=engine) for id_ in random.sample(ids, n): - q = baked.BakedQuery(lambda s: s.query(Customer)) + q = bakery(lambda s: s.query(Customer)) q += lambda q: q.filter(Customer.id == bindparam('id')) q(s).params(id=id_).one() @@ -83,9 +84,10 @@ def test_baked_query(n): @Profiler.profile def test_baked_query_cols_only(n): """test a baked query of only the entity columns.""" + bakery = baked.bakery() s = Session(bind=engine) for id_ in random.sample(ids, n): - q = baked.BakedQuery( + q = bakery( lambda s: s.query( Customer.id, Customer.name, Customer.description)) q += lambda q: q.filter(Customer.id == bindparam('id')) diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index a32154d21..65d6a8603 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -1,3 +1,18 @@ +# sqlalchemy/ext/baked.py +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# <see AUTHORS file> +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Baked query extension. + +Provides a creational pattern for the :class:`.query.Query` object which +allows the fully constructed object, Core select statement, and string +compiled result to be fully cached. + + +""" + from ..orm.query import Query from ..orm import strategies, attributes, properties, \ strategy_options, util as orm_util, interfaces @@ -14,11 +29,11 @@ log = logging.getLogger(__name__) class BakedQuery(object): - _global_bakery = util.LRUCache(1000) + """A builder object for :class:`.query.Query` objects.""" __slots__ = 'steps', '_bakery', '_cache_key', '_spoiled' - def __init__(self, initial_fn, args=(), bakery=None): + def __init__(self, bakery, initial_fn, args=()): if args: self._cache_key = tuple(args) else: @@ -26,10 +41,18 @@ class BakedQuery(object): self._update_cache_key(initial_fn) self.steps = [initial_fn] self._spoiled = False - if bakery is not None: - self._bakery = bakery - else: - self._bakery = self._global_bakery + self._bakery = bakery + + @classmethod + def bakery(cls, size=200): + """Construct a new bakery.""" + + _bakery = util.LRUCache(size) + + def call(initial_fn): + return cls(_bakery, initial_fn) + + return call def _clone(self): b1 = BakedQuery.__new__(BakedQuery) @@ -56,33 +79,71 @@ class BakedQuery(object): return self.with_criteria(other) def add_criteria(self, fn, *args): + """Add a criteria function to this :class:`.BakedQuery`. + + This is equivalent to using the ``+=`` operator to + modify a :class:`.BakedQuery` in-place. + + """ self._update_cache_key(fn, args) self.steps.append(fn) return self def with_criteria(self, fn, *args): + """Add a criteria function to a :class:`.BakedQuery` cloned from this one. + + This is equivalent to using the ``+`` operator to + produce a new :class:`.BakedQuery` with modifications. + + """ return self._clone().add_criteria(fn, *args) def for_session(self, session): + """Return a :class:`.Result` object for this :class:`.BakedQuery`. + + This is equivalent to calling the :class:`.BakedQuery` as a + Python callable, e.g. ``result = my_baked_query(session)``. + + """ return Result(self, session) def __call__(self, session): return self.for_session(session) - def spoil(self): + def spoil(self, full=False): """Cancel any query caching that will occur on this BakedQuery object. - The BakedQuery can continue to be used normally, however when it - actually iterates results, no caching will be used. + The BakedQuery can continue to be used normally, however additional + creational functions will not be cached; they will be called + on every invocation. This is to support the case where a particular step in constructing a baked query disqualifies the query from being cacheable, such as a variant that relies upon some uncacheable value. + :param full: if False, only functions added to this + :class:`.BakedQuery` object subsequent to the spoil step will be + non-cached; the state of the :class:`.BakedQuery` up until + this point will be pulled from the cache. If True, then the + entire :class:`.Query` object is built from scratch each + time, with all creational functions being called on each + invocation. + """ + if not full: + _spoil_point = self._clone() + _spoil_point._cache_key += ('_query_only', ) + self.steps = [_spoil_point._retrieve_baked_query] self._spoiled = True return self + def _retrieve_baked_query(self, session): + query = self._bakery.get(self._cache_key, None) + if query is None: + query = self._as_query(session) + self._bakery[self._cache_key] = query.with_session(None) + return query.with_session(session) + def _bake(self, session): query = self._as_query(session) @@ -101,6 +162,7 @@ class BakedQuery(object): '_joinpath', '_joinpoint'): query.__dict__.pop(attr, None) self._bakery[self._cache_key] = context + return context def _as_query(self, session): query = self.steps[0](session) @@ -122,7 +184,7 @@ class BakedQuery(object): for k, v in list(context.attributes.items()): if isinstance(v, Query): if 'subquery' in k: - bk = BakedQuery(lambda *args: v) + bk = BakedQuery(self._bakery, lambda *args: v) bk._cache_key = self._cache_key + k bk._bake(session) baked_queries.append((k, bk._cache_key, v)) @@ -135,12 +197,19 @@ class BakedQuery(object): """ for k, cache_key, query in context.attributes["baked_queries"]: - bk = BakedQuery(lambda sess: query.with_session(sess)) + bk = BakedQuery(self._bakery, lambda sess: query.with_session(sess)) bk._cache_key = cache_key context.attributes[k] = bk.for_session(session).params(**params) class Result(object): + """Invokes a :class:`.BakedQuery` against a :class:`.Session`. + + The :class:`.Result` object is where the actual :class:`.query.Query` + object gets created, or retrieved from the cache, + against a target :class:`.Session`, and is then invoked for results. + + """ __slots__ = 'bq', 'session', '_params' def __init__(self, bq, session): @@ -149,6 +218,8 @@ class Result(object): self._params = {} def params(self, *args, **kw): + """Specify parameters to be replaced into the string SQL statement.""" + if len(args) == 1: kw.update(args[0]) elif len(args) > 0: @@ -169,10 +240,9 @@ class Result(object): if bq._spoiled: return iter(self._as_query()) - if bq._cache_key not in bq._bakery: - bq._bake(self.session) - - baked_context = bq._bakery[bq._cache_key] + baked_context = bq._bakery.get(bq._cache_key, None) + if baked_context is None: + baked_context = bq._bake(self.session) context = copy.copy(baked_context) context.session = self.session @@ -187,6 +257,11 @@ class Result(object): with_session(self.session)._execute_and_instances(context) def first(self): + """Return the first row. + + Equivalent to :meth:`.Query.first`. + + """ bq = self.bq.with_criteria(lambda q: q.slice(0, 1)) ret = list(bq.for_session(self.session).params(self._params)) if len(ret) > 0: @@ -197,6 +272,8 @@ class Result(object): def one(self): """Return exactly one result or raise an exception. + Equivalent to :meth:`.Query.one`. + """ ret = list(self) @@ -210,9 +287,20 @@ class Result(object): "Multiple rows were found for one()") def all(self): + """Return all rows. + + Equivalent to :meth:`.Query.all`. + + """ return list(self) def get(self, ident): + """Retrieve an object based on identity. + + Equivalent to :meth:`.Query.get`. + + """ + query = self.bq.steps[0](self.session) return query._get_impl(ident, self._load_on_ident) @@ -268,6 +356,12 @@ class Result(object): def bake_lazy_loaders(): + """Enable the use of baked queries for all lazyloaders systemwide. + + This operation should be safe for all lazy loaders, and will reduce + Python overhead for these operations. + + """ strategies.LazyLoader._strategy_keys[:] = [] BakedLazyLoader._strategy_keys[:] = [] @@ -280,6 +374,11 @@ def bake_lazy_loaders(): def unbake_lazy_loaders(): + """Disable the use of baked queries for all lazyloaders systemwide. + + This operation reverts the changes produced by :func:`.bake_lazy_loaders`. + + """ strategies.LazyLoader._strategy_keys[:] = [] BakedLazyLoader._strategy_keys[:] = [] @@ -298,14 +397,14 @@ class BakedLazyLoader(strategies.LazyLoader): def _emit_lazyload(self, session, state, ident_key, passive): q = BakedQuery( - lambda session: session.query(self.mapper), - bakery=self.mapper._compiled_cache) + self.mapper._compiled_cache, + lambda session: session.query(self.mapper)) q.add_criteria( lambda q: q._adapt_all_clauses()._with_invoke_all_eagers(False), self.parent_property) if not self.parent_property.bake_queries: - q.spoil() + q.spoil(full=True) if self.parent_property.secondary is not None: q.add_criteria( @@ -396,3 +495,5 @@ def baked_lazyload_all(*keys): baked_lazyload = baked_lazyload._unbound_fn baked_lazyload_all = baked_lazyload_all._unbound_all_fn + +bakery = BakedQuery.bakery diff --git a/test/ext/test_baked.py b/test/ext/test_baked.py index 34b128244..fa2722fcd 100644 --- a/test/ext/test_baked.py +++ b/test/ext/test_baked.py @@ -16,6 +16,9 @@ class BakedTest(_fixtures.FixtureTest): run_inserts = 'once' run_deletes = None + def setup(self): + self.bakery = baked.bakery() + class StateChangeTest(BakedTest): @classmethod @@ -24,9 +27,6 @@ class StateChangeTest(BakedTest): mapper(User, cls.tables.users) - def setup(self): - self._cache = {} - def _assert_cache_key(self, key, elements): eq_( key, @@ -37,7 +37,7 @@ class StateChangeTest(BakedTest): User = self.classes.User session = Session() l1 = lambda: session.query(User) - q1 = BakedQuery(l1, bakery=self._cache) + q1 = self.bakery(l1) self._assert_cache_key( q1._cache_key, [l1] @@ -49,7 +49,7 @@ class StateChangeTest(BakedTest): session = Session() l1 = lambda: session.query(User) l2 = lambda q: q.filter(User.name == bindparam('name')) - q1 = BakedQuery(l1, bakery=self._cache) + q1 = self.bakery(l1) self._assert_cache_key( q1._cache_key, [l1] @@ -70,7 +70,7 @@ class StateChangeTest(BakedTest): session = Session() l1 = lambda: session.query(User) l2 = lambda q: q.filter(User.name == bindparam('name')) - q1 = BakedQuery(l1, bakery=self._cache) + q1 = self.bakery(l1) self._assert_cache_key( q1._cache_key, [l1] @@ -88,7 +88,7 @@ class StateChangeTest(BakedTest): session = Session() l1 = lambda: session.query(User) l2 = lambda q: q.filter(User.name == bindparam('name')) - q1 = BakedQuery(l1, bakery=self._cache) + q1 = self.bakery(l1) q2 = q1.with_criteria(l2) is_not_(q2, q1) @@ -107,7 +107,7 @@ class StateChangeTest(BakedTest): session = Session() l1 = lambda: session.query(User) l2 = lambda q: q.filter(User.name == bindparam('name')) - q1 = BakedQuery(l1, bakery=self._cache) + q1 = self.bakery(l1) q2 = q1 + l2 is_not_(q2, q1) @@ -132,7 +132,7 @@ class LikeQueryTest(BakedTest): def test_first_no_result(self): User = self.classes.User - bq = BakedQuery(lambda s: s.query(User)) + bq = self.bakery(lambda s: s.query(User)) bq += lambda q: q.filter(User.name == 'asdf') eq_( @@ -143,7 +143,7 @@ class LikeQueryTest(BakedTest): def test_first_multiple_result(self): User = self.classes.User - bq = BakedQuery(lambda s: s.query(User.id)) + bq = self.bakery(lambda s: s.query(User.id)) bq += lambda q: q.filter(User.name.like('%ed%')).order_by(User.id) eq_( @@ -154,7 +154,7 @@ class LikeQueryTest(BakedTest): def test_one_no_result(self): User = self.classes.User - bq = BakedQuery(lambda s: s.query(User)) + bq = self.bakery(lambda s: s.query(User)) bq += lambda q: q.filter(User.name == 'asdf') assert_raises( @@ -165,7 +165,7 @@ class LikeQueryTest(BakedTest): def test_one_multiple_result(self): User = self.classes.User - bq = BakedQuery(lambda s: s.query(User)) + bq = self.bakery(lambda s: s.query(User)) bq += lambda q: q.filter(User.name.like('%ed%')) assert_raises( @@ -176,7 +176,7 @@ class LikeQueryTest(BakedTest): def test_get(self): User = self.classes.User - bq = BakedQuery(lambda s: s.query(User)) + bq = self.bakery(lambda s: s.query(User)) sess = Session() @@ -211,7 +211,7 @@ class LikeQueryTest(BakedTest): } ) - bq = BakedQuery(lambda s: s.query(AddressUser)) + bq = self.bakery(lambda s: s.query(AddressUser)) sess = Session() @@ -245,7 +245,7 @@ class ResultTest(BakedTest): def test_no_steps(self): User = self.classes.User - bq = BakedQuery( + bq = self.bakery( lambda s: s.query(User.id, User.name).order_by(User.id)) for i in range(3): @@ -258,7 +258,7 @@ class ResultTest(BakedTest): def test_different_limits(self): User = self.classes.User - bq = BakedQuery( + bq = self.bakery( lambda s: s.query(User.id, User.name).order_by(User.id)) bq += lambda q: q.limit(bindparam('limit')).offset(bindparam('offset')) @@ -275,18 +275,77 @@ class ResultTest(BakedTest): exp ) - def test_spoiled_w_params(self): + def test_spoiled_full_w_params(self): User = self.classes.User - bq = BakedQuery( - lambda s: s.query(User.id, User.name).order_by(User.id)) + canary = mock.Mock() - bq += lambda q: q.filter(User.id == bindparam('id')) + def fn1(s): + canary.fn1() + return s.query(User.id, User.name).order_by(User.id) + + def fn2(q): + canary.fn2() + return q.filter(User.id == bindparam('id')) + + def fn3(q): + canary.fn3() + return q + + for x in range(3): + bq = self.bakery(fn1) + + bq += fn2 + + sess = Session() + eq_( + bq.spoil(full=True).add_criteria(fn3)(sess).params(id=7).all(), + [(7, 'jack')] + ) + + eq_( + canary.mock_calls, + [mock.call.fn1(), mock.call.fn2(), mock.call.fn3(), + mock.call.fn1(), mock.call.fn2(), mock.call.fn3(), + mock.call.fn1(), mock.call.fn2(), mock.call.fn3()] + ) + + def test_spoiled_half_w_params(self): + User = self.classes.User + + canary = mock.Mock() + + def fn1(s): + canary.fn1() + return s.query(User.id, User.name).order_by(User.id) + + def fn2(q): + canary.fn2() + return q.filter(User.id == bindparam('id')) + + def fn3(q): + canary.fn3() + return q + + bq = self.bakery(fn1) + + bq += fn2 + + for x in range(3): + bq = self.bakery(fn1) + + bq += fn2 + + sess = Session() + eq_( + bq.spoil().add_criteria(fn3)(sess).params(id=7).all(), + [(7, 'jack')] + ) - sess = Session() eq_( - bq.spoil()(sess).params(id=7).all(), - [(7, 'jack')] + canary.mock_calls, + [mock.call.fn1(), mock.call.fn2(), + mock.call.fn3(), mock.call.fn3(), mock.call.fn3()] ) def test_w_new_entities(self): @@ -297,7 +356,7 @@ class ResultTest(BakedTest): """ User = self.classes.User - bq = BakedQuery( + bq = self.bakery( lambda s: s.query(User.id, User.name)) bq += lambda q: q.from_self().with_entities( @@ -318,7 +377,7 @@ class ResultTest(BakedTest): """ User = self.classes.User - base_bq = BakedQuery( + base_bq = self.bakery( lambda s: s.query(User.id, User.name)) base_bq += lambda q: q.order_by(User.id) @@ -377,7 +436,7 @@ class ResultTest(BakedTest): def test_conditional_step_oneline(self): User = self.classes.User - base_bq = BakedQuery( + base_bq = self.bakery( lambda s: s.query(User.id, User.name)) base_bq += lambda q: q.order_by(User.id) @@ -401,7 +460,7 @@ class ResultTest(BakedTest): User = self.classes.User Address = self.classes.Address - base_bq = BakedQuery( + base_bq = self.bakery( lambda s: s.query(User)) base_bq += lambda q: q.options(subqueryload(User.addresses)) @@ -606,7 +665,7 @@ class LazyLoaderTest(BakedTest): def _test_baked_lazy_loading(self, set_option): User, Address = self.classes.User, self.classes.Address - base_bq = BakedQuery( + base_bq = self.bakery( lambda s: s.query(User)) if set_option: @@ -665,7 +724,7 @@ class LazyLoaderTest(BakedTest): def test_baked_lazy_loading_m2o(self): User, Address = self._m2o_fixture() - base_bq = BakedQuery( + base_bq = self.bakery( lambda s: s.query(Address)) base_bq += lambda q: q.options(baked_lazyload(Address.user)) |