summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-03-11 20:17:08 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-03-11 20:17:08 -0400
commit944fa00211db55c051dffc14bbf94169b8919a75 (patch)
treea8d7a733bfd04c72d2b24f6548040f0fceae6373
parent5ccda3f2d95d7fbf7713df7e4eab61059a6683cd (diff)
downloadsqlalchemy-ticket_3054.tar.gz
- API cleanupticket_3054
- docs - lets go
-rw-r--r--doc/build/index.rst3
-rw-r--r--doc/build/orm/extensions/baked.rst207
-rw-r--r--doc/build/orm/extensions/index.rst1
-rw-r--r--examples/performance/__init__.py2
-rw-r--r--examples/performance/short_selects.py6
-rw-r--r--lib/sqlalchemy/ext/baked.py137
-rw-r--r--test/ext/test_baked.py117
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))